[RFC PATCH v1 12/13] wpa_supplicant: Add DecoyAuth SAE initiator

Jeff Hansen x at jeffhansen.com
Fri Sep 11 12:20:04 PDT 2026


Negotiate H2E DecoyAuth in optional mode by default and evaluate
responder candidate data. Handle bounded fragments and extend the
authentication wait for large password sets.

Signed-off-by: Jeff Hansen <x at jeffhansen.com>
---
 wpa_supplicant/Makefile            |   3 +
 wpa_supplicant/config.c            |   8 ++
 wpa_supplicant/config_file.c       |   7 ++
 wpa_supplicant/config_ssid.h       |   6 ++
 wpa_supplicant/defconfig           |   3 +
 wpa_supplicant/sme.c               | 151 ++++++++++++++++++++++++++---
 wpa_supplicant/wpa_supplicant.c    |   7 +-
 wpa_supplicant/wpa_supplicant.conf |  10 ++
 wpa_supplicant/wpa_supplicant_i.h  |   1 +
 9 files changed, 182 insertions(+), 14 deletions(-)

diff --git a/wpa_supplicant/Makefile b/wpa_supplicant/Makefile
index 8c8626a98..6cf93af42 100644
--- a/wpa_supplicant/Makefile
+++ b/wpa_supplicant/Makefile
@@ -284,6 +284,9 @@ endif
 ifdef CONFIG_SAE
 CFLAGS += -DCONFIG_SAE
 OBJS += ../src/common/sae.o
+ifdef CONFIG_SAE_DECOYAUTH
+CFLAGS += -DCONFIG_SAE_DECOYAUTH
+endif
 ifdef CONFIG_SAE_PK
 CFLAGS += -DCONFIG_SAE_PK
 OBJS += ../src/common/sae_pk.o
