[RFC 19/30] wpa_supplicant: Add basic support for SMD

Ilan Peer ilan.peer at intel.com
Wed Oct 21 06:43:37 PDT 2026


Add station mode support for Seamless Mobility Domain (SMD) transition
(ST). When roaming to an AP MLD in the same SMD, SME requests the driver
to perform ST preparation and execution with the target AP MLD instead
of a new (re)association. For per-AP MLD PTK mode, an ECDH exchange
provides the DH shared secret for the target AP MLD PTK. Once the ST is
done, the connection state is resynced from the driver.

During the ST execution, the driver might not yet reflect the links of
the target AP MLD, so the WPA SM MLO state needs to be synced from the
wpa_supplicant state instead of being queried from the driver. To
support this, split the WPA SM MLO configuration out of
wpa_sm_set_ml_info() and add wpas_sm_set_ml_info_from_wpa_s(), which
builds it from the wpa_supplicant state. In addition, export
wpa_drv_get_mlo_info() so it can be used by the SMD code.

The code is built with the new CONFIG_SMD build option.

Signed-off-by: Ilan Peer <ilan.peer at intel.com>
---
 tests/hwsim/example-wpa_supplicant.config |   1 +
 wpa_supplicant/Makefile                   |   8 +
 wpa_supplicant/defconfig                  |   4 +
 wpa_supplicant/driver_i.h                 |  28 +
 wpa_supplicant/events.c                   | 139 ++++-
 wpa_supplicant/smd.c                      | 592 ++++++++++++++++++++++
 wpa_supplicant/sme.c                      |  25 +-
 wpa_supplicant/wpa_supplicant_i.h         | 103 ++++
 8 files changed, 878 insertions(+), 22 deletions(-)
 create mode 100644 wpa_supplicant/smd.c

diff --git a/tests/hwsim/example-wpa_supplicant.config b/tests/hwsim/example-wpa_supplicant.config
index f3c1727f60ce..6ea6aee3a562 100644
--- a/tests/hwsim/example-wpa_supplicant.config
+++ b/tests/hwsim/example-wpa_supplicant.config
@@ -170,6 +170,7 @@ CONFIG_NAN_USD=y
 
 CONFIG_IEEE80211BE=y
 CONFIG_IEEE80211BN=y
+CONFIG_SMD=y
 CONFIG_PROCESS_COORDINATION=y
 
 CONFIG_NAN=y
diff --git a/wpa_supplicant/Makefile b/wpa_supplicant/Makefile
index 6de5ce571d92..dd75d210c974 100644
--- a/wpa_supplicant/Makefile
+++ b/wpa_supplicant/Makefile
@@ -488,6 +488,11 @@ CONFIG_PASN=y
 CFLAGS += -DCONFIG_ENC_ASSOC
 endif
 
+ifdef CONFIG_SMD
+CFLAGS += -DCONFIG_SMD
+NEED_ECC=y
+endif
+
 ifdef CONFIG_PASN
 CFLAGS += -DCONFIG_PASN
 CFLAGS += -DCONFIG_PTKSA_CACHE
@@ -1875,6 +1880,9 @@ endif
 
 ifdef NEED_SME
 OBJS += sme.o
+ifdef CONFIG_SMD
+OBJS += smd.o
+endif
 CFLAGS += -DCONFIG_SME
 endif
 
diff --git a/wpa_supplicant/defconfig b/wpa_supplicant/defconfig
index 3ecd1e5f7631..ffc050c61ee2 100644
--- a/wpa_supplicant/defconfig
+++ b/wpa_supplicant/defconfig
@@ -643,6 +643,10 @@ CONFIG_DPP2=y
 # DPP version 3 support
 #CONFIG_DPP3=y
 
+# Seamless Mobility Domain (SMD) station support (IEEE P802.11bn)
+# This requires a crypto library with ECDH support (OpenSSL or wolfSSL).
+#CONFIG_SMD=y
+
 # Wired equivalent privacy (WEP)
 # WEP is an obsolete cryptographic data confidentiality algorithm that is not
 # considered secure. It should not be used for anything anymore. The
diff --git a/wpa_supplicant/driver_i.h b/wpa_supplicant/driver_i.h
index 9122b9353b25..8007bf81c3ca 100644
--- a/wpa_supplicant/driver_i.h
+++ b/wpa_supplicant/driver_i.h
@@ -1386,4 +1386,32 @@ static inline int wpa_drv_get_inact_sec(struct wpa_supplicant *wpa_s,
 
 #endif /* CONFIG_NAN */
 
+static inline int
+wpa_drv_st_prepare(struct wpa_supplicant *wpa_s,
+		   struct st_prepare_params *params)
+{
+	if (wpa_s->driver->st_prepare)
+		return wpa_s->driver->st_prepare(wpa_s->drv_priv, params);
+	return -1;
+}
+
+static inline int
+wpa_drv_st_execute(struct wpa_supplicant *wpa_s,
+		   struct st_execute_params *params)
+{
+	if (wpa_s->driver->st_execute)
+		return wpa_s->driver->st_execute(wpa_s->drv_priv, params);
+	return -1;
+}
+
+static inline int
+wpa_drv_st_abandon(struct wpa_supplicant *wpa_s, const u8 *addr,
+		   bool local_only)
+{
+	if (wpa_s->driver->st_abandon)
+		return wpa_s->driver->st_abandon(wpa_s->drv_priv, addr,
+						 local_only);
+	return -1;
+}
+
 #endif /* DRIVER_I_H */
diff --git a/wpa_supplicant/events.c b/wpa_supplicant/events.c
index e432ee7dae6c..16b35744c55d 100644
--- a/wpa_supplicant/events.c
+++ b/wpa_supplicant/events.c
@@ -366,6 +366,8 @@ void wpa_supplicant_mark_disassoc(struct wpa_supplicant *wpa_s)
 
 	wnm_bss_keep_alive_deinit(wpa_s);
 
+	wpas_smd_disconnected(wpa_s);
+
 #ifdef CONFIG_IBSS_RSN
 	ibss_rsn_deinit(wpa_s->ibss_rsn);
 	wpa_s->ibss_rsn = NULL;
@@ -4776,7 +4778,7 @@ out:
 }
 
 
