[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