[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