-static int wpa_drv_get_mlo_info(struct wpa_supplicant *wpa_s)
+int wpa_drv_get_mlo_info(struct wpa_supplicant *wpa_s)
 {
 	struct driver_sta_mlo_info mlo;
 	int i;
@@ -4824,32 +4826,27 @@ static int wpa_drv_get_mlo_info(struct wpa_supplicant *wpa_s)
 }
 
 
-static int wpa_sm_set_ml_info(struct wpa_supplicant *wpa_s)
+static int wpas_sm_set_mlo_info(struct wpa_supplicant *wpa_s,
+				const struct driver_sta_mlo_info *drv_mlo)
 {
-	struct driver_sta_mlo_info drv_mlo;
 	struct wpa_sm_mlo wpa_mlo;
 	int i;
 
-	os_memset(&drv_mlo, 0, sizeof(drv_mlo));
-	if (wpas_drv_get_sta_mlo_info(wpa_s, &drv_mlo)) {
-		wpa_dbg(wpa_s, MSG_INFO, "Failed to get MLO link info");
-		return -1;
-	}
-
 	os_memset(&wpa_mlo, 0, sizeof(wpa_mlo));
-	if (!drv_mlo.valid_links)
+	if (!drv_mlo->valid_links)
 		goto out;
 
-	os_memcpy(wpa_mlo.ap_mld_addr, drv_mlo.ap_mld_addr, ETH_ALEN);
-	wpa_mlo.assoc_link_id = drv_mlo.assoc_link_id;
-	wpa_mlo.valid_links = drv_mlo.valid_links;
-	wpa_mlo.req_links = drv_mlo.req_links;
+	os_memcpy(wpa_mlo.ap_mld_addr, drv_mlo->ap_mld_addr, ETH_ALEN);
+	wpa_mlo.assoc_link_id = drv_mlo->assoc_link_id;
+	wpa_mlo.valid_links = drv_mlo->valid_links;
+	wpa_mlo.req_links = drv_mlo->req_links;
 
-	for_each_link(drv_mlo.req_links, i) {
+	for_each_link(drv_mlo->req_links, i) {
 		struct wpa_bss *bss;
 		const u8 *rsne, *rsnxe, *rsnoe, *rsno2e, *rsnxoe;
 
-		bss = wpa_supplicant_get_new_bss(wpa_s, drv_mlo.links[i].bssid);
+		bss = wpa_supplicant_get_new_bss(wpa_s,
+						 drv_mlo->links[i].bssid);
 		if (!bss) {
 			wpa_dbg(wpa_s, MSG_INFO,
 				"Failed to get MLO link %d BSS", i);
@@ -4876,9 +4873,9 @@ static int wpa_sm_set_ml_info(struct wpa_supplicant *wpa_s)
 		wpa_mlo.links[i].ap_rsnxoe = rsnxoe ? (u8 *) rsnxoe : NULL;
 		wpa_mlo.links[i].ap_rsnxoe_len = rsnxoe ? 2 + rsnxoe[1] : 0;
 
-		os_memcpy(wpa_mlo.links[i].bssid, drv_mlo.links[i].bssid,
+		os_memcpy(wpa_mlo.links[i].bssid, drv_mlo->links[i].bssid,
 			  ETH_ALEN);
-		os_memcpy(wpa_mlo.links[i].addr, drv_mlo.links[i].addr,
+		os_memcpy(wpa_mlo.links[i].addr, drv_mlo->links[i].addr,
 			  ETH_ALEN);
 	}
 
@@ -4887,6 +4884,50 @@ out:
 }
 
 
+static int wpa_sm_set_ml_info(struct wpa_supplicant *wpa_s)
+{
+	struct driver_sta_mlo_info drv_mlo;
+
+	os_memset(&drv_mlo, 0, sizeof(drv_mlo));
+	if (wpas_drv_get_sta_mlo_info(wpa_s, &drv_mlo)) {
+		wpa_dbg(wpa_s, MSG_INFO, "Failed to get MLO link info");
+		return -1;
+	}
+
+	return wpas_sm_set_mlo_info(wpa_s, &drv_mlo);
+}
+
+
+/**
+ * wpas_sm_set_ml_info_from_wpa_s - Sync WPA SM MLO state from wpa_s state
+ * @wpa_s: Pointer to wpa_supplicant data
+ * Returns: 0 on success, -1 on failure
+ *
+ * Unlike wpa_sm_set_ml_info(), the driver is not queried, so this can be used
+ * when the driver state does not yet reflect the links, e.g., during an SMD
+ * Seamless Transition. The caller must ensure that the wpa_s state is
+ * up-to-date.
+ */
+int wpas_sm_set_ml_info_from_wpa_s(struct wpa_supplicant *wpa_s)
+{
+	struct driver_sta_mlo_info mlo;
+	int i;
+
+	os_memset(&mlo, 0, sizeof(mlo));
+	mlo.valid_links = wpa_s->valid_links;
+	mlo.req_links = wpa_s->valid_links;
+	mlo.assoc_link_id = wpa_s->mlo_assoc_link_id;
+	os_memcpy(mlo.ap_mld_addr, wpa_s->ap_mld_addr, ETH_ALEN);
+	for_each_link(wpa_s->valid_links, i) {
+		os_memcpy(mlo.links[i].addr, wpa_s->links[i].addr, ETH_ALEN);
+		os_memcpy(mlo.links[i].bssid, wpa_s->links[i].bssid, ETH_ALEN);
+		mlo.links[i].freq = wpa_s->links[i].freq;
+	}
+
+	return wpas_sm_set_mlo_info(wpa_s, &mlo);
+}
+
+
 static int wpa_supplicant_event_assoc(struct wpa_supplicant *wpa_s,
 				      union wpa_event_data *data)
 {
@@ -6945,6 +6986,49 @@ static void wpas_link_reconfig(struct wpa_supplicant *wpa_s)
 }
 
 
+static void wpas_st_done(struct wpa_supplicant *wpa_s,
+			 struct st_done_info *info)
+{
+	u8 bssid[ETH_ALEN];
+
+	/*
+	 * The connection moved to a different AP MLD without any
+	 * (re)association, so all of the connection state needs to be resynced
+	 * from the driver.
+	 */
+	if (wpa_drv_get_bssid(wpa_s, bssid) < 0) {
+		wpa_printf(MSG_ERROR, "SMD: Failed to get BSSID after ST");
+		wpa_supplicant_deauthenticate(wpa_s,
+					      WLAN_REASON_DEAUTH_LEAVING);
+		return;
+	}
+
+	if (!ether_addr_equal(bssid, wpa_s->bssid)) {
+		os_memcpy(wpa_s->bssid, bssid, ETH_ALEN);
+		wpa_supplicant_update_current_bss(wpa_s, wpa_s->bssid);
+		wpas_notify_bssid_changed(wpa_s);
+	}
+
+	if (wpa_drv_get_mlo_info(wpa_s) < 0) {
+		wpa_printf(MSG_ERROR,
+			   "SMD: Failed to get MLO connection info after ST");
+		wpa_supplicant_deauthenticate(wpa_s,
+					      WLAN_REASON_DEAUTH_LEAVING);
+		return;
+	}
+
+	if (wpa_sm_set_ml_info(wpa_s)) {
+		wpa_printf(MSG_ERROR,
+			   "SMD: Failed to set MLO connection info to wpa_sm after ST");
+		wpa_supplicant_deauthenticate(wpa_s,
+					      WLAN_REASON_DEAUTH_LEAVING);
+		return;
+	}
+
+	wpas_smd_st_done(wpa_s, info);
+}
+
+
 #ifdef CONFIG_PASN
 static int wpas_pasn_auth(struct wpa_supplicant *wpa_s,
 			  const struct ieee80211_mgmt *mgmt, size_t len,
@@ -8108,6 +8192,25 @@ void wpa_supplicant_event(void *ctx, enum wpa_event_type event,
 		if (data)
 			wpas_roam_status(wpa_s, &data->roam_auth_status);
 		break;
+	case EVENT_ST_PREP_STATUS:
+		if (data)
+			wpas_smd_st_prep_status(wpa_s,
+						&data->st_prep_status_info);
+		break;
+	case EVENT_ST_EXEC_STATUS:
+		if (data)
+			wpas_smd_st_exec_status(wpa_s,
+						&data->st_exec_status_info);
+		break;
+	case EVENT_ST_DONE:
+		if (data)
+			wpas_st_done(wpa_s, &data->st_done_info);
+		break;
+	case EVENT_ST_ABANDONED:
+		if (data)
+			wpas_smd_st_abandoned(wpa_s,
+					      &data->st_abandoned_info);
+		break;
 	default:
 		wpa_msg(wpa_s, MSG_INFO, "Unknown event %d", event);
 		break;
diff --git a/wpa_supplicant/smd.c b/wpa_supplicant/smd.c
new file mode 100644
index 000000000000..c3e6e7d1743f
--- /dev/null
+++ b/wpa_supplicant/smd.c
@@ -0,0 +1,592 @@
+/*
+ * wpa_supplicant - SME
+ * Copyright (c) 2009-2026, Jouni Malinen <j at w1.fi>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+#include "common.h"
+#include "wpa_supplicant_i.h"
+#include "driver_i.h"
+#include "bss.h"
+#include "common/ieee802_11_defs.h"
+#include "common/ieee802_11_common.h"
+#include "crypto/random.h"
+#include "crypto/crypto.h"
+#include "rsn_supp/wpa.h"
+#include "crypto/sha512.h"
+
+static void wpas_smd_reset(struct wpa_supplicant *wpa_s)
+{
+	wpa_printf(MSG_DEBUG, "SMD: Reset SMD state");
+
+	crypto_ecdh_deinit(wpa_s->smd.ecdh);
+	wpabuf_free(wpa_s->smd.dhss);
+	os_memset(&wpa_s->smd, 0, sizeof(wpa_s->smd));
+}
+
+
+static void wpas_smd_abandon(struct wpa_supplicant *wpa_s, bool local_only)
+{
+	if (wpa_s->smd.state != SMD_STATE_NONE) {
+		wpa_drv_st_abandon(wpa_s, wpa_s->smd.target_ap_mld_addr,
+				   local_only);
+
+		wpas_connect_work_done(wpa_s);
+	}
+
+	wpas_smd_reset(wpa_s);
+}
+
+
+void wpas_smd_deinit(struct wpa_supplicant *wpa_s)
+{
+	wpa_printf(MSG_DEBUG, "SMD: Deinit SMD state");
+
+	wpas_smd_abandon(wpa_s, false);
+}
+
+
+void wpas_smd_disconnected(struct wpa_supplicant *wpa_s)
+{
+	if (wpa_s->smd.state == SMD_STATE_NONE)
+		return;
+
+	wpas_smd_reset(wpa_s);
+}
+
+
+bool wpas_smd_st_candidate(struct wpa_supplicant *wpa_s, struct wpa_bss *bss)
+{
+	return wpa_s->current_bss && bss &&
+		!is_zero_ether_addr(wpa_s->current_bss->smd_id) &&
+		!is_zero_ether_addr(bss->smd_id) &&
+		ether_addr_equal(wpa_s->current_bss->smd_id, bss->smd_id);
+}
+
+
+int wpas_smd_st_start(struct wpa_supplicant *wpa_s, struct wpa_bss *bss,
+		      struct wpa_ssid *ssid)
+{
+	struct st_prepare_params params;
+	struct wpabuf *pubkey_buf = NULL, *elems = NULL;
+	size_t elems_len = 0;
+	bool first_link = true;
+	u8 i;
+	int ret = -1;
+
+	if (!(wpa_s->drv_flags2 & WPA_DRIVER_FLAGS2_SMD_STA_SUPPORT)) {
+		wpa_printf(MSG_DEBUG, "SMD: Driver does not support SMD");
+		return -1;
+	}
+
+	wpa_printf(MSG_DEBUG,
+		   "SMD: Request SMD Transition. Target AP MLD ID=" MACSTR ", valid_links=0x%x",
+		   MAC2STR(wpa_s->ap_mld_addr), wpa_s->valid_links);
+
+	if (wpa_s->wpa_state != WPA_COMPLETED) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: Not connected, cannot request SMD Transition");
+		return -1;
+	}
+
+	if (!wpa_s->valid_links) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: No valid links, cannot request SMD Transition");
+		return -1;
+	}
+
+	wpas_smd_reset(wpa_s);
+
+	os_memset(&params, 0, sizeof(params));
+	params.target_ap_mld_addr = wpa_s->ap_mld_addr;
+	params.valid_links = wpa_s->valid_links;
+
+	/*
+	 * Remember the target AP MLD address: wpa_s->ap_mld_addr still refers
+	 * to the current AP MLD until the transition completes, so we can't use
+	 * it later (e.g. when abandoning).
+	 */
+	os_memcpy(wpa_s->smd.target_ap_mld_addr, wpa_s->ap_mld_addr, ETH_ALEN);
+
+	wpa_s->smd.smd_per_ap_mld_ptk_mode = bss->smd_per_ap_mld_ptk_mode;
+
+	/* Setup information for per-AP PTK mode */
+	if (bss->smd_per_ap_mld_ptk_mode) {
+		if (random_get_bytes(wpa_s->smd.snonce, WPA_NONCE_LEN) < 0) {
+			wpa_printf(MSG_DEBUG,
+				   "SMD: Failed to generate SNonce");
+			goto fail;
+		}
+
+		wpa_s->smd.dh_group = 19;
+		wpa_s->smd.ecdh = crypto_ecdh_init(wpa_s->smd.dh_group);
+		if (!wpa_s->smd.ecdh) {
+			wpa_printf(MSG_DEBUG,
+				   "SMD: Failed to init ECDH group %d",
+				   wpa_s->smd.dh_group);
+			goto fail;
+		}
+
+		pubkey_buf = crypto_ecdh_get_pubkey(wpa_s->smd.ecdh, 0);
+		if (!pubkey_buf) {
+			wpa_printf(MSG_DEBUG,
+				   "SMD: Failed to get ECDH pubkey");
+			goto fail;
+		}
+
+		elems_len = 3 + WPA_NONCE_LEN + 3 + 2 + wpabuf_len(pubkey_buf);
+		elems = wpabuf_alloc(elems_len);
+		if (!elems) {
+			wpa_printf(MSG_DEBUG,
+				   "SMD: Failed to allocate memory for SMD Transition elements");
+			goto fail;
+		}
+
+		/* Add the Nonce Element */
+		wpabuf_put_u8(elems, WLAN_EID_EXTENSION);
+		wpabuf_put_u8(elems, 1 + WPA_NONCE_LEN);
+		wpabuf_put_u8(elems, WLAN_EID_EXT_NONCE);
+		wpabuf_put_data(elems, wpa_s->smd.snonce, WPA_NONCE_LEN);
+
+		/* Add the DH Parameter Element */
+		wpabuf_put_u8(elems, WLAN_EID_EXTENSION);
+		wpabuf_put_u8(elems, 1 + 2 + wpabuf_len(pubkey_buf));
+		wpabuf_put_u8(elems, WLAN_EID_EXT_OWE_DH_PARAM);
+		wpabuf_put_le16(elems, 19);
+		wpabuf_put_buf(elems, pubkey_buf);
+
+		params.ies = wpabuf_head(elems);
+		params.ies_len = wpabuf_len(elems);
+	}
+
+#ifdef CONFIG_WNM
+	params.ext_mld_capa_ops =
+		EHT_ML_EXT_MLD_CAPA_BTM_MLD_RECO_MULTI_AP;
+
+#endif /* CONFIG_WNM */
+
+	for_each_link(wpa_s->valid_links, i) {
+		params.mld_links[i].bssid = bss->mld_links[i].bssid;
+		params.mld_links[i].freq = bss->mld_links[i].freq;
+
+		/*
+		 * The elements are included only for the link with the lowest
+		 * link ID; the other links do not carry any elements.
+		 *
+		 * TODO: It is not clear if all the association elements should
+		 * be included in the ST Preparation Request, or only a subset.
+		 */
+		if (first_link) {
+			params.mld_links[i].ies = wpa_s->sme.assoc_req_ie;
+			params.mld_links[i].ies_len =
+				wpa_s->sme.assoc_req_ie_len;
+			first_link = false;
+		}
+	}
+
+	ret = wpa_drv_st_prepare(wpa_s, &params);
+	wpa_printf(MSG_DEBUG, "SMD: ST Prepare returned %d", ret);
+fail:
+	wpabuf_free(elems);
+	wpabuf_free(pubkey_buf);
+	if (ret < 0)
+		wpas_smd_deinit(wpa_s);
+	else
+		wpa_s->smd.state = SMD_STATE_PREPARE_REQUESTED;
+
+	return ret;
+}
+
+
+static int wpas_smd_st_execute(struct wpa_supplicant *wpa_s)
+{
+	struct st_execute_params params;
+	int ret;
+	u8 mic_elem[2 + SHA512_MAC_LEN];
+
+	os_memset(&params, 0, sizeof(params));
+	os_memset(&mic_elem, 0, sizeof(mic_elem));
+
+	params.target_ap_mld_addr = wpa_s->ap_mld_addr;
+
+	wpa_printf(MSG_DEBUG, "SMD: Starting ST execution");
+
+	if (wpa_s->smd.smd_per_ap_mld_ptk_mode) {
+		int ret;
+
+		ret = wpa_sm_smd_calc_mic(wpa_s->wpa,
+					  wpa_s->ap_mld_addr,
+					  wpa_s->smd.snonce,
+					  wpa_s->smd.anonce,
+					  false,
+					  mic_elem + 2);
+		if (ret <= 0) {
+			wpa_printf(MSG_DEBUG,
+				   "SMD: Failed to calc ST execute MIC");
+
+			wpas_smd_deinit(wpa_s);
+			return -1;
+		}
+
+		mic_elem[0] = WLAN_EID_MIC;
+		mic_elem[1] = ret;
+
+		params.ies = mic_elem;
+		params.ies_len = 2 + ret;
+	}
+
+	ret = wpa_drv_st_execute(wpa_s, &params);
+
+	wpa_printf(MSG_DEBUG, "SMD: ST Execute returned %d", ret);
+	if (!ret) {
+		wpa_s->smd.state = SMD_STATE_EXECUTE_REQUESTED;
+		return 0;
+	}
+
+	wpas_smd_deinit(wpa_s);
+
+	return ret;
+}
+
+
+void wpas_smd_st_prep_status(struct wpa_supplicant *wpa_s,
+			     struct st_prep_status_info *info)
+{
+	struct ieee80211_mgmt *mgmt;
+	struct ieee802_11_elems elems;
+	size_t len = info->resp_frame_len;
+	const u8 *pos;
+	u16 status;
+
+	wpa_printf(MSG_DEBUG, "SMD: ST Prep Status (ap_mld=" MACSTR ")",
+		   MAC2STR(info->ap_mld_addr));
+
+	if (wpa_s->smd.state != SMD_STATE_PREPARE_REQUESTED) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Prep Status without ST Prepare");
+		return;
+	}
+
+	if (os_memcmp(wpa_s->ap_mld_addr, info->ap_mld_addr, ETH_ALEN) != 0) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Prep Status for unexpected AP MLD (expected=" MACSTR ", received=" MACSTR ")",
+			   MAC2STR(wpa_s->ap_mld_addr),
+			   MAC2STR(info->ap_mld_addr));
+		return;
+	}
+
+	if (info->status) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Prep failed (ap_mld=" MACSTR ")",
+			   MAC2STR(info->ap_mld_addr));
+		goto fail;
+	}
+
+	if (!info->resp_frame ||
+	    len < offsetof(struct ieee80211_mgmt,
+			   u.action.u.uhr_link_conf.variable)) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Prep failed, invalid response frame (ap_mld=" MACSTR ")",
+			   MAC2STR(info->ap_mld_addr));
+		goto fail;
+	}
+
+	mgmt = (struct ieee80211_mgmt *)info->resp_frame;
+
+	if (mgmt->u.action.category != WLAN_ACTION_PROTECTED_UHR ||
+	    mgmt->u.action.u.uhr_link_conf.action !=
+	    UHR_PROT_ACTION_LINK_RECONF_RESP ||
+	    mgmt->u.action.u.uhr_link_conf.message_type !=
+	    UHR_LINK_RECONF_RESP_TYPE_ST_PREP) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Prep failed, unexpected response frame (ap_mld=" MACSTR ")",
+			   MAC2STR(info->ap_mld_addr));
+		goto fail;
+	}
+
+	len -= offsetof(struct ieee80211_mgmt,
+			u.action.u.uhr_link_conf.variable);
+	pos = mgmt->u.action.u.uhr_link_conf.variable;
+
+	if (len < 2) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Prep failed, response frame too short (ap_mld=" MACSTR ")",
+			   MAC2STR(info->ap_mld_addr));
+		goto fail;
+	}
+
+	status = WPA_GET_LE16(pos);
+	if (status != WLAN_STATUS_SUCCESS) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Prep failed, response frame status %u (ap_mld=" MACSTR ")",
+			   status, MAC2STR(info->ap_mld_addr));
+		goto fail;
+	}
+
+	len -= 2;
+	pos += 2;
+
+	if (ieee802_11_parse_elems(pos, len, &elems, 1) == ParseFailed) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: Could not parse Link Reconfiguration response");
+		goto fail;
+	}
+
+	if (wpa_s->smd.smd_per_ap_mld_ptk_mode) {
+		u8 mic[SHA512_MAC_LEN];
+		int mic_len;
+		u16 group;
+
+		if (!elems.owe_dh || elems.owe_dh_len < 2 ||
+		    !elems.nonce || elems.nonce_len != WPA_NONCE_LEN ||
+		    !elems.mic || !elems.mic_len ||
+		    !elems.basic_mle || !elems.basic_mle_len) {
+			wpa_printf(MSG_DEBUG,
+				   "SMD: ST Prep failed, missing elements for per-AP MLD PTK mode (ap_mld=" MACSTR ")",
+				   MAC2STR(info->ap_mld_addr));
+			goto fail;
+		}
+
+		group = WPA_GET_LE16(elems.owe_dh);
+		if (group != wpa_s->smd.dh_group) {
+			wpa_msg(wpa_s, MSG_DEBUG,
+				"SMD: DH group mismatch (AP=%u, local=%u)",
+				group, wpa_s->smd.dh_group);
+			goto fail;
+		}
+
+		wpa_s->smd.dhss =
+			crypto_ecdh_set_peerkey(wpa_s->smd.ecdh, 0,
+						elems.owe_dh + 2,
+						elems.owe_dh_len - 2);
+		if (!wpa_s->smd.dhss) {
+			wpa_printf(MSG_DEBUG,
+				   "SMD: Failed to set peer ECDH pubkey");
+			goto fail;
+		}
+
+		os_memcpy(wpa_s->smd.anonce, elems.nonce, WPA_NONCE_LEN);
+
+		if (wpa_sm_smd_derive_ptk(wpa_s->wpa, info->ap_mld_addr,
+					  wpa_s->smd.snonce,
+					  wpa_s->smd.anonce,
+					  wpabuf_head(wpa_s->smd.dhss),
+					  wpabuf_len(wpa_s->smd.dhss))) {
+			wpa_printf(MSG_DEBUG, "SMD: Failed to derive PTK");
+			goto fail;
+		}
+
+		mic_len = wpa_sm_smd_calc_mic(wpa_s->wpa,
+					      info->ap_mld_addr,
+					      wpa_s->smd.snonce,
+					      wpa_s->smd.anonce,
+					      true, mic);
+
+		if (mic_len < 0 || os_memcmp(mic, elems.mic, mic_len)) {
+			wpa_printf(MSG_DEBUG,
+				   "SMD: Failed MIC verification and installation");
+			goto fail;
+		}
+
+		wpa_printf(MSG_DEBUG, "SMD: Per-AP MLD PTK procedures done");
+	}
+
+	wpa_printf(MSG_DEBUG,
+		   "SMD: ST Prep succeeded, executing transition (ap_mld=" MACSTR ")",
+		   MAC2STR(info->ap_mld_addr));
+
+	wpas_smd_st_execute(wpa_s);
+	return;
+
+fail:
+	wpas_smd_deinit(wpa_s);
+}
+
+
+void wpas_smd_st_exec_status(struct wpa_supplicant *wpa_s,
+			     struct st_exec_status_info *info)
+{
+	struct ieee80211_mgmt *mgmt;
+	struct ieee802_11_elems elems;
+	size_t len = info->resp_frame_len;
+	const u8 *pos;
+	u16 status;
+
+	wpa_printf(MSG_DEBUG, "SMD: ST Exec Status (ap_mld=" MACSTR ")",
+		   MAC2STR(info->ap_mld_addr));
+
+	if (wpa_s->smd.state != SMD_STATE_EXECUTE_REQUESTED) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Exec Status without ST Execute");
+		return;
+	}
+
+	if (os_memcmp(wpa_s->ap_mld_addr, info->ap_mld_addr, ETH_ALEN) != 0) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Exec Status for unexpected AP MLD (expected=" MACSTR ", received=" MACSTR ")",
+			   MAC2STR(wpa_s->ap_mld_addr),
+			   MAC2STR(info->ap_mld_addr));
+		return;
+	}
+
+	if (info->status) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Exec failed (ap_mld=" MACSTR ")",
+			   MAC2STR(info->ap_mld_addr));
+		goto fail;
+	}
+
+	if (!info->resp_frame ||
+	    len < offsetof(struct ieee80211_mgmt,
+			   u.action.u.uhr_link_conf.variable)) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Exec failed, invalid response frame (ap_mld=" MACSTR ")",
+			   MAC2STR(info->ap_mld_addr));
+		goto fail;
+	}
+
+	mgmt = (struct ieee80211_mgmt *)info->resp_frame;
+
+	if (mgmt->u.action.category != WLAN_ACTION_PROTECTED_UHR ||
+	    mgmt->u.action.u.uhr_link_conf.action !=
+	    UHR_PROT_ACTION_LINK_RECONF_RESP ||
+	    mgmt->u.action.u.uhr_link_conf.message_type !=
+	    UHR_LINK_RECONF_RESP_TYPE_ST_EXEC) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Exec failed, unexpected response frame (ap_mld=" MACSTR ")",
+			   MAC2STR(info->ap_mld_addr));
+		goto fail;
+	}
+
+	len -= offsetof(struct ieee80211_mgmt,
+			u.action.u.uhr_link_conf.variable);
+	pos = mgmt->u.action.u.uhr_link_conf.variable;
+
+	if (len < 2) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Exec failed, response frame too short (ap_mld=" MACSTR ")",
+			   MAC2STR(info->ap_mld_addr));
+		goto fail;
+	}
+
+	status = WPA_GET_LE16(pos);
+	if (status != WLAN_STATUS_SUCCESS) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Exec failed, response frame status %u (ap_mld=" MACSTR ")",
+			   status, MAC2STR(info->ap_mld_addr));
+		goto fail;
+	}
+
+	len -= 2;
+	pos += 2;
+
+	if (ieee802_11_parse_elems(pos, len, &elems, 1) == ParseFailed) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: Could not parse ST Exec response elements");
+		goto fail;
+	}
+
+	if (!elems.key_delivery) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: No Key Delivery element in ST Exec response");
+		goto fail;
+	}
+
+	/*
+	 * Update wpa_supplicant state and WPA SM with the target AP MLD
+	 * information before installing group keys. The ST moves the
+	 * connection to a new AP MLD, so the AP MLD address and per-link
+	 * information must be updated accordingly.
+	 *
+	 * TODO: This is currently relying on MLD flows, that might not be
+	 * the case for ST.
+	 */
+	if (wpa_drv_get_mlo_info(wpa_s) < 0) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: Failed to get MLO info for target AP");
+		goto fail;
+	}
+
+	if (wpas_sm_set_ml_info_from_wpa_s(wpa_s)) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: Failed to set MLO info to wpa_sm");
+		goto fail;
+	}
+
+	/* Set the new PTK before setting the group keys */
+	if (wpa_s->smd.smd_per_ap_mld_ptk_mode &&
+	    wpa_sm_smd_set_tptk_to_ptk(wpa_s->wpa)) {
+		wpa_printf(MSG_DEBUG, "SMD: Failed to set TPTK as PTK");
+		goto fail;
+	}
+
+	if (wpa_sm_install_mlo_group_keys(wpa_s->wpa,
+					  elems.key_delivery + WPA_KEY_RSC_LEN,
+					  elems.key_delivery_len -
+					  WPA_KEY_RSC_LEN,
+					  info->links) < 0) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: Failed to install group keys from Key Delivery");
+		goto fail;
+	}
+
+	wpa_printf(MSG_DEBUG,
+		   "SMD: ST Exec succeeded, waiting for ST Done (ap_mld=" MACSTR ")",
+		   MAC2STR(info->ap_mld_addr));
+
+	wpa_s->smd.state = SMD_STATE_EXECUTION_DONE;
+	return;
+
+fail:
+	wpas_smd_deinit(wpa_s);
+
+	/*
+	 * The association with the current AP MLD is already gone and the
+	 * driver rejects an abandonment at this point, so the station would
+	 * otherwise be left associated with the target AP MLD without the
+	 * group keys.
+	 */
+	wpas_request_disconnection(wpa_s);
+}
+
+
+void wpas_smd_st_done(struct wpa_supplicant *wpa_s,
+		      struct st_done_info *info)
+{
+	int i;
+
+	wpa_printf(MSG_DEBUG, "SMD: ST Done (ap_mld=" MACSTR
+		   " valid_links=0x%04x)",
+		   MAC2STR(info->ap_mld_addr), info->valid_links);
+
+	if (wpa_s->smd.state != SMD_STATE_EXECUTION_DONE) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: ST Done without a pending SMD Transition");
+		return;
+	}
+
+	for (i = 0; i < MAX_NUM_MLD_LINKS; i++) {
+		if (!(info->valid_links & BIT(i)))
+			continue;
+
+		wpa_printf(MSG_DEBUG, "SMD: ST Done link_id=%d bssid=" MACSTR,
+			   i, MAC2STR(info->links[i].bssid));
+	}
+
+	wpas_smd_reset(wpa_s);
+}
+
+
+void wpas_smd_st_abandoned(struct wpa_supplicant *wpa_s,
+			   struct st_abandoned_info *info)
+{
+	wpa_printf(MSG_DEBUG, "SMD: ST Abandoned (ap_mld=" MACSTR ")",
+		   MAC2STR(info->ap_mld_addr));
+
+	wpas_smd_reset(wpa_s);
+
+	/* Assume that this is followed by a disconnection event */
+}
diff --git a/wpa_supplicant/sme.c b/wpa_supplicant/sme.c
index 658a7052ed22..26f76cc82a5a 100644
--- a/wpa_supplicant/sme.c
+++ b/wpa_supplicant/sme.c
@@ -1945,6 +1945,18 @@ static void sme_send_authentication(struct wpa_supplicant *wpa_s,
 		set_suites_done = true;
 	}
 
