From: Jouni Malinen Date: Sat, 22 Feb 2025 17:01:16 +0000 (+0200) Subject: SAE: Multiple default password iteration X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ca9f86a19c44ad6eccadf200b27681131587bcb0;p=thirdparty%2Fhostap.git SAE: Multiple default password iteration SAE was designed to protect against offline dictionary attacks and that prevents clean implementation of the multiple-password design similarly to what has been done with WPA2-Personal. SAE has a concept of multiple passwords with an explicit password identifier to identify which password a STA is using. However, that has unfortunately not been deployed in STAs so far which makes it inconvenient to use multiple passwords in a WPA3-Personal network since configuring passwords separately for each STA MAC address is both inconvenient and unrealistic if MAC address randomization is used. Allow hostapd to be configured to iterate over a small number of default SAE passwords (i.e., passwords that do not use a password identifier and that are allowed for any STA MAC address). This allows more than a single SAE password to be used in a network. However, this comes with risk of STAs delaying connection attempts since they might consider this type of behavior to be an active attack (which it strictly speaking is). In any case, this seems to be the only realistic method for SAE deployment with multiple passwords today and it seems to work with up to five SAE passwords at least with STAs that use wpa_supplicant. When enabled, hostapd will try to use default SAE passwords one by one until success. Successful authentication locks the selected password into use. Failed attempt tracks the STA's MAC address with the password and causes other passwords to be attempted on next tries. This works relatively well as long as the STA is willing to attempt SAE multiple times when the AP is rejecting attempts with Status Code 1 (unspecified failure), i.e., a different Status Code compared to the normal password mismatch case with 15 (challenge fail). Another possibility would have been to silently discard failed Confirm messages to make this look more like a lost frame than any explicitly indicated error case. However, that would result in longer delay for retry attemots at least with wpa_supplicant implementation. This functionality can be enabled by setting the new configuration parameter sae_track_password to a nonzero value. It should be set based on how many active STAs are expected to be used the network. Larger values use a bit more memory (12 bytes for each additional tracked STA for each configuured default SAE password) and slightly increased processing steps. The actual default passwords are set with sae_password. For example, this would allow three SAE passwords to be used: sae_track_password=50 sae_password=owner-pw sae_password=iot-pw sae_password=guest-pw If desired, each sae_password entry can use the vlanid parameter to assign the STA to the specied VLAN based on which password it used. Signed-off-by: Jouni Malinen --- diff --git a/hostapd/config_file.c b/hostapd/config_file.c index 78a61fb43..937383c54 100644 --- a/hostapd/config_file.c +++ b/hostapd/config_file.c @@ -4641,6 +4641,8 @@ static int hostapd_config_fill(struct hostapd_config *conf, line); return 1; } + } else if (os_strcmp(buf, "sae_track_password") == 0) { + bss->sae_track_password = atoi(pos); #endif /* CONFIG_SAE */ } else if (os_strcmp(buf, "vendor_elements") == 0) { if (parse_wpabuf_hex(line, buf, &bss->vendor_elements, pos)) diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf index e37e0417b..95e9a94ec 100644 --- a/hostapd/hostapd.conf +++ b/hostapd/hostapd.conf @@ -2128,6 +2128,24 @@ own_ip_addr=127.0.0.1 # contains and entry in the same format as sae_password uses. #sae_password_file=/tc/hostapd.sae_passwords +# Tracking of SAE password use +# While SAE design does not allow the AP to determine the used password robustly +# if multiple password are configured without use of password identifiers, a +# small number of such passwords might be usable with minimal impact to STAs. +# This parameter can be used to enable such mechanism by tracking which password +# STAs have tried and either succeeded or failed to complete authentication +# with. Configured passwords are then tried one by one until success. This shows +# up as a potential attack to the STA, though, and as such, may result in the AP +# getting rejected after a couple of attempts. Only one password can be tested +# per attempt, so this limits this mechanism to only a small number (e.g., 2-3) +# passwords without showing significant usability issues with some STAs. This +# is meant as a workaround until SAE with password identifiers is deployed on +# STAs. +# This parameter sets the maximum number of STA MAC addresses to track per +# SAE password. This should be set sufficiently high to cover the expected +# number of active STAs. +#sae_track_password=0 + # SAE threshold for anti-clogging mechanism (dot11RSNASAEAntiCloggingThreshold) # This parameter defines how many open SAE instances can be in progress at the # same time before the anti-clogging mechanism is taken into use. diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c index 2bae39e67..40243f160 100644 --- a/src/ap/ap_config.c +++ b/src/ap/ap_config.c @@ -791,6 +791,8 @@ static void hostapd_config_free_sae_passwords(struct hostapd_bss_config *conf) #ifdef CONFIG_SAE_PK sae_deinit_pk(tmp->pk); #endif /* CONFIG_SAE_PK */ + os_free(tmp->success_mac); + os_free(tmp->fail_mac); os_free(tmp); } } diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h index 4ef2d8c70..bac874ebb 100644 --- a/src/ap/ap_config.h +++ b/src/ap/ap_config.h @@ -260,6 +260,10 @@ struct sae_password_entry { int vlan_id; struct sae_pt *pt; struct sae_pk *pk; + u8 *success_mac; + unsigned int num_success_mac, next_success_mac; + u8 *fail_mac; + unsigned int num_fail_mac, next_fail_mac; }; struct dpp_controller_conf { @@ -688,6 +692,7 @@ struct hostapd_bss_config { enum sae_pwe sae_pwe; int *sae_groups; struct sae_password_entry *sae_passwords; + int sae_track_password; char *wowlan_triggers; /* Wake-on-WLAN triggers */ diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c index 30f8285c3..6e1f11924 100644 --- a/src/ap/ieee802_11.c +++ b/src/ap/ieee802_11.c @@ -488,6 +488,147 @@ static void sae_set_state(struct sta_info *sta, enum sae_state state, } +static bool in_mac_addr_list(const u8 *list, unsigned int num, const u8 *addr) +{ + unsigned int i; + + for (i = 0; list && i < num; i++) { + if (ether_addr_equal(&list[i * ETH_ALEN], addr)) + return true; + } + + return false; +} + + +static struct sae_password_entry * +sae_password_find_pw(struct hostapd_data *hapd, struct sta_info *sta) +{ + struct sae_password_entry *pw = NULL; + + if (!sta->sae || !sta->sae->tmp || !sta->sae->tmp->used_pw) + return NULL; + + + for (pw = hapd->conf->sae_passwords; pw; pw = pw->next) { + if (pw == sta->sae->tmp->used_pw) + return pw; + } + + return NULL; +} + + +static bool is_other_sae_password(struct hostapd_data *hapd, + struct sta_info *sta, + struct sae_password_entry *used_pw) +{ + struct sae_password_entry *pw; + + for (pw = hapd->conf->sae_passwords; pw; pw = pw->next) { + if (pw == used_pw || + pw->identifier || + !is_broadcast_ether_addr(pw->peer_addr)) + continue; + + if (in_mac_addr_list(pw->success_mac, + pw->num_success_mac, + sta->addr)) + return true; + + if (!in_mac_addr_list(pw->fail_mac, pw->num_fail_mac, + sta->addr)) + return true; + } + + return false; +} + + +static bool has_sae_success_seen(struct hostapd_data *hapd, + struct sta_info *sta) +{ + struct sae_password_entry *pw; + + for (pw = hapd->conf->sae_passwords; pw; pw = pw->next) { + if (pw->identifier || + !is_broadcast_ether_addr(pw->peer_addr)) + continue; + + if (in_mac_addr_list(pw->success_mac, + pw->num_success_mac, + sta->addr)) + return true; + } + + return false; +} + + +static void sae_password_track_success(struct hostapd_data *hapd, + struct sta_info *sta) +{ + struct sae_password_entry *pw; + + if (!hapd->conf->sae_track_password) + return; + + pw = sae_password_find_pw(hapd, sta); + if (!pw) + return; + + if (in_mac_addr_list(pw->success_mac, + pw->num_success_mac, + sta->addr)) + return; + + if (!pw->success_mac) { + pw->success_mac = os_zalloc(hapd->conf->sae_track_password * + ETH_ALEN); + if (!pw->success_mac) + return; + pw->num_success_mac = hapd->conf->sae_track_password; + } + + os_memcpy(&pw->success_mac[pw->next_success_mac * ETH_ALEN], sta->addr, + ETH_ALEN); + pw->next_success_mac = (pw->next_success_mac + 1) % pw->num_success_mac; +} + + +static bool sae_password_track_fail(struct hostapd_data *hapd, + struct sta_info *sta) +{ + struct sae_password_entry *pw; + + if (!hapd->conf->sae_track_password) + return false; + + pw = sae_password_find_pw(hapd, sta); + if (!pw) + return false; + + if (in_mac_addr_list(pw->fail_mac, + pw->num_fail_mac, + sta->addr)) + return is_other_sae_password(hapd, sta, pw); + + if (!pw->fail_mac) { + pw->fail_mac = os_zalloc(hapd->conf->sae_track_password * + ETH_ALEN); + if (!pw->fail_mac) + return false; + pw->num_fail_mac = hapd->conf->sae_track_password; + } + + os_memcpy(&pw->fail_mac[pw->next_fail_mac * ETH_ALEN], sta->addr, + ETH_ALEN); + pw->next_fail_mac = (pw->next_fail_mac + 1) % pw->num_fail_mac; + + return is_other_sae_password(hapd, sta, pw); +} + + const char * sae_get_password(struct hostapd_data *hapd, struct sta_info *sta, const char *rx_id, @@ -501,6 +642,45 @@ const char * sae_get_password(struct hostapd_data *hapd, const struct sae_pk *pk = NULL; struct hostapd_sta_wpa_psk_short *psk = NULL; + /* With sae_track_password functionality enabled, try to first find the + * next viable wildcard-address password if a password identifier was + * not used. Select an wildcard-addr entry if the STA is known to have + * used it successfully before. If no such entry exists, pick a + * wildcard-addr entry that does not have a failed entry tracked for the + * STA. */ + if (!rx_id && sta && hapd->conf->sae_track_password) { + struct sae_password_entry *success = NULL, *no_fail = NULL; + + for (pw = hapd->conf->sae_passwords; pw; pw = pw->next) { + if (pw->identifier || + !is_broadcast_ether_addr(pw->peer_addr)) + continue; + if (in_mac_addr_list(pw->success_mac, + pw->num_success_mac, + sta->addr)) { + success = pw; + break; + } + + if (!no_fail && + !in_mac_addr_list(pw->fail_mac, pw->num_fail_mac, + sta->addr)) + no_fail = pw; + } + + pw = success ? success : no_fail; + if (pw) { + password = pw->password; + pt = pw->pt; + if (!(hapd->conf->mesh & MESH_ENABLED)) + pk = pw->pk; + goto found; + } + } + + /* If sae_track_password functionality is not enabled or no suitable + * password entry was found with it, pick the first entry that matches + * the STA MAC address and password identifier (if used). */ for (pw = hapd->conf->sae_passwords; pw; pw = pw->next) { if (!is_broadcast_ether_addr(pw->peer_addr) && (!sta || @@ -531,6 +711,7 @@ const char * sae_get_password(struct hostapd_data *hapd, } } +found: if (pw_entry) *pw_entry = pw; if (s_pt) @@ -597,6 +778,9 @@ static struct wpabuf * auth_build_sae_commit(struct hostapd_data *hapd, return NULL; } + if (pw && sta->sae->tmp) + sta->sae->tmp->used_pw = pw; + if (pw && pw->vlan_id) { if (!sta->sae->tmp) { wpa_printf(MSG_INFO, @@ -945,6 +1129,7 @@ static int sae_sm_step(struct hostapd_data *hapd, struct sta_info *sta, case SAE_NOTHING: if (auth_transaction == 1) { struct sae_temporary_data *tmp = sta->sae->tmp; + bool immediate_confirm; if (tmp) { sta->sae->h2e = @@ -986,8 +1171,22 @@ static int sae_sm_step(struct hostapd_data *hapd, struct sta_info *sta, * overridden with explicit configuration so that the * infrastructure BSS case sends both frames together. */ - if ((hapd->conf->mesh & MESH_ENABLED) || - hapd->conf->sae_confirm_immediate) { + immediate_confirm = (hapd->conf->mesh & MESH_ENABLED) || + hapd->conf->sae_confirm_immediate; + + /* If sae_track_password is enabled and the STA has not + * yet been tracked to having successfully completed + * SAE authentication with the password that the AP + * tries to use, do not send Confirm immediately to + * avoid an explicit indication on the STA side on + * password mismatch. */ + if (immediate_confirm && + hapd->conf->sae_track_password && + (!sta->sae->tmp || !sta->sae->tmp->parsed_pw_id) && + !has_sae_success_seen(hapd, sta)) + immediate_confirm = false; + + if (immediate_confirm) { /* * Send both Commit and Confirm immediately * based on SAE finite state machine @@ -1435,7 +1634,9 @@ static void handle_auth_sae(struct hostapd_data *hapd, struct sta_info *sta, * previously set parameters. */ pos = mgmt->u.auth.variable; end = ((const u8 *) mgmt) + len; - if (end - pos >= (int) sizeof(le16) && + if ((!sta->sae->tmp || + !sta->sae->tmp->try_other_password) && + end - pos >= (int) sizeof(le16) && sae_group_allowed(sta->sae, groups, WPA_GET_LE16(pos)) == WLAN_STATUS_SUCCESS) { @@ -1561,9 +1762,21 @@ static void handle_auth_sae(struct hostapd_data *hapd, struct sta_info *sta, if (sae_check_confirm(sta->sae, var, var_len, NULL) < 0) { + if (sae_password_track_fail(hapd, sta)) { + wpa_printf(MSG_DEBUG, + "SAE: Reject mismatching Confirm so that another password can be attempted by " + MACSTR, + MAC2STR(sta->addr)); + if (sta->sae->tmp) + sta->sae->tmp-> + try_other_password = 1; + resp = WLAN_STATUS_UNSPECIFIED_FAILURE; + goto reply; + } resp = WLAN_STATUS_CHALLENGE_FAIL; goto reply; } + sae_password_track_success(hapd, sta); sta->sae->rc = peer_send_confirm; } resp = sae_sm_step(hapd, sta, auth_transaction, diff --git a/src/common/sae.h b/src/common/sae.h index 8f74353be..0d94e1f21 100644 --- a/src/common/sae.h +++ b/src/common/sae.h @@ -65,6 +65,7 @@ struct sae_temporary_data { struct wpabuf *own_rejected_groups; struct wpabuf *peer_rejected_groups; unsigned int own_addr_higher:1; + unsigned int try_other_password:1; #ifdef CONFIG_SAE_PK u8 kek[SAE_MAX_HASH_LEN]; @@ -85,6 +86,8 @@ struct sae_temporary_data { #endif /* CONFIG_SAE_PK */ struct os_reltime disabled_until; + + const void *used_pw; }; struct sae_pt {