[PATCH 2/2] hostapd: Add configurable PEAP cryptobinding policy

github at singe.za.net github at singe.za.net
Mon Oct 5 08:35:36 PDT 2026


From: Dominic White <singe-github at singe.za.net>

Add eap_peap_crypto_binding to control the integrated EAP server policy: 0
disables PEAPv0 cryptobinding, 1 keeps the existing optional behavior, and 2
requires a valid response. Preserve optional behavior by default and validate
configured values.

Add hwsim coverage for off, optional, and required policies.

Signed-off-by: Dominic White <singe-github at singe.za.net>
---
 eap_example/eap_example_server.c |  1 +
 hostapd/config_file.c            | 11 ++++++++
 hostapd/hostapd.conf             |  6 +++++
 src/ap/ap_config.c               |  1 +
 src/ap/ap_config.h               |  1 +
 src/ap/authsrv.c                 |  1 +
 src/eap_server/eap.h             |  1 +
 src/eap_server/eap_server_peap.c | 13 +++++++++-
 tests/hwsim/test_ap_eap.py       | 44 ++++++++++++++++++++++++++++++++
 9 files changed, 78 insertions(+), 1 deletion(-)

diff --git a/eap_example/eap_example_server.c b/eap_example/eap_example_server.c
index 3a132bb34..617031358 100644
--- a/eap_example/eap_example_server.c
+++ b/eap_example/eap_example_server.c
@@ -224,6 +224,7 @@ int eap_example_server_init(void)
 	os_memset(&eap_conf, 0, sizeof(eap_conf));
 	eap_conf.eap_server = 1;
 	eap_conf.ssl_ctx = eap_ctx.tls_ctx;