+	if (start && wpas_smd_st_candidate(wpa_s, bss)) {
+		wpa_printf(MSG_DEBUG,
+			   "SMD: Current and Target AP MLD in the same SMD: ID=" MACSTR,
+			   MAC2STR(bss->smd_id));
+
+		if (wpas_smd_st_start(wpa_s, bss, ssid) == 0) {
+			wpa_printf(MSG_DEBUG,
+				   "SMD: Started SMD ST");
+			return;
+		}
+	}
+
 	skip_auth = wpa_s->conf->reassoc_same_bss_optim &&
 		wpa_s->reassoc_same_bss;
 	wpa_s->current_bss = bss;
@@ -2785,10 +2797,15 @@ static void sme_auth_start_cb(struct wpa_radio_work *work, int deinit)
 	}
 
 	/* Starting new connection, so clear the possibly used WPA IE from the
-	 * previous association. */
-	wpa_sm_set_assoc_wpa_ie(wpa_s->wpa, NULL, 0);
-	wpa_sm_set_assoc_rsnxe(wpa_s->wpa, NULL, 0);
-	wpa_s->rsnxe_len = 0;
+	 * previous association. An SMD Transition reuses the security
+	 * negotiated with the current AP MLD, so in that case the state needs
+	 * to be maintained. It is anyway rebuilt below if the SMD Transition
+	 * cannot be started and a normal connection is performed instead. */
+	if (!wpas_smd_st_candidate(wpa_s, cwork->bss)) {
+		wpa_sm_set_assoc_wpa_ie(wpa_s->wpa, NULL, 0);
+		wpa_sm_set_assoc_rsnxe(wpa_s->wpa, NULL, 0);
+		wpa_s->rsnxe_len = 0;
+	}
 
 	sme_send_authentication(wpa_s, cwork->bss, cwork->ssid, 1);
 	wpas_notify_auth_changed(wpa_s);
