]> git.ipfire.org Git - thirdparty/hostap.git/commitdiff
SAE: Password identifier changing (AP)
authorJouni Malinen <jouni.malinen@oss.qualcomm.com>
Mon, 27 Oct 2025 11:44:27 +0000 (13:44 +0200)
committerJouni Malinen <j@w1.fi>
Mon, 27 Oct 2025 19:20:51 +0000 (21:20 +0200)
Add support for changing the SAE password identifier value that is sent
in SAE commit messages for privacy protection in cases where random MAC
addresses are used with per-STA (or per-user) SAE password identifiers.
This functionality can be enabled by setting the new hostapd
configuration parameter sae_pw_id_num and sae_pw_id_key.

The implemented functionality is for the definition that were added in
IEEE P802.11bi/D2.1.

Signed-off-by: Jouni Malinen <jouni.malinen@oss.qualcomm.com>
15 files changed:
hostapd/Android.mk
hostapd/Makefile
hostapd/config_file.c
hostapd/hostapd.conf
src/ap/ap_config.c
src/ap/ap_config.h
src/ap/ieee802_11.c
src/ap/sta_info.c
src/ap/sta_info.h
src/ap/wpa_auth.c
src/ap/wpa_auth.h
src/ap/wpa_auth_glue.c
src/ap/wpa_auth_i.h
src/common/sae.c
src/common/sae.h

index d097971f2dd7f6a74ac6710aac30428012dfce8c..4b1daa30b0c112ef4febe18ef88f57e0f3e6319b 100644 (file)
@@ -259,10 +259,10 @@ L_CFLAGS += -DCONFIG_SAE
 OBJS += src/common/sae.c
 ifdef CONFIG_SAE_PK
 L_CFLAGS += -DCONFIG_SAE_PK
-NEED_AES_SIV=y
 NEED_BASE64=y
 OBJS += src/common/sae_pk.c
 endif
+NEED_AES_SIV=y
 NEED_ECC=y
 NEED_DH_GROUPS=y
 NEED_HMAC_SHA256_KDF=y
index 93a11dd86326e8c3fa722266eca8a052d8dce0a0..afd2e1cf5cdf9e84e98c12671c26efb914bf81aa 100644 (file)
@@ -298,10 +298,10 @@ CFLAGS += -DCONFIG_SAE
 OBJS += ../src/common/sae.o
 ifdef CONFIG_SAE_PK
 CFLAGS += -DCONFIG_SAE_PK
-NEED_AES_SIV=y
 NEED_BASE64=y
 OBJS += ../src/common/sae_pk.o
 endif
+NEED_AES_SIV=y
 NEED_ECC=y
 NEED_DH_GROUPS=y
 NEED_HMAC_SHA256_KDF=y
