[PATCH 2/2] MKA: Prefer a principal participant with live peers

Eugen Hristev ehristev at kernel.org
Wed Oct 21 23:28:25 PDT 2026


A failure can occur when PN-triggered SAK generation overlaps EAP
reauthentication, before the replacement MKA participant has a live peer.
New EAP-authenticator participants are immediately marked principal and
inserted at the head of participant_list. The principal lookup returns the
first marked participant, including an unestablished replacement.

Meanwhile, the established participant can detect peer PN exhaustion and
generate a new SAK. Generation adds the SAK to that participant's sak_list,
but CP operations obtain their participant through the principal lookup.
If the unestablished replacement is selected, its empty SAK list does not
contain the distributed key identifier. The observed key-server log was:

  KaY: Peer LPN exhaustion
  KaY: generated new SAK - hexdump(len=16): [REMOVED]
  Distributed SAK parameter set
      Distributed AN........: 1
      Key Number............: 6
  CP: CP entering state RECEIVE
  KaY: lki related sak not found

The key was distributed to the peer, but the key server did not create its
own Rx or Tx SAs. The peer installed key 6 for reception, advertised latest
key Rx=Yes and Tx=No, and continued transmitting with key 5 while waiting
for the server to switch. The server retained only key 5. Eventually the
peer's old Tx PN reached 0xffffffff and traffic stopped.

To fix this, prefer a marked principal with live peers over one without
live peers.
Retain the first marked participant as a fallback when none has a live
peer, preserving initial setup behavior. Ordering among established
principals is unchanged. This keeps an unestablished reauthentication
candidate from redirecting the active participant's CP SAK operations.

To test and validate this, add a separate
macsec_hostapd_eap_reauth_overlap_rekey regression test.
Establish a working software-MACsec session over a veth pair and wait
for the minimum SAK lifetime to elapse.
Trigger EAPOL_REAUTH and immediately after EAP success raise the
supplicant's old Tx PN to 0xC1000000, then send traffic so MKA reports it.
Do not wait for participant cleanup before triggering rekeying.

Check the new hostapd log output for the exact SAK-lookup failure, require
an encoding-AN switch, and verify connectivity after the overlap window.
Without the fix, the test reproduced "lki related sak not found" in
about 21 seconds, with the server retaining only SA 0 and the peer holding
an inactive Tx SA 1. With this fix, the unchanged test passed: both
ends selected SA 1, retired SA 0, and passed bidirectional unicast and
broadcast checks. No PN exhaustion wait or hardware offload is required.

Assisted-by: Github Copilot
Signed-off-by: Eugen Hristev <ehristev at kernel.org>
---
 src/pae/ieee802_1x_kay.c   | 10 ++++-
 tests/hwsim/test_macsec.py | 75 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 84 insertions(+), 1 deletion(-)

diff --git a/src/pae/ieee802_1x_kay.c b/src/pae/ieee802_1x_kay.c
index 57b5e6064b5d..43124583b2fb 100644
--- a/src/pae/ieee802_1x_kay.c
+++ b/src/pae/ieee802_1x_kay.c
@@ -311,13 +311,21 @@ static struct ieee802_1x_mka_participant *
 ieee802_1x_kay_get_principal_participant(struct ieee802_1x_kay *kay)
 {
 	struct ieee802_1x_mka_participant *participant;
+	struct ieee802_1x_mka_participant *fallback = NULL;
 
 	dl_list_for_each(participant, &kay->participant_list,
 			 struct ieee802_1x_mka_participant, list) {
-		if (participant->principal)
+		if (!participant->principal)
+			continue;
+		if (!dl_list_empty(&participant->live_peers))
 			return participant;
+		if (!fallback)
+			fallback = participant;
 	}
 
+	if (fallback)
+		return fallback;
+
 	wpa_printf(MSG_DEBUG, "KaY: principal participant is not found");
 	return NULL;
 }
diff --git a/tests/hwsim/test_macsec.py b/tests/hwsim/test_macsec.py
index 8402f057b732..45bb03b64de6 100644
--- a/tests/hwsim/test_macsec.py
+++ b/tests/hwsim/test_macsec.py
@@ -847,6 +847,81 @@ def test_macsec_hostapd_eap_reauth_rekey(dev, apdev, params):
     finally:
         cleanup_macsec_hostapd()
 
