From: Jouni Malinen Date: Mon, 27 Oct 2025 11:44:27 +0000 (+0200) Subject: SAE: Password identifier changing (AP) X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=f373a067defcbea5fa0935ae4dcaf19213d18aab;p=thirdparty%2Fhostap.git SAE: Password identifier changing (AP) 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 --- diff --git a/hostapd/Android.mk b/hostapd/Android.mk index d097971f2..4b1daa30b 100644 --- a/hostapd/Android.mk +++ b/hostapd/Android.mk @@ -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 diff --git a/hostapd/Makefile b/hostapd/Makefile index 93a11dd86..afd2e1cf5 100644 --- a/hostapd/Makefile +++ b/hostapd/Makefile @@ -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 diff --git a/hostapd/config_file.c b/hostapd/config_file.c index 54ad77bcb..25e1a0f0d 100644 --- a/hostapd/config_file.c +++ b/hostapd/config_file.c @@ -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) { diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf index 2302e2abe..c76801965 100644 --- a/hostapd/hostapd.conf +++ b/hostapd/hostapd.conf @@ -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 diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c index bc20aa0b4..fbe646272 100644 --- a/src/ap/ap_config.c +++ b/src/ap/ap_config.c @@ -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); } diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h index 44988d261..8a7c9393f 100644 --- a/src/ap/ap_config.h +++ b/src/ap/ap_config.h @@ -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 */ diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c index ccb66db6c..dc3b397c5 100644 --- a/src/ap/ieee802_11.c +++ b/src/ap/ieee802_11.c @@ -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 | | 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, diff --git a/src/ap/sta_info.c b/src/ap/sta_info.c index bd5eed3dd..b23a856b6 100644 --- a/src/ap/sta_info.c +++ b/src/ap/sta_info.c @@ -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); } diff --git a/src/ap/sta_info.h b/src/ap/sta_info.h index bc7daa6a8..4f41f7028 100644 --- a/src/ap/sta_info.h +++ b/src/ap/sta_info.h @@ -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; }; diff --git a/src/ap/wpa_auth.c b/src/ap/wpa_auth.c index 7b324670d..d64ca62b3 100644 --- a/src/ap/wpa_auth.c +++ b/src/ap/wpa_auth.c @@ -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; + } +} diff --git a/src/ap/wpa_auth.h b/src/ap/wpa_auth.h index 47a248957..f813357a8 100644 --- a/src/ap/wpa_auth.h +++ b/src/ap/wpa_auth.h @@ -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 */ diff --git a/src/ap/wpa_auth_glue.c b/src/ap/wpa_auth_glue.c index 76f5a8cf1..8418bf8f0 100644 --- a/src/ap/wpa_auth_glue.c +++ b/src/ap/wpa_auth_glue.c @@ -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)); + } } diff --git a/src/ap/wpa_auth_i.h b/src/ap/wpa_auth_i.h index 276582821..e97db8c7f 100644 --- a/src/ap/wpa_auth_i.h +++ b/src/ap/wpa_auth_i.h @@ -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; }; diff --git a/src/common/sae.c b/src/common/sae.c index 14af5ffd1..2e519676b 100644 --- a/src/common/sae.c +++ b/src/common/sae.c @@ -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; } diff --git a/src/common/sae.h b/src/common/sae.h index e9bd0f748..96d7f115f 100644 --- a/src/common/sae.h +++ b/src/common/sae.h @@ -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;