diff --git a/wpa_supplicant/wpa_supplicant_i.h b/wpa_supplicant/wpa_supplicant_i.h
index 1ff84a472531..1effd518cfad 100644
--- a/wpa_supplicant/wpa_supplicant_i.h
+++ b/wpa_supplicant/wpa_supplicant_i.h
@@ -756,6 +756,45 @@ struct auth_802_1x_data {
 };
 #endif /* CONFIG_IEEE8021X_AUTH */
 
+/**
+ * enum smd_state - SMD state machine states
+ * @SMD_STATE_NONE: No SMD operation is in progress
+ * @SMD_STATE_PREPARE_REQUESTED: SMD Prepare operation has been requested
+ * @SMD_STATE_EXECUTE_REQUESTED: SMD Execute operation has been requested
+ * @SMD_STATE_EXECUTION_DONE: SMD Execute operation has completed
+ */
+enum smd_state {
+	SMD_STATE_NONE = 0,
+	SMD_STATE_PREPARE_REQUESTED = 1,
+	SMD_STATE_EXECUTE_REQUESTED = 2,
+	SMD_STATE_EXECUTION_DONE = 3,
+};
+
+/**
+ * struct smd_info - Data for SMD (Simultaneous Multi-Link Device) operations
+ * @snonce: SNonce for per-AP PTK mode
+ * @anonce: ANonce for per-AP PTK mode
+ * @dh_group: DH group for per-AP PTK mode
+ * @ecdh: ECDH context for per-AP PTK mode
+ * @dhss: DH shared secret for per-AP PTK mode
+ * @state: Current SMD state machine state
+ * @target_ap_mld_addr: Target AP MLD address of the ongoing transition; kept
+ *	here because wpa_s->ap_mld_addr still points at the current AP MLD until
+ *	the transition completes
+ */
+struct smd_info {
+	u8 snonce[WPA_NONCE_LEN];
+	u8 anonce[WPA_NONCE_LEN];
+	u16 dh_group;
+
+	struct crypto_ecdh *ecdh;
+	struct wpabuf *dhss;
+
+	enum smd_state state;
+	bool smd_per_ap_mld_ptk_mode;
+	u8 target_ap_mld_addr[ETH_ALEN];
+};
+
 /**
  * struct wpa_supplicant - Internal data for wpa_supplicant interface
  *
@@ -1838,6 +1877,8 @@ struct wpa_supplicant {
 	 * interface can be created with the same address when M1 arrives.
 	 */
 	u8 pr_responder_src_addr[ETH_ALEN];