index 54ad77bcbf46acc32ddf5dd34152ce4eda7cedf4..25e1a0f0daa7865e1fc8b378613638fd567c9d20 100644 (file)
@@ -4398,6 +4398,11 @@ static int hostapd_config_fill(struct hostapd_config *conf,
                bss->sae_confirm_immediate = atoi(pos);
        } else if (os_strcmp(buf, "sae_pwe") == 0) {
                bss->sae_pwe = atoi(pos);
+       } else if (os_strcmp(buf, "sae_pw_id_num") == 0) {
+               bss->sae_pw_id_num = atoi(pos);
+       } else if (os_strcmp(buf, "sae_pw_id_key") == 0) {
+               if (parse_wpabuf_hex(line, buf, &bss->sae_pw_id_key, pos))
+                       return 1;
        } else if (os_strcmp(buf, "local_pwr_constraint") == 0) {
                int val = atoi(pos);
                if (val < 0 || val > 255) {
index 2302e2abe2d8a887ee5a5788a37908f741e72eef..c76801965a4c33021b4230e72318d1d4357267a8 100644 (file)
@@ -2206,6 +2206,19 @@ own_ip_addr=127.0.0.1
 # regardless of the sae_pwe parameter value.
 #sae_pwe=0
 
+# Changing SAE password identifiers
+# AP can send a set of unique SAE password identifiers to allow the long term
+# password identifier to be changed for over the air exchanges to avoid tracking
+# of devices using password identifiers.
+#
+# Key for generating and interpreting encrypted password identifiers. This must
+# be same for each AP in the network. 32 octets as a hexdump
+#sae_pw_id_key=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
+#
+# How many over the air password identifier values to provide to a STA each time
+# SAE is used.
+#sae_pw_id_num=2
+
 # FILS Cache Identifier (16-bit value in hexdump format)
 #fils_cache_id=0011
 
index bc20aa0b4347a2ed430c7ab05e9b54c06d72b1fa..fbe646272db2685c51b13297307149baaebd23e2 100644 (file)
@@ -1008,6 +1008,8 @@ void hostapd_config_free_bss(struct hostapd_bss_config *conf)
        os_free(conf->pasn_groups);
 #endif /* CONFIG_PASN */
 
+       wpabuf_clear_free(conf->sae_pw_id_key);
+
        os_free(conf);
 }
 
index 44988d26135234c378b6cf8b810ecb0c4d27a7cc..8a7c9393f67531d225788e44fa61ca0a6273791d 100644 (file)
@@ -662,6 +662,8 @@ struct hostapd_bss_config {
        struct sae_password_entry *sae_passwords;
        int sae_password_psk;
        int sae_track_password;
+       struct wpabuf *sae_pw_id_key;
+       unsigned int sae_pw_id_num;
 
        char *wowlan_triggers; /* Wake-on-WLAN triggers */
 
index ccb66db6c70c0e881dcdea2848726bfc7703fd8b..dc3b397c5f3712813c6612ebf5a1e0e1ee32b796 100644 (file)
@@ -17,6 +17,8 @@
 #include "crypto/sha384.h"
 #include "crypto/sha512.h"
 #include "crypto/random.h"
+#include "crypto/aes.h"
+#include "crypto/aes_siv.h"
 #include "common/ieee802_11_defs.h"
 #include "common/ieee802_11_common.h"
 #include "common/wpa_ctrl.h"
@@ -735,6 +737,73 @@ const char * sae_get_password(struct hostapd_data *hapd,
                }
        }
 
+       /* Try to decrypt the received password identifier if no plaintext
+        * identifier match was found. */
+       if (!password && rx_id && rx_id_len > 4 + 4 + AES_BLOCK_SIZE &&
+           hapd->conf->sae_pw_id_key) {
+               u8 *plain, *pos, *counter;
+               size_t plain_len;
+               const u8 *id;
+               size_t id_len;
+
+               plain = os_malloc(rx_id_len);
+               if (!plain)
+                       goto fail;
+               if (aes_siv_decrypt(
+                           wpabuf_head(hapd->conf->sae_pw_id_key),
+                           wpabuf_len(hapd->conf->sae_pw_id_key),
+                           rx_id, rx_id_len, 0, NULL, NULL, plain) < 0)
+                       goto fail;
+               plain_len = rx_id_len - AES_BLOCK_SIZE;
+               wpa_hexdump_ascii(MSG_DEBUG,
+                                 "SAE: Decrypted password identifier info",
+                                 plain, plain_len);
+               /* 4 octet date | Password ID | <padding> | 4 octet counter */
+               counter = plain + plain_len - 4;
+               wpa_printf(MSG_DEBUG, "SAE: Generation time %u counter %u",
+                          WPA_GET_BE32(plain), WPA_GET_BE32(counter));
+               id = pos = plain + 4;
+               while (pos < counter) {
+                       if (*pos == 0x00)
+                               break;
+                       pos++;
+               }
+               id_len = pos - id;
+               wpa_hexdump_ascii(MSG_DEBUG,
+                                 "SAE: Decrypted password identifier",
+                                 id, id_len);
+               for (pw = hapd->conf->sae_passwords; pw; pw = pw->next) {
+                       if (!is_broadcast_ether_addr(pw->peer_addr) &&
+                           (!sta ||
+                            !ether_addr_equal(pw->peer_addr, sta->addr)))
+                               continue;
+                       if (!pw->identifier ||
+                           os_strlen(pw->identifier) != id_len ||
+                           os_memcmp(id, pw->identifier, id_len) != 0)
+                               continue;
+                       password = pw->password;
+                       if (!(hapd->conf->mesh & MESH_ENABLED))
+                               pk = pw->pk;
+                       if (sta && sta->sae && sta->sae->tmp) {
+                               os_free(sta->sae->tmp->dec_pw_id);
+                               sta->sae->tmp->dec_pw_id =
+                                       os_zalloc(id_len + 1);
+                               if (sta->sae->tmp->dec_pw_id) {
+                                       os_memcpy(sta->sae->tmp->dec_pw_id,
+                                                 id, id_len);
+                                       sta->sae->tmp->dec_pw_id_len = id_len;
+                                       sta->sae->tmp->pw_id_counter =
+                                               WPA_GET_BE32(counter);
+                                       wpa_printf(MSG_DEBUG,
+                                                  "SAE: Bound decrypted password identifier to STA");
+                               }
+                       }
+                       break;
+               }
+       fail:
+               os_free(plain);
+       }
+
 found:
        if (pw_entry)
                *pw_entry = pw;
@@ -788,15 +857,48 @@ static struct wpabuf * auth_build_sae_commit(struct hostapd_data *hapd,
                use_pt = 1;
 
        password = sae_get_password(hapd, sta, rx_id, rx_id_len, &pw, &pt, &pk);
-       if (!password || (use_pt && !pt)) {
+       if (!password) {
                wpa_printf(MSG_DEBUG, "SAE: No password available");
                return NULL;
        }
 
-       if (update && use_pt &&
-           sae_prepare_commit_pt(sta->sae, pt, own_addr, sta->addr,
-                                 NULL, pk) < 0)
-               return NULL;
+       if (use_pt) {
+               struct sae_pt *tmp_pt = NULL;
+               bool failed = false;
+
+               if (!pt && pw) {
+                       int groups[2] = { sta->sae->group, 0 };
+
+                       wpa_printf(MSG_DEBUG,
+                                  "SAE: Derive PT for encrypted PW ID");
+                       tmp_pt = sae_derive_pt(groups, hapd->conf->ssid.ssid,
+                                              hapd->conf->ssid.ssid_len,
+                                              (const u8 *) pw->password,
+                                              os_strlen(pw->password),
+                                              rx_id, rx_id_len);
+                       if (!tmp_pt) {
+                               wpa_printf(MSG_DEBUG,
+                                          "SAE: Could not derive PT");
+                               return NULL;
+                       }
+                       pt = tmp_pt;
+                       update = 1;
+               }
+
+               if (!pt) {
+                       wpa_printf(MSG_DEBUG, "SAE: No PT available");
+                       return NULL;
+               }
+
+               if (update &&
+                   sae_prepare_commit_pt(sta->sae, pt, own_addr, sta->addr,
+                                         NULL, pk) < 0)
+                       failed = true;
+
+               sae_deinit_pt(tmp_pt);
+               if (failed)
+                       return NULL;
+       }
 
        if (update && !use_pt &&
            sae_prepare_commit(own_addr, sta->addr,
@@ -1141,6 +1243,20 @@ void sae_accept_sta(struct hostapd_data *hapd, struct sta_info *sta)
                               sta->sae->pmkid, sta->sae->akmp,
                               ap_sta_is_mld(hapd, sta), sta->vlan_id);
        sae_sme_send_external_auth_status(hapd, sta, WLAN_STATUS_SUCCESS);
+       if (sta->sae->tmp) {
+               struct sae_temporary_data *tmp = sta->sae->tmp;
+
+               wpabuf_free(sta->sae_pw_id);
+               sta->sae_pw_id = NULL;
+               if (tmp->dec_pw_id) {
+                       sta->sae_pw_id = wpabuf_alloc_copy(
+                               tmp->dec_pw_id, tmp->dec_pw_id_len);
+                       sta->sae_pw_id_counter = tmp->pw_id_counter;
+               } else if (tmp->pw_id) {
+                       sta->sae_pw_id = wpabuf_alloc_copy(
+                               tmp->pw_id, tmp->pw_id_len);
+               }
+       }
 }
 
 
@@ -4498,6 +4614,9 @@ static int __check_assoc_ies(struct hostapd_data *hapd, struct sta_info *sta,
 #endif /* CONFIG_IEEE80211BE */
 
                wpa_auth_set_auth_alg(sta->wpa_sm, sta->auth_alg);
+               if (sta->auth_alg == WLAN_AUTH_SAE)
+                       wpa_auth_set_sae_pw_id(sta->wpa_sm, sta->sae_pw_id,
+                                              sta->sae_pw_id_counter);
                wpa_auth_set_rsn_selection(sta->wpa_sm, elems->rsn_selection,
                                           elems->rsn_selection_len);
                res = wpa_validate_wpa_ie(hapd->wpa_auth, sta->wpa_sm,
index bd5eed3dd4f3972b8f8b640a32a4b5563378d294..b23a856b6b6f077c04d18c9d1308b33a4c67276e 100644 (file)
@@ -514,6 +514,8 @@ void ap_free_sta(struct hostapd_data *hapd, struct sta_info *sta)
        forced_memzero(sta->last_tk, WPA_TK_MAX_LEN);
 #endif /* CONFIG_TESTING_OPTIONS */
 
+       wpabuf_free(sta->sae_pw_id);
+
        os_free(sta);
 }
 
index bc7daa6a85d445b3434ec93948bc38ff989a647e..4f41f7028dbdfcf33f118b528d597d038f0601d6 100644 (file)
@@ -323,6 +323,9 @@ struct sta_info {
                              * units of 1000 TUs */
 
        u64 last_known_sta_id_timestamp;
+
+       struct wpabuf *sae_pw_id;
+       unsigned int sae_pw_id_counter;
 };
 
 
index 7b324670d83ba1f7058915ecae9064f23129d1de..d64ca62b3f34289d898518a915975e84315db9e4 100644 (file)
@@ -1142,6 +1142,7 @@ static void wpa_free_sta_sm(struct wpa_state_machine *sm)
 #ifdef CONFIG_DPP2
        wpabuf_clear_free(sm->dpp_z);
 #endif /* CONFIG_DPP2 */
+       wpabuf_free(sm->sae_pw_id);
        bin_clear_free(sm, sizeof(*sm));
 }
 
@@ -4757,6 +4758,99 @@ static u8 * wpa_auth_ml_kdes(struct wpa_state_machine *sm, u8 *pos)
 }
 
 
+#ifdef CONFIG_SAE
+static u8 * add_sae_pw_ids(struct wpa_state_machine *sm, u8 *pos, u8 *end)
+{
+       static const size_t max_padding = 8;
+       u8 *start = pos, *len;
+       unsigned int i;
+       struct wpa_auth_config *conf = &sm->wpa_auth->conf;
+       u8 *data, *dpos;
+       unsigned int counter;
+       size_t pw_id_len = wpabuf_len(sm->sae_pw_id);
+       struct os_time t;
+       size_t kde_len;
+
+       wpa_printf(MSG_DEBUG, "RSN: Add SAE Password Identifiers KDE (num=%u)",
+                  conf->sae_pw_id_num);
+       wpa_hexdump_buf(MSG_DEBUG, "RSN: Real SAE Password Identifier",
+                       sm->sae_pw_id);
+       data = os_malloc(pw_id_len + max_padding + 4 + 4);
+       if (!data)
+               return NULL;
+
+       if (end - pos < 2 + RSN_SELECTOR_LEN + 1) {
+               pos = NULL;
+               goto fail;
+       }
+       *pos++ = WLAN_EID_VENDOR_SPECIFIC;
+       len = pos++; /* Length to be filled */
+       RSN_SELECTOR_PUT(pos, RSN_KEY_DATA_SAE_PW_IDS);
+       pos += RSN_SELECTOR_LEN;
+
+       *pos++ = 0; /* Flags */
+
+       counter = sm->sae_pw_id_counter + conf->sae_pw_id_num;
+
+       os_get_time(&t);
+
+       WPA_PUT_BE32(data, t.sec);
+       os_memcpy(data + 4, wpabuf_head(sm->sae_pw_id), pw_id_len);
+
+       for (i = 0; i < conf->sae_pw_id_num; i++) {
+               size_t pad_len, dlen, elen;
+
+               pad_len = 1 + os_random() % max_padding;
+               dpos = data + 4 + pw_id_len;
+               os_memset(dpos, 0, pad_len);
+               dpos += pad_len;
+               WPA_PUT_BE32(dpos, counter);
+               dpos += 4;
+               dlen = dpos - data;
+               elen = dlen + AES_BLOCK_SIZE;
+               counter++;
+
+               kde_len = (pos - len - 1) + (1 + elen);
+               if ((size_t) (end - pos) < 1 + elen || kde_len > 255) {
+                       wpa_printf(MSG_INFO,
+                                  "RSN: Not enough room in the buffer for a new SAE Password Identifier - send only %u",
+                                  i);
+                       break;
+               }
+
+               *pos++ = elen;
+               if (aes_siv_encrypt(conf->sae_pw_id_key,
+                                   sizeof(conf->sae_pw_id_key),
+                                   data, dlen, 0, NULL, NULL, pos) < 0) {
+                       wpa_printf(MSG_INFO,
+                                  "RSN: Failed to encrypt SAE Password Identifier");
+                       pos = NULL;
+                       goto fail;
+               }
+               pos += elen;
+       }
+
+       kde_len = pos - len - 1;
+       if (kde_len > 255) {
+               wpa_printf(MSG_INFO,
+                          "RSN: SAE Password Identifiers do not fit in a KDE");
+               wpa_hexdump_key(MSG_DEBUG, "RSN: KDE", start, pos - start);
+               pos = NULL;
+               goto fail;
+       }
+
+       *len = kde_len;
+
+       wpa_hexdump_key(MSG_DEBUG, "RSN: SAE Password Identifiers KDE",
+                       start, pos - start);
+
+fail:
+       os_free(data);
+       return pos;
+}
+#endif /* CONFIG_SAE */
+
+
 SM_STATE(WPA_PTK, PTKINITNEGOTIATING)
 {
        u8 rsc[WPA_KEY_RSC_LEN], *_rsc, *gtk, *kde = NULL, *pos, stub_gtk[32];
@@ -4772,6 +4866,9 @@ SM_STATE(WPA_PTK, PTKINITNEGOTIATING)
 #else /* CONFIG_IEEE80211BE */
        bool is_mld = false;
 #endif /* CONFIG_IEEE80211BE */
+#ifdef CONFIG_SAE
+       bool sae_pw_ids = false;
+#endif /* CONFIG_SAE */
 
        SM_ENTRY_MA(WPA_PTK, PTKINITNEGOTIATING, wpa_ptk);
        sm->TimeoutEvt = false;
@@ -4972,6 +5069,17 @@ SM_STATE(WPA_PTK, PTKINITNEGOTIATING)
        if (sm->ssid_protection)
                kde_len += 2 + conf->ssid_len;
 
+#ifdef CONFIG_SAE
+       if (wpa_key_mgmt_sae(sm->wpa_key_mgmt) &&
+           conf->sae_pw_id_num &&
+           sm->sae_pw_id &&
+           ieee802_11_rsnx_capab(sm->rsnxe,
+                                 WLAN_RSNX_CAPAB_SAE_PW_ID_CHANGE)) {
+               kde_len += 2 + 255;
+               sae_pw_ids = true;
+       }
+#endif /* CONFIG_SAE */
+
 #ifdef CONFIG_TESTING_OPTIONS
        if (conf->eapol_m3_elements)
                kde_len += wpabuf_len(conf->eapol_m3_elements);
@@ -5102,6 +5210,23 @@ SM_STATE(WPA_PTK, PTKINITNEGOTIATING)
                pos += conf->ssid_len;
        }
 
+#ifdef CONFIG_SAE
+       if (sae_pw_ids) {
+               u8 *npos;
+
+               npos = add_sae_pw_ids(sm, pos, kde + kde_len);
+               if (!npos) {
+                       wpa_printf(MSG_DEBUG,
+                                  "RSN: Failed to add SAE Password Identifiers KDE");
+                       /* Ignore this since it is not a fatal error for the
+                        * Authenticator and the STA can decide whether to
+                        * proceed without getting new identifiers. */
+               } else {
+                       pos = npos;
+               }
+       }
+#endif /* CONFIG_SAE */
+
 #ifdef CONFIG_TESTING_OPTIONS
        if (conf->eapol_m3_elements) {
                os_memcpy(pos, wpabuf_head(conf->eapol_m3_elements),
@@ -7733,3 +7858,15 @@ struct wpa_group * wpa_select_vlan_wpa_group(struct wpa_group *gsm, int vlan_id)
        return vlan_gsm;
 }
 #endif /* CONFIG_IEEE80211BE */
+
+
+void wpa_auth_set_sae_pw_id(struct wpa_state_machine *sm,
+                           const struct wpabuf *pw_id,
+                           unsigned int counter)
+{
+       if (sm) {
+               wpabuf_free(sm->sae_pw_id);
+               sm->sae_pw_id = wpabuf_dup(pw_id);
+               sm->sae_pw_id_counter = counter;
+       }
+}
index 47a24895736a25e2f3d09ecb312cc4ca3d573892..f813357a8e195f6b224cac674839c1bac66de7b5 100644 (file)
@@ -323,6 +323,9 @@ struct wpa_auth_config {
        int rsn_override_omit_rsnxe;
 
        bool spp_amsdu;
+
+       unsigned int sae_pw_id_num;
+       u8 sae_pw_id_key[32];
 };
 
 typedef enum {
@@ -714,5 +717,8 @@ bool wpa_auth_sm_known_sta_identification(struct wpa_state_machine *sm,
                                          const u8 *mic, size_t mic_len);
 struct wpa_group * wpa_select_vlan_wpa_group(struct wpa_group *gsm,
                                             int vlan_id);
+void wpa_auth_set_sae_pw_id(struct wpa_state_machine *sm,
+                           const struct wpabuf *pw_id,
+                           unsigned int counter);
 
 #endif /* WPA_AUTH_H */
index 76f5a8cf1349fe09c4e36b46ccad06553680cbfd..8418bf8f044c35cdf11b0c2a1acf8525a02c66fc 100644 (file)
@@ -341,6 +341,14 @@ static void hostapd_wpa_auth_conf(struct hostapd_iface *iface,
        wconf->rsn_override_omit_rsnxe = conf->rsn_override_omit_rsnxe;
        wconf->spp_amsdu = conf->spp_amsdu &&
                (iface->drv_flags2 & WPA_DRIVER_FLAGS2_SPP_AMSDU);
+
+       if (conf->sae_pw_id_num && conf->sae_pw_id_key &&
+           wpabuf_len(conf->sae_pw_id_key) == sizeof(wconf->sae_pw_id_key)) {
+               wconf->sae_pw_id_num = conf->sae_pw_id_num;
+               os_memcpy(wconf->sae_pw_id_key,
+                         wpabuf_head(conf->sae_pw_id_key),
+                         wpabuf_len(conf->sae_pw_id_key));
+       }
 }
 
 
index 276582821a0e6c9772596f015e58cc06324ec9f9..e97db8c7f5ba923a6bf414afdd7d7bc569e0c259 100644 (file)
@@ -192,6 +192,9 @@ struct wpa_state_machine {
 #endif /* CONFIG_IEEE80211BE */
 
        bool ssid_protection;
+
+       struct wpabuf *sae_pw_id;
+       unsigned int sae_pw_id_counter;
 };
 
 
index 14af5ffd193a713a0ecc1634a384132059743994..2e519676b1252ec606306d91cf12a76b25024ee6 100644 (file)
@@ -115,6 +115,7 @@ void sae_clear_temp_data(struct sae_data *sae)
        wpabuf_free(tmp->peer_rejected_groups);
        os_free(tmp->pw_id);
        os_free(tmp->parsed_pw_id);
+       os_free(tmp->dec_pw_id);
        bin_clear_free(tmp, sizeof(*tmp));
        sae->tmp = NULL;
 }
index e9bd0f748cb913504129fa5b653c34645fe75ba3..96d7f115f8d8fef14bdeeddb9d8b0e0ff4f5684c 100644 (file)
@@ -62,6 +62,9 @@ struct sae_temporary_data {
        size_t pw_id_len;
        u8 *parsed_pw_id;
        size_t parsed_pw_id_len;
+       char *dec_pw_id;
+       size_t dec_pw_id_len;
+       unsigned int pw_id_counter;
        int vlan_id;
        u8 bssid[ETH_ALEN];
        struct wpabuf *own_rejected_groups;