From 932a9e33d8740c9f323314e03e16008a468cedcd Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Mon, 29 Jun 2026 12:09:02 +0530 Subject: [PATCH 01/22] feat(wifi): add NAN group data and management frame protection Add Wi-Fi Aware group-key support to secured NDPs so group-addressed traffic can be protected, for interop with iOS/macOS peers: - GTK (NCS-GTK-CCM-128) protects group-addressed data. - IGTK/BIGTK (BIP) protect group management traffic - multicast SDFs, Beacons. Capabilities are advertised in the CSIA IE: group_data_prot maps to GTKSA, group_mgmt_prot to IGTKSA/BIGTKSA. The CSIA cannot encode IGTK/BIGTK without GTK (WiFi Aware spec 9.5.21.2, Table 122), so enabling group_mgmt_prot forces group_data_prot on for every secured service. Expose per-service group_data_prot and device-global group_mgmt_prot. --- .../esp_wifi/include/esp_private/wifi.h | 30 +- .../esp_wifi/include/esp_wifi_types_generic.h | 1 + components/esp_wifi/lib | 2 +- .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 155 +++- .../esp_wifi/wifi_apps/nan_app/src/nan_i.h | 111 ++- .../wifi_apps/nan_app/src/nan_pairing.c | 5 + .../wifi_apps/nan_app/src/nan_security.c | 811 ++++++++++++++++-- .../nan_publisher/main/Kconfig.projbuild | 10 + .../nan_publisher/main/publisher_main.c | 3 + .../nan_subscriber/main/Kconfig.projbuild | 10 + .../nan_subscriber/main/subscriber_main.c | 3 + 11 files changed, 1052 insertions(+), 89 deletions(-) diff --git a/components/esp_wifi/include/esp_private/wifi.h b/components/esp_wifi/include/esp_private/wifi.h index 9331077f919..c7d5d8d9402 100644 --- a/components/esp_wifi/include/esp_private/wifi.h +++ b/components/esp_wifi/include/esp_private/wifi.h @@ -89,10 +89,11 @@ typedef struct { uint8_t num_pmkids; /**< Number of parsed PMKIDs */ uint8_t pmkids[NAN_PEER_MAX_PMKIDS][ESP_WIFI_NAN_NDP_PMKID_LEN]; /**< Parsed ND-PMKIDs */ uint8_t group_data_prot: 1; /**< Peer advertises group data frame protection */ - uint8_t group_mgmt_prot: 1; /**< Peer advertises group mgmt frame protection */ + uint8_t group_mgmt_prot: 1; /**< Peer advertises IGTKSA (CSIA caps bits 1-2 != 0) */ uint8_t pairing_setup: 1; /**< Pairing setup: 0 - disabled, 1 - enabled */ uint8_t npk_nik_caching: 1; /**< NPK/NIK caching: 0 - disabled, 1 - enabled (valid if pairing_setup) */ - uint8_t reserved: 4; /**< Reserved */ + uint8_t group_bigtk_prot: 1; /**< Peer advertises BIGTKSA (CSIA caps bits 1-2 == 10) */ + uint8_t reserved: 3; /**< Reserved */ } wifi_nan_peer_sdf_security_t; /* NAN Peer info parsed from SDF */ @@ -230,9 +231,11 @@ struct nan_secure_dp_funcs { * with the given Key Data field length. */ uint32_t (*get_shared_key_desc_attr_len)(uint16_t key_data_len); - /* Byte length of the M4 Shared Key Descriptor including any - * encrypted KDE payload (GTK/IGTK/BIGTK). */ - int (*ndp_security_install_get_shared_desc_len)(void); + /* Exact byte length of the M4 (NDP Security Install) Shared Key Descriptor + * attribute for this NDP, including any encrypted KDE payload (GTK/IGTK/BIGTK). + * @ndp_id + @peer_nmi identify the NDL so the host returns the exact length the + * builder will write (no over-reservation / on-air zero-padding). */ + int (*ndp_security_install_get_shared_desc_len)(uint8_t ndp_id, const uint8_t *peer_nmi); uint8_t (*get_ndp_resp_num_pmkids)(uint8_t ndp_id, const uint8_t *peer_nmi); uint32_t (*get_ndp_resp_shared_key_desc_len)(uint8_t ndp_id, const uint8_t *peer_nmi); @@ -362,10 +365,21 @@ struct nan_secure_dp_funcs { const wifi_nan_discovery_security_params_t *sec_cfg, wifi_nan_security_params_t *out_derived); - /* Returns the cipher-suite bitmap negotiated for an in-flight NDP, - * or 0 if the NDP is open. Used by RX/TX paths to decide whether - * to apply CCMP/GCMP and which key length to use. */ + /* Returns the full cipher-suite bitmap negotiated for an in-flight NDP + * (including the group cipher NCS-GTK when group-addressed data protection + * is in use), or 0 if the NDP is open. The blob treats this as an opaque + * value passed straight to the host CSIA callbacks (construct_csia / + * get_csia_len) to build the on-air M1/M3 CSIA own-bitmap; it must NOT + * interpret individual CSID bits. Pairwise cipher selection and key install + * are host-driven and independent of this value. */ uint16_t (*get_ndp_security_csid)(uint8_t ndp_id, const uint8_t *peer_nmi); + + /* Exact byte length of the M3 (NDP Confirm) Shared Key Descriptor attribute for + * this NDP, including any encrypted KDE payload (GTK/IGTK/BIGTK). @ndp_id + + * @peer_nmi identify the NDL. Mirrors ndp_security_install_get_shared_desc_len + * for the initiator path. Appended at the end of the struct to preserve the + * layout of the fields above. */ + int (*ndp_confirm_get_shared_desc_len)(uint8_t ndp_id, const uint8_t *peer_nmi); }; /** diff --git a/components/esp_wifi/include/esp_wifi_types_generic.h b/components/esp_wifi/include/esp_wifi_types_generic.h index 1a92443aa5a..28b7d0bde6e 100644 --- a/components/esp_wifi/include/esp_wifi_types_generic.h +++ b/components/esp_wifi/include/esp_wifi_types_generic.h @@ -606,6 +606,7 @@ typedef struct { bool disable_random_mac;/**< Disable the MAC Randomisation in NAN */ bool reset_current_nvs_creds; /**< Erase all NAN credentials (own NIK and cached peer NIK/NPK entries) saved in NVS before starting. */ bool use_nvs_for_caching; /**< Persist newly-learned peer credentials (NIK/NPK) to NVS so they survive across reboots. */ + bool group_mgmt_prot; /**< Device-global group management protection (IGTKSA/BIGTKSA): BIP-protect Beacons + multicast SDFs. Forces GTKSA on all secured services (CSIA caps cannot encode IGTK/BIGTK without GTKSA). */ } wifi_nan_sync_config_t; /** diff --git a/components/esp_wifi/lib b/components/esp_wifi/lib index 5c5338ebbd3..dcdac54b984 160000 --- a/components/esp_wifi/lib +++ b/components/esp_wifi/lib @@ -1 +1 @@ -Subproject commit 5c5338ebbd32c72fcde1a46d28f0a2ce17b8b29b +Subproject commit dcdac54b98412c50586e67b73e34b3afb8447737 diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index f3c82d637f0..f72882e4274 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -264,13 +264,27 @@ static void nan_app_clear_one_peer_tks(const uint8_t *peer_nmi) key_rsc, sizeof(key_rsc), NULL, 0, NAN_KEY_ND_TK); + /* Drop the peer's RX GTK (bound to the peer NDI) and wipe local GTK + * state. The TX GTK keyed on the local NDI is released by the blob + * when the NDI/interface is torn down. */ + esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, + key_addr, ndl->gtk_keyid, 0, + key_rsc, sizeof(key_rsc), + NULL, 0, NAN_KEY_ND_GTK); + forced_memzero(ndl->nd_tk, sizeof(ndl->nd_tk)); forced_memzero(ndl->nd_kck, sizeof(ndl->nd_kck)); forced_memzero(ndl->nd_kek, sizeof(ndl->nd_kek)); + forced_memzero(ndl->gtk, sizeof(ndl->gtk)); + forced_memzero(ndl->own_gtk, sizeof(ndl->own_gtk)); ndl->ptk_set = 0; ndl->tk_len = 0; ndl->kck_len = 0; ndl->kek_len = 0; + ndl->gtk_set = 0; + ndl->own_gtk_set = 0; + ndl->gtk_len = 0; + ndl->own_gtk_len = 0; } /* Fallback when no NDL slot tracks peer_ndi yet. */ @@ -601,6 +615,18 @@ static struct own_svc_info *nan_claim_own_svc_slot(uint8_t type, const char svc_ forced_memzero(&p_svc->derived_security, sizeof(p_svc->derived_security)); if (security_cfg) { memcpy(&p_svc->user_cfg, security_cfg, sizeof(*security_cfg)); + /* Device-global group_mgmt_prot (IGTKSA/BIGTKSA) forces GTKSA on every + * secured service: the CSIA capability field has no "IGTK/BIGTK without + * GTKSA" encoding (§9.5.21.2 Table 122), so advertising group-management + * protection mandates advertising GTKSA. Warn and force it on if the app + * requested group_data_prot=0. Forcing support never blocks a peer that + * lacks protection — keys activate only after capability negotiation. */ + if (s_nan_ctx.group_mgmt_prot && !p_svc->user_cfg.group_data_prot) { + ESP_LOGW(TAG, "group_data_prot forced ON for '%s': group_mgmt_prot is " + "enabled device-wide; GTKSA cannot be advertised without it", + svc_name); + p_svc->user_cfg.group_data_prot = 1; + } } #ifdef CONFIG_ESP_WIFI_NAN_PAIRING if (pairing) { @@ -681,6 +707,7 @@ static void nan_record_new_ndl(uint8_t ndp_id, uint8_t publish_id, uint8_t peer_ if (ndl && reuse_slot) { ndl->ndp_id = ndp_id; ndl->own_role = own_role; + ndl->device_caps = device_caps; return; } if (ndl) { @@ -1024,7 +1051,7 @@ static void nan_app_replied_cb(uint8_t pub_id, struct nan_cb_peer_info *peer_inf evt->subscribe_id = sub_id; MACADDR_COPY(evt->sub_if_mac, sub_nmi); - ESP_LOGI(TAG, "Sent Publish to Peer "MACSTR" [Peer Subscribe id - %d]", MAC2STR(sub_nmi), sub_id); + //ESP_LOGI(TAG, "Sent Publish to Peer "MACSTR" [Peer Subscribe id - %d]", MAC2STR(sub_nmi), sub_id); if (ssi && ssi_len) { memcpy(evt->ssi, ssi, ssi_len); evt->ssi_len = ssi_len; @@ -1147,8 +1174,11 @@ static void nan_app_ndp_indication_cb(uint8_t pub_id, struct ndp_cb_peer_info *p nan_record_new_ndl(ndp_id, pub_id, peer_nmi, ESP_WIFI_NDP_ROLE_RESPONDER, device_caps); - if (!nan_find_peer_svc(pub_id, 0, peer_nmi)) { + struct peer_svc_info *p_peer_svc = nan_find_peer_svc(pub_id, 0, peer_nmi); + if (!p_peer_svc) { nan_record_peer_svc(pub_id, 0, peer_nmi, device_caps); + } else { + p_peer_svc->device_caps = device_caps; } struct ndl_info *ndl = nan_find_ndl(ndp_id, peer_nmi); @@ -1161,6 +1191,17 @@ static void nan_app_ndp_indication_cb(uint8_t pub_id, struct ndp_cb_peer_info *p nan_security_apply_pending(ndl, p_own_svc, pub_id, peer_nmi, peer_ndi); #endif + if (device_caps & NAN_CAPS_NDPE_ATTR) { + uint8_t own_bssid[6]; + esp_err_t err = esp_wifi_get_mac(WIFI_IF_NAN, own_bssid); + if (err != ESP_OK) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "Cannot get own BSSID!"); + return; + } + esp_wifi_nan_get_ipv6_linklocal_from_mac(&own_ipv6.u_addr.ip6, own_bssid); + } + if (p_own_svc->ndp_resp_needed) { ESP_LOGD(TAG, "NDP Req from "MACSTR" [NDP Id: %d], Accept OR Deny using NDP command", MAC2STR(peer_nmi), ndp_id); @@ -1359,10 +1400,6 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer goto done; } -#ifndef NAN_KEY_ND_TK -#define NAN_KEY_ND_TK 0 -#endif - #ifdef CONFIG_ESP_WIFI_NAN_SECURITY if (ndl->security_ctx.type == WIFI_NAN_SECURITY_ENCRYPTED) { uint8_t key_rsc[8] = {0}; @@ -1375,12 +1412,98 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, NAN_KEY_ND_TK); + ESP_LOG_BUFFER_HEXDUMP("## ND-TK ", ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, ESP_LOG_INFO); + ret = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, + peer_nmi, + 0, + 1, + key_rsc, + sizeof(key_rsc), + ndl->nd_tk, + NAN_NCS_SK_128_TK_LEN, + NAN_KEY_NM_TK); if (ret != 0) { ESP_LOGE(TAG, "NDP confirm: failed to install NAN pairwise key (ndp_id=%d, ret=%d)", ndp_id, ret); os_free(evt); nan_ndp_confirm_teardown(peer_nmi, ndp_id); goto done; } + + /* Group keys (ND-GTK) exchanged during NDP setup (§7.1.3.2): our GTK + * is the TX key bound to the local NDI; the peer's GTK is the RX key + * bound to the peer NDI. Best-effort — a GTK install failure must not + * tear down the working unicast datapath. */ + if (ndl->own_gtk_set && ndl->own_gtk_len) { + int gret = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, + own_ndi, ndl->own_gtk_keyid, 1, + ndl->own_gtk_rsc, NAN_KEY_RSC_LEN, + ndl->own_gtk, ndl->own_gtk_len, + NAN_KEY_ND_GTK); + if (gret != 0) { + ESP_LOGW(TAG, "NDP confirm: own GTK (TX) install failed (ndp_id=%d, ret=%d)", ndp_id, gret); + } else { + ESP_LOGI(TAG, "NDP confirm: own GTK (TX) installed (keyid=%d)", ndl->own_gtk_keyid); + } + ESP_LOG_BUFFER_HEXDUMP("## ND-GTK ", ndl->own_gtk, ndl->own_gtk_len, ESP_LOG_INFO); + } + if (ndl->gtk_set && ndl->gtk_len) { + int gret = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, + peer_ndi, ndl->gtk_keyid, 0, + ndl->gtk_rsc, NAN_KEY_RSC_LEN, + ndl->gtk, ndl->gtk_len, + NAN_KEY_ND_GTK); + if (gret != 0) { + ESP_LOGW(TAG, "NDP confirm: peer GTK (RX) install failed (ndp_id=%d, ret=%d)", ndp_id, gret); + } else { + ESP_LOGI(TAG, "NDP confirm: peer GTK (RX) installed (keyid=%d)", ndl->gtk_keyid); + } + } + + /* Own IGTK/BIGTK (TX) are installed once at NAN start (see + * nan_security_install_own_group_integrity_keys); not re-installed here, + * to preserve the blob's monotonic BIPN/IPN across the session. Only the + * peer RX keys are bound at NDP confirm. §7.1.3.3/§7.1.3.4; NMI==NDI today. + * Peer IGTK/BIGTK install RX-only against the peer NMI. seq (IPN/BIPN) + * starts at 0 for now; thread the peer's KDE IPN/BIPN once the blob + * consumes it for the BIP replay counter. */ + if (ndl->igtk_set && ndl->igtk_len) { + if (ndl->igtk_len != NAN_ND_GTK_LEN) { + ESP_LOGW(TAG, "NDP confirm: peer IGTK len=%d unsupported (BIP-CMAC-128 only); skipping", + ndl->igtk_len); + } else { + int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + peer_nmi, ndl->igtk_keyid, 0, + key_rsc, 6, + ndl->igtk, ndl->igtk_len, + NAN_KEY_ND_IGTK); + if (r != 0) { + ESP_LOGW(TAG, "NDP confirm: peer IGTK (RX) install failed, rc=0x%x (ndp_id=%d)", r, ndp_id); + } else { + ESP_LOGI(TAG, "NDP confirm: peer IGTK (RX) installed (keyid=%d)", ndl->igtk_keyid); + } + /* Peer IGTK bytes for sniffer MIC cross-check vs the peer's multicast SDFs. */ + ESP_LOG_BUFFER_HEXDUMP("## PEER ND-IGTK ", ndl->igtk, ndl->igtk_len, ESP_LOG_INFO); + } + } + if (ndl->bigtk_set && ndl->bigtk_len) { + if (ndl->bigtk_len != NAN_ND_GTK_LEN) { + ESP_LOGW(TAG, "NDP confirm: peer BIGTK len=%d unsupported (BIP-CMAC-128 only); skipping", + ndl->bigtk_len); + } else { + int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + peer_nmi, ndl->bigtk_keyid, 0, + key_rsc, 6, + ndl->bigtk, ndl->bigtk_len, + NAN_KEY_ND_BIGTK); + if (r != 0) { + ESP_LOGW(TAG, "NDP confirm: peer BIGTK (RX) install failed, rc=0x%x (ndp_id=%d)", r, ndp_id); + } else { + ESP_LOGI(TAG, "NDP confirm: peer BIGTK (RX) installed (keyid=%d)", ndl->bigtk_keyid); + } + /* Peer BIGTK bytes for sniffer MIC cross-check vs the peer's protected Beacons. */ + ESP_LOG_BUFFER_HEXDUMP("## PEER ND-BIGTK ", ndl->bigtk, ndl->bigtk_len, ESP_LOG_INFO); + } + } } #endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ evt->status = status; @@ -1506,6 +1629,7 @@ static struct nan_secure_dp_funcs s_nan_secure_dp_funcs = { .ndp_security_install_get_shared_desc_len = esp_nan_ndp_security_install_get_shared_desc_len, .get_ndp_resp_num_pmkids = esp_nan_get_ndp_resp_num_pmkids, .get_ndp_resp_shared_key_desc_len = esp_nan_get_ndp_resp_shared_key_desc_len, + .ndp_confirm_get_shared_desc_len = esp_nan_ndp_confirm_get_shared_desc_len, /* CSIA / SCIA construction */ .construct_csia = esp_nan_construct_csia, @@ -1686,6 +1810,24 @@ void esp_nan_action_start(esp_netif_t *nan_netif) }; esp_nan_internal_register_callbacks(&nan_cb); +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + /* Device-global group-management protection (IGTKSA/BIGTKSA), one per NMI, + * sourced from the NAN start config (wifi_nan_sync_config_t.group_mgmt_prot). + * The blob stores it at nan_start; we read it back here. Default on if the + * config can't be read, preserving protected-by-default behavior. */ + { + wifi_config_t nan_cfg = {0}; + s_nan_ctx.group_mgmt_prot = + (esp_wifi_get_config(WIFI_IF_NAN, &nan_cfg) == ESP_OK) + ? nan_cfg.nan.group_mgmt_prot : true; + } + /* Install the device-global IGTK/BIGTK for TX now (when enabled) so Beacons + * (BIGTK) and group-addressed SDFs (IGTK) are BIP-protected from the first + * frame, like iOS. The blob gates beacon BIP-TX on an active BIGTK index + * only (no NDP state), so installing here is sufficient. */ + nan_security_install_own_group_integrity_keys(); +#endif + ESP_LOGI(TAG, "NAN Discovery started."); os_event_group_clear_bits(nan_event_group, NAN_STOPPED_BIT); os_event_group_set_bits(nan_event_group, NAN_STARTED_BIT); @@ -2525,7 +2667,6 @@ esp_err_t esp_wifi_nan_datapath_resp(wifi_nan_datapath_resp_t *resp) ESP_LOGE(TAG, "Need NDP Indication before NDP Response can be sent"); goto fail; } - if (MACADDR_EQUAL(resp->peer_mac, null_mac)) { MACADDR_COPY(resp->peer_mac, ndl->peer_nmi); } diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h index c7ee0cd8a5a..69b4e997b08 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h @@ -130,6 +130,46 @@ extern void *s_nan_data_lock; #define NAN_KEY_INFO_ENC_KEY BIT(12) #define NAN_KEY_INFO_KEY_TYPE BIT(3) /* 1=Pairwise, 0=Group */ +/* NAN KDE OUIs and Data Types carried in the Key Data field (Wi-Fi Aware + * v4.0 §9.5.21.5 Table 126; formats per 802.11 Fig 12-36/12-42/12-47). */ +#define NAN_KDE_OUI_RSN_0 0x00 +#define NAN_KDE_OUI_RSN_1 0x0F +#define NAN_KDE_OUI_RSN_2 0xAC +#define NAN_KDE_OUI_WFA_0 0x50 +#define NAN_KDE_OUI_WFA_1 0x6F +#define NAN_KDE_OUI_WFA_2 0x9A +#define NAN_KDE_OUI_RSN 0x000FACUL +#define NAN_KDE_OUI_WFA 0x506F9AUL +#define NAN_KDE_TYPE_GTK 1 /* 00-0F-AC GTK KDE */ +#define NAN_KDE_TYPE_MAC 3 /* 00-0F-AC MAC address KDE */ +#define NAN_KDE_TYPE_IGTK 9 /* 00-0F-AC IGTK KDE */ +#define NAN_KDE_TYPE_BIGTK 14 /* 00-0F-AC BIGTK KDE */ +#define NAN_KDE_TYPE_NIK 36 /* 50-6F-9A NIK KDE */ +#define NAN_KDE_TYPE_KEY_LIFE 37 /* 50-6F-9A NAN Key Lifetime KDE */ + +/* KDE inner-prefix lengths (bytes before the actual key material). */ +#define NAN_KDE_HDR_LEN 6 /* DD(1) + len(1) + OUI(3) + DataType(1) */ +#define NAN_GTK_KDE_PREFIX_LEN 2 /* KeyID/Tx(1) + Reserved(1) */ +#define NAN_IGTK_KDE_PREFIX_LEN 8 /* KeyID(2) + IPN(6) */ +#define NAN_BIGTK_KDE_PREFIX_LEN 8 /* KeyID(2) + BIPN(6) */ + +/* CSIA Capabilities group-SA support (§9.5.21.2 Table 122, bits 1-2, bit 2 high + * order). Mirrors hostap nan_defs.h NAN_CS_INFO_CAPA_GTK_SUPP_*. */ +#define NAN_CSIA_CAP_GTK_SUPP_POS 1 +#define NAN_CSIA_CAP_GTK_SUPP_MASK 0x06 +#define NAN_CSIA_CAP_GTK_SUPP_NONE 0 /* 00: no group SA */ +#define NAN_CSIA_CAP_GTK_SUPP_IGTK 1 /* 01: GTKSA + IGTKSA */ +#define NAN_CSIA_CAP_GTK_SUPP_ALL 2 /* 10: GTKSA + IGTKSA + BIGTKSA */ + +/* ND-GTK is the data-path group key (CCMP-128). */ +#define NAN_ND_GTK_LEN 16 +/* Host-side bound for the group Key Data scratch buffers (plaintext + AES-wrap), + * sized for GTK+IGTK+BIGTK: GTK 24B + IGTK 30B + BIGTK 30B + optional Key Lifetime + * KDE(s), padded to a multiple of 8, + 8B NIST AES Key Wrap overhead (~104B worst + * case). NOT the descriptor-len reservation — the getters now return the EXACT + * per-NDP length, so the blob allocates exactly what the builder writes. */ +#define NAN_GROUP_KEY_DATA_MAX 128 + /* NCS-SK-128 only for now (Table 21): KCK 128 bits, KEK 128 bits, TK 128 bits, MIC 16 bytes */ #define NAN_NCS_SK_128_KCK_LEN 16 #define NAN_NCS_SK_128_KEK_LEN 16 @@ -138,11 +178,21 @@ extern void *s_nan_data_lock; #define NAN_NCS_SK_128_PTK_LEN (NAN_NCS_SK_128_KCK_LEN + NAN_NCS_SK_128_KEK_LEN + NAN_NCS_SK_128_TK_LEN) /* Internal key-install constants matching esp_wifi_set_sta_key_internal semantics. */ -#define NAN_WIFI_WPA_ALG_CCMP 3 +#define NAN_WIFI_WPA_ALG_CCMP 3 +#define NAN_WIFI_WPA_ALG_BIP_CMAC_128 7 /* IGTK/BIGTK BIP = blob WIFI_WPA_ALG_IGTK (confirmed by han2; 4 is SMS4) */ #define NAN_KEY_FLAG_RX BIT(2) #define NAN_KEY_FLAG_TX BIT(3) #define NAN_KEY_FLAG_PAIRWISE BIT(5) +/* NAN key-type selector passed as the last arg of esp_wifi_set_nan_key_internal; + * tells the blob which NAN SA the key belongs to. IGTK/BIGTK values are + * provisional — confirm with han2 before the lib bump. */ +#define NAN_KEY_ND_TK 0 +#define NAN_KEY_ND_GTK 1 +#define NAN_KEY_NM_TK 2 +#define NAN_KEY_ND_IGTK 3 +#define NAN_KEY_ND_BIGTK 4 + /* Handshake state */ enum nan_handshake_state { NAN_HANDSHAKE_IDLE = 0, @@ -195,6 +245,13 @@ struct peer_svc_info { * whose ND-PMKID matched the publisher SCIA. The initiator NDP-req path * uses this to pick the right credential for M1's pair-PMKID. */ uint8_t matched_cred_idx; + /* Set at SDF match if the publisher advertised an NCS-GTK suite in its CSIA; + * the initiator NDP-req path uses it (with our own group_data_prot) to + * decide whether to distribute a GTK during NDP setup. */ + uint8_t peer_group_data_cap: 1; + uint8_t peer_group_mgmt_cap: 1; /* peer advertised IGTKSA (CSIA caps bits 1-2 != 0) */ + uint8_t peer_group_bigtk_cap: 1; /* peer advertised BIGTKSA (CSIA caps bits 1-2 == 10) */ + uint8_t peer_sec_reserved: 5; #endif #if CONFIG_ESP_WIFI_NAN_PAIRING /* Peer NIK / cipher version / lifetime extracted from a NAN Shared Key @@ -273,20 +330,36 @@ struct ndl_info { uint8_t handshake_state; - /* Group key state (unsupported -- pairwise-only M1-M4 flow today; - * fields kept to match the spec-defined RSNA key descriptor layout - * and stay forward-compatible). */ + /* Received peer group keys (RX GTKSA). GTK protects group-addressed data + * frames the peer transmits; installed against the peer NDI. IGTK/BIGTK + * protect group-addressed management/Beacon frames (NMI plane). */ uint8_t gtk[NAN_GTK_MAX_LEN]; uint8_t igtk[NAN_GTK_MAX_LEN]; uint8_t bigtk[NAN_GTK_MAX_LEN]; uint8_t gtk_len; uint8_t igtk_len; uint8_t bigtk_len; + uint8_t gtk_keyid; /* peer GTK Key ID (1 or 2) */ + uint8_t igtk_keyid; /* peer IGTK Key ID (4 or 5) */ + uint8_t bigtk_keyid; /* peer BIGTK Key ID (6 or 7) */ + uint8_t gtk_rsc[NAN_KEY_RSC_LEN]; /* peer GTK RSC from Key RSC field */ uint8_t gtk_set: 1; uint8_t igtk_set: 1; uint8_t bigtk_set: 1; uint8_t group_keys_reserved: 5; + /* Own group key (TX GTKSA) distributed to the peer in M3 (initiator) or + * M4 (responder); installed against the local NDI as the TX group key. */ + uint8_t own_gtk[NAN_ND_GTK_LEN]; + uint8_t own_gtk_len; + uint8_t own_gtk_keyid; /* own GTK Key ID (1 or 2) */ + uint8_t own_gtk_rsc[NAN_KEY_RSC_LEN]; + uint8_t own_gtk_set: 1; + uint8_t gtk_required: 1; /* GTKSA negotiated for this NDP */ + uint8_t igtk_required: 1; /* IGTKSA negotiated for this NDP */ + uint8_t bigtk_required: 1; /* BIGTKSA negotiated for this NDP */ + uint8_t own_group_reserved: 4; + uint8_t key_rsc[NAN_KEY_RSC_LEN]; #endif }; @@ -304,6 +377,28 @@ typedef struct { #ifdef CONFIG_ESP_WIFI_NAN_SECURITY uint8_t own_nik[ESP_WIFI_NAN_NIK_LEN]; bool own_nik_valid; + /* Device-global IGTKSA/BIGTKSA (one per NMI, §7.1.3.3/§7.1.3.4). Generated + * once via os_get_random and reused across all secured NDPs; copied into + * each M3/M4 and installed against the local NMI. BIP-CMAC-128 (16-byte). + * On ESP the NMI and NDI are the same MAC today. */ + uint8_t own_igtk[NAN_ND_GTK_LEN]; + uint8_t own_igtk_ipn[6]; + uint8_t own_igtk_keyid; /* 4 or 5 */ + bool own_igtk_set; + uint8_t own_bigtk[NAN_ND_GTK_LEN]; + uint8_t own_bigtk_bipn[6]; + uint8_t own_bigtk_keyid; /* 6 or 7 */ + bool own_bigtk_set; + /* Device-global "support + use group management protection (IGTKSA/BIGTKSA)". + * One setting per NMI, not per service: Beacons are NMI-level and there is + * exactly one IGTKSA/BIGTKSA per NMI (§7.1.3.3/§7.1.3.4). Sourced from + * wifi_nan_sync_config_t.group_mgmt_prot at NAN start. When set it: forces + * GTKSA on every secured service (the CSIA caps field cannot encode IGTK/ + * BIGTK without GTKSA, §9.5.21.2 Table 122), generates own IGTK+BIGTK, + * advertises caps 0x04, and BIP-protects Beacons + multicast SDFs. + * Advertising never blocks a non-supporting peer — keys and protection are + * set up only after capability negotiation (§7.1.3.5). */ + bool group_mgmt_prot; uint8_t cached_nira_nonce[8]; uint8_t cached_nira_tag[8]; bool nira_cached; @@ -340,7 +435,8 @@ bool nan_compute_service_id(const char *service_name, uint8_t service_id[6]); uint32_t esp_nan_get_csia_len(uint16_t own_csid_bitmap, uint16_t peer_csid_bitmap); uint32_t esp_nan_get_scia_len(uint8_t num_pmkids); uint32_t esp_nan_get_shared_key_desc_attr_len(uint16_t key_data_len); -int esp_nan_ndp_security_install_get_shared_desc_len(void); +int esp_nan_ndp_security_install_get_shared_desc_len(uint8_t ndp_id, const uint8_t *peer_nmi); +int esp_nan_ndp_confirm_get_shared_desc_len(uint8_t ndp_id, const uint8_t *peer_nmi); uint8_t esp_nan_get_ndp_resp_num_pmkids(uint8_t ndp_id, const uint8_t *peer_nmi); uint32_t esp_nan_get_ndp_resp_shared_key_desc_len(uint8_t ndp_id, const uint8_t *peer_nmi); @@ -428,6 +524,11 @@ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, const struct peer_svc_info *peer_svc, const uint8_t *peer_nmi); +/* Generate (once) and TX-install the device-global IGTK/BIGTK so Beacons and + * group-addressed SDFs are BIP-protected from NAN start (§7.1.3.3/§7.1.3.4). + * Call once at NAN start; never per-NDP (would reset the blob's BIPN counter). */ +void nan_security_install_own_group_integrity_keys(void); + /* * Match subscriber discovery security to a publisher's params. * NCS-SK: Compare locally derived ND-PMKID (subscriber passphrase, publisher NMI) to diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_pairing.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_pairing.c index de1c7e36a75..eaaa6da324b 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_pairing.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_pairing.c @@ -510,6 +510,11 @@ int esp_nan_construct_nira(uint8_t *frm) #define NAN_PASN_KDE_OUI_TYPE_LIFETIME 37 #define NAN_PASN_KEY_LIFETIME_NIK_BIT BIT(3) #define NAN_ATTR_ID_SHARED_KEY_DESC 0x24 +/* iOS sends its NIK follow-up ~2.5-2.6 s after we derive the NM-TK; a 2 s window + * fired first and destructively removed the peer, so the late NIK arrived after + * teardown and the follow-up went out unprotected -> peer never started the NDP. + * 5 s lets the NIK land in time, so the cancel in the store-NIK path keeps the + * peer SA intact. */ #define NAN_PAIRING_NIK_FUP_TIMEOUT_SEC 5 struct nan_pairing_fup_ctx { diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index f25a2e70cd9..fde27c22890 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -24,6 +24,7 @@ #include "esp_nan.h" #include "utils/common.h" #include "crypto/sha256.h" +#include "crypto/aes_wrap.h" #include "nan_i.h" static const char *TAG = "nan_sec"; @@ -56,6 +57,8 @@ static struct { static struct { bool has_pmkid; bool has_csid; + bool peer_group_data; /* peer signalled group-data (GTK) support in its CSIA */ + uint8_t peer_caps; /* raw CSIA Capabilities byte; IGTK/BIGTK gating derives bits 1-2 */ uint8_t pub_id; uint8_t pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN]; uint16_t csid_bitmap; @@ -64,6 +67,12 @@ static struct { /* Spec Table 121: valid CSIDs are 1..8. Bit 0 and bits 9..15 are not assigned. */ #define NAN_CSID_VALID_BITMAP ((uint16_t)0x01FE) +/* NCS-GTK cipher-suite bits (group-addressed data). Advertised when a service + * sets group_data_prot; the basic default we generate/advertise is CCM-128. */ +#define NAN_CSID_GTK_BITS (WIFI_NAN_CSID_BIT_NCS_GTK_CCM_128 | \ + WIFI_NAN_CSID_BIT_NCS_GTK_GCM_256) +#define NAN_CSID_GTK_DEFAULT WIFI_NAN_CSID_BIT_NCS_GTK_CCM_128 + /* Sentinel for peer_svc->matched_cred_idx: no PMKID match remembered yet. */ #define NAN_NO_MATCHED_CRED 0xFF @@ -458,6 +467,34 @@ static bool nan_security_fill_from_paired_cache(struct ndl_info *ndl, const uint return false; } + /* Early-reject: if the NDP Request carried an ND-PMKID, our cached ND-PMK must reproduce it (§7.1.3.5) before we commit. */ + static const uint8_t zero_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN] = {0}; + if (memcmp(ndl->security_ctx.nd_pmkid, zero_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) != 0) { + uint8_t our_nmi[6]; + uint8_t service_id[6]; + struct own_svc_info *own_svc = nan_find_own_svc(ndl->publisher_id); + + if (esp_wifi_get_mac(WIFI_IF_NAN, our_nmi) != ESP_OK || !own_svc || + !own_svc->svc_name[0] || !nan_compute_service_id(own_svc->svc_name, service_id)) { + ESP_LOGW(TAG, "Paired ND-PMK: cannot verify peer ND-PMKID (own NMI/service unavailable for pub_id=%u), deferring to handshake MIC", + ndl->publisher_id); + } else { + uint8_t expected[ESP_WIFI_NAN_NDP_PMKID_LEN]; + /* Spec order (Initiator=peer, Responder=us), then reversed for interop, mirroring nan_match_pmkid(). */ + bool ok = (nan_derive_ndp_request_pmkid(paired->nd_pmk, peer_nmi, our_nmi, service_id, expected) && + memcmp(expected, ndl->security_ctx.nd_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) || + (nan_derive_ndp_request_pmkid(paired->nd_pmk, our_nmi, peer_nmi, service_id, expected) && + memcmp(expected, ndl->security_ctx.nd_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) == 0); + if (!ok) { + ESP_LOGE(TAG, "Paired ND-PMK rejected for peer "MACSTR": NDP-Request ND-PMKID does not match cached pairing key (csid=%u), secured NDP setup aborted", + MAC2STR(peer_nmi), paired->ndp_csid); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->security_ctx.nd_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN, ESP_LOG_WARN); + return false; + } + ESP_LOGD(TAG, "Paired ND-PMK: peer ND-PMKID verified for "MACSTR, MAC2STR(peer_nmi)); + } + } + ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; ndl->security_ctx.csid_bitmap = (uint16_t)(1u << paired->ndp_csid); memcpy(ndl->security_ctx.nd_pmk, paired->nd_pmk, ESP_WIFI_NAN_NDP_PMK_LEN); @@ -604,6 +641,330 @@ static int nan_build_rsna_key_descriptor(uint8_t *kd, uint16_t key_info_flags, return NAN_KEY_DESC_MIN_LEN; } +/*------------------------------------------------------------------------- + * Group Key Data (GTK/IGTK/BIGTK KDEs) — Wi-Fi Aware v4.0 §7.1.3.2/§7.1.3.5/ + * §9.5.21.5. Initiator distributes in M3, responder in M4; KDEs are always + * KEK-wrapped (NIST AES Key Wrap), never in clear. Structure mirrors hostap + * src/nan/nan_sec.c nan_sec_add_kdes(); IGTK/BIGTK are placeholders for now. + *-----------------------------------------------------------------------*/ + +/* True if a GTKSA was negotiated for this NDP. gtk_required is the explicit + * result of (our service has group_data_prot) AND (peer advertised NCS-GTK), + * computed at NDL finalize on both roles. We deliberately do NOT also gate on + * security_ctx.csid_bitmap: that field carries the advertised GTK bit on some + * paths and would over-fire when only one side enabled group protection. */ +static bool nan_ndl_gtk_negotiated(const struct ndl_info *ndl) +{ + return ndl->gtk_required; +} + +/* IGTK/BIGTK negotiation gates. Per §7.1.3.5: include the IGTK/BIGTK KDE iff BOTH + * peers advertise the capability in their CSIA. igtk_required/bigtk_required are + * set at NDL finalize on both roles from (our service has group_mgmt_prot) AND + * (peer advertised IGTKSA/BIGTKSA in its CSIA caps byte). */ +static bool nan_ndl_igtk_negotiated(const struct ndl_info *ndl) +{ + return ndl->igtk_required; +} + +static bool nan_ndl_bigtk_negotiated(const struct ndl_info *ndl) +{ + return ndl->bigtk_required; +} + +/* Lazily generate the local data-path GTK (CCMP-128). Fresh GTK starts at RSC 0. */ +static int nan_ensure_own_gtk(struct ndl_info *ndl) +{ + if (ndl->own_gtk_set) { + return 0; + } + if (os_get_random(ndl->own_gtk, NAN_ND_GTK_LEN) != 0) { + return -1; + } + ndl->own_gtk_len = NAN_ND_GTK_LEN; + ndl->own_gtk_keyid = 1; /* spec allows Key ID 1 or 2 */ + memset(ndl->own_gtk_rsc, 0, NAN_KEY_RSC_LEN); + ndl->own_gtk_set = 1; + return 0; +} + +/* Lazily generate the device-global IGTK/BIGTK (BIP-CMAC-128, §7.1.3.3/§7.1.3.4). + * Exactly one key per local NMI, reused across all secured NDPs; IPN/BIPN start + * at 0. Generated by this device (transmitter) per spec. */ +static int nan_ensure_own_igtk(void) +{ + if (s_nan_ctx.own_igtk_set) { + return 0; + } + if (os_get_random(s_nan_ctx.own_igtk, NAN_ND_GTK_LEN) != 0) { + return -1; + } + s_nan_ctx.own_igtk_keyid = 4; /* spec allows Key ID 4 or 5 */ + memset(s_nan_ctx.own_igtk_ipn, 0, sizeof(s_nan_ctx.own_igtk_ipn)); + s_nan_ctx.own_igtk_set = true; + return 0; +} + +static int nan_ensure_own_bigtk(void) +{ + if (s_nan_ctx.own_bigtk_set) { + return 0; + } + if (os_get_random(s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN) != 0) { + return -1; + } + s_nan_ctx.own_bigtk_keyid = 6; /* spec allows Key ID 6 or 7 */ + memset(s_nan_ctx.own_bigtk_bipn, 0, sizeof(s_nan_ctx.own_bigtk_bipn)); + s_nan_ctx.own_bigtk_set = true; + return 0; +} + +/* Generate (once) and TX-install the device-global IGTK/BIGTK at NAN start, so + * Beacons (BIGTK) and group-addressed SDFs (IGTK) are BIP-protected from the + * first frame — matching peers (e.g. iOS) that protect from NAN start, not just + * after the first NDP. The keys are device-global per §7.1.3.3/§7.1.3.4 and the + * same bytes are later distributed KEK-wrapped in M3/M4. Install MUST happen + * only here (not per-NDP), else re-installing with IPN/BIPN=0 would reset the + * blob's monotonic replay counter mid-session. Best-effort: a failed install + * warns but does not block NAN start. */ +void nan_security_install_own_group_integrity_keys(void) +{ + uint8_t own_nmi[6]; + if (!s_nan_ctx.group_mgmt_prot) { + return; /* group-management protection disabled device-wide */ + } + if (esp_wifi_get_mac(WIFI_IF_NAN, own_nmi) != ESP_OK) { + ESP_LOGW(TAG, "NAN start: get NMI failed; own IGTK/BIGTK TX not installed"); + return; + } + if (nan_ensure_own_igtk() == 0 && s_nan_ctx.own_igtk_set) { + int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + own_nmi, s_nan_ctx.own_igtk_keyid, 1, + s_nan_ctx.own_igtk_ipn, sizeof(s_nan_ctx.own_igtk_ipn), + s_nan_ctx.own_igtk, NAN_ND_GTK_LEN, + NAN_KEY_ND_IGTK); + if (r != 0) { + ESP_LOGW(TAG, "NAN start: own IGTK (TX) install failed, rc=0x%x", r); + } else { + ESP_LOGI(TAG, "NAN start: own IGTK (TX) installed (keyid=%d)", s_nan_ctx.own_igtk_keyid); + } + ESP_LOG_BUFFER_HEXDUMP("## ND-IGTK ", s_nan_ctx.own_igtk, NAN_ND_GTK_LEN, ESP_LOG_INFO); + } + if (nan_ensure_own_bigtk() == 0 && s_nan_ctx.own_bigtk_set) { + int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + own_nmi, s_nan_ctx.own_bigtk_keyid, 1, + s_nan_ctx.own_bigtk_bipn, sizeof(s_nan_ctx.own_bigtk_bipn), + s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN, + NAN_KEY_ND_BIGTK); + if (r != 0) { + ESP_LOGW(TAG, "NAN start: own BIGTK (TX) install failed, rc=0x%x", r); + } else { + ESP_LOGI(TAG, "NAN start: own BIGTK (TX) installed (keyid=%d)", s_nan_ctx.own_bigtk_keyid); + } + ESP_LOG_BUFFER_HEXDUMP("## ND-BIGTK ", s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN, ESP_LOG_INFO); + } +} + +/* NAN KDE header (802.11 Fig 12-34: DD len OUI(3) DataType(1)); mirrors hostap + * nan_add_kde_hdr(). data_len excludes the DD/len/OUI/type bytes. */ +static uint8_t *nan_kde_put_hdr(uint8_t *p, uint8_t data_type, uint8_t data_len) +{ + *p++ = 0xDD; + *p++ = (uint8_t)(4 + data_len); /* OUI(3) + DataType(1) + data */ + *p++ = NAN_KDE_OUI_RSN_0; + *p++ = NAN_KDE_OUI_RSN_1; + *p++ = NAN_KDE_OUI_RSN_2; + *p++ = data_type; + return p; +} + +/* IGTK KDE (§9.5.21.5: KeyID(2 LE) IPN(6) IGTK); mirrors hostap nan_sec_igtk_kde(). + * Carries the device-global IGTK (BIP-CMAC-128); the peer installs it RX-only to + * verify our group-addressed management frames. */ +static int nan_append_igtk_kde(struct ndl_info *ndl, uint8_t **p, const uint8_t *end) +{ + if (!nan_ndl_igtk_negotiated(ndl)) { + return 0; + } + if ((size_t)(end - *p) < NAN_KDE_HDR_LEN + NAN_IGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN) { + return -1; + } + uint8_t *q = nan_kde_put_hdr(*p, NAN_KDE_TYPE_IGTK, + NAN_IGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN); + *q++ = s_nan_ctx.own_igtk_keyid & 0xFF; *q++ = 0; /* KeyID (2, LE) — 4/5 */ + memcpy(q, s_nan_ctx.own_igtk_ipn, 6); q += 6; /* IPN (6) */ + memcpy(q, s_nan_ctx.own_igtk, NAN_ND_GTK_LEN); q += NAN_ND_GTK_LEN; + *p = q; + return 0; +} + +/* BIGTK KDE (§9.5.21.5: KeyID(2 LE) BIPN(6) BIGTK); mirrors hostap nan_sec_bigtk_kde(). + * Carries the device-global BIGTK (BIP-CMAC-128); the peer installs it RX-only to + * verify our protected Beacon frames. */ +static int nan_append_bigtk_kde(struct ndl_info *ndl, uint8_t **p, const uint8_t *end) +{ + if (!nan_ndl_bigtk_negotiated(ndl)) { + return 0; + } + if ((size_t)(end - *p) < NAN_KDE_HDR_LEN + NAN_BIGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN) { + return -1; + } + uint8_t *q = nan_kde_put_hdr(*p, NAN_KDE_TYPE_BIGTK, + NAN_BIGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN); + *q++ = s_nan_ctx.own_bigtk_keyid & 0xFF; *q++ = 0; /* KeyID (2, LE) — 6/7 */ + memcpy(q, s_nan_ctx.own_bigtk_bipn, 6); q += 6; /* BIPN (6) */ + memcpy(q, s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN); q += NAN_ND_GTK_LEN; + *p = q; + return 0; +} + +/* GTK KDE (§7.1.3.2/§9.5.21.5, 802.11 Fig 12-36); mirrors hostap nan_sec_gtk_kde(). + * Tx bit stays 0: the peer installs this as an RX-only group key. */ +static int nan_append_gtk_kde(struct ndl_info *ndl, uint8_t **p, const uint8_t *end) +{ + if (!ndl->own_gtk_set || !ndl->own_gtk_len) { + return 0; + } + if (ndl->own_gtk_keyid > 3) { /* CCMP/GCMP Key ID range (§7.1.3.2: 1 or 2) */ + ESP_LOGW(TAG, "GTK: invalid Key ID %u", ndl->own_gtk_keyid); + return -1; + } + if ((size_t)(end - *p) < NAN_KDE_HDR_LEN + NAN_GTK_KDE_PREFIX_LEN + ndl->own_gtk_len) { + return -1; + } + uint8_t *q = nan_kde_put_hdr(*p, NAN_KDE_TYPE_GTK, + NAN_GTK_KDE_PREFIX_LEN + ndl->own_gtk_len); + *q++ = ndl->own_gtk_keyid & 0x03; /* KeyID (b0-1); Tx (b2)=0; b3-7 reserved */ + *q++ = 0; /* Reserved */ + memcpy(q, ndl->own_gtk, ndl->own_gtk_len); + *p = q + ndl->own_gtk_len; + return 0; +} + +/* NIST AES Key Wrap of Key Data with the ND-KEK. Pads the plaintext to >=16 + * octets and a multiple of 8 (0xDD then 0x00, per §12.7.2). Cipher = padded+8. */ +static int nan_kek_wrap_key_data(struct ndl_info *ndl, const uint8_t *plain, + size_t plain_len, uint8_t *out, size_t out_cap, + size_t *out_len) +{ + uint8_t pad[NAN_GROUP_KEY_DATA_MAX]; + size_t padded = plain_len; + if (padded < 16) { + padded = 16; + } + if (padded % 8) { + padded = (padded + 7) & ~((size_t)7); + } + if (padded > sizeof(pad) || (padded + 8) > out_cap) { + return -1; + } + memcpy(pad, plain, plain_len); + if (padded > plain_len) { + pad[plain_len] = 0xDD; + memset(pad + plain_len + 1, 0, padded - plain_len - 1); + } + if (aes_wrap(ndl->nd_kek, ndl->kek_len, (int)(padded / 8), pad, out) != 0) { + return -1; + } + *out_len = padded + 8; + return 0; +} + +/* NIST AES Key Unwrap of received Key Data with the ND-KEK. Plain = cipher-8. */ +static int nan_kek_unwrap_key_data(struct ndl_info *ndl, const uint8_t *cipher, + size_t cipher_len, uint8_t *out, size_t out_cap, + size_t *out_len) +{ + if (cipher_len < 24 || (cipher_len % 8) != 0) { /* >=16 plaintext + 8 wrap */ + return -1; + } + size_t plain_len = cipher_len - 8; + if (plain_len > out_cap) { + return -1; + } + if (aes_unwrap(ndl->nd_kek, ndl->kek_len, (int)(plain_len / 8), cipher, out) != 0) { + return -1; + } + *out_len = plain_len; + return 0; +} + +/* Build the local group Key Data (KDE order IGTK, BIGTK, GTK per upstream), + * KEK-wrap it, and patch Encrypted-Data bit + Key Data Length + Key RSC into + * the 95-byte descriptor in place. Returns the extended descriptor length, or + * NAN_KEY_DESC_MIN_LEN (pairwise-only) on negotiation-off or any error so the + * handshake still completes. Mirrors hostap nan_sec_add_kdes(); §7.1.3.5: KDEs + * are sent only KEK-wrapped, never in clear. */ +static int nan_append_own_group_kdes(struct ndl_info *ndl, uint8_t *key_desc, size_t desc_cap) +{ + bool want_gtk = nan_ndl_gtk_negotiated(ndl); + bool want_igtk = nan_ndl_igtk_negotiated(ndl); + bool want_bigtk = nan_ndl_bigtk_negotiated(ndl); + if (!want_gtk && !want_igtk && !want_bigtk) { + return NAN_KEY_DESC_MIN_LEN; + } + if (!ndl->ptk_set) { + ESP_LOGW(TAG, "Group KDEs [%s%s%s]: PTK/KEK not set; sending without group keys", + want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); + return NAN_KEY_DESC_MIN_LEN; + } + if (want_gtk && nan_ensure_own_gtk(ndl) != 0) { + ESP_LOGW(TAG, "GTK: own key generation failed; sending without group keys"); + return NAN_KEY_DESC_MIN_LEN; + } + if (want_igtk && nan_ensure_own_igtk() != 0) { + ESP_LOGW(TAG, "IGTK: own key generation failed; sending without group keys"); + return NAN_KEY_DESC_MIN_LEN; + } + if (want_bigtk && nan_ensure_own_bigtk() != 0) { + ESP_LOGW(TAG, "BIGTK: own key generation failed; sending without group keys"); + return NAN_KEY_DESC_MIN_LEN; + } + + uint8_t plain[NAN_GROUP_KEY_DATA_MAX]; + uint8_t *p = plain; + const uint8_t *end = plain + sizeof(plain); + if (nan_append_igtk_kde(ndl, &p, end) < 0 || + nan_append_bigtk_kde(ndl, &p, end) < 0 || + nan_append_gtk_kde(ndl, &p, end) < 0) { + ESP_LOGW(TAG, "Group KDEs [%s%s%s]: assembly failed; sending without group keys", + want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); + return NAN_KEY_DESC_MIN_LEN; + } + + size_t plain_len = (size_t)(p - plain); + if (plain_len == 0) { + return NAN_KEY_DESC_MIN_LEN; /* nothing negotiated to send */ + } + + size_t wrapped_len = 0; + if (nan_kek_wrap_key_data(ndl, plain, plain_len, + &key_desc[NAN_KEY_DESC_DATA_OFF], + desc_cap > NAN_KEY_DESC_DATA_OFF ? + desc_cap - NAN_KEY_DESC_DATA_OFF : 0, + &wrapped_len) != 0) { + ESP_LOGW(TAG, "Group KDEs [%s%s%s]: KEK wrap failed or no headroom; sending without group keys", + want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); + return NAN_KEY_DESC_MIN_LEN; + } + + uint16_t key_info = (key_desc[NAN_KEY_DESC_KEY_INFO_OFF] << 8) | + key_desc[NAN_KEY_DESC_KEY_INFO_OFF + 1]; + key_info |= NAN_KEY_INFO_ENC_KEY; + key_desc[NAN_KEY_DESC_KEY_INFO_OFF] = (key_info >> 8) & 0xFF; + key_desc[NAN_KEY_DESC_KEY_INFO_OFF + 1] = key_info & 0xFF; + key_desc[NAN_KEY_DESC_DATA_LEN_OFF] = (wrapped_len >> 8) & 0xFF; + key_desc[NAN_KEY_DESC_DATA_LEN_OFF + 1] = wrapped_len & 0xFF; + + /* Key RSC carries the GTK's RSC when a GTK KDE is present (§7.1.3.5). */ + memcpy(&key_desc[NAN_KEY_DESC_RSC_OFF], ndl->own_gtk_rsc, NAN_KEY_RSC_LEN); + + ESP_LOGI(TAG, "Group Key Data wrapped: %u plain -> %u wrapped bytes, KDEs=[%s%s%s]", + (unsigned)plain_len, (unsigned)wrapped_len, + want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); + return NAN_KEY_DESC_MIN_LEN + (int)wrapped_len; +} + /* * Internal SCIA builder shared by Publish SDF and NDP-Response paths. * Per-entry layout: Len(2) + Type(1) + PubID(1) + Value(PMKID_LEN) = 20 bytes. @@ -636,7 +997,12 @@ static int nan_build_scia_attr(uint8_t *frm, uint8_t pub_id, p += ESP_WIFI_NAN_NDP_PMKID_LEN; } - return (int)(p - frm); + int written = (int)(p - frm); + /* + ESP_LOGI(TAG, "SCIA construct: frm=%p wrote=%d (hdr3+20*num_pmkids, num_pmkids=%u)", + frm, written, num_pmkids); + */ + return written; } /* @@ -871,10 +1237,24 @@ esp_err_t nan_derive_security_params(const char *service_name, } out_derived[i].type = WIFI_NAN_SECURITY_ENCRYPTED; out_derived[i].csid_bitmap = (uint16_t)(1u << cred->csid); + /* Advertise the basic-default NCS-GTK suite alongside the credential's + * PMK cipher when the service enables group-addressed data protection. + * The blob unions derived_security[].csid_bitmap into the on-air CSIA, + * so this is what makes us announce GTK support (§7.1.3.5). The bit is + * masked back out for the pairwise/link cipher query (see + * nan_get_ndp_security_csid). */ + if (sec_cfg->group_data_prot) { + out_derived[i].csid_bitmap |= NAN_CSID_GTK_DEFAULT; + } out_derived[i].group_data_prot = sec_cfg->group_data_prot; out_derived[i].group_mgmt_prot = sec_cfg->group_mgmt_prot; } + if (sec_cfg->group_data_prot) { + ESP_LOGI(TAG, "NAN GTK: advertising NCS-GTK-CCM-128 (group_data_prot=1) for svc='%s'", + service_name); + } + /* Mirror the derived material into the host's own_svc_info slot for this * service (claimed before the blob's publish/subscribe call). This lets * host paths — nan_match_pmkid (responder M1) and the NDL seeding at @@ -913,6 +1293,16 @@ uint16_t nan_get_ndp_security_csid(uint8_t ndp_id, const uint8_t *peer_nmi) struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); uint16_t csid = ndl ? ndl->security_ctx.csid_bitmap : 0; NAN_DATA_UNLOCK(); + /* Return the FULL negotiated bitmap, INCLUDING NCS-GTK when group-addressed + * data protection is in use, so the on-air NDP Request/Response CSIA + * advertises the group cipher and the peer can detect our group-data support + * (required for symmetric GTK distribution and third-party/iOS/Android + * interop). Safe to include NCS-GTK here: the blob treats this value as an + * opaque bitmap that it passes straight to the host CSIA callbacks + * (construct_csia / get_csia_len) and never interprets individual bits + * (confirmed by han2). Pairwise/link cipher selection and ND-TK install are + * host-driven and do not read this field; the group key has its own install + * path (NAN_KEY_ND_GTK) and gtk_required gate. */ return csid; } @@ -933,6 +1323,19 @@ bool nan_security_service_match(const struct own_svc_info *own_svc, const wifi_nan_discovery_security_params_t *cfg = &own_svc->user_cfg; + /* Remember whether this publisher signalled group-data (GTK) support, so the + * initiator NDP-req path can gate GTK distribution on mutual support. The + * spec-correct signal is the CSIA Capabilities byte (parsed into + * group_data_prot by esp_nan_parse_publish_security); an NCS-GTK cipher-suite + * ID is the legacy Android/ESP signal and is OR'd in for interop. */ + peer_svc->peer_group_data_cap = (peer_sec->group_data_prot || + (peer_sec->csid_bitmap & NAN_CSID_GTK_BITS)) ? 1 : 0; + /* IGTK/BIGTK (group management) support comes from the CSIA Capabilities + * byte (parsed into group_mgmt_prot/group_bigtk_prot by + * esp_nan_parse_publish_security): IGTKSA = bits 1-2 != 0, BIGTKSA = 10. */ + peer_svc->peer_group_mgmt_cap = peer_sec->group_mgmt_prot ? 1 : 0; + peer_svc->peer_group_bigtk_cap = peer_sec->group_bigtk_prot ? 1 : 0; + if (cfg->num_credentials == 0 || cfg->num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { return false; @@ -1117,6 +1520,35 @@ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; ndl->security_ctx.csid_bitmap = (uint16_t)(1u << ndp_csid); + /* Distribute a GTK in M3 only if we enabled group_data_prot AND the + * publisher advertised an NCS-GTK suite (recorded at SDF match). */ + ndl->gtk_required = (cfg->group_data_prot && peer_svc && + peer_svc->peer_group_data_cap) ? 1 : 0; + /* Advertise NCS-GTK in the NDP-Request CSIA so any responder (incl. + * third-party/iOS/Android) can detect our group-data support and + * reciprocate. Carried in ndl->security_ctx.csid_bitmap, which + * nan_get_ndp_security_csid() returns verbatim to the blob for the on-air + * M1/M2 CSIA. Pairwise cipher selection / ND-TK install do not read this + * field, so including the group bit here is safe. */ + if (ndl->gtk_required) { + ndl->security_ctx.csid_bitmap |= NAN_CSID_GTK_DEFAULT; + } + if (cfg->group_data_prot) { + ESP_LOGI(TAG, "NDP GTK: %s (initiator) - own group_prot=1, peer NCS-GTK=%d", + ndl->gtk_required ? "negotiated" : "NOT negotiated", + (peer_svc && peer_svc->peer_group_data_cap) ? 1 : 0); + } + /* IGTK distributed in M3 iff we enabled group_mgmt_prot AND the publisher + * advertised IGTKSA; BIGTK additionally requires BIGTKSA (§7.1.3.5). */ + bool peer_igtk = peer_svc && peer_svc->peer_group_mgmt_cap; + bool peer_bigtk = peer_svc && peer_svc->peer_group_bigtk_cap; + ndl->igtk_required = (s_nan_ctx.group_mgmt_prot && peer_igtk) ? 1 : 0; + ndl->bigtk_required = (s_nan_ctx.group_mgmt_prot && peer_bigtk) ? 1 : 0; + if (s_nan_ctx.group_mgmt_prot) { + ESP_LOGI(TAG, "NDP IGTK/BIGTK: igtk=%s bigtk=%s (initiator) - peer igtk=%d bigtk=%d", + ndl->igtk_required ? "yes" : "no", ndl->bigtk_required ? "yes" : "no", + peer_igtk, peer_bigtk); + } memcpy(ndl->security_ctx.nd_pmk, pmk, ESP_WIFI_NAN_NDP_PMK_LEN); memcpy(ndl->security_ctx.nd_pmkid, pair_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN); @@ -1153,8 +1585,27 @@ int esp_nan_construct_csia(uint8_t *frm, uint8_t pub_id, uint16_t own_csid_bitma *p++ = attr_len & 0xFF; *p++ = (attr_len >> 8) & 0xFF; - /* Capabilities byte (0x4 set for iOS interop) */ - *p++ = 0x4; + /* Capabilities group-SA bits (§9.5.21.2 Table 122) are strictly nested: + * 00 = none, 01 = GTKSA+IGTKSA, 10 = GTKSA+IGTKSA+BIGTKSA. + * There is no "IGTK/BIGTK without GTKSA" codepoint, so: + * - device-global group_mgmt_prot set -> 10 (BIGTK forces GTK+IGTK; we also + * force GTKSA on every secured service, see nan_claim_own_svc_slot); + * - else if this CSIA carries a GTK suite -> 01 (GTKSA mandates IGTKSA); + * - else -> 00. + * Advertising support never blocks a peer that lacks protection: the keys and + * frame protection are set up only AFTER mutual capability negotiation — the + * IGTK/BIGTK/GTK KDEs are exchanged iff BOTH peers advertise support + * (§7.1.3.5), and a non-supporting peer ignores the unknown MME elements and + * still connects. */ + uint8_t grp_caps; + if (s_nan_ctx.group_mgmt_prot) { + grp_caps = (uint8_t)(NAN_CSIA_CAP_GTK_SUPP_ALL << NAN_CSIA_CAP_GTK_SUPP_POS); /* 0x04 (10) */ + } else if (own_csid_bitmap & NAN_CSID_GTK_DEFAULT) { + grp_caps = (uint8_t)(NAN_CSIA_CAP_GTK_SUPP_IGTK << NAN_CSIA_CAP_GTK_SUPP_POS); /* 0x02 (01) */ + } else { + grp_caps = NAN_CSIA_CAP_GTK_SUPP_NONE; /* 0x00 (00) */ + } + *p++ = grp_caps; /* Cipher Suite ID list: [CSID(1)] [Publish ID(1)] */ for (uint8_t csid = 1; csid <= 8; csid++) { @@ -1164,7 +1615,12 @@ int esp_nan_construct_csia(uint8_t *frm, uint8_t pub_id, uint16_t own_csid_bitma } } - return (int)(p - frm); + int written = (int)(p - frm); + /* + ESP_LOGI(TAG, "CSIA construct: frm=%p wrote=%d (hdr3+cap1+2*csids=%u, bitmap=0x%04x)", + frm, written, num_csids, csid_bitmap); + */ + return written; } int esp_nan_construct_scia_publish(uint8_t *frm, uint8_t pub_id, @@ -1228,8 +1684,10 @@ uint32_t esp_nan_get_csia_len(uint16_t own_csid_bitmap, uint16_t peer_csid_bitma } uint8_t num_csids = __builtin_popcount(csid_bitmap); uint32_t len = 3 + 1 + 2 * num_csids; - ESP_LOGD(TAG, "GET CSIA LEN: %lu (own=0x%04x, peer=0x%04x, num_csids=%d)", - len, own_csid_bitmap, peer_csid_bitmap, num_csids); + /* + ESP_LOGI(TAG, "CSIA len getter -> %lu (own=0x%04x, peer=0x%04x, effective=0x%04x, num_csids=%d)", + len, own_csid_bitmap, peer_csid_bitmap, csid_bitmap, num_csids); + */ return len; } @@ -1243,7 +1701,7 @@ uint32_t esp_nan_get_scia_len(uint8_t num_pmkids) return 0; } uint32_t len = 3 + 20 * num_pmkids; - ESP_LOGD(TAG, "GET SCIA LEN: %lu (num_pmkids=%d)", len, num_pmkids); + // ESP_LOGI(TAG, "SCIA len getter -> %lu (num_pmkids=%d)", len, num_pmkids); return len; } @@ -1256,9 +1714,112 @@ uint32_t esp_nan_get_shared_key_desc_attr_len(uint16_t key_data_len) return total; } -int esp_nan_ndp_security_install_get_shared_desc_len(void) +/* Which group KDEs a key descriptor carries (for sizing + logging). */ +#define NAN_KDE_PRESENT_GTK BIT(0) +#define NAN_KDE_PRESENT_IGTK BIT(1) +#define NAN_KDE_PRESENT_BIGTK BIT(2) + +/* Exact wrapped group Key Data length this NDL will emit — mirrors what + * nan_append_own_group_kdes() writes, with NO side effects (does not generate the + * GTK). Sets *kde_mask to the included KDEs. 0 = no group keys (pairwise-only). + * Caller holds the lock. Keep in lockstep with nan_append_{igtk,bigtk,gtk}_kde(). */ +static size_t nan_group_key_data_wrapped_len(const struct ndl_info *ndl, uint8_t *kde_mask) { - return (int)esp_nan_get_shared_key_desc_attr_len(0); + uint8_t mask = 0; + size_t plain = 0; + if (!ndl || !ndl->ptk_set) { + if (kde_mask) { + *kde_mask = 0; + } + return 0; /* no KEK yet -> nothing can be wrapped */ + } + /* Order matches the builder: IGTK, BIGTK, GTK. Each branch contributes iff + * its negotiation gate (igtk/bigtk/gtk_required) fired for this NDP. */ + if (nan_ndl_igtk_negotiated(ndl)) { + plain += NAN_KDE_HDR_LEN + NAN_IGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN; + mask |= NAN_KDE_PRESENT_IGTK; + } + if (nan_ndl_bigtk_negotiated(ndl)) { + plain += NAN_KDE_HDR_LEN + NAN_BIGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN; + mask |= NAN_KDE_PRESENT_BIGTK; + } + if (nan_ndl_gtk_negotiated(ndl)) { + plain += NAN_KDE_HDR_LEN + NAN_GTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN; + mask |= NAN_KDE_PRESENT_GTK; + } + if (kde_mask) { + *kde_mask = mask; + } + if (plain == 0) { + return 0; + } + size_t padded = plain < 16 ? 16 : plain; /* NIST AES Key Wrap minimum */ + if (padded % 8) { + padded = (padded + 7) & ~((size_t)7); + } + return padded + 8; /* + AES-Key-Wrap overhead */ +} + +/* INFO log of the descriptor length and which group KDEs it carries, so on-device + * logs show what was sized/sent (not a silent length). */ +static void nan_log_group_desc_len(const char *msg, uint8_t ndp_id, int ret, + uint16_t key_data_len, uint8_t kde_mask, + bool found, bool ptk_set) +{ + if (!found) { + ESP_LOGW(TAG, "%s desc len: no NDL (ndp_id=%d) -> len=%d (no Key Data)", + msg, ndp_id, ret); + } else if (key_data_len == 0) { + ESP_LOGI(TAG, "%s desc len=%d (ndp_id=%d): no group KDEs (ptk_set=%d)", + msg, ret, ndp_id, ptk_set); + } else { + ESP_LOGI(TAG, "%s desc len=%d (ndp_id=%d): Key Data=%u KDEs=[%s%s%s]", + msg, ret, ndp_id, key_data_len, + (kde_mask & NAN_KDE_PRESENT_GTK) ? "GTK " : "", + (kde_mask & NAN_KDE_PRESENT_IGTK) ? "IGTK " : "", + (kde_mask & NAN_KDE_PRESENT_BIGTK) ? "BIGTK " : ""); + } +} + +/* Exact M4 (NDP Security Install) Shared Key Descriptor length for this NDP, so the + * blob allocates exactly what the builder writes (no on-air zero-padding). */ +int esp_nan_ndp_security_install_get_shared_desc_len(uint8_t ndp_id, const uint8_t *peer_nmi) +{ + uint16_t key_data_len = 0; + uint8_t kde_mask = 0; + bool found = false, ptk = false; + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (ndl) { + found = true; + ptk = ndl->ptk_set; + key_data_len = (uint16_t)nan_group_key_data_wrapped_len(ndl, &kde_mask); + } + NAN_DATA_UNLOCK(); + + int ret = (int)esp_nan_get_shared_key_desc_attr_len(key_data_len); + nan_log_group_desc_len("M4 Security Install", ndp_id, ret, key_data_len, kde_mask, found, ptk); + return ret; +} + +/* Exact M3 (NDP Confirm) length for the initiator path; same contract as M4. */ +int esp_nan_ndp_confirm_get_shared_desc_len(uint8_t ndp_id, const uint8_t *peer_nmi) +{ + uint16_t key_data_len = 0; + uint8_t kde_mask = 0; + bool found = false, ptk = false; + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (ndl) { + found = true; + ptk = ndl->ptk_set; + key_data_len = (uint16_t)nan_group_key_data_wrapped_len(ndl, &kde_mask); + } + NAN_DATA_UNLOCK(); + + int ret = (int)esp_nan_get_shared_key_desc_attr_len(key_data_len); + nan_log_group_desc_len("M3 Confirm", ndp_id, ret, key_data_len, kde_mask, found, ptk); + return ret; } int esp_nan_get_ndp_resp_shared_key_desc(uint8_t *buf, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi) @@ -1377,14 +1938,12 @@ int esp_nan_get_ndp_security_install_key_desc(uint8_t *buf, size_t buf_len, uint uint8_t *p = buf; *p++ = NAN_ATTR_ID_SHARED_KEY_DESC; - { - uint16_t body_len = 1 + NAN_KEY_DESC_MIN_LEN; - *p++ = body_len & 0xFF; - *p++ = (body_len >> 8) & 0xFF; - } + uint8_t *len_field = p; /* backpatched once Key Data length is known */ + p += 2; *p++ = ndl->publisher_id; - int n = nan_build_rsna_key_descriptor(p, + uint8_t *key_desc = p; + int n = nan_build_rsna_key_descriptor(key_desc, NAN_KEY_INFO_MIC | NAN_KEY_INFO_SECURE | NAN_KEY_INFO_INSTALL, @@ -1396,11 +1955,18 @@ int esp_nan_get_ndp_security_install_key_desc(uint8_t *buf, size_t buf_len, uint return 0; } - n = 3 + 1 + n; + /* Responder distributes its GTK in M4 (§7.1.3.2). Buffer headroom comes + * from esp_nan_ndp_security_install_get_shared_desc_len(); falls back to + * pairwise-only if the blob under-allocated the buffer. */ + n = nan_append_own_group_kdes(ndl, key_desc, buf_len - 4); + + uint16_t body_len = 1 + (uint16_t)n; + len_field[0] = body_len & 0xFF; + len_field[1] = (body_len >> 8) & 0xFF; NAN_DATA_UNLOCK(); - ESP_LOGD(TAG, "NDP M4 Key Desc: built for ndp_id=%d", ndp_id); - return n; + ESP_LOGD(TAG, "NDP M4 Key Desc: built (key_desc=%d bytes) for ndp_id=%d", n, ndp_id); + return 3 + 1 + n; } int esp_nan_update_ndp_resp_mic(uint8_t *m2_body, size_t body_len, uint8_t *key_desc_attr, @@ -1456,6 +2022,24 @@ void esp_nan_parse_ndp_csia(void *frm, size_t buf_len, wifi_nan_security_params_ s_pending_scia.has_csid = true; s_pending_scia.pub_id = pub_id; s_pending_scia.csid_bitmap = accum; + /* Peer wants group-data (GTK) protection if it advertises GTKSA + * support in the CSIA Capabilities byte (body[0] bits 1-2, + * §9.5.21.2 Table 122 — how iOS signals it) or lists an NCS-GTK + * cipher suite (how Android/ESP signal it). The Capabilities byte + * is the spec-correct indicator; an NCS-GTK CSID only selects WHICH + * group cipher and need not be present. */ + s_pending_scia.peer_group_data = + ((body[0] & NAN_CSIA_CAP_GTK_SUPP_MASK) || + (accum & NAN_CSID_GTK_BITS)) ? true : false; + if (s_pending_scia.peer_group_data) { + param->group_data_prot = 1; + } + /* Store the raw CSIA Capabilities byte; IGTK/BIGTK gating derives + * bits 1-2 (01=IGTKSA, 10=+BIGTKSA) independently at NDL finalize. */ + s_pending_scia.peer_caps = body[0]; + if (body[0] & NAN_CSIA_CAP_GTK_SUPP_MASK) { + param->group_mgmt_prot = 1; + } } } } @@ -1612,56 +2196,97 @@ void esp_nan_parse_ndp_key_desc(void *frm, size_t buf_len, uint8_t ndp_id, const memcpy(ndl->key_rsc, key_rsc, NAN_KEY_RSC_LEN); - /* Parse Key Data (KDEs) - only when not encrypted */ - if (key_data_len > 0 && (NAN_KEY_DESC_DATA_OFF + key_data_len <= key_desc_len) && !has_enc_key) { - uint8_t *key_data = &key_desc[NAN_KEY_DESC_DATA_OFF]; + /* Parse Key Data (KDEs). Per §7.1.3.5 group keys are always carried + * KEK-wrapped, so decrypt with the ND-KEK before walking. The plaintext + * may carry 0xDD/0x00 AES-Key-Wrap padding after the last KDE. */ + if (key_data_len > 0 && (NAN_KEY_DESC_DATA_OFF + key_data_len <= key_desc_len)) { + uint8_t plain[NAN_GROUP_KEY_DATA_MAX]; + const uint8_t *kde_buf = &key_desc[NAN_KEY_DESC_DATA_OFF]; + size_t kde_len = key_data_len; + + if (has_enc_key) { + size_t unwrapped = 0; + if (!ndl->ptk_set) { + ESP_LOGW(TAG, "NDP Key Desc: encrypted Key Data but PTK/KEK not set; skipping KDEs"); + kde_len = 0; + } else if (nan_kek_unwrap_key_data(ndl, &key_desc[NAN_KEY_DESC_DATA_OFF], + key_data_len, plain, sizeof(plain), + &unwrapped) != 0) { + ESP_LOGW(TAG, "NDP Key Desc: KEK unwrap of Key Data failed; skipping KDEs"); + kde_len = 0; + } else { + kde_buf = plain; + kde_len = unwrapped; + } + } + uint16_t kd_offset = 0; - - while (kd_offset + 2 <= key_data_len) { - uint8_t kde_type = key_data[kd_offset]; - uint8_t kde_len = key_data[kd_offset + 1]; - - if (kd_offset + 2 + kde_len > key_data_len) { + while (kd_offset + 2 <= kde_len) { + uint8_t kde_id = kde_buf[kd_offset]; + uint8_t kde_flen = kde_buf[kd_offset + 1]; + /* All KDEs start with 0xDD; a 0xDD/0x00 pad (or any short + * element) terminates the meaningful list. */ + if (kde_id != 0xDD || kde_flen < 4 || + kd_offset + 2 + kde_flen > kde_len) { break; } - if (kde_type == 0xDD && kde_len >= 4) { - uint32_t oui = (key_data[kd_offset + 2] << 16) | (key_data[kd_offset + 3] << 8) | key_data[kd_offset + 4]; - uint8_t data_type = key_data[kd_offset + 5]; - uint8_t *data = &key_data[kd_offset + 6]; - uint8_t data_len = kde_len - 4; + uint32_t oui = (kde_buf[kd_offset + 2] << 16) | + (kde_buf[kd_offset + 3] << 8) | kde_buf[kd_offset + 4]; + uint8_t data_type = kde_buf[kd_offset + 5]; + const uint8_t *body = &kde_buf[kd_offset + 6]; + uint8_t body_len = kde_flen - 4; /* after OUI(3) + DataType(1) */ - if (oui == 0x000FAC) { /* WFA OUI */ - if (data_type == 1 && data_len <= NAN_GTK_MAX_LEN) { - memcpy(ndl->gtk, data, data_len); - ndl->gtk_len = data_len; + if (oui == NAN_KDE_OUI_RSN) { + if (data_type == NAN_KDE_TYPE_GTK && body_len > NAN_GTK_KDE_PREFIX_LEN) { + /* GTK KDE: KeyID/Tx(1) Reserved(1) GTK(var) */ + uint8_t gtk_len = body_len - NAN_GTK_KDE_PREFIX_LEN; + if (gtk_len <= NAN_GTK_MAX_LEN) { + ndl->gtk_keyid = body[0] & 0x03; + memcpy(ndl->gtk, body + NAN_GTK_KDE_PREFIX_LEN, gtk_len); + ndl->gtk_len = gtk_len; + memcpy(ndl->gtk_rsc, key_rsc, NAN_KEY_RSC_LEN); ndl->gtk_set = 1; - ESP_LOGI(TAG, "KDE: GTK stored (len=%d)", data_len); - } else if (data_type == 3 && data_len >= 6) { - ESP_LOGI(TAG, "KDE: MAC Address: " MACSTR, MAC2STR(data)); - } else if (data_type == 4 && data_len <= NAN_GTK_MAX_LEN) { - memcpy(ndl->igtk, data, data_len); - ndl->igtk_len = data_len; - ndl->igtk_set = 1; - ESP_LOGI(TAG, "KDE: IGTK stored (len=%d)", data_len); - } else if (data_type == 5 && data_len <= NAN_GTK_MAX_LEN) { - memcpy(ndl->bigtk, data, data_len); - ndl->bigtk_len = data_len; - ndl->bigtk_set = 1; - ESP_LOGI(TAG, "KDE: BIGTK stored (len=%d)", data_len); + ESP_LOGI(TAG, "KDE: peer GTK stored (keyid=%d, len=%d)", + ndl->gtk_keyid, gtk_len); } - } else if (oui == 0x506F9A) { /* NAN OUI */ - if (data_type == 36 && data_len >= 1) { - ESP_LOGI(TAG, "KDE: NIK (Cipher Ver: %d)", data[0]); - } else if (data_type == 37 && data_len >= 6) { - uint16_t key_bitmap = data[0] | (data[1] << 8); - uint32_t lifetime = (data[2] << 24) | (data[3] << 16) | (data[4] << 8) | data[5]; - ESP_LOGI(TAG, "KDE: NAN Key Lifetime: %lu s, Bitmap: 0x%04x", - (unsigned long)lifetime, key_bitmap); + } else if (data_type == NAN_KDE_TYPE_MAC && body_len >= 6) { + ESP_LOGI(TAG, "KDE: MAC Address: " MACSTR, MAC2STR(body)); + } else if (data_type == NAN_KDE_TYPE_IGTK && body_len > NAN_IGTK_KDE_PREFIX_LEN) { + /* IGTK KDE: KeyID(2 LE) IPN(6) IGTK(var) */ + uint8_t igtk_len = body_len - NAN_IGTK_KDE_PREFIX_LEN; + if (igtk_len <= NAN_GTK_MAX_LEN) { + ndl->igtk_keyid = body[0]; + memcpy(ndl->igtk, body + NAN_IGTK_KDE_PREFIX_LEN, igtk_len); + ndl->igtk_len = igtk_len; + ndl->igtk_set = 1; + ESP_LOGI(TAG, "KDE: peer IGTK stored (keyid=%d, len=%d)", + ndl->igtk_keyid, igtk_len); + } + } else if (data_type == NAN_KDE_TYPE_BIGTK && body_len > NAN_BIGTK_KDE_PREFIX_LEN) { + /* BIGTK KDE: KeyID(2 LE) BIPN(6) BIGTK(var) */ + uint8_t bigtk_len = body_len - NAN_BIGTK_KDE_PREFIX_LEN; + if (bigtk_len <= NAN_GTK_MAX_LEN) { + ndl->bigtk_keyid = body[0]; + memcpy(ndl->bigtk, body + NAN_BIGTK_KDE_PREFIX_LEN, bigtk_len); + ndl->bigtk_len = bigtk_len; + ndl->bigtk_set = 1; + ESP_LOGI(TAG, "KDE: peer BIGTK stored (keyid=%d, len=%d)", + ndl->bigtk_keyid, bigtk_len); } } + } else if (oui == NAN_KDE_OUI_WFA) { + if (data_type == NAN_KDE_TYPE_NIK && body_len >= 1) { + ESP_LOGI(TAG, "KDE: NIK (Cipher Ver: %d)", body[0]); + } else if (data_type == NAN_KDE_TYPE_KEY_LIFE && body_len >= 6) { + uint16_t key_bitmap = body[0] | (body[1] << 8); + uint32_t lifetime = (body[2] << 24) | (body[3] << 16) | + (body[4] << 8) | body[5]; + ESP_LOGI(TAG, "KDE: NAN Key Lifetime: %lu s, Bitmap: 0x%04x", + (unsigned long)lifetime, key_bitmap); + } } - kd_offset += 2 + kde_len; + kd_offset += 2 + kde_flen; } } } else if (msg_type == 1) { @@ -1698,7 +2323,19 @@ esp_err_t esp_nan_parse_publish_security(const uint8_t *attrs, size_t attrs_len, ESP_LOGD(TAG, "Found CSIA in Publish SDF, len=%d", csia_len); ESP_LOG_BUFFER_HEXDUMP(TAG, csia, csia_len, ESP_LOG_DEBUG); - /* Skip Capabilities byte; iterate [CSID(1)] [Publish ID(1)] entries */ + /* Capabilities byte (csia[0]) bits 1-2 = GTKSA/IGTKSA/BIGTKSA support + * (§9.5.21.2 Table 122): 01 = GTKSA+IGTKSA, 10 = +BIGTKSA. This — not an + * NCS-GTK cipher-suite ID — is how a peer (notably iOS) advertises it. */ + uint8_t grp_supp = csia[0] & NAN_CSIA_CAP_GTK_SUPP_MASK; + if (grp_supp) { + security->group_data_prot = 1; + security->group_mgmt_prot = 1; /* IGTKSA */ + } + if (grp_supp == (NAN_CSIA_CAP_GTK_SUPP_ALL << NAN_CSIA_CAP_GTK_SUPP_POS)) { + security->group_bigtk_prot = 1; /* BIGTKSA */ + } + + /* iterate [CSID(1)] [Publish ID(1)] entries after the Capabilities byte */ size_t offset = 1; while (offset + 2 <= csia_len) { uint8_t csid = csia[offset]; @@ -1710,6 +2347,12 @@ esp_err_t esp_nan_parse_publish_security(const uint8_t *attrs, size_t attrs_len, offset += 2; } + + /* Android/ESP peers advertise group-data support by listing an NCS-GTK + * cipher suite instead of (or with) the Capabilities byte bits. */ + if (security->csid_bitmap & NAN_CSID_GTK_BITS) { + security->group_data_prot = 1; + } } /* 2. SCIA — PMKIDs */ @@ -1762,6 +2405,33 @@ void nan_security_apply_pending(struct ndl_info *ndl, (s_pending_scia.has_csid || s_pending_scia.has_pmkid)) { if (s_pending_scia.has_csid) { + /* GTK is exchanged only if both sides support it: our service must + * enable group_data_prot AND the peer must have signalled group-data + * support in its NDP-Request CSIA (Capabilities byte for iOS, or an + * NCS-GTK cipher suite for Android/ESP — captured in peer_group_data + * by esp_nan_parse_ndp_csia). */ + ndl->gtk_required = (p_own_svc && p_own_svc->user_cfg.group_data_prot && + s_pending_scia.peer_group_data) ? 1 : 0; + if (p_own_svc && p_own_svc->user_cfg.group_data_prot) { + ESP_LOGI(TAG, "NDP GTK: %s (responder) - own group_prot=1, peer group_data=%d", + ndl->gtk_required ? "negotiated" : "NOT negotiated", + s_pending_scia.peer_group_data); + } + /* IGTK distributed in M4 iff we enabled group_mgmt_prot AND the peer + * advertised IGTKSA; BIGTK additionally requires BIGTKSA (§7.1.3.5). */ + { + bool own_mgmt = s_nan_ctx.group_mgmt_prot; + uint8_t grp = s_pending_scia.peer_caps & NAN_CSIA_CAP_GTK_SUPP_MASK; + bool peer_igtk = grp != 0; + bool peer_bigtk = grp == (NAN_CSIA_CAP_GTK_SUPP_ALL << NAN_CSIA_CAP_GTK_SUPP_POS); + ndl->igtk_required = (own_mgmt && peer_igtk) ? 1 : 0; + ndl->bigtk_required = (own_mgmt && peer_bigtk) ? 1 : 0; + if (own_mgmt) { + ESP_LOGI(TAG, "NDP IGTK/BIGTK: igtk=%s bigtk=%s (responder) - peer caps=0x%02x", + ndl->igtk_required ? "yes" : "no", ndl->bigtk_required ? "yes" : "no", + s_pending_scia.peer_caps); + } + } ndl->security_ctx.csid_bitmap = s_pending_scia.csid_bitmap; ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; } @@ -2105,14 +2775,12 @@ int esp_nan_get_ndp_confirm_shared_key_desc(uint8_t *buf, size_t buf_len, uint8_ uint8_t *p = buf; *p++ = NAN_ATTR_ID_SHARED_KEY_DESC; - { - uint16_t body_len = 1 + NAN_KEY_DESC_MIN_LEN; - *p++ = body_len & 0xFF; - *p++ = (body_len >> 8) & 0xFF; - } + uint8_t *len_field = p; /* backpatched once Key Data length is known */ + p += 2; *p++ = ndl->publisher_id; - int n = nan_build_rsna_key_descriptor(p, + uint8_t *key_desc = p; + int n = nan_build_rsna_key_descriptor(key_desc, NAN_KEY_INFO_MIC | NAN_KEY_INFO_SECURE | NAN_KEY_INFO_ACK, @@ -2124,12 +2792,19 @@ int esp_nan_get_ndp_confirm_shared_key_desc(uint8_t *buf, size_t buf_len, uint8_ return 0; } - n = 3 + 1 + n; + /* Initiator distributes its GTK in M3 (§7.1.3.2). Needs the blob to size + * the M3 buffer with GTK headroom (ndp_confirm_get_shared_desc_len); + * falls back to pairwise-only otherwise. */ + n = nan_append_own_group_kdes(ndl, key_desc, buf_len - 4); + + uint16_t body_len = 1 + (uint16_t)n; + len_field[0] = body_len & 0xFF; + len_field[1] = (body_len >> 8) & 0xFF; NAN_DATA_UNLOCK(); /* State transition to M3_SENT happens in host TX-confirm hook */ - ESP_LOGD(TAG, "NDP M3 Key Desc: built (MIC=0, driver must call esp_nan_update_ndp_confirm_mic) for ndp_id=%d", ndp_id); - return n; + ESP_LOGD(TAG, "NDP M3 Key Desc: built (key_desc=%d bytes, MIC=0; driver must call esp_nan_update_ndp_confirm_mic) for ndp_id=%d", n, ndp_id); + return 3 + 1 + n; } /** diff --git a/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild b/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild index b336577c920..72c296bf369 100644 --- a/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild +++ b/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild @@ -35,6 +35,16 @@ menu "Example Configuration" the same credential (passphrase or PMK). Disable to advertise an open (unencrypted) service. + config EXAMPLE_NAN_GROUP_DATA_PROT + bool "Protect group-addressed datapath traffic (ND-GTK)" + depends on EXAMPLE_NAN_SECURITY_ENABLED + default y + help + Negotiate and install an ND-GTK so group-addressed (multicast/ + broadcast) frames on the NAN datapath are encrypted. This is + required for IPv6 over the secured datapath (Neighbor Discovery, + MLD). Both peers must enable this for group keys to be exchanged. + choice EXAMPLE_NAN_SECURITY_METHOD prompt "Security Method" depends on EXAMPLE_NAN_SECURITY_ENABLED diff --git a/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c b/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c index 47be5ab4c0b..febbea48085 100644 --- a/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c +++ b/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c @@ -133,6 +133,9 @@ void wifi_nan_publish(void) }; #ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED wifi_nan_discovery_security_params_t security_cfg = { +#ifdef CONFIG_EXAMPLE_NAN_GROUP_DATA_PROT + .group_data_prot = 1, /* distribute/accept ND-GTK for group-addressed data */ +#endif .num_credentials = 1, .creds = { { diff --git a/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild b/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild index 36c87d4eaa7..82ec413ec45 100644 --- a/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild +++ b/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild @@ -45,6 +45,16 @@ menu "Example Configuration" (passphrase or PMK) and sets up an encrypted NDP. Disable to discover open (unencrypted) services. + config EXAMPLE_NAN_GROUP_DATA_PROT + bool "Protect group-addressed datapath traffic (ND-GTK)" + depends on EXAMPLE_NAN_SECURITY_ENABLED + default y + help + Negotiate and install an ND-GTK so group-addressed (multicast/ + broadcast) frames on the NAN datapath are encrypted. This is + required for IPv6 over the secured datapath (Neighbor Discovery, + MLD). Both peers must enable this for group keys to be exchanged. + choice EXAMPLE_NAN_SECURITY_METHOD prompt "Security Method" depends on EXAMPLE_NAN_SECURITY_ENABLED diff --git a/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c b/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c index 870cc0e7cc9..dfb01df751b 100644 --- a/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c +++ b/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c @@ -227,6 +227,9 @@ void wifi_nan_subscribe(void) }; #ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED wifi_nan_discovery_security_params_t security_cfg = { +#ifdef CONFIG_EXAMPLE_NAN_GROUP_DATA_PROT + .group_data_prot = 1, /* distribute/accept ND-GTK for group-addressed data */ +#endif .num_credentials = 1, .creds = { { From 08e98f6f3015a2d0195a60bf822f039fb5ada3c5 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Tue, 30 Jun 2026 15:30:45 +0530 Subject: [PATCH 02/22] feat(nan): pin datapath IPv6 neighbor via esp_netif static entry - Add esp_wifi_netif_set_static_neighbor() that to add/remove IPv6 static entry using netif API to skip Neighbor Discovery Protocol - Move the fe80::/64 + EUI-64 derivation to esp_wifi_netif.c --- components/esp_wifi/include/esp_wifi_netif.h | 36 ++++++++++++++ components/esp_wifi/src/wifi_default.c | 4 ++ components/esp_wifi/src/wifi_netif.c | 43 ++++++++++++++++ .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 49 ++++++++++++------- 4 files changed, 114 insertions(+), 18 deletions(-) diff --git a/components/esp_wifi/include/esp_wifi_netif.h b/components/esp_wifi/include/esp_wifi_netif.h index 7dfa724b066..ca054e8a6e1 100644 --- a/components/esp_wifi/include/esp_wifi_netif.h +++ b/components/esp_wifi/include/esp_wifi_netif.h @@ -81,6 +81,42 @@ bool esp_wifi_is_if_ready_when_started(wifi_netif_driver_t ifx); */ esp_err_t esp_wifi_register_if_rxcb(wifi_netif_driver_t ifx, esp_netif_receive_t fn, void * arg); +/** + * @brief Derive an IPv6 link-local address from a link-layer (MAC) address + * + * Computes fe80::/64 combined with the EUI-64 form of the given MAC (the 802 + * group bit complemented) into an esp_ip6_addr_t (zone 0). Interface-agnostic. + * + * @param[out] ip6 destination, set to the derived IPv6 link-local address + * @param[in] mac source link-layer (MAC) address (6 bytes) + */ +void esp_wifi_netif_get_ip6_linklocal_from_mac(esp_ip6_addr_t *ip6, const uint8_t mac[6]); + +#if CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES +/** + * @brief Pin (or remove) a static IPv6 link-local neighbor mapping on a wifi netif + * + * Installs a fixed link-local IPv6 -> MAC mapping for a peer reachable on the + * given wifi interface so that traffic to the peer bypasses Neighbor Discovery + * (no NS/NA exchanged), or removes a previously installed one. This layer owns + * both the netif lookup (from the interface type) and the derivation of the + * peer's link-local address from its MAC, so the caller only supplies the + * interface and the peer MAC. Only available when lwIP static ND6 entries are + * enabled. + * + * @param[in] wifi_if wifi interface the peer is reachable on + * @param[in] mac peer's link-layer (MAC) address + * @param[in] add true to add the mapping, false to remove it + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if mac is NULL or wifi_if is out of range + * - ESP_ERR_INVALID_STATE if the interface's netif is not up + * - error code from the underlying esp_netif call otherwise + */ +esp_err_t esp_wifi_netif_set_static_neighbor(wifi_interface_t wifi_if, const uint8_t mac[6], bool add); +#endif /* CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES */ + #ifdef __cplusplus } #endif diff --git a/components/esp_wifi/src/wifi_default.c b/components/esp_wifi/src/wifi_default.c index 66ed536ac8a..3ba9d6197ab 100644 --- a/components/esp_wifi/src/wifi_default.c +++ b/components/esp_wifi/src/wifi_default.c @@ -184,6 +184,10 @@ static void wifi_default_action_nan_started(void *arg, esp_event_base_t base, in if (s_wifi_netifs[WIFI_IF_NAN] != NULL) { wifi_start(s_wifi_netifs[WIFI_IF_NAN], base, event_id, data); esp_nan_action_start(s_wifi_netifs[WIFI_IF_NAN]); + /* Bring the netif up before creating the link-local address; + * esp_netif_create_ip6_linklocal() is a no-op unless netif_is_up(). */ + esp_netif_action_connected(s_wifi_netifs[WIFI_IF_NAN], base, event_id, data); + esp_netif_create_ip6_linklocal(s_wifi_netifs[WIFI_IF_NAN]); } } diff --git a/components/esp_wifi/src/wifi_netif.c b/components/esp_wifi/src/wifi_netif.c index 96d4135dd21..b632d51ff40 100644 --- a/components/esp_wifi/src/wifi_netif.c +++ b/components/esp_wifi/src/wifi_netif.c @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ +#include #include "esp_wifi.h" #include "esp_netif.h" #include "esp_log.h" @@ -181,3 +182,45 @@ esp_err_t esp_wifi_register_if_rxcb(wifi_netif_driver_t ifx, esp_netif_receive_t } return ESP_OK; } + +void esp_wifi_netif_get_ip6_linklocal_from_mac(esp_ip6_addr_t *ip6, const uint8_t mac[6]) +{ + if (ip6 == NULL || mac == NULL) { + return; + } + + /* fe80::/64 + EUI-64 of the MAC (802 group bit complemented), laid out in + * network byte order straight into esp_ip6_addr_t. Zone stays 0. */ + const uint8_t linklocal[16] = { + 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + (uint8_t)(mac[0] ^ 0x02), mac[1], mac[2], 0xff, + 0xfe, mac[3], mac[4], mac[5], + }; + memset(ip6, 0, sizeof(*ip6)); + memcpy(ip6->addr, linklocal, sizeof(linklocal)); +} + +#if CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES +esp_err_t esp_wifi_netif_set_static_neighbor(wifi_interface_t wifi_if, const uint8_t mac[6], bool add) +{ + if (mac == NULL || wifi_if >= MAX_WIFI_IFS) { + return ESP_ERR_INVALID_ARG; + } + + /* The netif handle is owned by this layer (recorded when the interface's RX + * callback is registered), so callers only need to supply the interface and + * the peer MAC. */ + esp_netif_t *esp_netif = s_wifi_netifs[wifi_if]; + if (esp_netif == NULL) { + return ESP_ERR_INVALID_STATE; + } + + /* Derive the peer's link-local address; esp_netif copies only the address + * words for static neighbor entries, so no zone handling is needed here. */ + esp_ip6_addr_t addr6; + esp_wifi_netif_get_ip6_linklocal_from_mac(&addr6, mac); + + return add ? esp_netif_add_static_neighbor(esp_netif, &addr6, mac) + : esp_netif_remove_static_neighbor(esp_netif, &addr6); +} +#endif /* CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES */ diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index f72882e4274..9a85de3e439 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -351,20 +351,12 @@ void esp_wifi_nan_get_ipv6_linklocal_from_mac(ip6_addr_t *ip6, uint8_t *mac_addr if (ip6 == NULL || mac_addr == NULL) { return; } - /* Link-local prefix. */ - ip6->addr[0] = htonl(0xfe800000ul); - ip6->addr[1] = 0; - - /* Assume hwaddr is a 48-bit IEEE 802 MAC. Convert to EUI-64 address. Complement Group bit. */ - ip6->addr[2] = htonl((((uint32_t)(mac_addr[0] ^ 0x02)) << 24) | - ((uint32_t)(mac_addr[1]) << 16) | - ((uint32_t)(mac_addr[2]) << 8) | - (0xff)); - ip6->addr[3] = htonl((uint32_t)(0xfeul << 24) | - ((uint32_t)(mac_addr[3]) << 16) | - ((uint32_t)(mac_addr[4]) << 8) | - (mac_addr[5])); - + /* Reuse the interface-agnostic derivation in the esp_wifi netif layer, then + * copy the address words into the lwIP ip6_addr_t. The two structures share + * the same layout, which is how esp_netif converts between them. */ + esp_ip6_addr_t esp_ip6; + esp_wifi_netif_get_ip6_linklocal_from_mac(&esp_ip6, mac_addr); + memcpy(ip6->addr, esp_ip6.addr, sizeof(ip6->addr)); ip6->zone = IP6_NO_ZONE; } @@ -1063,8 +1055,8 @@ static void nan_app_replied_cb(uint8_t pub_id, struct nan_cb_peer_info *peer_inf } static void nan_app_receive_cb(uint8_t svc_id, struct nan_cb_peer_info *peer_info, - uint8_t *shared_key_attr, uint16_t shared_key_attr_buf_len, - struct nan_cb_npba_t *npba) + uint8_t *shared_key_attr, uint16_t shared_key_attr_buf_len, + struct nan_cb_npba_t *npba) { if (!peer_info) { return; @@ -1525,8 +1517,6 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ESP_LOG_BUFFER_HEXDUMP(TAG, ssi, ssi_len, ESP_LOG_DEBUG); } - esp_netif_action_connected(s_nan_ctx.nan_netif, WIFI_EVENT, WIFI_EVENT_NDP_CONFIRM, evt); - esp_netif_create_ip6_linklocal(s_nan_ctx.nan_netif); NAN_DATA_UNLOCK(); ip6_addr_t peer_ip6 = {0}; @@ -1537,6 +1527,20 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ESP_LOGI(TAG, "NDP confirmed with Peer "MACSTR" [NDP ID - %d, Peer IPv6 - %s]", MAC2STR(peer_nmi), ndp_id, inet6_ntoa(peer_ip6)); +#if CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES + /* Pin the peer's link-local -> NDI mapping so traffic to the peer skips + * Neighbor Discovery (no NS/NA) on the NAN link. The esp_wifi netif layer + * owns the netif lookup and derives the peer's link-local from its NDI + * (the address the peer actually sources from). */ + esp_err_t nbr_err = esp_wifi_netif_set_static_neighbor(WIFI_IF_NAN, peer_ndi, true); + if (nbr_err != ESP_OK) { + ESP_LOGW(TAG, "static nbr ADD failed: %s", esp_err_to_name(nbr_err)); + } +#else + esp_netif_action_connected(s_nan_ctx.nan_netif, WIFI_EVENT, WIFI_EVENT_NDP_CONFIRM, evt); + esp_netif_create_ip6_linklocal(s_nan_ctx.nan_netif); +#endif + os_event_group_set_bits(nan_event_group, NDP_ACCEPTED); nan_app_post_event(WIFI_EVENT_NDP_CONFIRM, evt, evt_data_len); os_free(evt); @@ -1559,6 +1563,15 @@ static void nan_app_ndp_terminated_cb(uint8_t reason, uint8_t ndp_id, uint8_t in s_nan_ctx.event &= ~(NDP_INDICATION); NAN_DATA_UNLOCK(); +#if CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES + /* Drop the peer's static neighbor mapping added on NDP confirm. (It is also + * cleared automatically if the NAN netif goes down on the last datapath.) */ + esp_err_t nbr_err = esp_wifi_netif_set_static_neighbor(WIFI_IF_NAN, init_ndi, false); + if (nbr_err != ESP_OK) { + ESP_LOGW(TAG, "static nbr DEL failed: %s", esp_err_to_name(nbr_err)); + } +#endif + wifi_event_ndp_terminated_t *evt = (wifi_event_ndp_terminated_t *)os_zalloc(sizeof(wifi_event_ndp_terminated_t)); if (!evt) { ESP_LOGE(TAG, "Failed to allocate for event"); From 0577b5b1aacb8a25984b95ba0f4412b097da2aae Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 13:51:16 +0530 Subject: [PATCH 03/22] fix(wifi): sync wifi-remote injected header for NAN group_mgmt_prot --- .../esp_wifi/remote/include/injected/esp_wifi_types_generic.h | 1 + 1 file changed, 1 insertion(+) diff --git a/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h b/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h index 3980683f70a..960f6a8e529 100644 --- a/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h +++ b/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h @@ -606,6 +606,7 @@ typedef struct { bool disable_random_mac;/**< Disable the MAC Randomisation in NAN */ bool reset_current_nvs_creds; /**< Erase all NAN credentials (own NIK and cached peer NIK/NPK entries) saved in NVS before starting. */ bool use_nvs_for_caching; /**< Persist newly-learned peer credentials (NIK/NPK) to NVS so they survive across reboots. */ + bool group_mgmt_prot; /**< Device-global group management protection (IGTKSA/BIGTKSA): BIP-protect Beacons + multicast SDFs. Forces GTKSA on all secured services (CSIA caps cannot encode IGTK/BIGTK without GTKSA). */ } wifi_nan_sync_config_t; /** From 22a9321feace02316fb71c9e95192bf17bb022b2 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 13:56:19 +0530 Subject: [PATCH 04/22] fix(nan): free NDL slot and deny peer when get_mac fails on NDP resp The NDP indication handler recorded an NDL slot, then on esp_wifi_get_mac failure unlocked and returned without releasing the slot or answering the peer: the slot leaked (counting against the NDL limit) and the peer waited indefinitely. On failure now reset the NDL and send a deny response, mirroring the existing allocation-failure cleanup path. Also drop the redundant pre-branch get_mac/IPv6-derive: its result was only used on the auto-response path, which recomputes it, so on the indication path it was dead work and a second leak site. --- .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index 9a85de3e439..06995d26a5b 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -1183,17 +1183,6 @@ static void nan_app_ndp_indication_cb(uint8_t pub_id, struct ndp_cb_peer_info *p nan_security_apply_pending(ndl, p_own_svc, pub_id, peer_nmi, peer_ndi); #endif - if (device_caps & NAN_CAPS_NDPE_ATTR) { - uint8_t own_bssid[6]; - esp_err_t err = esp_wifi_get_mac(WIFI_IF_NAN, own_bssid); - if (err != ESP_OK) { - NAN_DATA_UNLOCK(); - ESP_LOGE(TAG, "Cannot get own BSSID!"); - return; - } - esp_wifi_nan_get_ipv6_linklocal_from_mac(&own_ipv6.u_addr.ip6, own_bssid); - } - if (p_own_svc->ndp_resp_needed) { ESP_LOGD(TAG, "NDP Req from "MACSTR" [NDP Id: %d], Accept OR Deny using NDP command", MAC2STR(peer_nmi), ndp_id); @@ -1209,8 +1198,13 @@ static void nan_app_ndp_indication_cb(uint8_t pub_id, struct ndp_cb_peer_info *p uint8_t own_bssid[6]; esp_err_t err = esp_wifi_get_mac(WIFI_IF_NAN, own_bssid); if (err != ESP_OK) { + /* Cannot build the auto-response: free the NDL slot and deny the + * peer so it does not wait indefinitely. Send outside the lock. */ + ESP_LOGE(TAG, "get own NAN MAC failed, rc=0x%x; denying NDP ndp_id=%d", err, ndp_id); + nan_reset_ndl(ndp_id, false); NAN_DATA_UNLOCK(); - ESP_LOGE(TAG, "Cannot get own BSSID!"); + ndp_resp.accept = false; + esp_nan_internal_datapath_resp(&ndp_resp, (uint8_t *)&own_ipv6.u_addr.ip6.addr[2]); return; } esp_wifi_nan_get_ipv6_linklocal_from_mac(&own_ipv6.u_addr.ip6, own_bssid); From 65d822f20289281c67c90a146802cf1e3f75931d Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 14:01:25 +0530 Subject: [PATCH 05/22] fix(nan): detect ND-TK install failure at NDP confirm The ND-TK (pairwise data key) install return value was overwritten by the subsequent NM-TK install before being checked, so an ND-TK failure went undetected: the NDP was marked accepted and NDP_CONFIRM posted while unicast data frames had no encryption key installed. Check ND-TK first and tear down on failure, then install and check NM-TK separately. --- components/esp_wifi/wifi_apps/nan_app/src/nan_app.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index 06995d26a5b..358fb267326 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -1399,6 +1399,12 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer NAN_NCS_SK_128_TK_LEN, NAN_KEY_ND_TK); ESP_LOG_BUFFER_HEXDUMP("## ND-TK ", ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, ESP_LOG_INFO); + if (ret != 0) { + ESP_LOGE(TAG, "NDP confirm: failed to install ND-TK (ndp_id=%d, ret=%d)", ndp_id, ret); + os_free(evt); + nan_ndp_confirm_teardown(peer_nmi, ndp_id); + goto done; + } ret = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, peer_nmi, 0, @@ -1409,7 +1415,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer NAN_NCS_SK_128_TK_LEN, NAN_KEY_NM_TK); if (ret != 0) { - ESP_LOGE(TAG, "NDP confirm: failed to install NAN pairwise key (ndp_id=%d, ret=%d)", ndp_id, ret); + ESP_LOGE(TAG, "NDP confirm: failed to install NM-TK (ndp_id=%d, ret=%d)", ndp_id, ret); os_free(evt); nan_ndp_confirm_teardown(peer_nmi, ndp_id); goto done; From c2c825299a7f5477ef5b670d1dfd4f60a2bbc2ee Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 14:11:25 +0530 Subject: [PATCH 06/22] fix(nan): scrub group keys from stack, demote key hexdumps to DEBUG Harden group-key material handling on the secured NDP path: - Zero the stack buffers that hold plaintext/decrypted group keys before they leave scope: pad in nan_kek_wrap_key_data, plain in nan_append_own_group_kdes (single-exit cleanup), and the decrypted plain in esp_nan_parse_ndp_key_desc. Mirrors the existing forced_memzero(pmk) scrubs so GTK/IGTK/BIGTK bytes do not persist on the stack. - Demote the ND-TK / GTK / IGTK / BIGTK ESP_LOG_BUFFER_HEXDUMP calls (own keys in nan_security.c, pairwise and peer keys in nan_app.c) from ESP_LOG_INFO to ESP_LOG_DEBUG so raw key bytes are not printed at the default log level. --- .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 8 ++--- .../wifi_apps/nan_app/src/nan_security.c | 33 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index 358fb267326..d6c19c99cee 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -1398,7 +1398,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, NAN_KEY_ND_TK); - ESP_LOG_BUFFER_HEXDUMP("## ND-TK ", ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, ESP_LOG_INFO); + ESP_LOG_BUFFER_HEXDUMP("## ND-TK ", ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, ESP_LOG_DEBUG); if (ret != 0) { ESP_LOGE(TAG, "NDP confirm: failed to install ND-TK (ndp_id=%d, ret=%d)", ndp_id, ret); os_free(evt); @@ -1436,7 +1436,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer } else { ESP_LOGI(TAG, "NDP confirm: own GTK (TX) installed (keyid=%d)", ndl->own_gtk_keyid); } - ESP_LOG_BUFFER_HEXDUMP("## ND-GTK ", ndl->own_gtk, ndl->own_gtk_len, ESP_LOG_INFO); + ESP_LOG_BUFFER_HEXDUMP("## ND-GTK ", ndl->own_gtk, ndl->own_gtk_len, ESP_LOG_DEBUG); } if (ndl->gtk_set && ndl->gtk_len) { int gret = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, @@ -1474,7 +1474,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ESP_LOGI(TAG, "NDP confirm: peer IGTK (RX) installed (keyid=%d)", ndl->igtk_keyid); } /* Peer IGTK bytes for sniffer MIC cross-check vs the peer's multicast SDFs. */ - ESP_LOG_BUFFER_HEXDUMP("## PEER ND-IGTK ", ndl->igtk, ndl->igtk_len, ESP_LOG_INFO); + ESP_LOG_BUFFER_HEXDUMP("## PEER ND-IGTK ", ndl->igtk, ndl->igtk_len, ESP_LOG_DEBUG); } } if (ndl->bigtk_set && ndl->bigtk_len) { @@ -1493,7 +1493,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ESP_LOGI(TAG, "NDP confirm: peer BIGTK (RX) installed (keyid=%d)", ndl->bigtk_keyid); } /* Peer BIGTK bytes for sniffer MIC cross-check vs the peer's protected Beacons. */ - ESP_LOG_BUFFER_HEXDUMP("## PEER ND-BIGTK ", ndl->bigtk, ndl->bigtk_len, ESP_LOG_INFO); + ESP_LOG_BUFFER_HEXDUMP("## PEER ND-BIGTK ", ndl->bigtk, ndl->bigtk_len, ESP_LOG_DEBUG); } } } diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index fde27c22890..e0bbf67f5e3 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -748,7 +748,7 @@ void nan_security_install_own_group_integrity_keys(void) } else { ESP_LOGI(TAG, "NAN start: own IGTK (TX) installed (keyid=%d)", s_nan_ctx.own_igtk_keyid); } - ESP_LOG_BUFFER_HEXDUMP("## ND-IGTK ", s_nan_ctx.own_igtk, NAN_ND_GTK_LEN, ESP_LOG_INFO); + ESP_LOG_BUFFER_HEXDUMP("## ND-IGTK ", s_nan_ctx.own_igtk, NAN_ND_GTK_LEN, ESP_LOG_DEBUG); } if (nan_ensure_own_bigtk() == 0 && s_nan_ctx.own_bigtk_set) { int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, @@ -761,7 +761,7 @@ void nan_security_install_own_group_integrity_keys(void) } else { ESP_LOGI(TAG, "NAN start: own BIGTK (TX) installed (keyid=%d)", s_nan_ctx.own_bigtk_keyid); } - ESP_LOG_BUFFER_HEXDUMP("## ND-BIGTK ", s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN, ESP_LOG_INFO); + ESP_LOG_BUFFER_HEXDUMP("## ND-BIGTK ", s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN, ESP_LOG_DEBUG); } } @@ -863,7 +863,9 @@ static int nan_kek_wrap_key_data(struct ndl_info *ndl, const uint8_t *plain, pad[plain_len] = 0xDD; memset(pad + plain_len + 1, 0, padded - plain_len - 1); } - if (aes_wrap(ndl->nd_kek, ndl->kek_len, (int)(padded / 8), pad, out) != 0) { + int rc = aes_wrap(ndl->nd_kek, ndl->kek_len, (int)(padded / 8), pad, out); + forced_memzero(pad, sizeof(pad)); /* scrub plaintext group keys */ + if (rc != 0) { return -1; } *out_len = padded + 8; @@ -924,20 +926,24 @@ static int nan_append_own_group_kdes(struct ndl_info *ndl, uint8_t *key_desc, si uint8_t plain[NAN_GROUP_KEY_DATA_MAX]; uint8_t *p = plain; const uint8_t *end = plain + sizeof(plain); + size_t plain_len = 0; + size_t wrapped_len = 0; + uint16_t key_info = 0; + int ret = NAN_KEY_DESC_MIN_LEN; + if (nan_append_igtk_kde(ndl, &p, end) < 0 || nan_append_bigtk_kde(ndl, &p, end) < 0 || nan_append_gtk_kde(ndl, &p, end) < 0) { ESP_LOGW(TAG, "Group KDEs [%s%s%s]: assembly failed; sending without group keys", want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); - return NAN_KEY_DESC_MIN_LEN; + goto out; } - size_t plain_len = (size_t)(p - plain); + plain_len = (size_t)(p - plain); if (plain_len == 0) { - return NAN_KEY_DESC_MIN_LEN; /* nothing negotiated to send */ + goto out; /* nothing negotiated to send */ } - size_t wrapped_len = 0; if (nan_kek_wrap_key_data(ndl, plain, plain_len, &key_desc[NAN_KEY_DESC_DATA_OFF], desc_cap > NAN_KEY_DESC_DATA_OFF ? @@ -945,11 +951,11 @@ static int nan_append_own_group_kdes(struct ndl_info *ndl, uint8_t *key_desc, si &wrapped_len) != 0) { ESP_LOGW(TAG, "Group KDEs [%s%s%s]: KEK wrap failed or no headroom; sending without group keys", want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); - return NAN_KEY_DESC_MIN_LEN; + goto out; } - uint16_t key_info = (key_desc[NAN_KEY_DESC_KEY_INFO_OFF] << 8) | - key_desc[NAN_KEY_DESC_KEY_INFO_OFF + 1]; + key_info = (key_desc[NAN_KEY_DESC_KEY_INFO_OFF] << 8) | + key_desc[NAN_KEY_DESC_KEY_INFO_OFF + 1]; key_info |= NAN_KEY_INFO_ENC_KEY; key_desc[NAN_KEY_DESC_KEY_INFO_OFF] = (key_info >> 8) & 0xFF; key_desc[NAN_KEY_DESC_KEY_INFO_OFF + 1] = key_info & 0xFF; @@ -962,7 +968,11 @@ static int nan_append_own_group_kdes(struct ndl_info *ndl, uint8_t *key_desc, si ESP_LOGI(TAG, "Group Key Data wrapped: %u plain -> %u wrapped bytes, KDEs=[%s%s%s]", (unsigned)plain_len, (unsigned)wrapped_len, want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); - return NAN_KEY_DESC_MIN_LEN + (int)wrapped_len; + ret = NAN_KEY_DESC_MIN_LEN + (int)wrapped_len; + +out: + forced_memzero(plain, sizeof(plain)); /* scrub assembled plaintext group keys */ + return ret; } /* @@ -2288,6 +2298,7 @@ void esp_nan_parse_ndp_key_desc(void *frm, size_t buf_len, uint8_t ndp_id, const } kd_offset += 2 + kde_flen; } + forced_memzero(plain, sizeof(plain)); /* scrub decrypted group keys */ } } else if (msg_type == 1) { /* M1 received but NDL not created yet — store as pending */ From e74b5f7bdae20a888ef5cdad5912207df810b46a Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 14:26:28 +0530 Subject: [PATCH 07/22] fix(nan): clear group keys on NAN stop and peer NDP teardown Fix group-key lifecycle gaps on the secured NDP path: - On NAN stop, reset the device-global IGTK/BIGTK state via new nan_security_reset_own_group_keys() so the next start regenerates fresh keys. Previously the one-shot nan_ensure_own_igtk/bigtk kept own_*_set, so restart re-installed the stale key with IPN/BIPN=0, resetting the blob's monotonic replay counter (and reusing keys if the NMI changed). - On peer teardown (nan_app_clear_one_peer_tks), remove the peer RX IGTK/BIGTK from the blob (they were installed against the peer NMI at NDP confirm) and scrub ndl->igtk/bigtk + flags. Previously only the GTK was removed, leaving stale BIP keys installed and key bytes in memory. - Copy the GTK Key RSC into the descriptor only when a GTK KDE is present, matching the comment and avoiding stale RSC on an IGTK/BIGTK-only path. --- .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 28 +++++++++++++++++++ .../esp_wifi/wifi_apps/nan_app/src/nan_i.h | 6 ++++ .../wifi_apps/nan_app/src/nan_security.c | 19 +++++++++++-- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index d6c19c99cee..44695c0010e 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -272,11 +272,28 @@ static void nan_app_clear_one_peer_tks(const uint8_t *peer_nmi) key_rsc, sizeof(key_rsc), NULL, 0, NAN_KEY_ND_GTK); + /* Drop the peer's RX IGTK/BIGTK (BIP-CMAC-128, installed against the + * peer NMI at NDP confirm) so no stale BIP keys linger in the blob. */ + if (ndl->igtk_set) { + esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + (uint8_t *)peer_nmi, ndl->igtk_keyid, 0, + key_rsc, 6, + NULL, 0, NAN_KEY_ND_IGTK); + } + if (ndl->bigtk_set) { + esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + (uint8_t *)peer_nmi, ndl->bigtk_keyid, 0, + key_rsc, 6, + NULL, 0, NAN_KEY_ND_BIGTK); + } + forced_memzero(ndl->nd_tk, sizeof(ndl->nd_tk)); forced_memzero(ndl->nd_kck, sizeof(ndl->nd_kck)); forced_memzero(ndl->nd_kek, sizeof(ndl->nd_kek)); forced_memzero(ndl->gtk, sizeof(ndl->gtk)); forced_memzero(ndl->own_gtk, sizeof(ndl->own_gtk)); + forced_memzero(ndl->igtk, sizeof(ndl->igtk)); + forced_memzero(ndl->bigtk, sizeof(ndl->bigtk)); ndl->ptk_set = 0; ndl->tk_len = 0; ndl->kck_len = 0; @@ -285,6 +302,10 @@ static void nan_app_clear_one_peer_tks(const uint8_t *peer_nmi) ndl->own_gtk_set = 0; ndl->gtk_len = 0; ndl->own_gtk_len = 0; + ndl->igtk_set = 0; + ndl->bigtk_set = 0; + ndl->igtk_len = 0; + ndl->bigtk_len = 0; } /* Fallback when no NDL slot tracks peer_ndi yet. */ @@ -1868,6 +1889,13 @@ void esp_nan_action_stop(void) nan_app_clear_paired_peers(); #endif +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + /* Drop the device-global IGTK/BIGTK so the next start regenerates fresh + * keys instead of re-installing a stale key with IPN/BIPN=0 (which would + * reset the blob's replay counter) — see nan_security_reset_own_group_keys. */ + nan_security_reset_own_group_keys(); +#endif + esp_nan_internal_register_callbacks(NULL); os_event_group_clear_bits(nan_event_group, NAN_STARTED_BIT); os_event_group_set_bits(nan_event_group, NAN_STOPPED_BIT); diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h index 69b4e997b08..f0e15d4658e 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h @@ -529,6 +529,12 @@ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, * Call once at NAN start; never per-NDP (would reset the blob's BIPN counter). */ void nan_security_install_own_group_integrity_keys(void); +/* Clear the device-global IGTK/BIGTK state on NAN stop so the next NAN start + * regenerates fresh keys. Prevents re-installing a stale key with IPN/BIPN=0 + * (which resets the blob's monotonic replay counter) and key reuse if the NMI + * is re-randomized on restart. */ +void nan_security_reset_own_group_keys(void); + /* * Match subscriber discovery security to a publisher's params. * NCS-SK: Compare locally derived ND-PMKID (subscriber passphrase, publisher NMI) to diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index e0bbf67f5e3..694cae921a5 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -765,6 +765,19 @@ void nan_security_install_own_group_integrity_keys(void) } } +void nan_security_reset_own_group_keys(void) +{ + forced_memzero(s_nan_ctx.own_igtk, sizeof(s_nan_ctx.own_igtk)); + memset(s_nan_ctx.own_igtk_ipn, 0, sizeof(s_nan_ctx.own_igtk_ipn)); + s_nan_ctx.own_igtk_keyid = 0; + s_nan_ctx.own_igtk_set = false; + + forced_memzero(s_nan_ctx.own_bigtk, sizeof(s_nan_ctx.own_bigtk)); + memset(s_nan_ctx.own_bigtk_bipn, 0, sizeof(s_nan_ctx.own_bigtk_bipn)); + s_nan_ctx.own_bigtk_keyid = 0; + s_nan_ctx.own_bigtk_set = false; +} + /* NAN KDE header (802.11 Fig 12-34: DD len OUI(3) DataType(1)); mirrors hostap * nan_add_kde_hdr(). data_len excludes the DD/len/OUI/type bytes. */ static uint8_t *nan_kde_put_hdr(uint8_t *p, uint8_t data_type, uint8_t data_len) @@ -962,8 +975,10 @@ static int nan_append_own_group_kdes(struct ndl_info *ndl, uint8_t *key_desc, si key_desc[NAN_KEY_DESC_DATA_LEN_OFF] = (wrapped_len >> 8) & 0xFF; key_desc[NAN_KEY_DESC_DATA_LEN_OFF + 1] = wrapped_len & 0xFF; - /* Key RSC carries the GTK's RSC when a GTK KDE is present (§7.1.3.5). */ - memcpy(&key_desc[NAN_KEY_DESC_RSC_OFF], ndl->own_gtk_rsc, NAN_KEY_RSC_LEN); + /* Key RSC carries the GTK's RSC only when a GTK KDE is present (§7.1.3.5). */ + if (want_gtk) { + memcpy(&key_desc[NAN_KEY_DESC_RSC_OFF], ndl->own_gtk_rsc, NAN_KEY_RSC_LEN); + } ESP_LOGI(TAG, "Group Key Data wrapped: %u plain -> %u wrapped bytes, KDEs=[%s%s%s]", (unsigned)plain_len, (unsigned)wrapped_len, From a870826c5ab8ab021a394fda4054a724f6316c28 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 14:57:25 +0530 Subject: [PATCH 08/22] fix(nan): set group_mgmt_prot in WIFI_NAN_SYNC_CONFIG_DEFAULT The default initializer explicitly sets every other bool field but omitted the new group_mgmt_prot, leaving the intended default ambiguous. Set it to false so the macro stays exhaustive; both example apps use it. --- components/esp_wifi/wifi_apps/nan_app/include/esp_nan.h | 1 + 1 file changed, 1 insertion(+) diff --git a/components/esp_wifi/wifi_apps/nan_app/include/esp_nan.h b/components/esp_wifi/wifi_apps/nan_app/include/esp_nan.h index a1cdabb441b..0d947bb78f3 100644 --- a/components/esp_wifi/wifi_apps/nan_app/include/esp_nan.h +++ b/components/esp_wifi/wifi_apps/nan_app/include/esp_nan.h @@ -24,6 +24,7 @@ extern "C" { .disable_random_mac = false, \ .reset_current_nvs_creds = false, \ .use_nvs_for_caching = false, \ + .group_mgmt_prot = false, \ }; #define NDP_STATUS_ACCEPTED 1 From d0e2943621055fc0314d5bb40261290640a28c19 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 15:39:20 +0530 Subject: [PATCH 09/22] fix(nan): source group_mgmt_prot from start config, not blob read-back esp_nan_action_start() read group_mgmt_prot back from the blob via esp_wifi_get_config() and defaulted to true when the read failed, which force-enabled device-global IGTK/BIGTK (BIP beacons, forced GTKSA) on a path where the user's intent is unknown. group_mgmt_prot is a pure pass-through user flag, so capture it directly into s_nan_ctx from the config in esp_wifi_nan_sync_start() (next to use_nvs_for_caching) and drop the read-back and its ambiguous default. Behaviour now follows the user's config exactly (default false via WIFI_NAN_SYNC_CONFIG_DEFAULT). --- .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index 44695c0010e..379519f78fb 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -1845,17 +1845,9 @@ void esp_nan_action_start(esp_netif_t *nan_netif) esp_nan_internal_register_callbacks(&nan_cb); #ifdef CONFIG_ESP_WIFI_NAN_SECURITY - /* Device-global group-management protection (IGTKSA/BIGTKSA), one per NMI, - * sourced from the NAN start config (wifi_nan_sync_config_t.group_mgmt_prot). - * The blob stores it at nan_start; we read it back here. Default on if the - * config can't be read, preserving protected-by-default behavior. */ - { - wifi_config_t nan_cfg = {0}; - s_nan_ctx.group_mgmt_prot = - (esp_wifi_get_config(WIFI_IF_NAN, &nan_cfg) == ESP_OK) - ? nan_cfg.nan.group_mgmt_prot : true; - } - /* Install the device-global IGTK/BIGTK for TX now (when enabled) so Beacons + /* s_nan_ctx.group_mgmt_prot (device-global IGTKSA/BIGTKSA, one per NMI) was + * captured from the user's start config in esp_wifi_nan_sync_start(). + * Install the device-global IGTK/BIGTK for TX now (when enabled) so Beacons * (BIGTK) and group-addressed SDFs (IGTK) are BIP-protected from the first * frame, like iOS. The blob gates beacon BIP-TX on an active BIGTK index * only (no NDP state), so installing here is sufficient. */ @@ -1939,6 +1931,7 @@ esp_err_t esp_wifi_nan_sync_start(const wifi_nan_sync_config_t *nan_cfg) s_nan_ctx.num_peer_creds = 0; memset(s_nan_ctx.peer_creds, 0, sizeof(s_nan_ctx.peer_creds)); s_nan_ctx.use_nvs_for_caching = nan_cfg->use_nvs_for_caching; + s_nan_ctx.group_mgmt_prot = nan_cfg->group_mgmt_prot; s_nan_ctx.nik_lifetime = 0; if (nan_cfg->reset_current_nvs_creds) { From 0baa39835e548e44d90328bc7eab01b6cbd5d130 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 15:44:14 +0530 Subject: [PATCH 10/22] fix(nan): restore SCIA length getter debug log The MR had turned the active ESP_LOGD("GET SCIA LEN") into a commented-out ESP_LOGI. Restore the original ESP_LOGD so the log stays live at DEBUG and no dead commented code is left behind. --- components/esp_wifi/wifi_apps/nan_app/src/nan_security.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index 694cae921a5..acedc766c85 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -1726,7 +1726,7 @@ uint32_t esp_nan_get_scia_len(uint8_t num_pmkids) return 0; } uint32_t len = 3 + 20 * num_pmkids; - // ESP_LOGI(TAG, "SCIA len getter -> %lu (num_pmkids=%d)", len, num_pmkids); + ESP_LOGD(TAG, "GET SCIA LEN: %lu (num_pmkids=%d)", len, num_pmkids); return len; } From 553d969c4fc6863ff91d2fd95bc223cb765fbef5 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 15:49:39 +0530 Subject: [PATCH 11/22] fix(nan): pass no event payload to NAN netif bring-up action The NAN-started handler brought the netif up via esp_netif_action_connected() using the NAN-started event's base/event_id/data. Feeding a "connected" action from a "started" event and handing it an unrelated event payload is fragile: it would misbehave if the action handler ever interpreted data (which is not a wifi_event_sta_connected_t here). esp_netif_up() is private to the esp_netif component, so keep the public esp_netif_action_connected() but pass NULL base, 0 event_id, NULL data. This is safe because the NAN netif is not a DHCP client: the handler only calls esp_netif_up() and never reads the event args. --- components/esp_wifi/src/wifi_default.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/components/esp_wifi/src/wifi_default.c b/components/esp_wifi/src/wifi_default.c index 3ba9d6197ab..c73f4294abc 100644 --- a/components/esp_wifi/src/wifi_default.c +++ b/components/esp_wifi/src/wifi_default.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2019-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2019-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -184,9 +184,10 @@ static void wifi_default_action_nan_started(void *arg, esp_event_base_t base, in if (s_wifi_netifs[WIFI_IF_NAN] != NULL) { wifi_start(s_wifi_netifs[WIFI_IF_NAN], base, event_id, data); esp_nan_action_start(s_wifi_netifs[WIFI_IF_NAN]); - /* Bring the netif up before creating the link-local address; - * esp_netif_create_ip6_linklocal() is a no-op unless netif_is_up(). */ - esp_netif_action_connected(s_wifi_netifs[WIFI_IF_NAN], base, event_id, data); + /* Bring the netif up before esp_netif_create_ip6_linklocal() (a no-op unless + * netif_is_up()). esp_netif_up() is private, so use the public action handler; + * NAN is non-DHCP, so it only calls esp_netif_up() and ignores the event args. */ + esp_netif_action_connected(s_wifi_netifs[WIFI_IF_NAN], NULL, 0, NULL); esp_netif_create_ip6_linklocal(s_wifi_netifs[WIFI_IF_NAN]); } } From dc3ada69b56c87b69668ceb8969ebc1ce606a62a Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 16:01:56 +0530 Subject: [PATCH 12/22] refactor(nan): drop commented-out debug logging Remove the dead commented-out ESP_LOGI debug prints flagged in review: the "Sent Publish to Peer" line in nan_app.c, and the SCIA-construct, CSIA-construct and CSIA-len-getter blocks in nan_security.c. --- components/esp_wifi/wifi_apps/nan_app/src/nan_app.c | 1 - .../esp_wifi/wifi_apps/nan_app/src/nan_security.c | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index 379519f78fb..434924bc5d7 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -1064,7 +1064,6 @@ static void nan_app_replied_cb(uint8_t pub_id, struct nan_cb_peer_info *peer_inf evt->subscribe_id = sub_id; MACADDR_COPY(evt->sub_if_mac, sub_nmi); - //ESP_LOGI(TAG, "Sent Publish to Peer "MACSTR" [Peer Subscribe id - %d]", MAC2STR(sub_nmi), sub_id); if (ssi && ssi_len) { memcpy(evt->ssi, ssi, ssi_len); evt->ssi_len = ssi_len; diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index acedc766c85..7c5c0ff81e6 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -1023,10 +1023,6 @@ static int nan_build_scia_attr(uint8_t *frm, uint8_t pub_id, } int written = (int)(p - frm); - /* - ESP_LOGI(TAG, "SCIA construct: frm=%p wrote=%d (hdr3+20*num_pmkids, num_pmkids=%u)", - frm, written, num_pmkids); - */ return written; } @@ -1641,10 +1637,6 @@ int esp_nan_construct_csia(uint8_t *frm, uint8_t pub_id, uint16_t own_csid_bitma } int written = (int)(p - frm); - /* - ESP_LOGI(TAG, "CSIA construct: frm=%p wrote=%d (hdr3+cap1+2*csids=%u, bitmap=0x%04x)", - frm, written, num_csids, csid_bitmap); - */ return written; } @@ -1709,10 +1701,6 @@ uint32_t esp_nan_get_csia_len(uint16_t own_csid_bitmap, uint16_t peer_csid_bitma } uint8_t num_csids = __builtin_popcount(csid_bitmap); uint32_t len = 3 + 1 + 2 * num_csids; - /* - ESP_LOGI(TAG, "CSIA len getter -> %lu (own=0x%04x, peer=0x%04x, effective=0x%04x, num_csids=%d)", - len, own_csid_bitmap, peer_csid_bitmap, csid_bitmap, num_csids); - */ return len; } From 5275d5ef08eec928ebbb1fd17f2fdcb4d8e9579a Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 16:05:36 +0530 Subject: [PATCH 13/22] refactor(nan): put RSN KDE OUI in one array, drop dead OUI byte macros Replace the three NAN_KDE_OUI_RSN_* byte writes in nan_kde_put_hdr() with a single nan_kde_rsn_oui[] array, and remove the now-unused NAN_KDE_OUI_RSN_* byte macros and the never-used NAN_KDE_OUI_WFA_* byte macros. The combined NAN_KDE_OUI_RSN / NAN_KDE_OUI_WFA (used by the KDE parser) are kept. --- components/esp_wifi/wifi_apps/nan_app/src/nan_i.h | 6 ------ components/esp_wifi/wifi_apps/nan_app/src/nan_security.c | 8 +++++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h index f0e15d4658e..c083497eaf6 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h @@ -132,12 +132,6 @@ extern void *s_nan_data_lock; /* NAN KDE OUIs and Data Types carried in the Key Data field (Wi-Fi Aware * v4.0 §9.5.21.5 Table 126; formats per 802.11 Fig 12-36/12-42/12-47). */ -#define NAN_KDE_OUI_RSN_0 0x00 -#define NAN_KDE_OUI_RSN_1 0x0F -#define NAN_KDE_OUI_RSN_2 0xAC -#define NAN_KDE_OUI_WFA_0 0x50 -#define NAN_KDE_OUI_WFA_1 0x6F -#define NAN_KDE_OUI_WFA_2 0x9A #define NAN_KDE_OUI_RSN 0x000FACUL #define NAN_KDE_OUI_WFA 0x506F9AUL #define NAN_KDE_TYPE_GTK 1 /* 00-0F-AC GTK KDE */ diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index 7c5c0ff81e6..8ad56e5d256 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -778,15 +778,17 @@ void nan_security_reset_own_group_keys(void) s_nan_ctx.own_bigtk_set = false; } +/* 00-0F-AC RSN OUI written into every NAN KDE header. */ +static const uint8_t nan_kde_rsn_oui[3] = {0x00, 0x0f, 0xac}; + /* NAN KDE header (802.11 Fig 12-34: DD len OUI(3) DataType(1)); mirrors hostap * nan_add_kde_hdr(). data_len excludes the DD/len/OUI/type bytes. */ static uint8_t *nan_kde_put_hdr(uint8_t *p, uint8_t data_type, uint8_t data_len) { *p++ = 0xDD; *p++ = (uint8_t)(4 + data_len); /* OUI(3) + DataType(1) + data */ - *p++ = NAN_KDE_OUI_RSN_0; - *p++ = NAN_KDE_OUI_RSN_1; - *p++ = NAN_KDE_OUI_RSN_2; + memcpy(p, nan_kde_rsn_oui, sizeof(nan_kde_rsn_oui)); + p += sizeof(nan_kde_rsn_oui); *p++ = data_type; return p; } From 8b2b38682555a1a7debe0c68ffc8e897b987a1f4 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 16:12:03 +0530 Subject: [PATCH 14/22] refactor(nan): declare NAN key types as nan_key_type_t enum Move the NAN_KEY_ND_TK/ND_GTK/NM_TK/ND_IGTK/ND_BIGTK selectors from #defines into a nan_key_type_t enum, and finalize their doc wording (drop the "provisional" note now that the IGTK/BIGTK values are verified against the blob ABI). They are still passed to esp_wifi_set_nan_key_internal() as the int key_flag argument, so no call-site or ABI change. Also tidy the surrounding doc comments. --- .../esp_wifi/wifi_apps/nan_app/src/nan_i.h | 18 ++++++++++-------- .../wifi_apps/nan_app/src/nan_security.c | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h index c083497eaf6..767f6f32736 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h @@ -173,19 +173,21 @@ extern void *s_nan_data_lock; /* Internal key-install constants matching esp_wifi_set_sta_key_internal semantics. */ #define NAN_WIFI_WPA_ALG_CCMP 3 -#define NAN_WIFI_WPA_ALG_BIP_CMAC_128 7 /* IGTK/BIGTK BIP = blob WIFI_WPA_ALG_IGTK (confirmed by han2; 4 is SMS4) */ +#define NAN_WIFI_WPA_ALG_BIP_CMAC_128 7 /* IGTK/BIGTK BIP = blob WIFI_WPA_ALG_IGTK; 4 is SMS4 */ #define NAN_KEY_FLAG_RX BIT(2) #define NAN_KEY_FLAG_TX BIT(3) #define NAN_KEY_FLAG_PAIRWISE BIT(5) /* NAN key-type selector passed as the last arg of esp_wifi_set_nan_key_internal; - * tells the blob which NAN SA the key belongs to. IGTK/BIGTK values are - * provisional — confirm with han2 before the lib bump. */ -#define NAN_KEY_ND_TK 0 -#define NAN_KEY_ND_GTK 1 -#define NAN_KEY_NM_TK 2 -#define NAN_KEY_ND_IGTK 3 -#define NAN_KEY_ND_BIGTK 4 + * tells the blob which NAN SA the key belongs to. Values match the blob's + * key-type enum. */ +typedef enum { + NAN_KEY_ND_TK = 0, + NAN_KEY_ND_GTK = 1, + NAN_KEY_NM_TK = 2, + NAN_KEY_ND_IGTK = 3, + NAN_KEY_ND_BIGTK = 4, +} nan_key_type_t; /* Handshake state */ enum nan_handshake_state { diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index 8ad56e5d256..ee029d90e89 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -1322,8 +1322,8 @@ uint16_t nan_get_ndp_security_csid(uint8_t ndp_id, const uint8_t *peer_nmi) * (required for symmetric GTK distribution and third-party/iOS/Android * interop). Safe to include NCS-GTK here: the blob treats this value as an * opaque bitmap that it passes straight to the host CSIA callbacks - * (construct_csia / get_csia_len) and never interprets individual bits - * (confirmed by han2). Pairwise/link cipher selection and ND-TK install are + * (construct_csia / get_csia_len) and never interprets individual bits. + * Pairwise/link cipher selection and ND-TK install are * host-driven and do not read this field; the group key has its own install * path (NAN_KEY_ND_GTK) and gtk_required gate. */ return csid; From ebb9539d17a19f71264e48cd8f961c5d158d3e64 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 17:29:14 +0530 Subject: [PATCH 15/22] refactor(nan): use shared nan_key_type_t from esp_wifi_driver.h The NAN key-type selectors are defined by the blob in esp_wifi_driver.h (nan_key_type_t), which nan_i.h already includes. Add the group-integrity key types NAN_KEY_ND_IGTK (3) and NAN_KEY_ND_BIGTK (4) there to match the blob, and drop the duplicate host definitions from nan_i.h so a single shared enum is used. Resolves the review request to declare these in nan_key_type_t and avoids redefining the typedef. --- components/esp_wifi/wifi_apps/nan_app/src/nan_i.h | 13 +++---------- .../esp_supplicant/src/esp_wifi_driver.h | 2 ++ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h index 767f6f32736..74d038e6b15 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h @@ -178,16 +178,9 @@ extern void *s_nan_data_lock; #define NAN_KEY_FLAG_TX BIT(3) #define NAN_KEY_FLAG_PAIRWISE BIT(5) -/* NAN key-type selector passed as the last arg of esp_wifi_set_nan_key_internal; - * tells the blob which NAN SA the key belongs to. Values match the blob's - * key-type enum. */ -typedef enum { - NAN_KEY_ND_TK = 0, - NAN_KEY_ND_GTK = 1, - NAN_KEY_NM_TK = 2, - NAN_KEY_ND_IGTK = 3, - NAN_KEY_ND_BIGTK = 4, -} nan_key_type_t; +/* NAN key-type selector (nan_key_type_t: NAN_KEY_ND_TK / ND_GTK / NM_TK / ND_IGTK / + * ND_BIGTK), passed as the last arg of esp_wifi_set_nan_key_internal, is defined in + * esp_wifi_driver.h (included above) and shared with the blob. */ /* Handshake state */ enum nan_handshake_state { diff --git a/components/wpa_supplicant/esp_supplicant/src/esp_wifi_driver.h b/components/wpa_supplicant/esp_supplicant/src/esp_wifi_driver.h index 1ee7ff95827..e58fa811542 100644 --- a/components/wpa_supplicant/esp_supplicant/src/esp_wifi_driver.h +++ b/components/wpa_supplicant/esp_supplicant/src/esp_wifi_driver.h @@ -238,6 +238,8 @@ typedef enum { NAN_KEY_ND_TK = 0, NAN_KEY_ND_GTK, NAN_KEY_NM_TK, + NAN_KEY_ND_IGTK, /* 3 - NAN Integrity Group Temporal Key (BIP-CMAC-128) */ + NAN_KEY_ND_BIGTK, /* 4 - NAN Beacon Integrity Group Temporal Key (BIP-CMAC-128) */ } nan_key_type_t; typedef struct { From e952a580db8b384f9dfb93a919874850dd5e9969 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 17:29:14 +0530 Subject: [PATCH 16/22] docs(nan): document group-protection fields, CSID ciphers and NIRA Refresh stale/missing documentation now that the features are implemented: - group_data_prot / group_mgmt_prot in wifi_nan_discovery_security_params_t (esp_wifi_types_generic.h) and wifi_nan_security_params_t (esp_private/ wifi.h): describe GTKSA / IGTKSA+BIGTKSA instead of "not supported". - CSID enum: document NCS-GTK-CCM-128 (set internally via group_data_prot, not user-selectable) and NCS-PK-PASN-128 (NAN Pairing, via the Wi-Fi Aware component). - Drop "dummy" from the esp_nan_construct_nira() doc (it builds a real NIRA) and a stale "IGTK/BIGTK are placeholders" comment. --- components/esp_wifi/include/esp_private/wifi.h | 6 +++--- components/esp_wifi/include/esp_wifi_types_generic.h | 10 +++++----- .../esp_wifi/wifi_apps/nan_app/src/nan_security.c | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/components/esp_wifi/include/esp_private/wifi.h b/components/esp_wifi/include/esp_private/wifi.h index c7d5d8d9402..6047ad67a1f 100644 --- a/components/esp_wifi/include/esp_private/wifi.h +++ b/components/esp_wifi/include/esp_private/wifi.h @@ -67,8 +67,8 @@ typedef struct { uint16_t csid_bitmap; /**< Selected Cipher Suite ID bit (WIFI_NAN_CSID_BIT_*) */ uint8_t nd_pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; /**< ND-PMK */ uint8_t nd_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN]; /**< ND-PMKID */ - uint8_t group_data_prot: 1; /**< Group addressed data frame protection. Reserved: not supported right now. */ - uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection. Reserved: not supported right now. */ + uint8_t group_data_prot: 1; /**< Group addressed data frame protection (GTKSA): distribute a GTK on the secured NDP so multicast data frames are protected. */ + uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection (IGTKSA/BIGTKSA): BIP-protect multicast SDFs and Beacons. */ uint8_t reserved: 6; /**< Reserved */ } wifi_nan_security_params_t; @@ -1209,7 +1209,7 @@ esp_err_t esp_wifi_disconnect_internal(void); uint32_t esp_nan_get_nira_len(void); /** - * @brief Construct dummy NAN Identity Resolution Attribute (NIRA) + * @brief Construct NAN Identity Resolution Attribute (NIRA) * * @param[out] frm Buffer to write the attribute to * diff --git a/components/esp_wifi/include/esp_wifi_types_generic.h b/components/esp_wifi/include/esp_wifi_types_generic.h index 28b7d0bde6e..3827f022db0 100644 --- a/components/esp_wifi/include/esp_wifi_types_generic.h +++ b/components/esp_wifi/include/esp_wifi_types_generic.h @@ -933,9 +933,9 @@ typedef enum { WIFI_NAN_CSID_NCS_SK_256 = 2, /**< NCS-SK-256 (PSK/Passphrase). Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_2WDH_128 = 3, /**< NCS-PK-2WDH-128. Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_2WDH_256 = 4, /**< NCS-PK-2WDH-256. Reserved: not supported right now. */ - WIFI_NAN_CSID_NCS_GTK_CCM_128 = 5, - WIFI_NAN_CSID_NCS_GTK_GCM_256 = 6, - WIFI_NAN_CSID_NCS_PK_PASN_128 = 7, /**< NCS-PK-PASN-128. Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_GTK_CCM_128 = 5, /**< Group-data cipher (GTKSA). Selected internally when group_data_prot is set; not user-selectable via csid_bitmap. */ + WIFI_NAN_CSID_NCS_GTK_GCM_256 = 6, /**< Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_PK_PASN_128 = 7, /**< NCS-PK-PASN-128 (NAN Pairing). Requires CONFIG_ESP_WIFI_NAN_PAIRING and the Wi-Fi Aware component (esp-wifi-apps); not usable with stand-alone ESP-IDF. */ WIFI_NAN_CSID_NCS_PK_PASN_256 = 8, /**< NCS-PK-PASN-256. Reserved: not supported right now. */ } wifi_nan_cipher_suite_id_t; @@ -975,8 +975,8 @@ typedef struct { * is computed by the stack as the union of each credential's @c csid. */ typedef struct { - uint8_t group_data_prot: 1; /**< Group addressed data frame protection. Reserved: not supported right now. */ - uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection. Reserved: not supported right now. */ + uint8_t group_data_prot: 1; /**< Group addressed data frame protection (GTKSA): distribute a GTK on the secured NDP so multicast data frames are protected. */ + uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection (IGTKSA/BIGTKSA): BIP-protect multicast SDFs and Beacons. */ uint8_t reserved: 6; /**< Reserved */ uint8_t num_credentials; /**< Number of valid entries in @c creds (0..ESP_WIFI_NAN_MAX_CREDS_PER_SVC). 0 = open service. */ wifi_nan_credential_t creds[ESP_WIFI_NAN_MAX_CREDS_PER_SVC]; /**< Credentials list. */ diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index ee029d90e89..03fc2f4e52b 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -645,7 +645,7 @@ static int nan_build_rsna_key_descriptor(uint8_t *kd, uint16_t key_info_flags, * Group Key Data (GTK/IGTK/BIGTK KDEs) — Wi-Fi Aware v4.0 §7.1.3.2/§7.1.3.5/ * §9.5.21.5. Initiator distributes in M3, responder in M4; KDEs are always * KEK-wrapped (NIST AES Key Wrap), never in clear. Structure mirrors hostap - * src/nan/nan_sec.c nan_sec_add_kdes(); IGTK/BIGTK are placeholders for now. + * src/nan/nan_sec.c nan_sec_add_kdes(). *-----------------------------------------------------------------------*/ /* True if a GTKSA was negotiated for this NDP. gtk_required is the explicit From 1c59572c072e79e248a3af4a742fd7b59e1e4da1 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 17:29:14 +0530 Subject: [PATCH 17/22] change(wifi): update libs [2e77d2e] Rebuilt libs with NAN group-key (GTK/IGTK/BIGTK) support, matching the nan_key_type_t and group-protection header updates so the MD5-checked esp_wifi_driver.h and esp_wifi_types_generic.h verify. --- components/esp_wifi/lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/esp_wifi/lib b/components/esp_wifi/lib index dcdac54b984..df62dea0bcf 160000 --- a/components/esp_wifi/lib +++ b/components/esp_wifi/lib @@ -1 +1 @@ -Subproject commit dcdac54b98412c50586e67b73e34b3afb8447737 +Subproject commit df62dea0bcf2f81ce3f3a841347441ed07f6fca0 From 235207bb2a4f1e75717495973ffae417f237914d Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 17:51:08 +0530 Subject: [PATCH 18/22] fix(nan): seed peer BIP RX replay counter from the KDE IPN/BIPN The peer IGTK/BIGTK were installed with an all-zero seq, so the blob's BIP RX replay counter started at 0 instead of the peer's advertised value. Store the 6-octet IPN/BIPN from the IGTK/BIGTK KDE (the octets after the 2-byte Key ID, per 802.11 Fig 12-42/12-47) into the NDL and pass them as the install seq. The parser side of this lands with the group-KDE guards. --- components/esp_wifi/wifi_apps/nan_app/src/nan_app.c | 9 ++++----- components/esp_wifi/wifi_apps/nan_app/src/nan_i.h | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index 434924bc5d7..f9c569c9b22 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -1475,9 +1475,8 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer * nan_security_install_own_group_integrity_keys); not re-installed here, * to preserve the blob's monotonic BIPN/IPN across the session. Only the * peer RX keys are bound at NDP confirm. §7.1.3.3/§7.1.3.4; NMI==NDI today. - * Peer IGTK/BIGTK install RX-only against the peer NMI. seq (IPN/BIPN) - * starts at 0 for now; thread the peer's KDE IPN/BIPN once the blob - * consumes it for the BIP replay counter. */ + * Peer IGTK/BIGTK install RX-only against the peer NMI, seeding the BIP + * RX replay counter with the peer's advertised IPN/BIPN from the KDE. */ if (ndl->igtk_set && ndl->igtk_len) { if (ndl->igtk_len != NAN_ND_GTK_LEN) { ESP_LOGW(TAG, "NDP confirm: peer IGTK len=%d unsupported (BIP-CMAC-128 only); skipping", @@ -1485,7 +1484,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer } else { int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, peer_nmi, ndl->igtk_keyid, 0, - key_rsc, 6, + ndl->igtk_ipn, 6, ndl->igtk, ndl->igtk_len, NAN_KEY_ND_IGTK); if (r != 0) { @@ -1504,7 +1503,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer } else { int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, peer_nmi, ndl->bigtk_keyid, 0, - key_rsc, 6, + ndl->bigtk_ipn, 6, ndl->bigtk, ndl->bigtk_len, NAN_KEY_ND_BIGTK); if (r != 0) { diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h index 74d038e6b15..7bbb4d8b0f0 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h @@ -332,6 +332,8 @@ struct ndl_info { uint8_t igtk_keyid; /* peer IGTK Key ID (4 or 5) */ uint8_t bigtk_keyid; /* peer BIGTK Key ID (6 or 7) */ uint8_t gtk_rsc[NAN_KEY_RSC_LEN]; /* peer GTK RSC from Key RSC field */ + uint8_t igtk_ipn[6]; /* peer IGTK IPN (seeds BIP RX replay counter) */ + uint8_t bigtk_ipn[6]; /* peer BIGTK BIPN (seeds BIP RX replay counter) */ uint8_t gtk_set: 1; uint8_t igtk_set: 1; uint8_t bigtk_set: 1; From 2cc14d131f1dde2323ddcda3dc3a63e0d9f079ce Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 17:51:19 +0530 Subject: [PATCH 19/22] fix(nan): restrict GTK Key ID to 1/2 and ignore cleartext group KDEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tighten the own GTK Key ID guard from ">3" (which admitted 0 and 3) to the spec range 1..2 (Wi-Fi Aware v4.0 §7.1.3.2). - When the Encrypted-Key-Data bit is clear, ignore the Key Data instead of parsing KDEs from the clear: group KDEs are only ever carried KEK-wrapped (§7.1.3.5; 802.11-2020 §12.7.2). Also store the peer IPN/BIPN from the IGTK/BIGTK KDEs for the BIP RX replay-counter seed. --- components/esp_wifi/wifi_apps/nan_app/src/nan_security.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index 03fc2f4e52b..b282aac6a73 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -840,7 +840,7 @@ static int nan_append_gtk_kde(struct ndl_info *ndl, uint8_t **p, const uint8_t * if (!ndl->own_gtk_set || !ndl->own_gtk_len) { return 0; } - if (ndl->own_gtk_keyid > 3) { /* CCMP/GCMP Key ID range (§7.1.3.2: 1 or 2) */ + if (ndl->own_gtk_keyid < 1 || ndl->own_gtk_keyid > 2) { /* §7.1.3.2: GTK Key ID is 1 or 2 */ ESP_LOGW(TAG, "GTK: invalid Key ID %u", ndl->own_gtk_keyid); return -1; } @@ -2233,6 +2233,11 @@ void esp_nan_parse_ndp_key_desc(void *frm, size_t buf_len, uint8_t ndp_id, const kde_buf = plain; kde_len = unwrapped; } + } else { + /* §7.1.3.5 / 802.11 §12.7.2: group KDEs are only ever carried + * KEK-wrapped; ignore any Key Data presented in the clear. */ + ESP_LOGW(TAG, "NDP Key Desc: Key Data not encrypted; ignoring KDEs"); + kde_len = 0; } uint16_t kd_offset = 0; @@ -2272,6 +2277,7 @@ void esp_nan_parse_ndp_key_desc(void *frm, size_t buf_len, uint8_t ndp_id, const uint8_t igtk_len = body_len - NAN_IGTK_KDE_PREFIX_LEN; if (igtk_len <= NAN_GTK_MAX_LEN) { ndl->igtk_keyid = body[0]; + memcpy(ndl->igtk_ipn, body + 2, 6); /* IPN follows KeyID(2) */ memcpy(ndl->igtk, body + NAN_IGTK_KDE_PREFIX_LEN, igtk_len); ndl->igtk_len = igtk_len; ndl->igtk_set = 1; @@ -2283,6 +2289,7 @@ void esp_nan_parse_ndp_key_desc(void *frm, size_t buf_len, uint8_t ndp_id, const uint8_t bigtk_len = body_len - NAN_BIGTK_KDE_PREFIX_LEN; if (bigtk_len <= NAN_GTK_MAX_LEN) { ndl->bigtk_keyid = body[0]; + memcpy(ndl->bigtk_ipn, body + 2, 6); /* BIPN follows KeyID(2) */ memcpy(ndl->bigtk, body + NAN_BIGTK_KDE_PREFIX_LEN, bigtk_len); ndl->bigtk_len = bigtk_len; ndl->bigtk_set = 1; From 21a5c603e8bae1b6ce009f5b224a36661f71cbd3 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 18:49:04 +0530 Subject: [PATCH 20/22] change(wifi): sync NAN/netif wifi headers + rebuilt libs [d3da506] - injected esp_wifi_netif.h: add esp_wifi_netif_get_ip6_linklocal_from_mac() and esp_wifi_netif_set_static_neighbor() (static ND6 IPv6-pin API). - esp_wifi_types_generic.h (public + injected): rename the NCS-GTK cipher suites CCM_128 -> CCMP_128 / GCM_256 -> GCMP_256 to match Wi-Fi Aware v4.0 (Table 121, section 9.5.21.1); refresh NAN group-protection docs. Enum values (5/6), struct layout and on-air behaviour are unchanged. - nan_security.c: use the renamed macros and update the advertise log. - esp_wifi/lib: bump to the rebuilt blobs carrying the renamed MD5-checked header (han2 d3da506). --- .../esp_wifi/include/esp_wifi_types_generic.h | 8 ++--- components/esp_wifi/lib | 2 +- .../remote/include/injected/esp_wifi_netif.h | 36 +++++++++++++++++++ .../include/injected/esp_wifi_types_generic.h | 14 ++++---- .../wifi_apps/nan_app/src/nan_security.c | 10 +++--- 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/components/esp_wifi/include/esp_wifi_types_generic.h b/components/esp_wifi/include/esp_wifi_types_generic.h index 3827f022db0..cebdcb45497 100644 --- a/components/esp_wifi/include/esp_wifi_types_generic.h +++ b/components/esp_wifi/include/esp_wifi_types_generic.h @@ -933,8 +933,8 @@ typedef enum { WIFI_NAN_CSID_NCS_SK_256 = 2, /**< NCS-SK-256 (PSK/Passphrase). Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_2WDH_128 = 3, /**< NCS-PK-2WDH-128. Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_2WDH_256 = 4, /**< NCS-PK-2WDH-256. Reserved: not supported right now. */ - WIFI_NAN_CSID_NCS_GTK_CCM_128 = 5, /**< Group-data cipher (GTKSA). Selected internally when group_data_prot is set; not user-selectable via csid_bitmap. */ - WIFI_NAN_CSID_NCS_GTK_GCM_256 = 6, /**< Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_GTK_CCMP_128 = 5, /**< NCS-GTK-CCMP-128, the group-data cipher (GTKSA). Selected internally when group_data_prot is set; not user-selectable via csid_bitmap. */ + WIFI_NAN_CSID_NCS_GTK_GCMP_256 = 6, /**< NCS-GTK-GCMP-256. Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_PASN_128 = 7, /**< NCS-PK-PASN-128 (NAN Pairing). Requires CONFIG_ESP_WIFI_NAN_PAIRING and the Wi-Fi Aware component (esp-wifi-apps); not usable with stand-alone ESP-IDF. */ WIFI_NAN_CSID_NCS_PK_PASN_256 = 8, /**< NCS-PK-PASN-256. Reserved: not supported right now. */ } wifi_nan_cipher_suite_id_t; @@ -943,8 +943,8 @@ typedef enum { #define WIFI_NAN_CSID_BIT_NCS_SK_256 (1 << WIFI_NAN_CSID_NCS_SK_256) #define WIFI_NAN_CSID_BIT_NCS_PK_2WDH_128 (1 << WIFI_NAN_CSID_NCS_PK_2WDH_128) #define WIFI_NAN_CSID_BIT_NCS_PK_2WDH_256 (1 << WIFI_NAN_CSID_NCS_PK_2WDH_256) -#define WIFI_NAN_CSID_BIT_NCS_GTK_CCM_128 (1 << WIFI_NAN_CSID_NCS_GTK_CCM_128) -#define WIFI_NAN_CSID_BIT_NCS_GTK_GCM_256 (1 << WIFI_NAN_CSID_NCS_GTK_GCM_256) +#define WIFI_NAN_CSID_BIT_NCS_GTK_CCMP_128 (1 << WIFI_NAN_CSID_NCS_GTK_CCMP_128) +#define WIFI_NAN_CSID_BIT_NCS_GTK_GCMP_256 (1 << WIFI_NAN_CSID_NCS_GTK_GCMP_256) #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_128 (1 << WIFI_NAN_CSID_NCS_PK_PASN_128) #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_256 (1 << WIFI_NAN_CSID_NCS_PK_PASN_256) diff --git a/components/esp_wifi/lib b/components/esp_wifi/lib index df62dea0bcf..c9950ae98a8 160000 --- a/components/esp_wifi/lib +++ b/components/esp_wifi/lib @@ -1 +1 @@ -Subproject commit df62dea0bcf2f81ce3f3a841347441ed07f6fca0 +Subproject commit c9950ae98a8a422a98cc80726c5e91d8b191b319 diff --git a/components/esp_wifi/remote/include/injected/esp_wifi_netif.h b/components/esp_wifi/remote/include/injected/esp_wifi_netif.h index 7dfa724b066..ca054e8a6e1 100644 --- a/components/esp_wifi/remote/include/injected/esp_wifi_netif.h +++ b/components/esp_wifi/remote/include/injected/esp_wifi_netif.h @@ -81,6 +81,42 @@ bool esp_wifi_is_if_ready_when_started(wifi_netif_driver_t ifx); */ esp_err_t esp_wifi_register_if_rxcb(wifi_netif_driver_t ifx, esp_netif_receive_t fn, void * arg); +/** + * @brief Derive an IPv6 link-local address from a link-layer (MAC) address + * + * Computes fe80::/64 combined with the EUI-64 form of the given MAC (the 802 + * group bit complemented) into an esp_ip6_addr_t (zone 0). Interface-agnostic. + * + * @param[out] ip6 destination, set to the derived IPv6 link-local address + * @param[in] mac source link-layer (MAC) address (6 bytes) + */ +void esp_wifi_netif_get_ip6_linklocal_from_mac(esp_ip6_addr_t *ip6, const uint8_t mac[6]); + +#if CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES +/** + * @brief Pin (or remove) a static IPv6 link-local neighbor mapping on a wifi netif + * + * Installs a fixed link-local IPv6 -> MAC mapping for a peer reachable on the + * given wifi interface so that traffic to the peer bypasses Neighbor Discovery + * (no NS/NA exchanged), or removes a previously installed one. This layer owns + * both the netif lookup (from the interface type) and the derivation of the + * peer's link-local address from its MAC, so the caller only supplies the + * interface and the peer MAC. Only available when lwIP static ND6 entries are + * enabled. + * + * @param[in] wifi_if wifi interface the peer is reachable on + * @param[in] mac peer's link-layer (MAC) address + * @param[in] add true to add the mapping, false to remove it + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if mac is NULL or wifi_if is out of range + * - ESP_ERR_INVALID_STATE if the interface's netif is not up + * - error code from the underlying esp_netif call otherwise + */ +esp_err_t esp_wifi_netif_set_static_neighbor(wifi_interface_t wifi_if, const uint8_t mac[6], bool add); +#endif /* CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES */ + #ifdef __cplusplus } #endif diff --git a/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h b/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h index 960f6a8e529..4b6bb15b336 100644 --- a/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h +++ b/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h @@ -933,9 +933,9 @@ typedef enum { WIFI_NAN_CSID_NCS_SK_256 = 2, /**< NCS-SK-256 (PSK/Passphrase). Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_2WDH_128 = 3, /**< NCS-PK-2WDH-128. Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_2WDH_256 = 4, /**< NCS-PK-2WDH-256. Reserved: not supported right now. */ - WIFI_NAN_CSID_NCS_GTK_CCM_128 = 5, - WIFI_NAN_CSID_NCS_GTK_GCM_256 = 6, - WIFI_NAN_CSID_NCS_PK_PASN_128 = 7, /**< NCS-PK-PASN-128. Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_GTK_CCMP_128 = 5, /**< NCS-GTK-CCMP-128, the group-data cipher (GTKSA). Selected internally when group_data_prot is set; not user-selectable via csid_bitmap. */ + WIFI_NAN_CSID_NCS_GTK_GCMP_256 = 6, /**< NCS-GTK-GCMP-256. Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_PK_PASN_128 = 7, /**< NCS-PK-PASN-128 (NAN Pairing). Requires CONFIG_WIFI_RMT_NAN_PAIRING and the Wi-Fi Aware component (esp-wifi-apps); not usable with stand-alone ESP-IDF. */ WIFI_NAN_CSID_NCS_PK_PASN_256 = 8, /**< NCS-PK-PASN-256. Reserved: not supported right now. */ } wifi_nan_cipher_suite_id_t; @@ -943,8 +943,8 @@ typedef enum { #define WIFI_NAN_CSID_BIT_NCS_SK_256 (1 << WIFI_NAN_CSID_NCS_SK_256) #define WIFI_NAN_CSID_BIT_NCS_PK_2WDH_128 (1 << WIFI_NAN_CSID_NCS_PK_2WDH_128) #define WIFI_NAN_CSID_BIT_NCS_PK_2WDH_256 (1 << WIFI_NAN_CSID_NCS_PK_2WDH_256) -#define WIFI_NAN_CSID_BIT_NCS_GTK_CCM_128 (1 << WIFI_NAN_CSID_NCS_GTK_CCM_128) -#define WIFI_NAN_CSID_BIT_NCS_GTK_GCM_256 (1 << WIFI_NAN_CSID_NCS_GTK_GCM_256) +#define WIFI_NAN_CSID_BIT_NCS_GTK_CCMP_128 (1 << WIFI_NAN_CSID_NCS_GTK_CCMP_128) +#define WIFI_NAN_CSID_BIT_NCS_GTK_GCMP_256 (1 << WIFI_NAN_CSID_NCS_GTK_GCMP_256) #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_128 (1 << WIFI_NAN_CSID_NCS_PK_PASN_128) #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_256 (1 << WIFI_NAN_CSID_NCS_PK_PASN_256) @@ -975,8 +975,8 @@ typedef struct { * is computed by the stack as the union of each credential's @c csid. */ typedef struct { - uint8_t group_data_prot: 1; /**< Group addressed data frame protection. Reserved: not supported right now. */ - uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection. Reserved: not supported right now. */ + uint8_t group_data_prot: 1; /**< Group addressed data frame protection (GTKSA): distribute a GTK on the secured NDP so multicast data frames are protected. */ + uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection (IGTKSA/BIGTKSA): BIP-protect multicast SDFs and Beacons. */ uint8_t reserved: 6; /**< Reserved */ uint8_t num_credentials; /**< Number of valid entries in @c creds (0..ESP_WIFI_NAN_MAX_CREDS_PER_SVC). 0 = open service. */ wifi_nan_credential_t creds[ESP_WIFI_NAN_MAX_CREDS_PER_SVC]; /**< Credentials list. */ diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index b282aac6a73..56e094ded71 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -68,10 +68,10 @@ static struct { #define NAN_CSID_VALID_BITMAP ((uint16_t)0x01FE) /* NCS-GTK cipher-suite bits (group-addressed data). Advertised when a service - * sets group_data_prot; the basic default we generate/advertise is CCM-128. */ -#define NAN_CSID_GTK_BITS (WIFI_NAN_CSID_BIT_NCS_GTK_CCM_128 | \ - WIFI_NAN_CSID_BIT_NCS_GTK_GCM_256) -#define NAN_CSID_GTK_DEFAULT WIFI_NAN_CSID_BIT_NCS_GTK_CCM_128 + * sets group_data_prot; the basic default we generate/advertise is CCMP-128. */ +#define NAN_CSID_GTK_BITS (WIFI_NAN_CSID_BIT_NCS_GTK_CCMP_128 | \ + WIFI_NAN_CSID_BIT_NCS_GTK_GCMP_256) +#define NAN_CSID_GTK_DEFAULT WIFI_NAN_CSID_BIT_NCS_GTK_CCMP_128 /* Sentinel for peer_svc->matched_cred_idx: no PMKID match remembered yet. */ #define NAN_NO_MATCHED_CRED 0xFF @@ -1274,7 +1274,7 @@ esp_err_t nan_derive_security_params(const char *service_name, } if (sec_cfg->group_data_prot) { - ESP_LOGI(TAG, "NAN GTK: advertising NCS-GTK-CCM-128 (group_data_prot=1) for svc='%s'", + ESP_LOGI(TAG, "NAN GTK: advertising NCS-GTK-CCMP-128 (group_data_prot=1) for svc='%s'", service_name); } From 4639b5748575bae7773b62284f122a187285289c Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 20:26:10 +0530 Subject: [PATCH 21/22] refactor(nan): drop '##' debug marker from key hexdump tags The six key-material hexdumps (ND-IGTK/BIGTK/TK/GTK and the peer IGTK/BIGTK) kept a leftover '##' dev-grep marker on their tag string. Drop it; the descriptive labels stay and the dumps remain at ESP_LOG_DEBUG. --- components/esp_wifi/wifi_apps/nan_app/src/nan_app.c | 8 ++++---- components/esp_wifi/wifi_apps/nan_app/src/nan_security.c | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index f9c569c9b22..6dca0787e16 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -1418,7 +1418,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, NAN_KEY_ND_TK); - ESP_LOG_BUFFER_HEXDUMP("## ND-TK ", ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, ESP_LOG_DEBUG); + ESP_LOG_BUFFER_HEXDUMP("ND-TK", ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, ESP_LOG_DEBUG); if (ret != 0) { ESP_LOGE(TAG, "NDP confirm: failed to install ND-TK (ndp_id=%d, ret=%d)", ndp_id, ret); os_free(evt); @@ -1456,7 +1456,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer } else { ESP_LOGI(TAG, "NDP confirm: own GTK (TX) installed (keyid=%d)", ndl->own_gtk_keyid); } - ESP_LOG_BUFFER_HEXDUMP("## ND-GTK ", ndl->own_gtk, ndl->own_gtk_len, ESP_LOG_DEBUG); + ESP_LOG_BUFFER_HEXDUMP("ND-GTK", ndl->own_gtk, ndl->own_gtk_len, ESP_LOG_DEBUG); } if (ndl->gtk_set && ndl->gtk_len) { int gret = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, @@ -1493,7 +1493,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ESP_LOGI(TAG, "NDP confirm: peer IGTK (RX) installed (keyid=%d)", ndl->igtk_keyid); } /* Peer IGTK bytes for sniffer MIC cross-check vs the peer's multicast SDFs. */ - ESP_LOG_BUFFER_HEXDUMP("## PEER ND-IGTK ", ndl->igtk, ndl->igtk_len, ESP_LOG_DEBUG); + ESP_LOG_BUFFER_HEXDUMP("PEER ND-IGTK", ndl->igtk, ndl->igtk_len, ESP_LOG_DEBUG); } } if (ndl->bigtk_set && ndl->bigtk_len) { @@ -1512,7 +1512,7 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ESP_LOGI(TAG, "NDP confirm: peer BIGTK (RX) installed (keyid=%d)", ndl->bigtk_keyid); } /* Peer BIGTK bytes for sniffer MIC cross-check vs the peer's protected Beacons. */ - ESP_LOG_BUFFER_HEXDUMP("## PEER ND-BIGTK ", ndl->bigtk, ndl->bigtk_len, ESP_LOG_DEBUG); + ESP_LOG_BUFFER_HEXDUMP("PEER ND-BIGTK", ndl->bigtk, ndl->bigtk_len, ESP_LOG_DEBUG); } } } diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index 56e094ded71..73b1a1a9d59 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -748,7 +748,7 @@ void nan_security_install_own_group_integrity_keys(void) } else { ESP_LOGI(TAG, "NAN start: own IGTK (TX) installed (keyid=%d)", s_nan_ctx.own_igtk_keyid); } - ESP_LOG_BUFFER_HEXDUMP("## ND-IGTK ", s_nan_ctx.own_igtk, NAN_ND_GTK_LEN, ESP_LOG_DEBUG); + ESP_LOG_BUFFER_HEXDUMP("ND-IGTK", s_nan_ctx.own_igtk, NAN_ND_GTK_LEN, ESP_LOG_DEBUG); } if (nan_ensure_own_bigtk() == 0 && s_nan_ctx.own_bigtk_set) { int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, @@ -761,7 +761,7 @@ void nan_security_install_own_group_integrity_keys(void) } else { ESP_LOGI(TAG, "NAN start: own BIGTK (TX) installed (keyid=%d)", s_nan_ctx.own_bigtk_keyid); } - ESP_LOG_BUFFER_HEXDUMP("## ND-BIGTK ", s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN, ESP_LOG_DEBUG); + ESP_LOG_BUFFER_HEXDUMP("ND-BIGTK", s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN, ESP_LOG_DEBUG); } } From 61b705f03073272729c013ae3c417cae4d1771e8 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Wed, 1 Jul 2026 20:30:47 +0530 Subject: [PATCH 22/22] refactor(wifi): astyle-format wifi_netif.c The ternary continuation in esp_wifi_netif_set_static_neighbor() was not astyle-formatted; re-indent it so the pre-commit astyle hook passes in CI. --- components/esp_wifi/src/wifi_netif.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/esp_wifi/src/wifi_netif.c b/components/esp_wifi/src/wifi_netif.c index b632d51ff40..418b249479d 100644 --- a/components/esp_wifi/src/wifi_netif.c +++ b/components/esp_wifi/src/wifi_netif.c @@ -221,6 +221,6 @@ esp_err_t esp_wifi_netif_set_static_neighbor(wifi_interface_t wifi_if, const uin esp_wifi_netif_get_ip6_linklocal_from_mac(&addr6, mac); return add ? esp_netif_add_static_neighbor(esp_netif, &addr6, mac) - : esp_netif_remove_static_neighbor(esp_netif, &addr6); + : esp_netif_remove_static_neighbor(esp_netif, &addr6); } #endif /* CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES */