[PATCH v3 3/3] tests: FT RSNXE Used without RSNXE in Beacon or request

Koral Ilgun koralilgun at gmail.com
Fri Oct 30 07:58:19 PDT 2026


From: Koral Ilgun <koral.ilgun at adtran.com>

Cover an FT-PSK roam of a STA without RSN Overriding in WPA3-Personal
Compatibility Mode, with and without rsn_override_omit_rsnxe, and RSNXE
Used toward a STA that sends no RSNXE, with and without
ft_rsnxe_used_only_with_rsnxe.

Signed-off-by: Koral Ilgun <koral.ilgun at adtran.com>
---
 tests/hwsim/test_ap_ft.py | 80 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 79 insertions(+), 1 deletion(-)

diff --git a/tests/hwsim/test_ap_ft.py b/tests/hwsim/test_ap_ft.py
index 29bec3de9..9bdf45a43 100644
--- a/tests/hwsim/test_ap_ft.py
+++ b/tests/hwsim/test_ap_ft.py
@@ -1140,7 +1140,7 @@ def start_ft_sae(dev, apdev, wpa_ptk_rekey=None, sae_pwe=None,
                  no_beacon_rsnxe2=False, ext_key_id=False,
                  skip_prune_assoc=False, ft_rsnxe_used=False,
                  sae_transition=False, ext_key=False, sae_groups=None,
-                 wpa_gtk_rekey=None):
+                 wpa_gtk_rekey=None, ft_rsnxe_used_only_with_rsnxe=False):
     check_sae_capab(dev)
     ssid = "test-ft"
     passphrase = "12345678"
@@ -1167,6 +1167,8 @@ def start_ft_sae(dev, apdev, wpa_ptk_rekey=None, sae_pwe=None,
         params['ft_rsnxe_used'] = '1'
     if sae_groups:
         params['sae_groups'] = sae_groups
+    if ft_rsnxe_used_only_with_rsnxe:
+        params['ft_rsnxe_used_only_with_rsnxe'] = '1'
     hapd0 = hostapd.add_ap(apdev[0], params)
     params = ft_params2(ssid=ssid, passphrase=passphrase)
     if not sae_transition:
@@ -1191,6 +1193,8 @@ def start_ft_sae(dev, apdev, wpa_ptk_rekey=None, sae_pwe=None,
         params['ft_rsnxe_used'] = '1'
     if sae_groups:
         params['sae_groups'] = sae_groups
+    if ft_rsnxe_used_only_with_rsnxe:
+        params['ft_rsnxe_used_only_with_rsnxe'] = '1'
     hapd1 = hostapd.add_ap(apdev[1], params)
     key_mgmt = hapd1.get_config()['key_mgmt']
     if key_mgmt.split(' ')[0] != key_mgmt and not sae_transition:
@@ -1476,6 +1480,80 @@ def test_ap_ft_sae_rsnxe_used_mismatch2(dev, apdev):
     finally:
         dev[0].set("sae_pwe", "0")
 
+def ft_compat_params(params, omit_rsnxe):
+    # WPA3-Personal Compatibility Mode: FT-PSK in the base RSNE, FT-SAE with
+    # H2E in the RSN Override elements, so the base RSNE has no RSNXE.
+    params['wpa_key_mgmt'] = 'WPA-PSK FT-PSK'
+    params['ieee80211w'] = '0'
+    params['rsn_override_key_mgmt'] = 'SAE FT-SAE'
+    params['rsn_override_pairwise'] = 'CCMP'
+    params['rsn_override_mfp'] = '2'
+    params['sae_pwe'] = '2'
+    if omit_rsnxe:
+        params['rsn_override_omit_rsnxe'] = '1'
+    return params
+
+def run_ft_psk_rsn_override_no_rsno_sta(dev, apdev, omit_rsnxe):
+    # A STA without RSN Overriding uses FT-PSK and gets no RSNXE Used.
+    check_sae_capab(dev[0])
+    ssid = "test-ft"
+    passphrase = "12345678"
+    params = ft_params1(ssid=ssid, passphrase=passphrase)
+    hapd0 = hostapd.add_ap(apdev[0], ft_compat_params(params, omit_rsnxe))
+    params = ft_params2(ssid=ssid, passphrase=passphrase)
+    hapd1 = hostapd.add_ap(apdev[1], ft_compat_params(params, omit_rsnxe))
+    dev[0].set("rsn_overriding", "0")
+    run_roams(dev[0], apdev, hapd0, hapd1, ssid, passphrase, ieee80211w="0")
+
+def test_ap_ft_psk_rsn_override_no_rsno_sta(dev, apdev):
+    """FT-PSK roam of a STA without RSN Overriding in Compatibility Mode"""
+    try:
+        run_ft_psk_rsn_override_no_rsno_sta(dev, apdev, False)
+    finally:
+        dev[0].set("rsn_overriding", "0")
+
+def test_ap_ft_psk_rsn_override_no_rsno_sta_omit_rsnxe(dev, apdev):
+    """FT-PSK roam of a STA without RSN Overriding, RSNXE omitted"""
+    try:
+        run_ft_psk_rsn_override_no_rsno_sta(dev, apdev, True)
+    finally:
+        dev[0].set("rsn_overriding", "0")
+
+def run_ft_sae_no_beacon_rsnxe_sta_loop(dev, apdev, only_with_rsnxe):
+    hapd0, hapd1 = start_ft_sae(dev[0], apdev, sae_pwe="2",
+                                no_beacon_rsnxe2=True,
+                                ft_rsnxe_used_only_with_rsnxe=only_with_rsnxe)
+    dev[0].set("sae_pwe", "0")
+    run_roams(dev[0], apdev, hapd0, hapd1, "test-ft", "12345678", sae=True,
+              force_initial_conn_to_first_ap=True,
+              return_after_initial=True)
+    dev[0].scan_for_bss(hapd1.own_addr(), freq="2412")
+    if "OK" not in dev[0].request("ROAM " + hapd1.own_addr()):
+        raise Exception("ROAM command failed")
+    ev = dev[0].wait_event(["CTRL-EVENT-CONNECTED"], timeout=10)
+    return ev is not None and hapd1.own_addr() in ev
+
+def test_ap_ft_sae_rsnxe_used_no_rsnxe(dev, apdev):
+    """FT-SAE AP sets RSNXE Used for a STA without RSNXE by default"""
+    try:
+        # The STA sends no RSNXE and the target AP has no RSNXE in its
+        # Beacon frames. By default, the response still has RSNXE Used set,
+        # and the STA rejects it.
+        if run_ft_sae_no_beacon_rsnxe_sta_loop(dev, apdev, False):
+            raise Exception("Roaming succeeded unexpectedly")
+    finally:
+        dev[0].set("sae_pwe", "0")
+
+def test_ap_ft_sae_rsnxe_used_only_with_rsnxe(dev, apdev):
+    """FT-SAE AP with ft_rsnxe_used_only_with_rsnxe"""
+    try:
+        # With the option, the response to a STA without RSNXE has RSNXE Used
+        # cleared, so the STA accepts it even without RSNXE in the Beacon.
+        if not run_ft_sae_no_beacon_rsnxe_sta_loop(dev, apdev, True):
+            raise Exception("Roaming failed")
+    finally:
+        dev[0].set("sae_pwe", "0")
+
 def test_ap_ft_sae_pw_id(dev, apdev):
     """FT-SAE with Password Identifier"""
     check_sae_capab(dev[0])
-- 
2.43.0




More information about the Hostap mailing list