+	eap_conf.peap_crypto_binding = 1;
 
 	os_memset(&eap_sess, 0, sizeof(eap_sess));
 	eap_ctx.eap = eap_server_sm_init(&eap_ctx, &eap_cb, &eap_conf,
diff --git a/hostapd/config_file.c b/hostapd/config_file.c
index 59827966e..4a1b241f8 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -2600,6 +2600,17 @@ static int hostapd_config_fill(struct hostapd_config *conf,
 		bss->crl_reload_interval = atoi(pos);
 	} else if (os_strcmp(buf, "tls_session_lifetime") == 0) {
 		bss->tls_session_lifetime = atoi(pos);
+	} else if (os_strcmp(buf, "eap_peap_crypto_binding") == 0) {
+		char *end;
+		long int val = strtol(pos, &end, 10);
+
+		if (end == pos || *end || val < 0 || val > 2) {
+			wpa_printf(MSG_ERROR,
+				   "Line %d: invalid eap_peap_crypto_binding '%s'",
+				   line, pos);
+			return 1;
+		}
+		bss->eap_peap_crypto_binding = val;
 	} else if (os_strcmp(buf, "tls_flags") == 0) {
 		bss->tls_flags = parse_tls_flags(pos);
 	} else if (os_strcmp(buf, "max_auth_rounds") == 0) {
diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf
index 0e9001221..b4cc0d6f4 100644
--- a/hostapd/hostapd.conf
+++ b/hostapd/hostapd.conf
@@ -1344,6 +1344,12 @@ eap_server=0
 # to use SQLite database instead of a text file.
 #eap_user_file=/etc/hostapd.eap_user
 
+# PEAPv0 cryptobinding policy for the integrated EAP server
+# 0 = do not use cryptobinding
+# 1 = use cryptobinding when the peer supports it (default)
+# 2 = require cryptobinding
+#eap_peap_crypto_binding=1
+
 # CA certificate (PEM or DER file) for EAP-TLS/PEAP/TTLS
 #ca_cert=/etc/hostapd.ca.pem
 
diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c
index d00c2c580..38f29aa40 100644
--- a/src/ap/ap_config.c
+++ b/src/ap/ap_config.c
@@ -157,6 +157,7 @@ void hostapd_config_defaults_bss(struct hostapd_bss_config *bss)
 	 * This can be enabled by default once the implementation has been fully
 	 * completed and tested with other implementations. */
 	bss->tls_flags = TLS_CONN_DISABLE_TLSv1_3;
+	bss->eap_peap_crypto_binding = 1;
 
 	bss->max_auth_rounds = 100;
 	bss->max_auth_rounds_short = 50;
diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h
index c96481df7..62dcb1165 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -444,6 +444,7 @@ struct hostapd_bss_config {
 	int check_crl_strict;
 	unsigned int crl_reload_interval;
 	unsigned int tls_session_lifetime;
+	int eap_peap_crypto_binding; /* 0=off, 1=optional, 2=required */
 	unsigned int tls_flags;
 	unsigned int max_auth_rounds;
 	unsigned int max_auth_rounds_short;
diff --git a/src/ap/authsrv.c b/src/ap/authsrv.c
index 3e8031810..1cc691bbe 100644
--- a/src/ap/authsrv.c
+++ b/src/ap/authsrv.c
@@ -314,6 +314,7 @@ static struct eap_config * authsrv_eap_config(struct hostapd_data *hapd)
 	cfg->eap_sim_db_priv = hapd->eap_sim_db_priv;
 	cfg->tls_session_lifetime = hapd->conf->tls_session_lifetime;
 	cfg->tls_flags = hapd->conf->tls_flags;
+	cfg->peap_crypto_binding = hapd->conf->eap_peap_crypto_binding;
 	cfg->max_auth_rounds = hapd->conf->max_auth_rounds;
 	cfg->max_auth_rounds_short = hapd->conf->max_auth_rounds_short;
 	if (hapd->conf->pac_opaque_encr_key)
diff --git a/src/eap_server/eap.h b/src/eap_server/eap.h
index dd02b0cdb..f379022df 100644
--- a/src/eap_server/eap.h
+++ b/src/eap_server/eap.h
@@ -261,6 +261,7 @@ struct eap_config {
 	int erp;
 	unsigned int tls_session_lifetime;
 	unsigned int tls_flags;
+	int peap_crypto_binding; /* 0=off, 1=optional, 2=required */
 
 	unsigned int max_auth_rounds;
 	unsigned int max_auth_rounds_short;
diff --git a/src/eap_server/eap_server_peap.c b/src/eap_server/eap_server_peap.c
index a4d4e4331..c13f500c7 100644
--- a/src/eap_server/eap_server_peap.c
+++ b/src/eap_server/eap_server_peap.c
@@ -184,7 +184,18 @@ static void * eap_peap_init(struct eap_sm *sm)
 		data->peap_version = data->force_version;
 	}
 	data->state = START;
-	data->crypto_binding = OPTIONAL_BINDING;
+	switch (sm->cfg->peap_crypto_binding) {
+	case NO_BINDING:
+		data->crypto_binding = NO_BINDING;
+		break;
+	case REQUIRE_BINDING:
+		data->crypto_binding = REQUIRE_BINDING;
+		break;
+	case OPTIONAL_BINDING:
+	default:
+		data->crypto_binding = OPTIONAL_BINDING;
+		break;
+	}
 
 	if (eap_server_tls_ssl_init(sm, &data->ssl, 0, EAP_TYPE_PEAP)) {
 		wpa_printf(MSG_INFO, "EAP-PEAP: Failed to initialize SSL.");
diff --git a/tests/hwsim/test_ap_eap.py b/tests/hwsim/test_ap_eap.py
index 29c702ff7..0f6b2b091 100644
--- a/tests/hwsim/test_ap_eap.py
+++ b/tests/hwsim/test_ap_eap.py
@@ -2448,6 +2448,50 @@ def test_ap_wpa2_eap_peap_crypto_binding(dev, apdev):
                 phase1="peapver=0 crypto_binding=0",
                 phase2="auth=MSCHAPV2")
 
+def test_ap_wpa2_eap_peap_crypto_binding_config(dev, apdev):
+    """PEAP cryptobinding policy configuration on the server"""
+    check_eap_capa(dev[0], "MSCHAPV2")
+
+    tests = [(0, 2, True), (1, 2, False), (2, 0, True), (2, 2, False)]
+    for server_policy, peer_policy, expect_failure in tests:
+        params = int_eap_server_params()
+        params["eap_peap_crypto_binding"] = str(server_policy)
+        hapd = hostapd.add_ap(apdev[0], params)
+        phase1 = f"peapver=0 crypto_binding={peer_policy}"
+        if server_policy == 2 and peer_policy == 0:
+            # This supplicant does not return cryptobinding. The server
+            # rejects the exchange with an outer EAP-Failure; wpa_supplicant
+            # reports that as a disconnection rather than EAP-FAILURE.
+            dev[0].connect("test-wpa2-eap",
+                           key_mgmt="WPA-EAP WPA-EAP-SHA256 WPA-EAP-SHA384",
+                           eap="PEAP", identity="user", password="password",
+                           ca_cert="auth_serv/ca.pem", phase1=phase1,
+                           phase2="auth=MSCHAPV2", wait_connect=False,
+                           scan_freq="2412", ieee80211w="1")
+            ev = dev[0].wait_event(["CTRL-EVENT-EAP-STARTED"], timeout=16)
+            if ev is None:
+                raise Exception("Association and EAP start timed out")
+            ev = dev[0].wait_event(["CTRL-EVENT-EAP-METHOD",
+                                    "CTRL-EVENT-EAP-FAILURE"], timeout=10)
+            if ev is None or "PEAP" not in ev:
+                raise Exception("PEAP method selection timed out")
+            ev = dev[0].wait_event(["CTRL-EVENT-CONNECTED",
+                                    "CTRL-EVENT-DISCONNECTED"], timeout=10)
+            if ev is None or "CTRL-EVENT-CONNECTED" in ev:
+                raise Exception("Required server cryptobinding was not enforced")
+            if "reason=23" not in ev:
+                raise Exception("Unexpected disconnect after required "
+                                "cryptobinding rejection: " + ev)
+        else:
+            eap_connect(dev[0], hapd, "PEAP", "user", password="password",
+                        ca_cert="auth_serv/ca.pem", phase1=phase1,
+                        phase2="auth=MSCHAPV2",
+                        expect_failure=expect_failure,
+                        local_error_report=expect_failure)
+        dev[0].request("REMOVE_NETWORK all")
+        if not expect_failure:
+            dev[0].wait_disconnected()
+
 def test_ap_wpa2_eap_peap_crypto_binding_server_oom(dev, apdev):
     """WPA2-Enterprise connection using EAP-PEAPv0/EAP-MSCHAPv2 and crypto binding with server OOM"""
     check_eap_capa(dev[0], "MSCHAPV2")
-- 
2.54.0 (Apple Git-157)




More information about the Hostap mailing list