]> git.ipfire.org Git - thirdparty/hostap.git/commitdiff
SAE: Multiple default password iteration
authorJouni Malinen <j@w1.fi>
Sat, 22 Feb 2025 17:01:16 +0000 (19:01 +0200)
committerJouni Malinen <j@w1.fi>
Sat, 22 Feb 2025 17:01:16 +0000 (19:01 +0200)
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 <j@w1.fi>
hostapd/config_file.c
hostapd/hostapd.conf
src/ap/ap_config.c
src/ap/ap_config.h
src/ap/ieee802_11.c
src/common/sae.h

index 78a61fb4393cda9d0940fe8d10c26d98184c1b68..937383c5425b9bdd0801b7ef7b4d22655cebedab 100644 (file)
@@ -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))
index e37e0417b4dcd2e343e126d6ce6e2cda37df1223..95e9a94ecbc41afa7247019d0b66e4dfdc5ba665 100644 (file)
@@ -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.
index 2bae39e67cdab20904f9013bac397a9c942e639c..40243f160318f2bd231f9ea369a388122ee1d697 100644 (file)
@@ -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);
        }
 }
index 4ef2d8c7017b27ba37d128059040e52ceadd9e81..bac874ebbe4afdfd9fb6e9f6ad2f35f9832e0597 100644 (file)
@@ -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 */
 
index 30f8285c388550d9cbb23f57c26abdbe92339924..6e1f119240ae3d099c74946d8a43fe753aabf374 100644 (file)
@@ -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,
index 8f74353bebf76ba3985005c3f20cb1a33dc81762..0d94e1f211336d667836aa6924e83f02100a3a71 100644 (file)
@@ -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 {