+def test_macsec_hostapd_eap_reauth_overlap_rekey(dev, apdev, params):
+    """MACsec rekey while EAP reauthentication participants overlap"""
+    try:
+        run_macsec_hostapd_eap(dev, apdev, params,
+                               "macsec_hostapd_eap_reauth_overlap_rekey")
+        wpas = WpaSupplicant('veth0', '/tmp/wpas-wlan5')
+        hapd = hostapd.Hostapd('veth1')
+        supp_ifname = wpas.get_driver_status_field("parent_ifname")
+        auth_ifname = hapd.get_driver_status_field("parent_ifname")
+        supp_an = macsec_tx_an(supp_ifname)
+        auth_an = macsec_tx_an(auth_ifname)
+        old_ckn = wpas.get_status_field("ckn")
+        if not old_ckn:
+            raise Exception("Initial MKA participant CKN is unavailable")
+
+        time.sleep(7)
+        with open(os.path.join(params['logdir'], 'hostapd'),
+                  encoding='utf-8', errors='replace') as auth_log:
+            auth_log.seek(0, os.SEEK_END)
+            wpas.dump_monitor()
+            hapd.dump_monitor()
+            sta_addr = wpas.get_status_field("address")
+            res = hapd.request("EAPOL_REAUTH " + sta_addr)
+            if "OK" not in res:
+                raise Exception("Hostapd rejected reauthentication: " + res)
+            if wpas.wait_event(["CTRL-EVENT-EAP-STARTED"], timeout=10) is None:
+                raise Exception("EAP reauthentication did not start")
+            if wpas.wait_event(["CTRL-EVENT-EAP-SUCCESS"], timeout=15) is None:
+                raise Exception("EAP reauthentication did not succeed")
+            if hapd.wait_event(["CTRL-EVENT-EAP-SUCCESS2"], timeout=5) is None:
+                raise Exception("Hostapd did not report EAP success")
+
+            subprocess.check_call(["ip", "macsec", "set", supp_ifname,
+                                   "tx", "sa", str(supp_an), "pn",
+                                   "0xC1000000"])
+            hwsim_utils.test_connectivity(wpas, hapd,
+                                          ifname1=supp_ifname,
+                                          ifname2=auth_ifname, send_len=1400)
+
+            overlap_log = ""
+            deadline = time.monotonic() + 20
+            while time.monotonic() < deadline:
+                overlap_log += auth_log.read()
+                if "KaY: lki related sak not found" in overlap_log:
+                    log_ip_macsec()
+                    raise Exception("Reproduced MKA participant/key mismatch: "
+                                    "hostapd could not install its SAK during "
+                                    "reauthentication overlap")
+                if macsec_tx_an(auth_ifname) != auth_an:
+                    break
+                time.sleep(0.1)
+            else:
+                log_ip_macsec()
+                raise Exception("No Tx AN switch after overlap rekey; "
+                                "the expected SAK-lookup error was not seen")
+
+            if "KaY: Create MKA" not in overlap_log:
+                raise Exception("Replacement MKA participant was not observed")
+            if "KaY: generated new SAK" not in overlap_log:
+                raise Exception("Overlap did not trigger a new SAK")
+            if wpas.get_status_field("ckn") != old_ckn:
+                raise Exception("Principal participant changed; original "
+                                "participant overlap was not reproduced")
+
+            time.sleep(8)
+            overlap_log += auth_log.read()
+            if "KaY: lki related sak not found" in overlap_log:
+                raise Exception("Hostapd SAK lookup failed after overlap rekey")
+            hwsim_utils.test_connectivity(wpas, hapd,
+                                          ifname1=supp_ifname,
+                                          ifname2=auth_ifname, send_len=1400)
+            log_ip_macsec()
+    finally:
+        cleanup_macsec_hostapd()
+
 def test_macsec_hostapd_eap_psk(dev, apdev, params):
     """MACsec EAP-PSK with hostapd"""
     try:
-- 
2.43.0




More information about the Hostap mailing list