+
+	struct smd_info smd;
 };
 
 
@@ -2301,4 +2342,66 @@ int security_profile_get_rsn_caps(const u8 *sp);
 bool sec_prof_list_has_eap_over_auth(const int *numbers);
 bool security_profile_is_enabled(const u8 *sp, int profile);
 
+int wpa_drv_get_mlo_info(struct wpa_supplicant *wpa_s);
+int wpas_sm_set_ml_info_from_wpa_s(struct wpa_supplicant *wpa_s);
+
+#if defined(CONFIG_SME) && defined(CONFIG_SMD)
+void wpas_smd_deinit(struct wpa_supplicant *wpa_s);
+void wpas_smd_disconnected(struct wpa_supplicant *wpa_s);
+bool wpas_smd_st_candidate(struct wpa_supplicant *wpa_s, struct wpa_bss *bss);
+int wpas_smd_st_start(struct wpa_supplicant *wpa_s, struct wpa_bss *bss,
+		      struct wpa_ssid *ssid);
+void wpas_smd_st_prep_status(struct wpa_supplicant *wpa_s,
+			     struct st_prep_status_info *info);
+void wpas_smd_st_exec_status(struct wpa_supplicant *wpa_s,
+			     struct st_exec_status_info *info);
+void wpas_smd_st_done(struct wpa_supplicant *wpa_s,
+		      struct st_done_info *info);
+void wpas_smd_st_abandoned(struct wpa_supplicant *wpa_s,
+			   struct st_abandoned_info *info);
+#else /* CONFIG_SME && CONFIG_SMD */
+static inline void wpas_smd_deinit(struct wpa_supplicant *wpa_s)
+{
+}
+
+static inline void wpas_smd_disconnected(struct wpa_supplicant *wpa_s)
+{
+}
+
+static inline bool wpas_smd_st_candidate(struct wpa_supplicant *wpa_s,
+					 struct wpa_bss *bss)
+{
+	return false;
+}
+
+static inline int wpas_smd_st_start(struct wpa_supplicant *wpa_s,
+				    struct wpa_bss *bss,
+				    struct wpa_ssid *ssid)
+{
+	return -1;
+}
+
+static inline void
+wpas_smd_st_prep_status(struct wpa_supplicant *wpa_s,
+			struct st_prep_status_info *info)
+{
+}
+
+static inline void
+wpas_smd_st_exec_status(struct wpa_supplicant *wpa_s,
+			struct st_exec_status_info *info)
+{
+}
+
+static inline void wpas_smd_st_done(struct wpa_supplicant *wpa_s,
+				    struct st_done_info *info)
+{
+}
+
+static inline void wpas_smd_st_abandoned(struct wpa_supplicant *wpa_s,
+					 struct st_abandoned_info *info)
+{
+}
+#endif /* CONFIG_SME && CONFIG_SMD */
+
 #endif /* WPA_SUPPLICANT_I_H */
-- 
2.55.0




More information about the Hostap mailing list