[PATCH 4/4] tests: add DPP GAS comeback fault injection and regression tests
Gustavo Bertoli
gubertoli at gmail.com
Tue Jun 23 08:57:17 PDT 2026
The recovery reacts to fragment loss and reordering on a remain-on-channel
constrained link during DPP Enterprise certBag provisioning, which plain
hwsim does not reproduce. A failed DPP configuration is terminal, so drive
both regressions through the DPP Enterprise (sta-dot1x) certBag exchange,
the path the recovery is scoped to.
Reuse the existing dpp_test knob with two new values that inject one adverse
comeback condition at the GAS server, and add two DPP Enterprise tests, one
per recovery case:
test_dpp_gas_comeback_enterprise_missed_response - a missed comeback
response (DPP_TEST_GAS_COMEBACK_DROP): the Configurator drops one comeback
response while preparing the certBag; the Enrollee must re-request on the
same dialog token. An upstream new-token restart abandons the
Configurator's pending entry (keyed by dialog token) and fails.
test_dpp_gas_comeback_enterprise_lost_ack - a lost fragment ACK
(DPP_TEST_GAS_COMEBACK_NO_ACK): the Configurator treats one certBag
fragment's TX as not ACKed. It advances its offset only after a fragment
is acknowledged, so it resends the unacknowledged one; upstream advanced
on send and dropped the fragment for good.
Both fail (DPP-CONF-FAILED) on the unpatched tree.
Signed-off-by: Gustavo Bertoli <gubertoli at gmail.com>
---
src/common/dpp.h | 2 +
src/common/gas_server.c | 21 +++++++++++
tests/hwsim/test_dpp.py | 82 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 105 insertions(+)
diff --git a/src/common/dpp.h b/src/common/dpp.h
index 6aef72165..3cdf86105 100644
--- a/src/common/dpp.h
+++ b/src/common/dpp.h
@@ -568,6 +568,8 @@ enum dpp_test_behavior {
DPP_TEST_INVALID_R_BOOTSTRAP_KEY_HASH_PB_REQ = 98,
DPP_TEST_INVALID_I_BOOTSTRAP_KEY_HASH_PB_RESP = 99,
DPP_TEST_INVALID_R_BOOTSTRAP_KEY_HASH_PB_RESP = 100,
+ DPP_TEST_GAS_COMEBACK_NO_ACK = 101,
+ DPP_TEST_GAS_COMEBACK_DROP = 102,
};
extern enum dpp_test_behavior dpp_test;
diff --git a/src/common/gas_server.c b/src/common/gas_server.c
index 80c75251a..880b5c968 100644
--- a/src/common/gas_server.c
+++ b/src/common/gas_server.c
@@ -16,6 +16,9 @@
#include "ieee802_11_defs.h"
#include "gas.h"
#include "gas_server.h"
+#ifdef CONFIG_TESTING_OPTIONS
+#include "dpp.h"
+#endif /* CONFIG_TESTING_OPTIONS */
#define MAX_ADV_PROTO_ID_LEN 10
@@ -277,6 +280,14 @@ gas_server_handle_rx_comeback_req(struct gas_server_response *response)
struct wpabuf *resp;
unsigned int wait_time = 0;
+#ifdef CONFIG_TESTING_OPTIONS
+ if (dpp_test == DPP_TEST_GAS_COMEBACK_DROP) {
+ dpp_test = DPP_TEST_DISABLED;
+ wpa_printf(MSG_INFO, "GAS: TESTING - drop one comeback response");
+ return;
+ }
+#endif /* CONFIG_TESTING_OPTIONS */
+
if (response->tx_pending && response->resp) {
/* Previous fragment's TX status still pending; wait for it */
return;
@@ -492,6 +503,16 @@ void gas_server_tx_status(struct gas_server *gas, const u8 *dst, const u8 *data,
if (response->dialog_token != dialog_token ||
!ether_addr_equal(dst, response->dst))
continue;
+#ifdef CONFIG_TESTING_OPTIONS
+ /* Pretend the first fragment was not ACKed once */
+ if (dpp_test == DPP_TEST_GAS_COMEBACK_NO_ACK &&
+ response->resp && response->frag_id == 1) {
+ dpp_test = DPP_TEST_DISABLED;
+ wpa_printf(MSG_INFO,
+ "GAS: TESTING - treat comeback fragment TX as not ACKed");
+ ack = 0;
+ }
+#endif /* CONFIG_TESTING_OPTIONS */
gas_server_handle_tx_status(response, ack);
return;
}
diff --git a/tests/hwsim/test_dpp.py b/tests/hwsim/test_dpp.py
index 13636801f..94ce391a9 100644
--- a/tests/hwsim/test_dpp.py
+++ b/tests/hwsim/test_dpp.py
@@ -7981,3 +7981,85 @@ def test_dpp_proto_stop_after_auth_hostapd(dev, apdev):
ev = hapd.wait_event(["DPP-CONF-FAILED"], timeout=11)
if ev is None:
raise Exception("DPP config failure not reported")
+
+def run_dpp_gas_comeback_enterprise(dev, params, fault):
+ # Drive DPP Enterprise (sta-dot1x) certBag provisioning while injecting one
+ # GAS comeback fault at the Configurator's gas_server, and assert the
+ # Enrollee still completes. The large certBag fragments the Configuration
+ # response and the external CA signing forces comeback rounds, so this is
+ # the path the recovery is scoped to (auth->csr set on the Enrollee).
+ if not openssl_imported:
+ raise HwsimSkip("OpenSSL python method not available")
+ check_dpp_capab(dev[0])
+ check_dpp_capab(dev[1])
+ with open("auth_serv/ec-ca.pem", "rb") as f:
+ cacert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
+ f.read())
+ with open("auth_serv/ec-ca.key", "rb") as f:
+ cakey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,
+ f.read())
+ conf_id = dev[1].dpp_configurator_add()
+ id0 = dev[0].dpp_bootstrap_gen(chan="81/1", mac=True)
+ uri0 = dev[0].request("DPP_BOOTSTRAP_GET_URI %d" % id0)
+ dev[0].dpp_listen(2412)
+ csrattrs = "MAsGCSqGSIb3DQEJBw=="
+ cert_file = params['prefix'] + ".cert.pem"
+ pkcs7_file = params['prefix'] + ".pkcs7.der"
+ try:
+ dev[1].set("dpp_test", str(fault))
+ id1 = dev[1].dpp_auth_init(uri=uri0, configurator=conf_id,
+ conf="sta-dot1x", csrattrs=csrattrs)
+ ev = dev[1].wait_event(["DPP-CSR"], timeout=10)
+ if ev is None:
+ raise Exception("Configurator did not receive CSR")
+ id1_csr = int(ev.split(' ')[1].split('=')[1])
+ csr = ev.split(' ')[2]
+ if not csr.startswith("csr="):
+ raise Exception("Could not parse CSR event: " + ev)
+ csr = base64.b64decode(csr[4:].encode())
+ cert = dpp_sign_cert(cacert, cakey, csr)
+ with open(cert_file, 'wb') as f:
+ f.write(OpenSSL.crypto.dump_certificate(
+ OpenSSL.crypto.FILETYPE_PEM, cert))
+ subprocess.check_call(['openssl', 'crl2pkcs7', '-nocrl',
+ '-certfile', cert_file,
+ '-certfile', 'auth_serv/ec-ca.pem',
+ '-outform', 'DER', '-out', pkcs7_file])
+ with open(pkcs7_file, 'rb') as f:
+ certbag = base64.b64encode(f.read()).decode()
+ res = dev[1].request("DPP_CA_SET peer=%d name=certBag value=%s" %
+ (id1_csr, certbag))
+ if "OK" not in res:
+ raise Exception("Failed to set certBag")
+ ev = dev[0].wait_event(["DPP-CONF-RECEIVED", "DPP-CONF-FAILED"],
+ timeout=20)
+ finally:
+ dev[1].set("dpp_test", "0", allow_fail=True)
+ if ev is None:
+ raise Exception("DPP configuration result not reported (Enrollee)")
+ return ev
+
+def test_dpp_gas_comeback_enterprise_missed_response(dev, apdev, params):
+ """DPP Enterprise certBag survives a missed GAS comeback response
+
+ Case: a missed comeback response keeping the dialog token. The Configurator
+ drops one comeback response while still preparing the certBag. The Enrollee
+ must re-request on the SAME dialog token; an upstream restart on a new token
+ abandons the Configurator's pending entry (keyed by dialog token) and the
+ certBag is lost."""
+ ev = run_dpp_gas_comeback_enterprise(dev, params,
+ 102) # DPP_TEST_GAS_COMEBACK_DROP
+ if "DPP-CONF-FAILED" in ev:
+ raise Exception("Enrollee did not recover from a missed comeback response")
+
+def test_dpp_gas_comeback_enterprise_lost_ack(dev, apdev, params):
+ """DPP Enterprise certBag survives a lost GAS comeback fragment ACK
+
+ Case: a certBag fragment whose TX is not ACKed. The Configurator advances
+ its offset only after a fragment's TX is acknowledged, so it can resend an
+ unacknowledged fragment. Upstream advanced on send regardless of the TX
+ result, losing a fragment dropped to dwell misalignment and the certBag."""
+ ev = run_dpp_gas_comeback_enterprise(dev, params,
+ 101) # DPP_TEST_GAS_COMEBACK_NO_ACK
+ if "DPP-CONF-FAILED" in ev:
+ raise Exception("Enrollee did not recover from a lost comeback ACK")
--
2.39.5
More information about the Hostap
mailing list