diff --git a/wpa_supplicant/config.c b/wpa_supplicant/config.c
index d56ae3b95..e9aa00004 100644
--- a/wpa_supplicant/config.c
+++ b/wpa_supplicant/config.c
@@ -2717,6 +2717,9 @@ static const struct parse_data ssid_fields[] = {
 	{ STR(sae_password_id) },
 	{ FUNC(alt_sae_password_ids) },
 	{ INT(sae_pwe) },
+#ifdef CONFIG_SAE_DECOYAUTH
+	{ INT_RANGE(decoyauth, 0, 2) },
+#endif /* CONFIG_SAE_DECOYAUTH */
 	{ FUNC(proto) },
 	{ FUNC(key_mgmt) },
 	{ INT(bg_scan_period) },
@@ -3530,6 +3533,11 @@ void wpa_config_set_network_defaults(struct wpa_ssid *ssid)
 	ssid->proactive_key_caching = -1;
 	ssid->ieee80211w = MGMT_FRAME_PROTECTION_DEFAULT;
 	ssid->sae_pwe = DEFAULT_SAE_PWE;
+#ifdef CONFIG_SAE_DECOYAUTH
+	ssid->decoyauth = SAE_DECOYAUTH_OPTIONAL;
+#else /* CONFIG_SAE_DECOYAUTH */
+	ssid->decoyauth = SAE_DECOYAUTH_DISABLED;
+#endif /* CONFIG_SAE_DECOYAUTH */
 #ifdef CONFIG_MACSEC
 	ssid->mka_priority = DEFAULT_PRIO_NOT_KEY_SERVER;
 #endif /* CONFIG_MACSEC */
diff --git a/wpa_supplicant/config_file.c b/wpa_supplicant/config_file.c
index f6a870b6f..ece00deed 100644
--- a/wpa_supplicant/config_file.c
+++ b/wpa_supplicant/config_file.c
@@ -19,6 +19,9 @@
 #include "base64.h"
 #include "uuid.h"
 #include "common/ieee802_1x_defs.h"
+#ifdef CONFIG_SAE_DECOYAUTH
+#include "common/sae.h"
+#endif /* CONFIG_SAE_DECOYAUTH */
 #include "p2p/p2p.h"
 #include "eap_peer/eap_methods.h"
 #include "eap_peer/eap.h"
@@ -791,6 +794,10 @@ static void wpa_config_write_network(FILE *f, struct wpa_ssid *ssid,
 	STR(sae_password);
 	STR(sae_password_id);
 	write_int(f, "sae_pwe", ssid->sae_pwe, DEFAULT_SAE_PWE);
+#ifdef CONFIG_SAE_DECOYAUTH
+	write_int(f, "decoyauth", ssid->decoyauth,
+		  SAE_DECOYAUTH_OPTIONAL);
+#endif /* CONFIG_SAE_DECOYAUTH */
 	INT(sae_password_id_change);
 	write_proto(f, ssid);
 	write_key_mgmt(f, ssid);
diff --git a/wpa_supplicant/config_ssid.h b/wpa_supplicant/config_ssid.h
index 369b867b8..e5a13d3fc 100644
--- a/wpa_supplicant/config_ssid.h
+++ b/wpa_supplicant/config_ssid.h
@@ -1292,6 +1292,12 @@ struct wpa_ssid {
 	 */
 	enum sae_pwe sae_pwe;
 
+	/**
+	 * decoyauth - DecoyAuth negotiation mode
+	 * 0 = disabled, 1 = optional, 2 = required
+	 */
+	int decoyauth;
+
 	/**
 	 * disable_eht - Disable EHT (IEEE 802.11be) for this network
 	 *
diff --git a/wpa_supplicant/defconfig b/wpa_supplicant/defconfig
index 3ecd1e5f7..0ad46d14f 100644
--- a/wpa_supplicant/defconfig
+++ b/wpa_supplicant/defconfig
@@ -257,6 +257,9 @@ CONFIG_CTRL_IFACE=y
 # Simultaneous Authentication of Equals (SAE), WPA3-Personal
 CONFIG_SAE=y
 
+# Experimental DecoyAuth extension for SAE multi-password authentication
+#CONFIG_SAE_DECOYAUTH=y
+
 # SAE Public Key, WPA3-Personal
 #CONFIG_SAE_PK=y
 
diff --git a/wpa_supplicant/sme.c b/wpa_supplicant/sme.c
index 0c226aa31..b8ea62cac 100644
--- a/wpa_supplicant/sme.c
+++ b/wpa_supplicant/sme.c
@@ -41,6 +41,7 @@
 #include "hs20_supplicant.h"
 
 #define SME_AUTH_TIMEOUT 5
+#define SME_DECOYAUTH_AUTH_TIMEOUT 15
 #define SME_ASSOC_TIMEOUT 5
 #ifdef CONFIG_ENC_ASSOC
 static const int dot11RSNAConfigPMKLifetime = 43200;
@@ -64,6 +65,15 @@ static void sme_process_802_1x_auth_response(struct wpa_supplicant *wpa_s,
 					     bool external);
 #endif /* CONFIG_IEEE8021X_AUTH */
 
+static unsigned int sme_auth_timeout(const struct wpa_ssid *ssid)
+{
+#ifdef CONFIG_SAE
+	if (ssid && ssid->decoyauth != SAE_DECOYAUTH_DISABLED)
+		return SME_DECOYAUTH_AUTH_TIMEOUT;
+#endif /* CONFIG_SAE */
+	return SME_AUTH_TIMEOUT;
+}
+
 #if defined(CONFIG_IEEE8021X_AUTH) || defined(CONFIG_ENC_ASSOC)
 
 static const u8 * sme_get_peer_addr(struct wpa_supplicant *wpa_s, bool external)
@@ -241,6 +251,8 @@ static struct wpabuf * sme_auth_build_sae_commit(struct wpa_supplicant *wpa_s,
 	struct wpa_bss *bss;
 	int use_pt = 0;
 	bool use_pk = false;
+	bool advertise_decoyauth;
+	u16 fragment_request = 0;
 	u8 rsnxe_capa = 0;
 	int key_mgmt = external ? wpa_s->sme.ext_auth_key_mgmt :
 		wpa_s->key_mgmt;
@@ -310,6 +322,8 @@ static struct wpabuf * sme_auth_build_sae_commit(struct wpa_supplicant *wpa_s,
 		wpa_printf(MSG_DEBUG, "SAE: No password available");
 		goto fail;
 	}
+	str_clear_free(wpa_s->sme.sae_password);
+	wpa_s->sme.sae_password = NULL;
 
 	if (reuse && wpa_s->sme.sae.tmp &&
 	    ether_addr_equal(addr, wpa_s->sme.sae.tmp->bssid)) {
@@ -433,9 +447,24 @@ static struct wpabuf * sme_auth_build_sae_commit(struct wpa_supplicant *wpa_s,
 	}
 
 reuse_data:
+	if (wpa_s->sme.sae.tmp)
+		fragment_request = wpa_s->sme.sae.tmp->decoyauth_rx_next;
+	advertise_decoyauth =
+		ssid->decoyauth != SAE_DECOYAUTH_DISABLED && use_pt && !use_pk &&
+		!ssid->sae_password_id && ssid->mode != WPAS_MODE_MESH &&
+		wpa_s->sme.sae.tmp && wpa_s->sme.sae.tmp->ec;
+	if (ssid->decoyauth == SAE_DECOYAUTH_REQUIRED &&
+	    !advertise_decoyauth) {
+		wpa_printf(MSG_INFO,
+			   "SAE: Required DecoyAuth is incompatible with this SAE exchange");
+		goto fail;
+	}
 	len = wpa_s->sme.sae_token ? 3 + wpabuf_len(wpa_s->sme.sae_token) : 0;
 	if (ssid->sae_password_id)
 		len += 4 + os_strlen(ssid->sae_password_id);
+	if (advertise_decoyauth)
+		len += SAE_DECOYAUTH_CAPABILITY_IE_LEN +
+			(fragment_request ? 2 : 0);
 	buf = wpabuf_alloc(4 + SAE_COMMIT_MAX_LEN + len);
 	if (buf == NULL)
 		goto fail;
@@ -458,6 +487,21 @@ reuse_data:
 		wpabuf_free(buf);
 		goto fail;
 	}
+	if (advertise_decoyauth) {
+		wpabuf_put_u8(buf, WLAN_EID_VENDOR_SPECIFIC);
+		wpabuf_put_u8(buf, fragment_request ? 8 : 6);
+		WPA_PUT_BE32(wpabuf_put(buf, 4), SAE_DECOYAUTH_VENDOR_TYPE);
+		wpabuf_put_u8(buf, SAE_DECOYAUTH_VENDOR_VERSION);
+		wpabuf_put_u8(buf, ssid->decoyauth);
+		if (fragment_request)
+			wpabuf_put_le16(buf, fragment_request);
+	}
+	str_clear_free(wpa_s->sme.sae_password);
+	wpa_s->sme.sae_password = os_strdup(password);
+	if (!wpa_s->sme.sae_password) {
+		wpabuf_free(buf);
+		goto fail;
+	}
 	if (ret_use_pt)
 		*ret_use_pt = use_pt;
 	if (ret_use_pk)
@@ -2331,8 +2375,8 @@ no_fils:
 		return;
 	}
 
-	eloop_register_timeout(SME_AUTH_TIMEOUT, 0, sme_auth_timer, wpa_s,
-			       NULL);
+	eloop_register_timeout(sme_auth_timeout(ssid), 0, sme_auth_timer,
+			       wpa_s, NULL);
 
 	/*
 	 * Association will be started based on the authentication event from
@@ -2495,7 +2539,7 @@ static int sme_external_auth_build_buf(struct wpabuf *buf,
 
 static int sme_external_auth_send_sae_commit(struct wpa_supplicant *wpa_s,
 					     const u8 *bssid,
-					     struct wpa_ssid *ssid)
+					     struct wpa_ssid *ssid, int reuse)
 {
 	struct wpabuf *resp, *buf;
 	int use_pt;
@@ -2505,7 +2549,7 @@ static int sme_external_auth_send_sae_commit(struct wpa_supplicant *wpa_s,
 	resp = sme_auth_build_sae_commit(wpa_s, ssid, bssid,
 					 wpa_s->sme.ext_ml_auth ?
 					 wpa_s->sme.ext_auth_ap_mld_addr : NULL,
-					 1, 0, &use_pt, &use_pk);
+					 1, reuse, &use_pt, &use_pk);
 	if (!resp) {
 		wpa_printf(MSG_DEBUG, "SAE: Failed to build SAE commit");
 		return -1;
@@ -2618,7 +2662,7 @@ static int sme_handle_external_auth_start(struct wpa_supplicant *wpa_s,
 	}
 	if (!ssid ||
 	    sme_external_auth_send_sae_commit(wpa_s, ext_auth->bssid,
-					      ssid) < 0)
+					      ssid, 0) < 0)
 		return -1;
 
 	return 0;
@@ -3231,7 +3275,38 @@ static int sme_sae_auth(struct wpa_supplicant *wpa_s, u16 auth_transaction,
 
 			sme_external_auth_send_sae_commit(
 				wpa_s, wpa_s->sme.ext_auth_bssid,
-				wpa_s->sme.ext_auth_wpa_ssid);
+				wpa_s->sme.ext_auth_wpa_ssid, 0);
+		}
+		return 0;
+	}
+
+	if (auth_transaction == WLAN_AUTH_TR_SEQ_SAE_COMMIT &&
+	    status_code == SAE_DECOYAUTH_FRAGMENT_STATUS &&
+	    wpa_s->sme.sae.state == SAE_COMMITTED &&
+	    ((external && wpa_s->sme.ext_auth_wpa_ssid) ||
+	     (!external && wpa_s->current_bss && wpa_s->current_ssid))) {
+		if (sae_parse_decoyauth_fragment(&wpa_s->sme.sae, data, len,
+						 ie_offset) < 0) {
+			wpa_printf(MSG_DEBUG,
+				   "SAE: Invalid DecoyAuth fragment");
+			return -1;
+		}
+		wpa_printf(MSG_DEBUG,
+			   "SAE: Request DecoyAuth fragment %u of %u",
+			   wpa_s->sme.sae.tmp->decoyauth_rx_next,
+			   wpa_s->sme.sae.tmp->decoyauth_rx_count);
+		if (!external) {
+			sme_send_authentication(wpa_s, wpa_s->current_bss,
+						wpa_s->current_ssid, 2);
+		} else {
+			if (wpa_s->sme.ext_ml_auth &&
+			    sme_external_ml_auth(wpa_s, data, len, *ie_offset,
+						 status_code))
+				return -1;
+			if (sme_external_auth_send_sae_commit(
+				    wpa_s, wpa_s->sme.ext_auth_bssid,
+				    wpa_s->sme.ext_auth_wpa_ssid, 1) < 0)
+				return -1;
 		}
 		return 0;
 	}
@@ -3259,7 +3334,7 @@ static int sme_sae_auth(struct wpa_supplicant *wpa_s, u16 auth_transaction,
 
 			sme_external_auth_send_sae_commit(
 				wpa_s, wpa_s->sme.ext_auth_bssid,
-				wpa_s->sme.ext_auth_wpa_ssid);
+				wpa_s->sme.ext_auth_wpa_ssid, 0);
 		}
 		return 0;
 	}
@@ -3304,7 +3379,10 @@ static int sme_sae_auth(struct wpa_supplicant *wpa_s, u16 auth_transaction,
 	}
 
 	if (auth_transaction == WLAN_AUTH_TR_SEQ_SAE_COMMIT) {
+		struct wpa_ssid *auth_ssid;
 		u16 res;
+		int decoyauth_used = 0;
+		const u8 *event_peer = sa ? sa : wpa_s->pending_bssid;
 
 		groups = wpa_s->conf->sae_groups;
 
@@ -3313,6 +3391,8 @@ static int sme_sae_auth(struct wpa_supplicant *wpa_s, u16 auth_transaction,
 		    (!external &&
 		     (!wpa_s->current_bss || !wpa_s->current_ssid)))
 			return -1;
+		auth_ssid = external ? wpa_s->sme.ext_auth_wpa_ssid :
+			wpa_s->current_ssid;
 		if (wpa_s->sme.sae.state != SAE_COMMITTED) {
 			wpa_printf(MSG_DEBUG,
 				   "SAE: Ignore commit message while waiting for confirm");
@@ -3338,11 +3418,40 @@ static int sme_sae_auth(struct wpa_supplicant *wpa_s, u16 auth_transaction,
 
 		if (groups && groups[0] <= 0)
 			groups = NULL;
-		res = sae_parse_commit(&wpa_s->sme.sae, data, len, NULL, NULL,
-				       groups, status_code ==
-				       WLAN_STATUS_SAE_HASH_TO_ELEMENT ||
-				       status_code == WLAN_STATUS_SAE_PK,
-				       ie_offset);
+		if (!wpa_s->sme.sae_password) {
+			wpa_printf(MSG_DEBUG,
+				   "SAE: No retained password for peer commit");
+			return -1;
+		}
+
+		wpa_printf(MSG_DEBUG,
+			   "SAE: DecoyAuth STA Commit parse begin h2e=%d mlo=%d group=%d own=" MACSTR
+			   " ap_link=" MACSTR " ap_mld=" MACSTR " event_peer=" MACSTR
+			   " body_len=%zu",
+			   status_code == WLAN_STATUS_SAE_HASH_TO_ELEMENT,
+			   wpa_s->valid_links != 0, wpa_s->sme.sae.group,
+			   MAC2STR(wpa_s->own_addr), MAC2STR(wpa_s->pending_bssid),
+			   MAC2STR(wpa_s->ap_mld_addr), MAC2STR(event_peer), len);
+		if (auth_ssid->decoyauth == SAE_DECOYAUTH_DISABLED ||
+		    status_code != WLAN_STATUS_SAE_HASH_TO_ELEMENT) {
+			res = sae_parse_commit(
+				&wpa_s->sme.sae, data, len, NULL, NULL, groups,
+				status_code == WLAN_STATUS_SAE_HASH_TO_ELEMENT ||
+				status_code == WLAN_STATUS_SAE_PK, ie_offset);
+		} else {
+			res = sae_ap_parse_commit(
+				&wpa_s->sme.sae,
+				(u8 *) wpa_s->sme.sae_password,
+				os_strlen(wpa_s->sme.sae_password), data, len,
+				NULL, NULL, groups, ie_offset,
+				&decoyauth_used);
+		}
+		wpa_printf(MSG_DEBUG,
+			   "SAE: DecoyAuth STA Commit parse result=%u "
+			   "decoyauth_used=%d ie_offset=%d",
+			   res, decoyauth_used, ie_offset ? *ie_offset : -1);
+		str_clear_free(wpa_s->sme.sae_password);
+		wpa_s->sme.sae_password = NULL;
 		if (res == SAE_SILENTLY_DISCARD) {
 			wpa_printf(MSG_DEBUG,
 				   "SAE: Drop commit message due to reflection attack");
@@ -3350,6 +3459,12 @@ static int sme_sae_auth(struct wpa_supplicant *wpa_s, u16 auth_transaction,
 		}
 		if (res != WLAN_STATUS_SUCCESS)
 			return -1;
+		if (auth_ssid->decoyauth == SAE_DECOYAUTH_REQUIRED &&
+		    !decoyauth_used) {
+			wpa_printf(MSG_INFO,
+				   "SAE: AP did not use required DecoyAuth exchange");
+			return -1;
+		}
 
 		if (wpa_s->sme.sae.tmp &&
 		    sme_check_sae_rejected_groups(
@@ -5431,6 +5546,16 @@ void sme_event_assoc_reject(struct wpa_supplicant *wpa_s,
 void sme_event_auth_timed_out(struct wpa_supplicant *wpa_s,
 			      union wpa_event_data *data)
 {
+	if (wpa_s->wpa_state == WPA_AUTHENTICATING &&
+	    sme_auth_timeout(wpa_s->current_ssid) ==
+	    SME_DECOYAUTH_AUTH_TIMEOUT &&
+	    eloop_is_timeout_registered(sme_auth_timer, wpa_s, NULL)) {
+		wpa_dbg(wpa_s, MSG_DEBUG,
+			"SME: Defer driver authentication timeout until the "
+			"15-second DecoyAuth deadline");
+		return;
+	}
+
 	wpa_dbg(wpa_s, MSG_DEBUG, "SME: Authentication timed out");
 	wpas_connection_failed(wpa_s, wpa_s->pending_bssid, NULL);
 	wpa_supplicant_mark_disassoc(wpa_s);
@@ -5537,6 +5662,8 @@ void sme_clear_on_disassoc(struct wpa_supplicant *wpa_s)
 #ifdef CONFIG_SAE
 	wpabuf_free(wpa_s->sme.sae_token);
 	wpa_s->sme.sae_token = NULL;
+	str_clear_free(wpa_s->sme.sae_password);
+	wpa_s->sme.sae_password = NULL;
 	sae_clear_data(&wpa_s->sme.sae);
 #endif /* CONFIG_SAE */
 #ifdef CONFIG_IEEE80211R
diff --git a/wpa_supplicant/wpa_supplicant.c b/wpa_supplicant/wpa_supplicant.c
index d2241f336..716a4dc40 100644
--- a/wpa_supplicant/wpa_supplicant.c
+++ b/wpa_supplicant/wpa_supplicant.c
@@ -2330,7 +2330,8 @@ int wpa_supplicant_set_suites(struct wpa_supplicant *wpa_s,
 	sel = ie.key_mgmt & ssid->key_mgmt;
 #ifdef CONFIG_SAE
 	if ((!(wpa_s->drv_flags & WPA_DRIVER_FLAGS_SAE) &&
-	     !(wpa_s->drv_flags2 & WPA_DRIVER_FLAGS2_SAE_OFFLOAD_STA)) ||
+	     (!(wpa_s->drv_flags2 & WPA_DRIVER_FLAGS2_SAE_OFFLOAD_STA) ||
+	      ssid->decoyauth == SAE_DECOYAUTH_REQUIRED)) ||
 	    wpas_is_sae_avoided(wpa_s, ssid, &ie))
 		sel &= ~(WPA_KEY_MGMT_SAE | WPA_KEY_MGMT_SAE_EXT_KEY |
 			 WPA_KEY_MGMT_FT_SAE | WPA_KEY_MGMT_FT_SAE_EXT_KEY);
@@ -5578,7 +5579,9 @@ static void wpas_start_assoc_cb(struct wpa_radio_work *work, int deinit)
 	}
 
 	if ((wpa_s->drv_flags2 & WPA_DRIVER_FLAGS2_SAE_OFFLOAD_STA) &&
-	    wpa_key_mgmt_sae(params.key_mgmt_suite)) {
+	    wpa_key_mgmt_sae(params.key_mgmt_suite) &&
+	    (ssid->decoyauth == SAE_DECOYAUTH_DISABLED ||
+	     !(wpa_s->drv_flags & WPA_DRIVER_FLAGS_SAE))) {
 		params.auth_alg = WPA_AUTH_ALG_SAE;
 		if (ssid->sae_password) {
 			params.sae_password = ssid->sae_password;
diff --git a/wpa_supplicant/wpa_supplicant.conf b/wpa_supplicant/wpa_supplicant.conf
index 56533cb17..04ea9828f 100644
--- a/wpa_supplicant/wpa_supplicant.conf
+++ b/wpa_supplicant/wpa_supplicant.conf
@@ -508,6 +508,16 @@ fast_reauth=1
 # regardless of the sae_pwe parameter value.
 #sae_pwe=0
 
+# DecoyAuth mode for SAE authentication (network block parameter)
+# 0 = disabled (use standard SAE)
+# 1 = optional (default; negotiate when supported and accept standard SAE
+#     fallback)
+# 2 = required (require DecoyAuth and reject standard SAE fallback)
+# DecoyAuth requires H2E and supports MLO. Password identifiers, SAE-PK, mesh,
+# and SAE offload are not supported. Optional mode uses standard SAE fallback
+# when H2E or DecoyAuth is unavailable.
+#decoyauth=1
+
 # Default value for DTIM period (if not overridden in network block)
 #dtim_period=2
 
diff --git a/wpa_supplicant/wpa_supplicant_i.h b/wpa_supplicant/wpa_supplicant_i.h
index 57bd6f846..1f9596f28 100644
--- a/wpa_supplicant/wpa_supplicant_i.h
+++ b/wpa_supplicant/wpa_supplicant_i.h
@@ -1138,6 +1138,7 @@ struct wpa_supplicant {
 #ifdef CONFIG_SAE
 		struct sae_data sae;
 		struct wpabuf *sae_token;
+		char *sae_password;
 		int sae_group_index;
 		unsigned int sae_pmksa_caching:1;
 		u16 seq_num;
-- 
2.53.0




More information about the Hostap mailing list