From 67aeac85e5338f42b8180378a74c475b17190269 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Mon, 11 May 2026 16:17:52 +0530 Subject: [PATCH 1/8] feat(wpa_supplicant): expose pbkdf2_sha256 for NAN crypto Re-export the pbkdf2_sha256 declaration from sha256.h and add the mbedTLS-backed implementation in crypto_mbedtls.c. NAN uses this for ND-PMK derivation from a passphrase; the helper is also available for any future caller that needs RFC 8018 PBKDF2 over SHA-256. --- .../src/crypto/crypto_mbedtls.c | 24 +++++++++++++++++++ components/wpa_supplicant/src/crypto/sha256.h | 2 ++ 2 files changed, 26 insertions(+) diff --git a/components/wpa_supplicant/esp_supplicant/src/crypto/crypto_mbedtls.c b/components/wpa_supplicant/esp_supplicant/src/crypto/crypto_mbedtls.c index 3ce59e457c2..cd3b603e2b4 100644 --- a/components/wpa_supplicant/esp_supplicant/src/crypto/crypto_mbedtls.c +++ b/components/wpa_supplicant/esp_supplicant/src/crypto/crypto_mbedtls.c @@ -1117,6 +1117,30 @@ int pbkdf2_sha1(const char *passphrase, const u8 *ssid, size_t ssid_len, } #endif /* defined(CONFIG_MBEDTLS_SHA1_C) || defined(CONFIG_MBEDTLS_HARDWARE_SHA) */ +#if defined(MBEDTLS_PKCS5_C) && defined(MBEDTLS_MD_C) +#include "mbedtls/private/pkcs5.h" + +#if defined(MBEDTLS_SHA256_C) +int pbkdf2_sha256(const char *passphrase, const u8 *salt, size_t salt_len, + int iterations, u8 *buf, size_t buflen) +{ + int ret; + + if (!passphrase || !salt || !buf || !salt_len || !buflen || iterations <= 0) { + return -1; + } + + ret = mbedtls_pkcs5_pbkdf2_hmac_ext(MBEDTLS_MD_SHA256, + (const u8 *) passphrase, + os_strlen(passphrase), + salt, salt_len, + (unsigned int) iterations, (uint32_t) buflen, buf); + return ret == 0 ? 0 : -1; +} +#endif /* MBEDTLS_SHA256_C */ + +#endif /* MBEDTLS_PKCS5_C && MBEDTLS_MD_C */ + #ifdef MBEDTLS_DES_C int des_encrypt(const u8 *clear, const u8 *key, u8 *cypher) { diff --git a/components/wpa_supplicant/src/crypto/sha256.h b/components/wpa_supplicant/src/crypto/sha256.h index 8054bbe5c51..82702ae3e11 100644 --- a/components/wpa_supplicant/src/crypto/sha256.h +++ b/components/wpa_supplicant/src/crypto/sha256.h @@ -26,5 +26,7 @@ int tls_prf_sha256(const u8 *secret, size_t secret_len, int hmac_sha256_kdf(const u8 *secret, size_t secret_len, const char *label, const u8 *seed, size_t seed_len, u8 *out, size_t outlen); +int pbkdf2_sha256(const char *passphrase, const u8 *salt, size_t salt_len, + int iterations, u8 *buf, size_t buflen); #endif /* SHA256_H */ From 94f226d73b731e7d902de03899eb3c0f2d327152 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Mon, 11 May 2026 16:18:10 +0530 Subject: [PATCH 2/8] feat(wifi): NAN encrypted datapath (Wi-Fi Aware M1-M4 handshake) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the NAN Data Path encrypted datapath per Wi-Fi Aware v4.0 (§7.1.3.5, §9.5.16): - Responder + initiator sides of the M1-M4 Shared-Key Descriptor exchange, with MIC compute/verify, PTK derivation, and PMK/PMKID derivation via PBKDF2-SHA256 over passphrase or pre-shared PMK. - CSIA / SCIA attribute build + parse, NCS-SK-128 cipher suite. - Per-NDL security context on ndl_info::security_ctx; per-svc PMK cache. - ndp_response_indication callback for initiator peer-NDI binding. - host<->blob ABI migrated from 27 direct esp_nan_* externs to a single nan_secure_dp_funcs callback struct in esp_private/wifi.h. - nan_security.c split out of nan_app.c (~340 lines de-duplicated into shared M1-M4 helpers). - CONFIG_ESP_WIFI_NAN_ENCRYPTED_DATAPATH gates the secure path so non- security builds compile out the crypto/handshake code. - ROM patch (esp32s31): mask ieee80211_encap_esfbuf to match the c5/c6/c61 pattern for NAN-capable chips. Hardening: PMK stack copies zeroized on every return, NDP attribute parsers bounds-checked, CSID range-checked before shifting, NDL slot reuse only when handshake state is IDLE, get_csia/scia_len aligned with their builders on empty input. API surface: NDP security types moved out of esp_wifi_types_generic.h into esp_private/wifi.h (internal-only). security pointer dropped from struct ndp_cb_peer_info. Discovery-side wifi_nan_security_type_t and the NDP Info callbacks removed (subsumed by csid_bitmap and SSI respectively). --- Kconfig | 1 + .../esp32s31/ld/esp32s31.rom.net80211.ld | 2 +- components/esp_wifi/CMakeLists.txt | 3 + components/esp_wifi/Kconfig | 17 + .../esp_wifi/include/esp_private/wifi.h | 145 +- .../esp_wifi/include/esp_wifi_types_generic.h | 73 +- components/esp_wifi/lib | 2 +- components/esp_wifi/remote/Kconfig.wifi.in | 17 + .../esp_wifi/remote/Kconfig.wifi_is_remote.in | 7 + .../include/injected/esp_wifi_types_generic.h | 69 +- .../wifi_apps/nan_app/include/esp_nan.h | 6 +- .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 641 ++++- .../esp_wifi/wifi_apps/nan_app/src/nan_i.h | 350 +++ .../wifi_apps/nan_app/src/nan_security.c | 2133 +++++++++++++++++ 14 files changed, 3329 insertions(+), 137 deletions(-) create mode 100644 components/esp_wifi/wifi_apps/nan_app/src/nan_i.h create mode 100644 components/esp_wifi/wifi_apps/nan_app/src/nan_security.c diff --git a/Kconfig b/Kconfig index 7f97713dac2..0ccdeb5cc6e 100644 --- a/Kconfig +++ b/Kconfig @@ -852,5 +852,6 @@ mainmenu "Espressif IoT Development Framework Configuration" - CONFIG_SPIRAM_SPEED_120M && CONFIG_SPIRAM_MODE_OCT - CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH - CONFIG_ESP_WIFI_EAP_TLS1_3 + - CONFIG_ESP_WIFI_NAN_SECURITY - CONFIG_USB_HOST_EXT_PORT_RESET_ATTEMPTS - CONFIG_GDMA_ENABLE_WEIGHTED_ARBITRATION diff --git a/components/esp_rom/esp32s31/ld/esp32s31.rom.net80211.ld b/components/esp_rom/esp32s31/ld/esp32s31.rom.net80211.ld index d9612528096..801702ff642 100644 --- a/components/esp_rom/esp32s31/ld/esp32s31.rom.net80211.ld +++ b/components/esp_rom/esp32s31/ld/esp32s31.rom.net80211.ld @@ -36,7 +36,7 @@ ic_reset_extra_softap_rx_ba = 0x2f800c78; ieee80211_align_eb = 0x2f800c7c; ieee80211_ampdu_reorder = 0x2f800c80; ieee80211_ampdu_start_age_timer = 0x2f800c84; -ieee80211_encap_esfbuf = 0x2f800c88; +/*ieee80211_encap_esfbuf = 0x2f800c88;*/ ieee80211_is_tx_allowed = 0x2f800c8c; ieee80211_output_pending_eb = 0x2f800c90; ieee80211_output_process = 0x2f800c94; diff --git a/components/esp_wifi/CMakeLists.txt b/components/esp_wifi/CMakeLists.txt index 3e673cae6cd..f9e8fb8ee57 100644 --- a/components/esp_wifi/CMakeLists.txt +++ b/components/esp_wifi/CMakeLists.txt @@ -70,6 +70,9 @@ if(CONFIG_ESP_WIFI_ENABLED OR CONFIG_ESP_HOST_WIFI_ENABLED) if(CONFIG_ESP_WIFI_NAN_SYNC_ENABLE OR CONFIG_ESP_WIFI_NAN_USD_ENABLE) list(APPEND srcs "wifi_apps/nan_app/src/nan_app.c") + if(CONFIG_ESP_WIFI_NAN_SECURITY) + list(APPEND srcs "wifi_apps/nan_app/src/nan_security.c") + endif() endif() if(CONFIG_ESP_WIFI_ENABLE_ROAMING_APP) list(APPEND srcs "wifi_apps/roaming_app/src/roaming_app.c") diff --git a/components/esp_wifi/Kconfig b/components/esp_wifi/Kconfig index ed767f61498..f41bd426e6f 100644 --- a/components/esp_wifi/Kconfig +++ b/components/esp_wifi/Kconfig @@ -593,6 +593,23 @@ menu "Wi-Fi" help Enable Wi-Fi Aware: Synchronization feature (NAN-Sync). + config ESP_WIFI_NAN_SECURITY + bool "Enable Wi-Fi Aware: Encrypted Pairwise Datapath (NDP Security)" + depends on ESP_WIFI_NAN_SYNC_ENABLE && IDF_EXPERIMENTAL_FEATURES && ESP_WIFI_MBEDTLS_CRYPTO + select MBEDTLS_PKCS5_C + select MBEDTLS_SHA256_C + default n + help + Enable encrypted pairwise datapath for Wi-Fi Aware (NAN). + Adds PBKDF2 key derivation (mbedTLS PKCS#5), 4-way handshake (M1-M4), + PTK derivation, and CCMP key installation for secured NAN + data links. Disable to save code size when only open + datapaths are needed. + + Requires ESP_WIFI_MBEDTLS_CRYPTO + MBEDTLS_PKCS5_C + MBEDTLS_SHA256_C + for ND-PMK derivation via pbkdf2_sha256() (MBEDTLS_MD_C is pulled in + transitively by MBEDTLS_PKCS5_C). + config ESP_WIFI_NAN_USD_ENABLE bool "Enable Wi-Fi Aware: Unsynchronized service discovery (NAN-USD)" depends on IDF_EXPERIMENTAL_FEATURES diff --git a/components/esp_wifi/include/esp_private/wifi.h b/components/esp_wifi/include/esp_private/wifi.h index 16f52068b54..d7c5752fc03 100644 --- a/components/esp_wifi/include/esp_private/wifi.h +++ b/components/esp_wifi/include/esp_private/wifi.h @@ -39,6 +39,29 @@ typedef struct { void *storage; /**< storage for FreeRTOS queue */ } wifi_static_queue_t; +/** + * @brief NAN Datapath Security Type (Wi-Fi Aware v4.0 §9.5.16.1 Table 85, "Security Present" bit) + */ +typedef enum { + WIFI_NAN_SECURITY_OPEN = 0, /**< NDP does not require security */ + WIFI_NAN_SECURITY_ENCRYPTED = 1, /**< NDP requires security */ +} wifi_nan_security_type_t; + +/** + * @brief NAN Datapath security parameters (Spec 6.1.1 - Data Path Request/Response) + * + * @note Shared between WiFi libraries and NAN app layer. + */ +typedef struct { + wifi_nan_security_type_t type; /**< Security Type (Open/Encrypted) */ + uint16_t csid_bitmap; /**< Bitmap of Cipher Suite IDs (WIFI_NAN_CSID_BIT_*) */ + uint8_t nd_pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; /**< ND-PMK (Required for Datapath) */ + 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 reserved: 6; /**< Reserved */ +} wifi_nan_datapath_security_params_t; + /* NAN Peer info parsed from SDF */ struct nan_cb_peer_info { uint8_t peer_mac[6]; /**< Peer NMI / interface MAC */ @@ -48,10 +71,11 @@ struct nan_cb_peer_info { uint8_t ssi_ver; /**< SSI version (service_match) */ uint8_t *ssi; /**< Service-specific information */ uint16_t ssi_len; /**< SSI length in bytes */ + wifi_nan_discovery_security_params_t *peer_security_params; /**< Peer's discovery security params (cipher / PMKIDs) */ nan_vendor_ie_t *vendor_ie; /**< Vendor-specific IE, if any */ }; -/* NDP Peer info parsed from NAF */ +/* NDP Peer info parsed from NAF. */ struct ndp_cb_peer_info { uint8_t ndp_id; uint8_t peer_nmi[6]; @@ -69,6 +93,95 @@ struct nan_sync_callbacks { uint8_t own_ndi[6], uint8_t ipv6_identifier[8]); void (* ndp_terminated)(uint8_t reason, uint8_t ndp_id, uint8_t init_ndi[6]); void (* action_txdone)(uint32_t context, bool tx_status); + /* Initiator-side M2 RX indication. Blob fires this after parsing the + * Responder NDI from the M2 NDP attribute (Wi-Fi Aware v4.0 §9.5.16.1 + * Table 82) and before invoking esp_nan_verify_ndp_resp_mic, so the + * host can populate ndl->peer_ndi for spec-correct PTK derivation + * (§7.1.3.5: PTK uses Data Interface addresses). */ + void (* ndp_response_indication)(struct ndp_cb_peer_info *peer_info); +}; + +/* Host helpers for NAN encrypted-datapath, registered via + * esp_nan_internal_register_secure_dp_funcs() at nan_app init. + * Security-gated fields are NULL when CONFIG_ESP_WIFI_NAN_SECURITY=n. */ +struct nan_secure_dp_funcs { + /* === Always-present helpers === */ + + /* TX completion notification for M2/M4 frames */ + void (*ndp_tx_done_cb)(uint8_t ndp_id, const uint8_t *peer_nmi, + uint8_t msg_type, bool tx_status); + + /* === Security-gated helpers (24 fields, NULL when SECURITY=n) === */ + + /* Length getters */ + uint32_t (*get_csia_len)(uint16_t own_csid_bitmap, uint16_t peer_csid_bitmap); + uint32_t (*get_scia_len)(uint8_t num_pmkids); + uint32_t (*get_shared_key_desc_attr_len)(uint16_t key_data_len); + int (*ndp_security_install_get_shared_desc_len)(void); + + /* CSIA / SCIA construction (publish + NDP req/resp) */ + int (*construct_csia)(uint8_t *frm, uint8_t pub_id, + uint16_t own_csid_bitmap, uint16_t peer_csid_bitmap); + int (*construct_scia_publish)(uint8_t *frm, uint8_t pub_id, + uint8_t num_pmkids, + const uint8_t pmkids[][ESP_WIFI_NAN_NDP_PMKID_LEN]); + int (*construct_scia_ndp_req)(uint8_t *frm, uint8_t ndp_id, + const uint8_t *peer_nmi); + int (*construct_scia_ndp_resp)(uint8_t *frm, uint8_t ndp_id, + const uint8_t *peer_nmi); + + /* Shared Key Descriptor builders (M1 / M2 / M3 / M4) -- MIC left zeroed */ + int (*get_ndp_req_shared_key_desc)(uint8_t *buf, size_t buf_len, + uint8_t ndp_id, const uint8_t *peer_nmi); + int (*get_ndp_resp_shared_key_desc)(uint8_t *buf, size_t buf_len, + uint8_t ndp_id, const uint8_t *peer_nmi); + int (*get_ndp_confirm_shared_key_desc)(uint8_t *buf, size_t buf_len, + uint8_t ndp_id, const uint8_t *peer_nmi); + int (*get_ndp_security_install_key_desc)(uint8_t *buf, size_t buf_len, + uint8_t ndp_id, const uint8_t *peer_nmi); + + /* M1 Auth_Token capture (host stores SHA-256(M1_body)[0:16] for M3 MIC) */ + int (*capture_m1_auth_token)(const uint8_t *m1_body, size_t body_len, + uint8_t ndp_id, const uint8_t *peer_nmi); + + /* MIC compute (TX path) -- fills MIC field in already-built key descriptor */ + int (*update_ndp_resp_mic)(uint8_t *m2_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); + int (*update_ndp_confirm_mic)(uint8_t *m3_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); + int (*update_ndp_security_install_mic)(uint8_t *m4_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); + + /* MIC verify (RX path) -- 0 on pass, -1 on mismatch (caller tears down NDP) */ + int (*verify_ndp_resp_mic)(uint8_t *m2_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); + int (*verify_ndp_confirm_mic)(uint8_t *m3_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); + int (*verify_ndp_security_install_mic)(uint8_t *m4_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); + + /* RX-path attribute parsers (CSIA / SCIA / key-desc). */ + void (*parse_ndp_csia)(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param); + void (*parse_ndp_scia)(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param); + void (*parse_ndp_key_desc)(void *frm, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi); + + /* Publish-side security parser (called when blob processes inbound publish SDF) */ + esp_err_t (*parse_publish_security)(const uint8_t *attrs, size_t attrs_len, + wifi_nan_discovery_security_params_t *security); + + /* Publish-init helper -- derives ND-PMK / ND-PMKID from the publish cfg + * passphrase and stores them on the host service record. Result is + * unused when CONFIG_ESP_WIFI_NAN_SECURITY=n. */ + esp_err_t (*derive_security_params)(wifi_nan_publish_cfg_t *cfg); + + /* NDP security gate: cipher-suite bitmap for (ndp_id, peer_nmi), or 0 for open. */ + uint16_t (*get_ndp_security_csid)(uint8_t ndp_id, const uint8_t *peer_nmi); }; /** @@ -810,6 +923,36 @@ esp_err_t esp_nan_internal_datapath_end(wifi_nan_datapath_end_req_t *req); */ esp_err_t esp_nan_internal_register_callbacks(struct nan_sync_callbacks *cb); +/** + * @brief Register the NAN secure-datapath helper table with the WiFi libraries. + * + * Pass a pointer to a static struct populated by the host; pass NULL to + * deregister at deinit time. + * + * @param fns Pointer to populated nan_secure_dp_funcs (or NULL to deregister) + * @return ESP_OK on success + */ +esp_err_t esp_nan_internal_register_secure_dp_funcs(struct nan_secure_dp_funcs *fns); + +/** + * @brief Install NAN pairwise/group key into Wi-Fi firmware key table + * + + * @param[in] alg Cipher algorithm identifier (for CCMP use 3) + * @param[in] addr Peer address used for key lookup (NDI for NAN data path) + * @param[in] key_idx Key index (use 0 for pairwise key) + * @param[in] set_tx Set key as TX key when non-zero + * @param[in] seq Initial sequence/RSC value (typically 8 bytes) + * @param[in] seq_len Length of seq in bytes + * @param[in] key Key material + * @param[in] key_len Key length in bytes + * @param[in] key_flag Key usage flags bitmask + * + * @return 0 on success, negative value on failure + */ +int esp_wifi_set_nan_key_internal(int alg, uint8_t *addr, int key_idx, int set_tx, + uint8_t *seq, size_t seq_len, uint8_t *key, size_t key_len, int key_flag); + /** * @brief Connect WiFi station to the AP. * diff --git a/components/esp_wifi/include/esp_wifi_types_generic.h b/components/esp_wifi/include/esp_wifi_types_generic.h index cef75825688..4dc5a26a5d4 100644 --- a/components/esp_wifi/include/esp_wifi_types_generic.h +++ b/components/esp_wifi/include/esp_wifi_types_generic.h @@ -17,6 +17,8 @@ extern "C" { #endif #define WIFI_AP_DEFAULT_MAX_IDLE_PERIOD 292 /**< Default timeout for SoftAP BSS Max Idle. Unit: 1000TUs >**/ +#define MAX_SSID_LEN 32 /**< Maximum length of SSID */ +#define MAX_PASSPHRASE_LEN 64 /**< Maximum length of passphrase */ /** * @brief Wi-Fi mode type @@ -864,6 +866,10 @@ typedef struct { #define ESP_WIFI_NDP_ROLE_INITIATOR 1 /**< Initiator role for NAN Data Path */ #define ESP_WIFI_NDP_ROLE_RESPONDER 2 /**< Responder role for NAN Data Path */ +#define ESP_WIFI_NAN_NDP_PMK_LEN 32 /**< Length of NAN Datapath PMK */ +#define ESP_WIFI_NAN_NDP_PMKID_LEN 16 /**< Length of NAN Datapath PMKID */ +#define ESP_WIFI_NAN_MAX_PMKIDS 2 /**< Maximum number of PMKIDs supported */ + #define ESP_WIFI_MAX_SVC_NAME_LEN 256 /**< Maximum length of NAN service name */ #define ESP_WIFI_MAX_FILTER_LEN 256 /**< Maximum length of NAN service filter */ #define ESP_WIFI_MAX_SVC_INFO_LEN 64 /**< Maximum length of NAN service info */ @@ -905,6 +911,46 @@ typedef enum { NAN_SUBSCRIBE_PASSIVE, /**< Passively listens to Publish frames */ } wifi_nan_service_type_t; +/** + * @brief NAN Cipher Suite IDs (Spec 4.1.1 & 6.1.1) + * + * @note Only WIFI_NAN_CSID_NCS_SK_128 is currently supported by the firmware. + * The other values are reserved for future support; selecting any of + * them via csid_bitmap will cause esp_wifi_nan_publish_service() and + * esp_wifi_nan_subscribe_service() to fail. + */ +typedef enum { + WIFI_NAN_CSID_NCS_SK_128 = 1, /**< NCS-SK-128 (PSK/Passphrase) */ + 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_PK_PASN_128 = 7, /**< NCS-PK-PASN-128. Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_PK_PASN_256 = 8, /**< NCS-PK-PASN-256. Reserved: not supported right now. */ +} wifi_nan_cipher_suite_id_t; + +#define WIFI_NAN_CSID_BIT_NCS_SK_128 (1 << WIFI_NAN_CSID_NCS_SK_128) +#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_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) + +/** + * @brief NAN Discovery security parameters (Spec 4.1.1 - Publish/Subscribe) + * + */ +typedef struct { + uint16_t csid_bitmap; /**< Bitmap of Supported Cipher Suite IDs (WIFI_NAN_CSID_BIT_*) */ + uint8_t num_pmkids; /**< Number of PMKIDs */ + uint8_t pmkids[ESP_WIFI_NAN_MAX_PMKIDS][ESP_WIFI_NAN_NDP_PMKID_LEN]; /**< ND-PMKIDs */ + 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 use_pmk: 1; /**< 0 - Use passphrase, 1 - Use PMK directly */ + uint8_t reserved: 5; /**< Reserved */ + char passphrase[MAX_PASSPHRASE_LEN]; /**< NCS-SK passphrase (use_pmk=0). NUL-terminated. */ + uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; /**< NCS-SK PMK (use_pmk=1). Raw bytes, not NUL-terminated. */ +} wifi_nan_discovery_security_params_t; + /** * @brief USD specific configuration parameters * @@ -943,12 +989,14 @@ typedef struct { uint8_t fsd_gas: 1; /**< 0 - Follow-up used for FSD, 1 - GAS used for FSD */ uint8_t ndp_resp_needed: 1; /**< 0 - Auto-Accept NDP Requests, 1 - Require explicit response with esp_wifi_nan_datapath_resp */ uint8_t usd_discovery_flag: 1; /**< 0 - NAN Synchronization for Discovery, 1 - USD for Discovery. 'NAN Discovery flag' from specification */ - uint8_t reserved: 2; /**< Reserved */ + uint8_t security_reqd: 1; /**< Security: 0 - Open, 1 - Required (NDP Security) */ + uint8_t reserved: 1; /**< Reserved */ uint16_t ssi_len; /**< Length of service specific info, maximum allowed length - ESP_WIFI_MAX_SVC_SSI_LEN */ uint8_t *ssi; /**< Service Specific Info of type wifi_nan_wfa_ssi_t for WFA defined protocols, otherwise proprietary and defined by Applications */ unsigned int ttl; /**< Run publish function for a given time interval in seconds. If ttl=0 and usd_discovery_flag is enabled, only one Publish message is transmitted */ wifi_nan_usd_config_t usd_publish_config; /**< USD configuration parameters. Relevant only when 'usd_discovery_flag' is set. */ + wifi_nan_discovery_security_params_t security_cfg; /**< Security configuration parameters */ nan_vendor_ie_t *vendor_ie; /**< Vendor specific IE to be added in publish frames */ } wifi_nan_publish_cfg_t; @@ -965,12 +1013,14 @@ typedef struct { uint8_t fsd_reqd: 1; /**< Further Service Discovery(FSD) required */ uint8_t fsd_gas: 1; /**< 0 - Follow-up used for FSD, 1 - GAS used for FSD */ uint8_t usd_discovery_flag: 1; /**< 0 - NAN Synchronization for Discovery, 1 - USD for Discovery. 'NAN Discovery flag' from specification */ - uint8_t reserved: 3; /**< Reserved */ + uint8_t security_reqd: 1; /**< Security: 0 - Open, 1 - Required (NDP Security) */ + uint8_t reserved: 2; /**< Reserved */ uint16_t ssi_len; /**< Length of service specific info, maximum allowed length - ESP_WIFI_MAX_SVC_SSI_LEN */ uint8_t *ssi; /**< Service Specific Info of type wifi_nan_wfa_ssi_t for WFA defined protocols, otherwise proprietary and defined by Applications */ unsigned int ttl; /**< Run subscribe function for a given time interval in seconds. If ttl=0 and usd_discovery_flag is enabled, the subscriber listens until the first service match is reported. */ wifi_nan_usd_config_t usd_subscribe_config; /**< USD configuration parameters. Relevant only when 'usd_discovery_flag' is set. */ + wifi_nan_discovery_security_params_t security_cfg; /**< Security configuration parameters */ nan_vendor_ie_t *vendor_ie; /**< Vendor specific IE to be added in subscribe frames */ } wifi_nan_subscribe_cfg_t; @@ -990,16 +1040,28 @@ typedef struct { /** * @brief NAN Datapath Request parameters * + * @note Datapath security is governed by the security_cfg passed to + * esp_wifi_nan_subscribe_service(); the NAN library derives ND-PMK, + * ND-PMKID and cipher selection internally from that subscribe-time + * configuration and applies them to every NDP initiated against the + * matched publisher. Per-NDP security parameters are not exposed on + * this struct: the caller never handles raw key material. */ typedef struct { uint8_t pub_id; /**< Publisher's service instance id */ uint8_t peer_mac[6]; /**< Peer's MAC address */ - bool confirm_required; /**< NDP Confirm frame required */ + bool confirm_required; /**< NDP Confirm frame required. Always used for the secure NDP handshake. */ } wifi_nan_datapath_req_t; /** * @brief NAN Datapath Response parameters * + * @note Datapath security is governed by the security_cfg passed to + * esp_wifi_nan_publish_service(); the NAN library derives ND-PMK, + * ND-PMKID and cipher selection internally from that publish-time + * configuration and applies them to every NDP this responder + * accepts. Per-NDP security parameters are not exposed on this + * struct: the caller never handles raw key material. */ typedef struct { bool accept; /**< True - Accept incoming NDP, False - Reject it */ @@ -1230,8 +1292,6 @@ typedef enum { WPS_FAIL_REASON_MAX /**< Max WPS fail reason */ } wifi_event_sta_wps_fail_reason_t; -#define MAX_SSID_LEN 32 /**< Maximum length of SSID */ -#define MAX_PASSPHRASE_LEN 64 /**< Maximum length of passphrase */ #define MAX_WPS_AP_CRED 3 /**< Maximum number of AP credentials received from WPS handshake */ /** @@ -1412,7 +1472,8 @@ typedef struct { uint8_t fsd_reqd: 1; /**< Further Service Discovery(FSD) required */ uint8_t fsd_gas: 1; /**< 0 - Follow-up used for FSD, 1 - GAS used for FSD */ uint8_t ndpe_support: 1; /**< NDPE supported by peer */ - uint8_t reserved: 4; /**< Reserved */ + uint8_t security_reqd: 1; /**< Security: 0 - Open, 1 - Required (NDP Security) */ + uint8_t reserved: 3; /**< Reserved */ uint32_t reserved_1; /**< Reserved */ uint32_t reserved_2; /**< Reserved */ uint8_t ssi_version; /**< Indicates version of SSI in Publish instance, 0 if not available */ diff --git a/components/esp_wifi/lib b/components/esp_wifi/lib index 4bc9760929b..bf7ccb11fe8 160000 --- a/components/esp_wifi/lib +++ b/components/esp_wifi/lib @@ -1 +1 @@ -Subproject commit 4bc9760929bb2bca6a30be1245c45a157893d486 +Subproject commit bf7ccb11fe8125539f3709dc49e6d7523f3cdeb0 diff --git a/components/esp_wifi/remote/Kconfig.wifi.in b/components/esp_wifi/remote/Kconfig.wifi.in index 7d67b92cb97..f8a575c50e9 100644 --- a/components/esp_wifi/remote/Kconfig.wifi.in +++ b/components/esp_wifi/remote/Kconfig.wifi.in @@ -572,6 +572,23 @@ config WIFI_RMT_NAN_SYNC_ENABLE help Enable Wi-Fi Aware: Synchronization feature (NAN-Sync). +config WIFI_RMT_NAN_SECURITY + bool "Enable Wi-Fi Aware: Encrypted Pairwise Datapath (NDP Security)" + depends on WIFI_RMT_NAN_SYNC_ENABLE && IDF_EXPERIMENTAL_FEATURES && WIFI_RMT_MBEDTLS_CRYPTO + select MBEDTLS_PKCS5_C + select MBEDTLS_SHA256_C + default n + help + Enable encrypted pairwise datapath for Wi-Fi Aware (NAN). + Adds PBKDF2 key derivation (mbedTLS PKCS#5), 4-way handshake (M1-M4), + PTK derivation, and CCMP key installation for secured NAN + data links. Disable to save code size when only open + datapaths are needed. + + Requires WIFI_RMT_MBEDTLS_CRYPTO + MBEDTLS_PKCS5_C + MBEDTLS_SHA256_C + for ND-PMK derivation via pbkdf2_sha256() (MBEDTLS_MD_C is pulled in + transitively by MBEDTLS_PKCS5_C). + config WIFI_RMT_NAN_USD_ENABLE bool "Enable Wi-Fi Aware: Unsynchronized service discovery (NAN-USD)" depends on IDF_EXPERIMENTAL_FEATURES diff --git a/components/esp_wifi/remote/Kconfig.wifi_is_remote.in b/components/esp_wifi/remote/Kconfig.wifi_is_remote.in index 24ea34928c9..5968090db27 100644 --- a/components/esp_wifi/remote/Kconfig.wifi_is_remote.in +++ b/components/esp_wifi/remote/Kconfig.wifi_is_remote.in @@ -290,6 +290,13 @@ if WIFI_RMT_NAN_SYNC_ENABLE default WIFI_RMT_NAN_SYNC_ENABLE endif +if WIFI_RMT_NAN_SECURITY + config ESP_WIFI_NAN_SECURITY # ignore: multiple-definition + bool + depends on WIFI_RMT_NAN_SYNC_ENABLE && IDF_EXPERIMENTAL_FEATURES && WIFI_RMT_MBEDTLS_CRYPTO + default WIFI_RMT_NAN_SECURITY +endif + if WIFI_RMT_NAN_USD_ENABLE config ESP_WIFI_NAN_USD_ENABLE # ignore: multiple-definition bool 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 44a24d329fb..6b0b143e397 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 @@ -864,6 +864,10 @@ typedef struct { #define ESP_WIFI_NDP_ROLE_INITIATOR 1 /**< Initiator role for NAN Data Path */ #define ESP_WIFI_NDP_ROLE_RESPONDER 2 /**< Responder role for NAN Data Path */ +#define ESP_WIFI_NAN_NDP_PMK_LEN 32 /**< Length of NAN Datapath PMK */ +#define ESP_WIFI_NAN_NDP_PMKID_LEN 16 /**< Length of NAN Datapath PMKID */ +#define ESP_WIFI_NAN_MAX_PMKIDS 2 /**< Maximum number of PMKIDs supported */ + #define ESP_WIFI_MAX_SVC_NAME_LEN 256 /**< Maximum length of NAN service name */ #define ESP_WIFI_MAX_FILTER_LEN 256 /**< Maximum length of NAN service filter */ #define ESP_WIFI_MAX_SVC_INFO_LEN 64 /**< Maximum length of NAN service info */ @@ -905,6 +909,46 @@ typedef enum { NAN_SUBSCRIBE_PASSIVE, /**< Passively listens to Publish frames */ } wifi_nan_service_type_t; +/** + * @brief NAN Cipher Suite IDs (Spec 4.1.1 & 6.1.1) + * + * @note Only WIFI_NAN_CSID_NCS_SK_128 is currently supported by the firmware. + * The other values are reserved for future support; selecting any of + * them via csid_bitmap will cause esp_wifi_nan_publish_service() and + * esp_wifi_nan_subscribe_service() to fail. + */ +typedef enum { + WIFI_NAN_CSID_NCS_SK_128 = 1, /**< NCS-SK-128 (PSK/Passphrase) */ + 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_PK_PASN_128 = 7, /**< NCS-PK-PASN-128. Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_PK_PASN_256 = 8, /**< NCS-PK-PASN-256. Reserved: not supported right now. */ +} wifi_nan_cipher_suite_id_t; + +#define WIFI_NAN_CSID_BIT_NCS_SK_128 (1 << WIFI_NAN_CSID_NCS_SK_128) +#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_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) + +/** + * @brief NAN Discovery security parameters (Spec 4.1.1 - Publish/Subscribe) + * + */ +typedef struct { + uint16_t csid_bitmap; /**< Bitmap of Supported Cipher Suite IDs (WIFI_NAN_CSID_BIT_*) */ + uint8_t num_pmkids; /**< Number of PMKIDs */ + uint8_t pmkids[ESP_WIFI_NAN_MAX_PMKIDS][ESP_WIFI_NAN_NDP_PMKID_LEN]; /**< ND-PMKIDs */ + 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 use_pmk: 1; /**< 0 - Use passphrase, 1 - Use PMK directly */ + uint8_t reserved: 5; /**< Reserved */ + char passphrase[MAX_PASSPHRASE_LEN]; /**< NCS-SK passphrase (use_pmk=0). NUL-terminated. */ + uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; /**< NCS-SK PMK (use_pmk=1). Raw bytes, not NUL-terminated. */ +} wifi_nan_discovery_security_params_t; + /** * @brief USD specific configuration parameters * @@ -943,12 +987,14 @@ typedef struct { uint8_t fsd_gas: 1; /**< 0 - Follow-up used for FSD, 1 - GAS used for FSD */ uint8_t ndp_resp_needed: 1; /**< 0 - Auto-Accept NDP Requests, 1 - Require explicit response with esp_wifi_nan_datapath_resp */ uint8_t usd_discovery_flag: 1; /**< 0 - NAN Synchronization for Discovery, 1 - USD for Discovery. 'NAN Discovery flag' from specification */ - uint8_t reserved: 2; /**< Reserved */ + uint8_t security_reqd: 1; /**< Security: 0 - Open, 1 - Required (NDP Security) */ + uint8_t reserved: 1; /**< Reserved */ uint16_t ssi_len; /**< Length of service specific info, maximum allowed length - ESP_WIFI_MAX_SVC_SSI_LEN */ uint8_t *ssi; /**< Service Specific Info of type wifi_nan_wfa_ssi_t for WFA defined protocols, otherwise proprietary and defined by Applications */ unsigned int ttl; /**< Run publish function for a given time interval in seconds. If ttl=0 and usd_discovery_flag is enabled, only one Publish message is transmitted */ wifi_nan_usd_config_t usd_publish_config; /**< USD configuration parameters. Relevant only when 'usd_discovery_flag' is set. */ + wifi_nan_discovery_security_params_t security_cfg; /**< Security configuration parameters */ nan_vendor_ie_t *vendor_ie; /**< Vendor specific IE to be added in publish frames */ } wifi_nan_publish_cfg_t; @@ -965,12 +1011,14 @@ typedef struct { uint8_t fsd_reqd: 1; /**< Further Service Discovery(FSD) required */ uint8_t fsd_gas: 1; /**< 0 - Follow-up used for FSD, 1 - GAS used for FSD */ uint8_t usd_discovery_flag: 1; /**< 0 - NAN Synchronization for Discovery, 1 - USD for Discovery. 'NAN Discovery flag' from specification */ - uint8_t reserved: 3; /**< Reserved */ + uint8_t security_reqd: 1; /**< Security: 0 - Open, 1 - Required (NDP Security) */ + uint8_t reserved: 2; /**< Reserved */ uint16_t ssi_len; /**< Length of service specific info, maximum allowed length - ESP_WIFI_MAX_SVC_SSI_LEN */ uint8_t *ssi; /**< Service Specific Info of type wifi_nan_wfa_ssi_t for WFA defined protocols, otherwise proprietary and defined by Applications */ unsigned int ttl; /**< Run subscribe function for a given time interval in seconds. If ttl=0 and usd_discovery_flag is enabled, the subscriber listens until the first service match is reported. */ wifi_nan_usd_config_t usd_subscribe_config; /**< USD configuration parameters. Relevant only when 'usd_discovery_flag' is set. */ + wifi_nan_discovery_security_params_t security_cfg; /**< Security configuration parameters */ nan_vendor_ie_t *vendor_ie; /**< Vendor specific IE to be added in subscribe frames */ } wifi_nan_subscribe_cfg_t; @@ -990,16 +1038,28 @@ typedef struct { /** * @brief NAN Datapath Request parameters * + * @note Datapath security is governed by the security_cfg passed to + * esp_wifi_nan_subscribe_service(); the NAN library derives ND-PMK, + * ND-PMKID and cipher selection internally from that subscribe-time + * configuration and applies them to every NDP initiated against the + * matched publisher. Per-NDP security parameters are not exposed on + * this struct: the caller never handles raw key material. */ typedef struct { uint8_t pub_id; /**< Publisher's service instance id */ uint8_t peer_mac[6]; /**< Peer's MAC address */ - bool confirm_required; /**< NDP Confirm frame required */ + bool confirm_required; /**< NDP Confirm frame required. Always used for the secure NDP handshake. */ } wifi_nan_datapath_req_t; /** * @brief NAN Datapath Response parameters * + * @note Datapath security is governed by the security_cfg passed to + * esp_wifi_nan_publish_service(); the NAN library derives ND-PMK, + * ND-PMKID and cipher selection internally from that publish-time + * configuration and applies them to every NDP this responder + * accepts. Per-NDP security parameters are not exposed on this + * struct: the caller never handles raw key material. */ typedef struct { bool accept; /**< True - Accept incoming NDP, False - Reject it */ @@ -1412,7 +1472,8 @@ typedef struct { uint8_t fsd_reqd: 1; /**< Further Service Discovery(FSD) required */ uint8_t fsd_gas: 1; /**< 0 - Follow-up used for FSD, 1 - GAS used for FSD */ uint8_t ndpe_support: 1; /**< NDPE supported by peer */ - uint8_t reserved: 4; /**< Reserved */ + uint8_t security_reqd: 1; /**< Security: 0 - Open, 1 - Required (NDP Security) */ + uint8_t reserved: 3; /**< Reserved */ uint32_t reserved_1; /**< Reserved */ uint32_t reserved_2; /**< Reserved */ uint8_t ssi_version; /**< Indicates version of SSI in Publish instance, 0 if not available */ 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 63c58bc44c8..d1edab16bba 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 @@ -35,6 +35,8 @@ extern "C" { #define IS_ZERO_NAN_ADDR_ID(a) (!((a)[0] | (a)[1] | (a)[2] | (a)[3] | \ (a)[4] | (a)[5] | (a)[6] | (a)[7])) +#define NAN_IPV6_ADDR_ID_LEN 8 + #define ESP_NAN_SET_IPV6_LINKLOCAL_FROM_IDENTIFIER(_target_addr, _identifier) \ do { \ (_target_addr).type = IPADDR_TYPE_V6; \ @@ -85,7 +87,7 @@ esp_err_t esp_wifi_nan_sync_stop(void); * * @attention This API should be called by the Subscriber after a match occurs with a Publisher. * - * @param req NAN Datapath Request parameters. + * @param req NAN Datapath Request parameters * * @return * - non-zero NAN Datapath identifier: If NAN datapath req was accepted by publisher @@ -99,7 +101,7 @@ uint8_t esp_wifi_nan_datapath_req(wifi_nan_datapath_req_t *req); * @attention This API should be called if ndp_resp_needed is set 1 in wifi_nan_publish_cfg_t and * a WIFI_EVENT_NDP_INDICATION event is received due to an incoming NDP request. * - * @param resp NAN Datapath Response parameters. + * @param resp NAN Datapath Response parameters * * @return * - ESP_OK: succeed 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 6a56bd5d1d4..67ee7db5e58 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 @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include #include "esp_wifi.h" #include "esp_private/wifi.h" #include "esp_wifi_netif.h" @@ -17,6 +18,7 @@ #include "os.h" #include "esp_nan.h" #include "utils/common.h" +#include "nan_i.h" #ifdef CONFIG_ESP_WIFI_NAN_USD_ENABLE #include "esp_private/esp_nan_usd.h" #endif /* CONFIG_ESP_WIFI_NAN_USD_ENABLE */ @@ -38,21 +40,18 @@ /* Macros */ #define MACADDR_LEN 6 -#define MACADDR_EQUAL(a1, a2) (memcmp(a1, a2, MACADDR_LEN)) +#define MACADDR_EQUAL(a1, a2) (memcmp(a1, a2, MACADDR_LEN) == 0) #define MACADDR_COPY(dst, src) (memcpy(dst, src, MACADDR_LEN)) #define NAN_DW_INTVL_MS 524 /* NAN DW interval (512 TU's ~= 524 mSec) */ #define NAN_ACTION_TIMEOUT 4*NAN_DW_INTVL_MS -#define NAN_DATA_LOCK() os_mutex_lock(s_nan_data_lock) -#define NAN_DATA_UNLOCK() os_mutex_unlock(s_nan_data_lock) - /* Global Variables */ static const char *TAG = "nan_app"; #ifdef CONFIG_ESP_WIFI_NAN_SYNC_ENABLE static EventGroupHandle_t nan_event_group; static bool s_app_default_handlers_set = false; static uint8_t null_mac[MACADDR_LEN] = {0}; -static void *s_nan_data_lock = NULL; +void *s_nan_data_lock = NULL; /* extern in nan_i.h */ static uint32_t s_fup_context; #endif /* CONFIG_ESP_WIFI_NAN_SYNC_ENABLE */ #ifdef CONFIG_ESP_WIFI_NAN_USD_ENABLE @@ -64,48 +63,12 @@ static const uint8_t s_wfa_oui[3] = {0x50, 0x6f, 0x9a}; #define NAN_SDEA_CTRL_FSD_REQD BIT(0) #define NAN_SDEA_CTRL_FSD_GAS BIT(1) #define NAN_SDEA_CTRL_DATAPATH_REQD BIT(2) +#define NAN_SDEA_CTRL_SECURITY_REQD BIT(6) #ifdef CONFIG_ESP_WIFI_NAN_SYNC_ENABLE -#define NAN_DATA_LOCK() os_mutex_lock(s_nan_data_lock) -#define NAN_DATA_UNLOCK() os_mutex_unlock(s_nan_data_lock) -struct peer_svc_info { - SLIST_ENTRY(peer_svc_info) next; - uint8_t peer_svc_info[ESP_WIFI_MAX_SVC_INFO_LEN]; /**< Information for followup message */ - uint8_t svc_id; /**< Identifier of peer's service */ - uint8_t own_svc_id; /**< Identifier for own service */ - uint8_t type; /**< Service type (Publish/Subscribe) */ - uint8_t peer_nmi[MACADDR_LEN]; /**< Peer's NAN Management Interface address */ - uint32_t device_caps; -}; - -struct own_svc_info { - char svc_name[ESP_WIFI_MAX_SVC_NAME_LEN]; /**< Name identifying a service */ - uint8_t svc_id; /**< Identifier for a service */ - uint8_t type; /**< Service type (Publish/Subscribe) */ - bool ndp_resp_needed; /**< If enabled, NDP response is required */ - uint8_t num_peer_records; /**< Count of peer records associated with svc_id */ - SLIST_HEAD(peer_list_t, peer_svc_info) peer_list; /**< List of peers matched for specific service */ -}; - -struct ndl_info { - uint8_t ndp_id; /**< Identifier for instance of NDP */ - uint8_t peer_ndi[MACADDR_LEN]; /**< Peer's NAN Data Interface address */ - uint8_t peer_nmi[MACADDR_LEN]; /**< Peer's NAN Management Interface address */ - uint8_t publisher_id; /**< Publisher's service identifier */ - uint8_t own_role; /**< Own role (Publisher/Subscriber) */ - uint32_t device_caps; /**< Peer's Device Capabilities from NDP Indication/Confirm */ -}; - -typedef struct { - uint8_t state; - uint8_t event; - struct ndl_info ndl[ESP_WIFI_NAN_DATAPATH_MAX_PEERS]; /**< Record of NDL of all peers */ - struct own_svc_info own_svc[ESP_WIFI_NAN_MAX_SVC_SUPPORTED]; /**< Record of own service(s) */ - esp_netif_t *nan_netif; -} nan_ctx_t; - -static nan_ctx_t s_nan_ctx; +/* Definition of nan_ctx_t storage shared via nan_i.h. */ +nan_ctx_t s_nan_ctx; void esp_wifi_nan_get_ipv6_linklocal_from_mac(ip6_addr_t *ip6, uint8_t *mac_addr) { @@ -129,7 +92,7 @@ void esp_wifi_nan_get_ipv6_linklocal_from_mac(ip6_addr_t *ip6, uint8_t *mac_addr ip6->zone = IP6_NO_ZONE; } -static struct own_svc_info *nan_find_own_svc(uint8_t svc_id) +struct own_svc_info *nan_find_own_svc(uint8_t svc_id) { struct own_svc_info *p_svc = NULL; @@ -174,7 +137,7 @@ static struct peer_svc_info *nan_find_peer_svc(uint8_t own_svc_id, uint8_t peer_ uint8_t *peer_nmi_valid = NULL; int idx = 0; - if (MACADDR_EQUAL(peer_nmi, null_mac)) { + if (!MACADDR_EQUAL(peer_nmi, null_mac)) { /* non-zero Peer NMI given, use it */ peer_nmi_valid = peer_nmi; } @@ -190,7 +153,7 @@ static struct peer_svc_info *nan_find_peer_svc(uint8_t own_svc_id, uint8_t peer_ } SLIST_FOREACH(temp, &(p_own_svc->peer_list), next) { if (peer_svc_id != 0 && peer_nmi_valid) { - if (temp->svc_id == peer_svc_id && !MACADDR_EQUAL(temp->peer_nmi, peer_nmi_valid)) { + if (temp->svc_id == peer_svc_id && MACADDR_EQUAL(temp->peer_nmi, peer_nmi_valid)) { p_peer_svc = temp; break; } @@ -200,7 +163,7 @@ static struct peer_svc_info *nan_find_peer_svc(uint8_t own_svc_id, uint8_t peer_ break; } } else { - if (peer_nmi_valid && !MACADDR_EQUAL(temp->peer_nmi, peer_nmi_valid)) { + if (peer_nmi_valid && MACADDR_EQUAL(temp->peer_nmi, peer_nmi_valid)) { p_peer_svc = temp; break; } @@ -308,7 +271,9 @@ static bool nan_services_limit_reached(void) return true; } -static void nan_record_own_svc(uint8_t id, uint8_t type, const char svc_name[], bool ndp_resp_needed) +static void nan_record_own_svc(uint8_t id, uint8_t type, const char svc_name[], + bool ndp_resp_needed, + const wifi_nan_discovery_security_params_t *security_cfg) { struct own_svc_info *p_svc = NULL; @@ -330,12 +295,38 @@ static void nan_record_own_svc(uint8_t id, uint8_t type, const char svc_name[], if (type == ESP_NAN_PUBLISH) { p_svc->ndp_resp_needed = ndp_resp_needed; } +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + /* Wipe to drop stale PMK material if this slot was previously used. */ + forced_memzero(&p_svc->security_cfg, sizeof(p_svc->security_cfg)); + forced_memzero(p_svc->pmk_cache, sizeof(p_svc->pmk_cache)); + + if (security_cfg) { + memcpy(&p_svc->security_cfg, security_cfg, sizeof(wifi_nan_discovery_security_params_t)); + if (security_cfg->num_pmkids > 0) { + memcpy(p_svc->pmk_cache[0], security_cfg->pmk, ESP_WIFI_NAN_NDP_PMK_LEN); + /* Public struct has one pmk[32]; cap num_pmkids to the count we cached. */ + p_svc->security_cfg.num_pmkids = 1; + } + } +#else + (void)security_cfg; +#endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ +} + +/* A slot is in use once nan_record_new_ndl/preclaim has stamped peer_nmi, + * even if ndp_id is still 0 (initiator pre-claim window between M1 build + * and the WiFi library returning the real ndp_id). Treating ndp_id==0 alone as + * "free" lets a concurrent claim for a different peer overwrite a + * pre-claimed slot's security context. */ +static inline bool nan_ndl_slot_in_use(const struct ndl_info *ndl) +{ + return (ndl->ndp_id != 0) || !MACADDR_EQUAL(ndl->peer_nmi, null_mac); } static bool ndl_limit_reached(void) { for (int i = 0; i < ESP_WIFI_NAN_DATAPATH_MAX_PEERS; i++) { - if (s_nan_ctx.ndl[i].ndp_id == 0) { + if (!nan_ndl_slot_in_use(&s_nan_ctx.ndl[i])) { return false; } } @@ -344,34 +335,56 @@ static bool ndl_limit_reached(void) static void nan_record_new_ndl(uint8_t ndp_id, uint8_t publish_id, uint8_t peer_nmi[], uint8_t own_role, uint32_t device_caps) { - struct ndl_info *ndl = NULL; - - for (int i = 0; i < ESP_WIFI_NAN_DATAPATH_MAX_PEERS; i++) { - if (s_nan_ctx.ndl[i].ndp_id == 0) { - ndl = &s_nan_ctx.ndl[i]; - break; - } + struct ndl_info *ndl = nan_find_ndl_by_pub_id_and_peer(publish_id, peer_nmi); + /* Reuse the slot when either: + * - it is an IDLE pre-claim from the initiator security path, or + * - it already holds an active NDP with the same ndp_id. */ + bool reuse_slot = false; +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + if (ndl && ndl->handshake_state == NAN_HANDSHAKE_IDLE) { + reuse_slot = true; } - if (!ndl) { +#endif + if (ndl && ndp_id != 0 && ndl->ndp_id == ndp_id) { + reuse_slot = true; + } + if (ndl && reuse_slot) { + ndl->ndp_id = ndp_id; + ndl->own_role = own_role; return; } + if (ndl) { + /* Stale slot from a prior session; wipe PMK/PTK/handshake state before re-binding. */ + forced_memzero(ndl, sizeof(*ndl)); + } else { + for (int i = 0; i < ESP_WIFI_NAN_DATAPATH_MAX_PEERS; i++) { + if (!nan_ndl_slot_in_use(&s_nan_ctx.ndl[i])) { + ndl = &s_nan_ctx.ndl[i]; + break; + } + } + if (!ndl) { + ESP_LOGE(TAG, "No free NDL slot for ndp_id=%u pub_id=%u peer="MACSTR, + ndp_id, publish_id, MAC2STR(peer_nmi)); + return; + } + } ndl->ndp_id = ndp_id; ndl->device_caps = device_caps; if (peer_nmi) { MACADDR_COPY(ndl->peer_nmi, peer_nmi); } + /* peer_ndi is populated by WiFi-library callbacks (responder: M1 RX; initiator: M2 RX before MIC verify). */ ndl->publisher_id = publish_id; ndl->own_role = own_role; } -static struct ndl_info *nan_find_ndl(uint8_t ndp_id, uint8_t peer_nmi[]) +struct ndl_info *nan_find_ndl(uint8_t ndp_id, uint8_t peer_nmi[]) { - struct ndl_info *ndl = NULL; - for (int i = 0; i < ESP_WIFI_NAN_DATAPATH_MAX_PEERS; i++) { - ndl = &s_nan_ctx.ndl[i]; + struct ndl_info *ndl = &s_nan_ctx.ndl[i]; if (ndp_id != 0 && peer_nmi) { - if (ndl->ndp_id == ndp_id && !MACADDR_EQUAL(ndl->peer_nmi, peer_nmi)) { + if (ndl->ndp_id == ndp_id && MACADDR_EQUAL(ndl->peer_nmi, peer_nmi)) { return ndl; } } else if (ndp_id != 0) { @@ -379,7 +392,7 @@ static struct ndl_info *nan_find_ndl(uint8_t ndp_id, uint8_t peer_nmi[]) return ndl; } } else if (peer_nmi) { - if (!MACADDR_EQUAL(ndl->peer_nmi, peer_nmi)) { + if (MACADDR_EQUAL(ndl->peer_nmi, peer_nmi)) { return ndl; } } @@ -387,6 +400,21 @@ static struct ndl_info *nan_find_ndl(uint8_t ndp_id, uint8_t peer_nmi[]) return NULL; } +/** Find NDL by publisher_id + peer_nmi (e.g. when CSIA/SCIA/key desc parsed before NDP/NDL attribute) */ +struct ndl_info *nan_find_ndl_by_pub_id_and_peer(uint8_t pub_id, const uint8_t *peer_nmi) +{ + if (!peer_nmi) { + return NULL; + } + for (int i = 0; i < ESP_WIFI_NAN_DATAPATH_MAX_PEERS; i++) { + struct ndl_info *ndl = &s_nan_ctx.ndl[i]; + if (ndl->publisher_id == pub_id && MACADDR_EQUAL(ndl->peer_nmi, peer_nmi)) { + return ndl; + } + } + return NULL; +} + static bool nan_is_datapath_active(void) { for (int i = 0; i < ESP_WIFI_NAN_DATAPATH_MAX_PEERS; i++) { @@ -397,6 +425,56 @@ static bool nan_is_datapath_active(void) return false; } +/* + * Initiator NDL pre-claim / finalize. + * + * Problem: + * The M1 Shared-Key Descriptor + SCIA security callbacks + * (Wi-Fi Aware v4.0 §7.1.3.5) run inside + * esp_nan_internal_datapath_req() and need an NDL to look up by + * ndp_id -- but the real ndp_id is only known after that call + * returns. + * + * Workaround: + * Pre-claim a slot at ndp_id=0 with security_ctx already populated, + * so the security callbacks find it. Once datapath_req returns the + * real ndp_id, stamp it onto the pre-claimed slot. + * + * Notes: + * - Pre-claim is SECURITY-only. With CONFIG_ESP_WIFI_NAN_SECURITY=n + * the security callbacks are NULL in nan_secure_dp_funcs and never + * run, so no pre-lookup is needed. + * - Finalize stays unconditional: nan_record_new_ndl() reuses a + * pre-claimed slot when one exists, otherwise allocates fresh. + * - Other finalize call sites: + * * nan_app_ndp_response_indication_cb (M2 RX, if datapath_req + * return races M2 indication) + * * key-desc parser claim path in nan_security.c + */ +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY +static struct ndl_info *nan_ndl_preclaim_initiator(uint8_t pub_id, + uint8_t peer_nmi[], + uint32_t device_caps) +{ + nan_record_new_ndl(0, pub_id, peer_nmi, ESP_WIFI_NDP_ROLE_INITIATOR, device_caps); + return nan_find_ndl_by_pub_id_and_peer(pub_id, peer_nmi); +} +#endif + +static void nan_ndl_finalize_ndp_id(uint8_t real_ndp_id, uint8_t pub_id, + uint8_t peer_nmi[], uint32_t device_caps) +{ + /* nan_record_new_ndl() stamps real_ndp_id onto the pre-claimed + * slot found by (pub_id, peer_nmi), or allocates a fresh slot if + * no pre-claim exists (SECURITY=n path). */ + nan_record_new_ndl(real_ndp_id, pub_id, peer_nmi, ESP_WIFI_NDP_ROLE_INITIATOR, device_caps); +} + +static void nan_ndl_release(uint8_t ndp_id_or_zero) +{ + nan_reset_ndl(ndp_id_or_zero, false); +} + /* types of ipv6 addresses to be displayed on ipv6 events */ static const char *s_ipv6_addr_types[] = { "UNKNOWN", @@ -471,23 +549,31 @@ void nan_app_service_match_cb(uint8_t sub_id, struct nan_cb_peer_info *peer_info NAN_DATA_LOCK(); struct peer_svc_info *p_peer_svc = nan_find_peer_svc(sub_id, 0, pub_mac); - if (p_peer_svc) { + if (p_peer_svc && p_peer_svc->svc_id != pub_id) { struct ndl_info *ndl = nan_find_ndl(0, pub_mac); + p_peer_svc->svc_id = pub_id; p_peer_svc->device_caps = device_caps; - if (p_peer_svc->svc_id != pub_id) { - p_peer_svc->svc_id = pub_id; - if (ndl) { - ndl->publisher_id = pub_id; - } - } else if (ndl) { + if (ndl) { ndl->publisher_id = pub_id; + ndl->device_caps = device_caps; } } else { nan_record_peer_svc(sub_id, pub_id, pub_mac, device_caps); } NAN_DATA_UNLOCK(); + ESP_LOGI(TAG, "Service matched with capabilities: 0x%04x", capab); + +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + if (peer_info->peer_security_params) { + if (!nan_security_service_match(pub_mac, peer_info->peer_security_params)) { + ESP_LOGD(TAG, "PMKID mismatch with "MACSTR, MAC2STR(pub_mac)); + return; + } + } +#endif + size_t evt_data_len = sizeof(wifi_event_nan_svc_match_t) + ssi_len; wifi_event_nan_svc_match_t *evt = (wifi_event_nan_svc_match_t *)os_zalloc(evt_data_len); if (!evt) { @@ -503,6 +589,7 @@ void nan_app_service_match_cb(uint8_t sub_id, struct nan_cb_peer_info *peer_info evt->fsd_gas = (capab & NAN_SDEA_CTRL_FSD_GAS) ? 1 : 0; evt->datapath_reqd = (capab & NAN_SDEA_CTRL_DATAPATH_REQD) ? 1 : 0; evt->ndpe_support = (device_caps & NAN_CAPS_NDPE_ATTR) ? 1 : 0; + evt->security_reqd = (capab & NAN_SDEA_CTRL_SECURITY_REQD) ? 1 : 0; evt->ssi_version = ssi_ver; if (ssi && ssi_len) { if (ssi_ver) { @@ -532,10 +619,7 @@ void nan_app_replied_cb(uint8_t pub_id, struct nan_cb_peer_info *peer_info) uint32_t device_caps = peer_info->device_caps; NAN_DATA_LOCK(); - struct peer_svc_info *p_peer_svc = nan_find_peer_svc(pub_id, sub_id, sub_nmi); - if (p_peer_svc) { - p_peer_svc->device_caps = device_caps; - } else { + if (!nan_find_peer_svc(pub_id, sub_id, sub_nmi)) { nan_record_peer_svc(pub_id, sub_id, sub_nmi, device_caps); } NAN_DATA_UNLOCK(); @@ -574,10 +658,7 @@ void nan_app_receive_cb(uint8_t svc_id, struct nan_cb_peer_info *peer_info) uint32_t device_caps = peer_info->device_caps; NAN_DATA_LOCK(); - struct peer_svc_info *p_peer_svc = nan_find_peer_svc(svc_id, peer_svc_id, peer_mac); - if (p_peer_svc) { - p_peer_svc->device_caps = device_caps; - } else { + if (!nan_find_peer_svc(svc_id, peer_svc_id, peer_mac)) { nan_record_peer_svc(svc_id, peer_svc_id, peer_mac, device_caps); } NAN_DATA_UNLOCK(); @@ -605,6 +686,14 @@ void nan_app_receive_cb(uint8_t svc_id, struct nan_cb_peer_info *peer_info) void nan_app_ndp_indication_cb(uint8_t pub_id, struct ndp_cb_peer_info *peer_info, uint32_t device_caps) { + /* + * Responder-side NDP indication. Security parsers (CSIA/SCIA/ + * Shared-Key) have already run and stashed the peer's security + * inputs in static pending caches; nan_security_apply_pending() + * below promotes them onto the NDL. NAN_DATA_LOCK guards + * s_nan_ctx mutation only and is released before any + * esp_nan_internal_* call (see lock contract in nan_i.h). + */ if (!peer_info) { return; } @@ -615,57 +704,81 @@ void nan_app_ndp_indication_cb(uint8_t pub_id, struct ndp_cb_peer_info *peer_inf uint16_t ssi_len = peer_info->ssi_len; bool ndp_resp_needed = false; + bool send_auto_resp = false; + wifi_nan_datapath_resp_t ndp_resp = {0}; + ip_addr_t own_ipv6 = {0}; + NAN_DATA_LOCK(); struct own_svc_info *p_own_svc = nan_find_own_svc(pub_id); if (!p_own_svc) { - ESP_LOGE(TAG, "No Publish found with id %d", pub_id); NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "No Publish found with id %d", pub_id); return; } ndp_resp_needed = p_own_svc->ndp_resp_needed; if (ndl_limit_reached()) { - ESP_LOGE(TAG, "NDP limit reached"); NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP limit reached"); return; } + 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)) { nan_record_peer_svc(pub_id, 0, peer_nmi, device_caps); } + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (ndl && peer_ndi) { + MACADDR_COPY(ndl->peer_ndi, peer_ndi); + } + +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + /* Apply pending CSIA/SCIA/M1 (captured before this indication) to the NDL. */ + nan_security_apply_pending(ndl, p_own_svc, pub_id, peer_nmi, peer_ndi); +#endif + if (p_own_svc->ndp_resp_needed) { - nan_record_new_ndl(ndp_id, pub_id, peer_nmi, ESP_WIFI_NDP_ROLE_RESPONDER, device_caps); - ESP_LOGI(TAG, "NDP Req from "MACSTR" [NDP Id: %d], Accept OR Deny using NDP command", + ESP_LOGD(TAG, "NDP Req from "MACSTR" [NDP Id: %d], Accept OR Deny using NDP command", MAC2STR(peer_nmi), ndp_id); s_nan_ctx.event |= NDP_INDICATION; } else { - uint8_t own_bssid[6]; - ip_addr_t own_ipv6 = {0}; - - wifi_nan_datapath_resp_t ndp_resp = {0}; + /* Build auto-response from NDL state under lock; dispatch the WiFi-library + * call after release (esp_nan_internal_* must not be called with + * NAN_DATA_LOCK held -- see nan_i.h). */ ndp_resp.accept = true; ndp_resp.ndp_id = ndp_id; MACADDR_COPY(ndp_resp.peer_mac, peer_nmi); - 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!"); - ndp_resp.accept = false; - } else { - esp_wifi_nan_get_ipv6_linklocal_from_mac(&own_ipv6.u_addr.ip6, own_bssid); + return; } + esp_wifi_nan_get_ipv6_linklocal_from_mac(&own_ipv6.u_addr.ip6, own_bssid); } - if (ndp_resp.accept) { - nan_record_new_ndl(ndp_id, pub_id, peer_nmi, ESP_WIFI_NDP_ROLE_RESPONDER, device_caps); - } - - esp_nan_internal_datapath_resp(&ndp_resp, (uint8_t *)&own_ipv6.u_addr.ip6.addr[2] ); + /* Datapath security on the auto-respond path is sourced from the + * NDL's security_ctx (populated by nan_security_apply_pending above + * from publish-time cfg). The WiFi library's M2 builder callbacks look it + * up by (ndp_id, peer_nmi); no per-resp security field needed. */ + send_auto_resp = true; } NAN_DATA_UNLOCK(); + if (send_auto_resp) { + esp_err_t resp_err = esp_nan_internal_datapath_resp(&ndp_resp, (uint8_t *)&own_ipv6.u_addr.ip6.addr[2]); + if (resp_err != ESP_OK) { + ESP_LOGE(TAG, "Auto NDP response failed for ndp_id=%u peer="MACSTR" err=%d", + ndp_id, MAC2STR(peer_nmi), resp_err); + } + } + + /* Event-post path reads only peer_info / ssi (WiFi-library-owned, not s_nan_ctx), + * so it runs outside the lock. */ size_t evt_data_len = sizeof(wifi_event_ndp_indication_t) + ssi_len; wifi_event_ndp_indication_t *evt = (wifi_event_ndp_indication_t *)os_zalloc(evt_data_len); if (!evt) { @@ -702,6 +815,56 @@ void nan_app_ndp_indication_cb(uint8_t pub_id, struct ndp_cb_peer_info *peer_inf os_free(evt); } +void nan_app_ndp_response_indication_cb(struct ndp_cb_peer_info *peer_info) +{ + if (!peer_info) { + return; + } + uint8_t ndp_id = peer_info->ndp_id; + uint8_t *peer_nmi = peer_info->peer_nmi; + uint8_t *peer_ndi = peer_info->peer_ndi; + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, peer_nmi); + if (!ndl) { + /* Initiator pre-claim path may still have ndp_id=0 if the M1 builder + * did not run through the SCIA / key-desc helpers (unsecured path). + * Fall back to peer-only lookup and stamp ndp_id. */ + ndl = nan_find_ndl(0, peer_nmi); + if (ndl && ndl->ndp_id == 0 && ndp_id != 0) { + ndl->ndp_id = ndp_id; + } + } + if (ndl && peer_ndi) { + MACADDR_COPY(ndl->peer_ndi, peer_ndi); + ESP_LOGD(TAG, "NDP M2 RX: stored peer NDI "MACSTR" (ndp_id=%d)", + MAC2STR(peer_ndi), ndp_id); + } else if (!ndl) { + ESP_LOGW(TAG, "NDP M2 RX: no NDL for ndp_id=%d peer="MACSTR, + ndp_id, MAC2STR(peer_nmi)); + } + NAN_DATA_UNLOCK(); +} + +/* Tear down an NDP whose confirm callback fired but cannot be completed + * (TK not ready, initiator handshake not COMPLETE, key install failed, + * event alloc failed). Notifies the peer via datapath_end, wipes the NDL + * (including TK / KCK / KEK material), and unblocks any waiting app. */ +static void nan_ndp_confirm_teardown(const uint8_t peer_nmi[6], uint8_t ndp_id) +{ + wifi_nan_datapath_end_req_t ndp_end = {0}; + + MACADDR_COPY(ndp_end.peer_mac, peer_nmi); + ndp_end.ndp_id = ndp_id; + + NAN_DATA_UNLOCK(); + esp_nan_internal_datapath_end(&ndp_end); + NAN_DATA_LOCK(); + + nan_reset_ndl(ndp_id, false); + os_event_group_set_bits(nan_event_group, NDP_REJECTED); +} + void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer_info, uint8_t own_ndi[6], uint8_t ipv6_identifier[8]) { @@ -742,19 +905,58 @@ void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer_info, goto done; } +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + if (ndl->security_ctx.type == WIFI_NAN_SECURITY_ENCRYPTED) { + if (!ndl->ptk_set || ndl->tk_len < NAN_NCS_SK_128_TK_LEN) { + ESP_LOGE(TAG, "NDP confirm: encrypted datapath but TK is not ready (ndp_id=%d)", ndp_id); + nan_ndp_confirm_teardown(peer_nmi, ndp_id); + goto done; + } + /* Initiator: refuse TK install unless M4 MIC verifier promoted state to COMPLETE. + * Parser sets M4_RCVD on receipt; verifier transitions to COMPLETE on a passing + * HMAC-SHA256(KCK, M4_body). If we're still at M4_RCVD here, MIC verify failed + * or was skipped — do not install the TK on a possibly tampered handshake. */ + if (ndl->own_role == ESP_WIFI_NDP_ROLE_INITIATOR && + ndl->handshake_state != NAN_HANDSHAKE_COMPLETE) { + ESP_LOGE(TAG, "NDP confirm (initiator): handshake_state=%d (not COMPLETE); skipping TK install", + ndl->handshake_state); + nan_ndp_confirm_teardown(peer_nmi, ndp_id); + goto done; + } + } +#endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ + /* Allocate the confirm event before installing the pairwise key, so an + * allocation failure can tear the NDP down without leaving a stale key + * bound to peer_ndi in the MAC's key store. */ size_t evt_data_len = sizeof(wifi_event_ndp_confirm_t) + ssi_len; wifi_event_ndp_confirm_t *evt = (wifi_event_ndp_confirm_t *)os_zalloc(evt_data_len); if (!evt) { - wifi_nan_datapath_end_req_t ndp_end = {0}; - - MACADDR_COPY(ndp_end.peer_mac, peer_nmi); - ndp_end.ndp_id = ndp_id; - esp_nan_internal_datapath_end(&ndp_end); ESP_LOGE(TAG, "Failed to allocate for event, terminate NDP"); - nan_reset_ndl(ndp_id, false); + nan_ndp_confirm_teardown(peer_nmi, ndp_id); goto done; } + +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + if (ndl->security_ctx.type == WIFI_NAN_SECURITY_ENCRYPTED) { + uint8_t key_rsc[8] = {0}; + int ret = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, + peer_ndi, + 0, + 1, + key_rsc, + sizeof(key_rsc), + ndl->nd_tk, + NAN_NCS_SK_128_TK_LEN, + NAN_KEY_FLAG_PAIRWISE | NAN_KEY_FLAG_RX | NAN_KEY_FLAG_TX); + 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; + } + } +#endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ evt->status = status; evt->ndp_id = ndp_id; MACADDR_COPY(evt->peer_nmi, peer_nmi); @@ -768,7 +970,6 @@ void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer_info, } else { memcpy(evt->ipv6_identifier, ipv6_identifier, NAN_IPV6_ADDR_ID_LEN); } - if (ssi && ssi_len) { memcpy(evt->ssi, ssi, ssi_len); evt->ssi_len = ssi_len; @@ -788,7 +989,6 @@ void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer_info, MAC2STR(peer_nmi), ndp_id, inet6_ntoa(peer_ip6)); 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); return; @@ -835,8 +1035,106 @@ void nan_action_txdone_cb(uint32_t context, bool tx_status) } } +void esp_nan_ndp_tx_done_cb(uint8_t ndp_id, const uint8_t *peer_nmi, uint8_t msg_type, bool tx_status) +{ + NAN_DATA_LOCK(); + + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + ESP_LOGE(TAG, "NDP TX Confirm: No NDL found for ndp_id=%d", ndp_id); + NAN_DATA_UNLOCK(); + return; + } + + if (!tx_status) { + ESP_LOGE(TAG, "NDP TX Confirm: msg_type=%d transmission failed for ndp_id=%d", msg_type, ndp_id); + NAN_DATA_UNLOCK(); + return; + } + + ESP_LOGD(TAG, "NDP TX Confirm: msg_type=%d sent successfully, ndp_id=%d", msg_type, ndp_id); + +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + /* Update security handshake state if encrypted datapath is active */ + if (msg_type == 2 && ndl->handshake_state == NAN_HANDSHAKE_M1_RCVD) { + ndl->handshake_state = NAN_HANDSHAKE_M2_SENT; + } else if (msg_type == 4 && ndl->handshake_state == NAN_HANDSHAKE_M3_RCVD) { + ndl->handshake_state = NAN_HANDSHAKE_COMPLETE; + } +#endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ + + NAN_DATA_UNLOCK(); +} + +/* NAN secure-datapath helpers registered with the WiFi libraries. */ +static struct nan_secure_dp_funcs s_nan_secure_dp_funcs = { + /* Always-present helpers */ + .ndp_tx_done_cb = esp_nan_ndp_tx_done_cb, + +#if CONFIG_ESP_WIFI_NAN_SECURITY + /* Length getters */ + .get_csia_len = esp_nan_get_csia_len, + .get_scia_len = esp_nan_get_scia_len, + .get_shared_key_desc_attr_len = esp_nan_get_shared_key_desc_attr_len, + .ndp_security_install_get_shared_desc_len = esp_nan_ndp_security_install_get_shared_desc_len, + + /* CSIA / SCIA construction */ + .construct_csia = esp_nan_construct_csia, + .construct_scia_publish = esp_nan_construct_scia_publish, + .construct_scia_ndp_req = esp_nan_construct_scia_ndp_req, + .construct_scia_ndp_resp = esp_nan_construct_scia_ndp_resp, + + /* Shared Key Descriptor builders */ + .get_ndp_req_shared_key_desc = esp_nan_get_ndp_req_shared_key_desc, + .get_ndp_resp_shared_key_desc = esp_nan_get_ndp_resp_shared_key_desc, + .get_ndp_confirm_shared_key_desc = esp_nan_get_ndp_confirm_shared_key_desc, + .get_ndp_security_install_key_desc = esp_nan_get_ndp_security_install_key_desc, + + /* M1 Auth_Token capture */ + .capture_m1_auth_token = esp_nan_capture_m1_auth_token, + + /* MIC compute (TX path) */ + .update_ndp_resp_mic = esp_nan_update_ndp_resp_mic, + .update_ndp_confirm_mic = esp_nan_update_ndp_confirm_mic, + .update_ndp_security_install_mic = esp_nan_update_ndp_security_install_mic, + + /* MIC verify (RX path) */ + .verify_ndp_resp_mic = esp_nan_verify_ndp_resp_mic, + .verify_ndp_confirm_mic = esp_nan_verify_ndp_confirm_mic, + .verify_ndp_security_install_mic = esp_nan_verify_ndp_security_install_mic, + + /* RX-path attribute parsers */ + .parse_ndp_csia = esp_nan_parse_ndp_csia, + .parse_ndp_scia = esp_nan_parse_ndp_scia, + .parse_ndp_key_desc = esp_nan_parse_ndp_key_desc, + + /* Publish-side security parser */ + .parse_publish_security = esp_nan_parse_publish_security, + + /* Publish-init helper */ + .derive_security_params = nan_derive_security_params, + + /* NDP security gate query */ + .get_ndp_security_csid = nan_get_ndp_security_csid, +#endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ +}; + void esp_nan_app_deinit(void) { + esp_nan_internal_register_secure_dp_funcs(NULL); + + /* Free per-peer/NDL state in case Wi-Fi is being deinit'd without a + * prior esp_wifi_nan_sync_stop. Not SECURITY-gated: reset helpers + * touch only fields that exist in both configs; conditional key + * material lives inside the #ifdef'd region of struct ndl_info. */ + if (s_nan_data_lock) { + NAN_DATA_LOCK(); + nan_reset_service(0, true); + nan_reset_ndl(0, true); + memset(&s_nan_ctx, 0, sizeof(s_nan_ctx)); + NAN_DATA_UNLOCK(); + } + if (nan_event_group) { os_event_group_delete(nan_event_group); nan_event_group = NULL; @@ -860,7 +1158,10 @@ void esp_nan_app_init(void) if (!s_nan_data_lock) { ESP_LOGE(TAG, "Failed to create NAN data lock"); esp_nan_app_deinit(); + return; } + + esp_nan_internal_register_secure_dp_funcs(&s_nan_secure_dp_funcs); } void esp_nan_action_start(esp_netif_t *nan_netif) @@ -880,6 +1181,7 @@ void esp_nan_action_start(esp_netif_t *nan_netif) .ndp_confirm = nan_app_ndp_confirm_cb, .ndp_terminated = nan_app_ndp_terminated_cb, .action_txdone = nan_action_txdone_cb, + .ndp_response_indication = nan_app_ndp_response_indication_cb, }; esp_nan_internal_register_callbacks(&nan_cb); @@ -996,6 +1298,10 @@ esp_err_t esp_wifi_nan_sync_stop(void) } NAN_DATA_LOCK(); + /* Free per-peer linked lists before zeroing own_svc[] heads, else the + * peer_svc_info heap allocations leak. */ + nan_reset_service(0, true); + nan_reset_ndl(0, true); memset(&s_nan_ctx, 0, sizeof(nan_ctx_t)); NAN_DATA_UNLOCK(); return ESP_OK; @@ -1052,6 +1358,8 @@ uint8_t esp_wifi_nan_publish_service(const wifi_nan_publish_cfg_t *publish_cfg) #endif /* CONFIG_ESP_WIFI_NAN_USD_ENABLE */ #ifdef CONFIG_ESP_WIFI_NAN_SYNC_ENABLE + wifi_nan_publish_cfg_t *cfg = NULL; + NAN_DATA_LOCK(); if (!(s_nan_ctx.state & NAN_STARTED_BIT)) { ESP_LOGE(TAG, "NAN not started!"); @@ -1069,18 +1377,50 @@ uint8_t esp_wifi_nan_publish_service(const wifi_nan_publish_cfg_t *publish_cfg) goto fail; } + if (publish_cfg->security_reqd) { +#ifndef CONFIG_ESP_WIFI_NAN_SECURITY + ESP_LOGE(TAG, "Encrypted datapath not enabled (CONFIG_ESP_WIFI_NAN_SECURITY)"); + goto fail; +#else + if (!(publish_cfg->security_cfg.csid_bitmap & WIFI_NAN_CSID_BIT_NCS_SK_128)) { + ESP_LOGE(TAG, "Unsupported cipher suite in csid_bitmap (only NCS-SK-128 is supported)"); + goto fail; + } +#endif + } - if (esp_nan_internal_publish_service(publish_cfg, (uint8_t *) &pub_id, false) != ESP_OK) { + /* Heap-allocate config copy to avoid ~700 bytes on stack */ + cfg = os_zalloc(sizeof(*cfg)); + if (!cfg) { + ESP_LOGE(TAG, "Failed to allocate publish config"); + goto fail; + } + memcpy(cfg, publish_cfg, sizeof(*cfg)); + + /* Security derivation (PMK, PMKID) now runs in WiFi task context — + * the WiFi library calls nan_derive_security_params(cfg) during publish processing. + * After esp_nan_internal_publish_service returns, cfg->security_cfg has + * the derived PMK and PMKID populated by the WiFi task. */ + + if (esp_nan_internal_publish_service(cfg, (uint8_t *) &pub_id, false) != ESP_OK) { ESP_LOGE(TAG, "Failed to publish service '%s'", publish_cfg->service_name); goto fail; } ESP_LOGI(TAG, "Started Publishing %s [Service ID - %u]", publish_cfg->service_name, pub_id); - nan_record_own_svc(pub_id, ESP_NAN_PUBLISH, publish_cfg->service_name, publish_cfg->ndp_resp_needed); + nan_record_own_svc(pub_id, ESP_NAN_PUBLISH, publish_cfg->service_name, publish_cfg->ndp_resp_needed, +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + &cfg->security_cfg +#else + NULL +#endif + ); + os_free(cfg); NAN_DATA_UNLOCK(); return pub_id; fail: + os_free(cfg); NAN_DATA_UNLOCK(); return 0; #endif /* CONFIG_ESP_WIFI_NAN_SYNC_ENABLE */ @@ -1127,11 +1467,26 @@ uint8_t esp_wifi_nan_subscribe_service(const wifi_nan_subscribe_cfg_t *subscribe } ESP_LOGI(TAG, "Started Subscribing to %s [Service ID - %u]", subscribe_cfg->service_name, sub_id); +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + nan_security_cache_subscriber_params(subscribe_cfg->service_name, + &subscribe_cfg->security_cfg); +#endif return sub_id; } #endif /* CONFIG_ESP_WIFI_NAN_USD_ENABLE */ #ifdef CONFIG_ESP_WIFI_NAN_SYNC_ENABLE + if (subscribe_cfg->security_reqd) { +#ifndef CONFIG_ESP_WIFI_NAN_SECURITY + ESP_LOGE(TAG, "Encrypted datapath not enabled (CONFIG_ESP_WIFI_NAN_SECURITY)"); + return 0; +#else + if (!(subscribe_cfg->security_cfg.csid_bitmap & WIFI_NAN_CSID_BIT_NCS_SK_128)) { + ESP_LOGE(TAG, "Unsupported cipher suite in csid_bitmap (only NCS-SK-128 is supported)"); + return 0; + } +#endif + } NAN_DATA_LOCK(); if (!(s_nan_ctx.state & NAN_STARTED_BIT)) { ESP_LOGE(TAG, "NAN not started!"); @@ -1154,7 +1509,10 @@ uint8_t esp_wifi_nan_subscribe_service(const wifi_nan_subscribe_cfg_t *subscribe } ESP_LOGI(TAG, "Started Subscribing to %s [Service ID - %u]", subscribe_cfg->service_name, sub_id); - nan_record_own_svc((uint8_t) sub_id, ESP_NAN_SUBSCRIBE, subscribe_cfg->service_name, false); + nan_record_own_svc((uint8_t) sub_id, ESP_NAN_SUBSCRIBE, subscribe_cfg->service_name, false, &subscribe_cfg->security_cfg); +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + nan_security_cache_subscriber_params(subscribe_cfg->service_name, &subscribe_cfg->security_cfg); +#endif NAN_DATA_UNLOCK(); return sub_id; @@ -1217,7 +1575,7 @@ esp_err_t esp_wifi_nan_send_message(wifi_nan_followup_params_t *fup_params) if (!fup_params->peer_inst_id) { fup_params->peer_inst_id = p_peer_svc->svc_id; } - if (!MACADDR_EQUAL(fup_params->peer_mac, null_mac)) { + if (MACADDR_EQUAL(fup_params->peer_mac, null_mac)) { MACADDR_COPY(fup_params->peer_mac, p_peer_svc->peer_nmi); } @@ -1331,17 +1689,48 @@ uint8_t esp_wifi_nan_datapath_req(wifi_nan_datapath_req_t *req) goto fail; } - if (!MACADDR_EQUAL(req->peer_mac, null_mac)) { + if (MACADDR_EQUAL(req->peer_mac, null_mac)) { MACADDR_COPY(req->peer_mac, p_peer_svc->peer_nmi); } os_event_group_clear_bits(nan_event_group, NDP_ACCEPTED | NDP_REJECTED); - if (esp_nan_internal_datapath_req(req, &ndp_id,(uint8_t *)&own_ipv6.u_addr.ip6.addr[2]) != ESP_OK) { + + uint32_t saved_pub_id = req->pub_id; + uint32_t saved_device_caps = p_peer_svc->device_caps; +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + /* Encrypted datapath: pre-claim an NDL at ndp_id=0 with security_ctx + * populated from the subscribe-time security_cfg. The WiFi library's M1 builder + * callbacks (construct_scia_ndp_req / get_ndp_req_shared_key_desc / + * capture_m1_auth_token) fire inside esp_nan_internal_datapath_req() + * before it returns the real ndp_id, and they look up the NDL by + * (ndp_id=0, peer_nmi). Open datapath skips the pre-claim entirely -- + * no security callbacks fire, so there is nothing to look up. */ + struct ndl_info *preclaimed = nan_ndl_preclaim_initiator(saved_pub_id, + req->peer_mac, + saved_device_caps); + if (preclaimed) { + nan_security_populate_initiator_ndl(preclaimed, p_peer_svc->peer_nmi); + } +#endif + + /* Release lock before internal call: the WiFi library may invoke + * esp_nan_get_ndp_req_shared_key_desc / esp_nan_construct_scia_ndp_req / + * esp_nan_capture_m1_auth_token which all take NAN_DATA_LOCK. Holding + * it here would deadlock those callbacks. */ + NAN_DATA_UNLOCK(); + + if (esp_nan_internal_datapath_req(req, &ndp_id, (uint8_t *)&own_ipv6.u_addr.ip6.addr[2]) != ESP_OK) { ESP_LOGE(TAG, "Failed to initiate NDP req"); - goto fail; +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + NAN_DATA_LOCK(); + nan_ndl_release(0); /* clean up pre-claimed NDL */ + NAN_DATA_UNLOCK(); +#endif + return 0; } - nan_record_new_ndl(ndp_id, req->pub_id, req->peer_mac, ESP_WIFI_NDP_ROLE_INITIATOR, p_peer_svc->device_caps); + NAN_DATA_LOCK(); + nan_ndl_finalize_ndp_id(ndp_id, saved_pub_id, req->peer_mac, saved_device_caps); NAN_DATA_UNLOCK(); ESP_LOGD(TAG, "Requested NDP with "MACSTR" [NDP ID - %d]", MAC2STR(req->peer_mac), ndp_id); @@ -1354,7 +1743,7 @@ uint8_t esp_wifi_nan_datapath_req(wifi_nan_datapath_req_t *req) } else { ESP_LOGE(TAG, "NDP request timed out"); NAN_DATA_LOCK(); - nan_reset_ndl(ndp_id, false); + nan_ndl_release(ndp_id); NAN_DATA_UNLOCK(); return 0; } @@ -1379,25 +1768,33 @@ esp_err_t esp_wifi_nan_datapath_resp(wifi_nan_datapath_resp_t *resp) goto fail; } - if (!MACADDR_EQUAL(resp->peer_mac, null_mac)) { + if (MACADDR_EQUAL(resp->peer_mac, null_mac)) { MACADDR_COPY(resp->peer_mac, ndl->peer_nmi); } - if (ndl->device_caps & NAN_CAPS_NDPE_ATTR) { - esp_err_t err = esp_wifi_get_mac(WIFI_IF_NAN, own_bssid); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Cannot get own BSSID!"); - goto fail; - } - esp_wifi_nan_get_ipv6_linklocal_from_mac(&own_ipv6.u_addr.ip6, own_bssid); + if (ndl->device_caps & NAN_CAPS_NDPE_ATTR) { + esp_err_t err = esp_wifi_get_mac(WIFI_IF_NAN, own_bssid); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Cannot get own BSSID!"); + NAN_DATA_UNLOCK(); + goto fail; } + esp_wifi_nan_get_ipv6_linklocal_from_mac(&own_ipv6.u_addr.ip6, own_bssid); + } + + /* Release lock before internal call: esp_nan_internal_datapath_resp() may call + * esp_nan_get_ndp_resp_shared_key_desc() which takes NAN_DATA_LOCK again → deadlock if we keep the lock. */ + NAN_DATA_UNLOCK(); if (esp_nan_internal_datapath_resp(resp, (uint8_t *)&own_ipv6.u_addr.ip6.addr[2]) == ESP_OK) { + NAN_DATA_LOCK(); s_nan_ctx.event &= ~NDP_INDICATION; NAN_DATA_UNLOCK(); return ESP_OK; } + return ESP_FAIL; + fail: NAN_DATA_UNLOCK(); return ESP_FAIL; @@ -1422,7 +1819,7 @@ esp_err_t esp_wifi_nan_datapath_end(wifi_nan_datapath_end_req_t *req) return ESP_FAIL; } - if (!MACADDR_EQUAL(req->peer_mac, null_mac)) { + if (MACADDR_EQUAL(req->peer_mac, null_mac)) { MACADDR_COPY(req->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 new file mode 100644 index 00000000000..94bf3bd7602 --- /dev/null +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h @@ -0,0 +1,350 @@ +/* + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + * + * Internal declarations shared between nan_app.c and nan_security.c. + */ + +#pragma once + +#include +#include +#include +#include +#include "esp_err.h" +#include "esp_wifi_types_generic.h" +#include "esp_private/wifi.h" +#include "esp_nan.h" +#include "os.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Macros */ +#ifndef MACADDR_LEN +#define MACADDR_LEN 6 +#endif +#define MACADDR_EQUAL(a1, a2) (memcmp(a1, a2, MACADDR_LEN) == 0) +#define MACADDR_COPY(dst, src) (memcpy(dst, src, MACADDR_LEN)) + +/* + * Shared lock used by both files. + * + * NAN_DATA_LOCK contract + * ---------------------- + * s_nan_data_lock guards s_nan_ctx (NDL[], own_svc[], state, event, + * netif). Anything that reads or mutates these fields takes the lock. + * + * !!! MUST NOT be held across any esp_nan_internal_* call !!! + * + * The blob-side esp_nan_internal_* entry points (datapath_req, + * datapath_resp, datapath_end, publish_service, subscribe_service, + * send_followup, register_callbacks) re-enter host code from the WiFi + * task to: + * - assemble M1 / M2 / M3 / M4 NDP frames (esp_nan_get_ndp_*_key_desc, + * esp_nan_construct_csia / scia, esp_nan_capture_m1_auth_token, + * esp_nan_update_ndp_*_mic, esp_nan_verify_ndp_*_mic) + * - parse inbound NDP frames (esp_nan_parse_ndp_*) + * - fire indication / confirm / response_indication / terminated + * callbacks (nan_app_ndp_*_cb) + * + * All of those re-entry points take NAN_DATA_LOCK themselves. Holding + * the lock around the blob call deadlocks the WiFi task as soon as it + * tries to call back. Pattern: + * + * NAN_DATA_LOCK(); + * ... mutate / capture state needed by the call ... + * NAN_DATA_UNLOCK(); + * err = esp_nan_internal_datapath_req(...); + * NAN_DATA_LOCK(); + * ... record result ... + * NAN_DATA_UNLOCK(); + * + * Several past bug-fix commits on this branch (`fix(nan): release + * NAN_DATA_LOCK before initiator datapath_req`, `fix(nan): unlock + * NAN_DATA on all paths in ndp_indication_cb`) trace back to forgotten + * unlocks; new sites that call the blob must follow this pattern. + */ +extern void *s_nan_data_lock; +#define NAN_DATA_LOCK() os_mutex_lock(s_nan_data_lock) +#define NAN_DATA_UNLOCK() os_mutex_unlock(s_nan_data_lock) + +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY +/* NAN 4-way handshake constants (RSNA key descriptor layout) */ +#define NAN_NONCE_LEN 32 +#define NAN_REPLAY_COUNTER_LEN 8 +#define NAN_KEY_RSC_LEN 8 +#define NAN_KEY_MIC_LEN 16 +/* KCK/KEK/TK buffer sizes are sized for the largest cipher suite the host + * could potentially run (NCS-SK-256 KCK=24, KEK=32, TK=32). Only NCS-SK-128 + * is wired through M1–M4 today (see nan_get_first_csid) so the *_set fields + * mark the live length in the buffer. */ +#define NAN_ND_KCK_MAX_LEN 24 +#define NAN_ND_KEK_MAX_LEN 32 +#define NAN_ND_TK_MAX_LEN 32 +#define NAN_GTK_MAX_LEN 32 +#define NAN_AUTH_TOKEN_MAX_LEN 24 + +/* RSNA Key Descriptor offsets (same as 802.11 EAPOL-Key) */ +#define NAN_KEY_DESC_TYPE_OFF 0 +#define NAN_KEY_DESC_KEY_INFO_OFF 1 +#define NAN_KEY_DESC_KEY_LEN_OFF 3 +#define NAN_KEY_DESC_REPLAY_OFF 5 +#define NAN_KEY_DESC_NONCE_OFF 13 +#define NAN_KEY_DESC_IV_OFF 45 +#define NAN_KEY_DESC_RSC_OFF 61 +#define NAN_KEY_DESC_KEY_ID_OFF 69 +#define NAN_KEY_DESC_MIC_OFF 77 +#define NAN_KEY_DESC_DATA_LEN_OFF 93 +#define NAN_KEY_DESC_DATA_OFF 95 +#define NAN_KEY_DESC_MIN_LEN 95 + +/* Key Descriptor Type (same as 802.11 EAPOL-Key) */ +#define NAN_KEY_DESC_TYPE_RSN 2 + +/* Security Context Identifier (SCID) types (Table 123) */ +#define NAN_SEC_CTX_TYPE_ND_PMKID 1 + +/* Key Info bits (same semantics as RSNA) */ +#define NAN_KEY_INFO_MIC BIT(8) +#define NAN_KEY_INFO_SECURE BIT(9) +#define NAN_KEY_INFO_INSTALL BIT(6) +#define NAN_KEY_INFO_ACK BIT(7) +#define NAN_KEY_INFO_ENC_KEY BIT(12) +#define NAN_KEY_INFO_KEY_TYPE BIT(3) /* 1=Pairwise, 0=Group */ + +/* 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 +#define NAN_NCS_SK_128_TK_LEN 16 +#define NAN_NCS_SK_128_MIC_LEN 16 +#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_KEY_FLAG_RX BIT(2) +#define NAN_KEY_FLAG_TX BIT(3) +#define NAN_KEY_FLAG_PAIRWISE BIT(5) + +/* Handshake state */ +enum nan_handshake_state { + NAN_HANDSHAKE_IDLE = 0, + NAN_HANDSHAKE_M1_SENT, /* Initiator: sent NDP Request (M1) */ + NAN_HANDSHAKE_M1_RCVD, + NAN_HANDSHAKE_M2_SENT, + NAN_HANDSHAKE_M2_RCVD, + NAN_HANDSHAKE_M3_SENT, + NAN_HANDSHAKE_M3_PENDING_VERIFY, /* Responder: M3 parsed, awaits Auth_Token||body MIC verify */ + NAN_HANDSHAKE_M3_RCVD, + NAN_HANDSHAKE_M4_RCVD, + NAN_HANDSHAKE_COMPLETE +}; +#endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ + +/* Per-peer service info */ +struct peer_svc_info { + SLIST_ENTRY(peer_svc_info) next; + uint8_t peer_svc_info[ESP_WIFI_MAX_SVC_INFO_LEN]; + uint8_t svc_id; + uint8_t own_svc_id; + uint8_t type; + uint8_t peer_nmi[MACADDR_LEN]; + uint32_t device_caps; +}; + +/* Own (locally registered) service info */ +struct own_svc_info { + char svc_name[ESP_WIFI_MAX_SVC_NAME_LEN]; + uint8_t svc_id; + uint8_t type; + + bool ndp_resp_needed; +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + wifi_nan_discovery_security_params_t security_cfg; + uint8_t pmk_cache[ESP_WIFI_NAN_MAX_PMKIDS][ESP_WIFI_NAN_NDP_PMK_LEN]; +#endif + uint8_t num_peer_records; + SLIST_HEAD(peer_list_t, peer_svc_info) peer_list; +}; + +/* Per-NDP link state */ +struct ndl_info { + uint8_t ndp_id; + uint8_t peer_ndi[MACADDR_LEN]; + uint8_t peer_nmi[MACADDR_LEN]; + uint8_t publisher_id; + uint8_t own_role; + uint32_t device_caps; +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + wifi_nan_datapath_security_params_t security_ctx; + + uint8_t anonce[NAN_NONCE_LEN]; + uint8_t snonce[NAN_NONCE_LEN]; + + uint8_t nd_kck[NAN_ND_KCK_MAX_LEN]; + uint8_t nd_kek[NAN_ND_KEK_MAX_LEN]; + uint8_t nd_tk[NAN_ND_TK_MAX_LEN]; + uint8_t kck_len; + uint8_t kek_len; + uint8_t tk_len; + uint8_t ptk_set: 1; + uint8_t ptk_reserved: 7; + + uint8_t tx_replay_counter[NAN_REPLAY_COUNTER_LEN]; + uint8_t rx_replay_counter[NAN_REPLAY_COUNTER_LEN]; + uint8_t rx_replay_counter_set: 1; + uint8_t replay_reserved: 7; + + uint8_t auth_token[NAN_AUTH_TOKEN_MAX_LEN]; + uint8_t auth_token_len; + + 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). */ + 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_set: 1; + uint8_t igtk_set: 1; + uint8_t bigtk_set: 1; + uint8_t group_keys_reserved: 5; + + uint8_t key_rsc[NAN_KEY_RSC_LEN]; +#endif +}; + +/* NAN context shared between files */ +typedef struct { + uint8_t state; + uint8_t event; + struct ndl_info ndl[ESP_WIFI_NAN_DATAPATH_MAX_PEERS]; + struct own_svc_info own_svc[ESP_WIFI_NAN_MAX_SVC_SUPPORTED]; + esp_netif_t *nan_netif; +} nan_ctx_t; + +extern nan_ctx_t s_nan_ctx; + +/* Helpers defined in nan_app.c, used by nan_security.c */ +struct own_svc_info *nan_find_own_svc(uint8_t svc_id); +struct ndl_info *nan_find_ndl(uint8_t ndp_id, uint8_t peer_nmi[]); +struct ndl_info *nan_find_ndl_by_pub_id_and_peer(uint8_t pub_id, const uint8_t *peer_nmi); + +/* === nan_secure_dp_funcs initializer targets === */ + +/* Always-present (defined in nan_app.c) */ +void esp_nan_ndp_tx_done_cb(uint8_t ndp_id, const uint8_t *peer_nmi, + uint8_t msg_type, bool tx_status); + +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY +/* Security-gated (defined in nan_security.c) */ +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_construct_csia(uint8_t *frm, uint8_t pub_id, + uint16_t own_csid_bitmap, uint16_t peer_csid_bitmap); +int esp_nan_construct_scia_publish(uint8_t *frm, uint8_t pub_id, + uint8_t num_pmkids, + const uint8_t pmkids[][ESP_WIFI_NAN_NDP_PMKID_LEN]); +int esp_nan_construct_scia_ndp_req(uint8_t *frm, uint8_t ndp_id, + const uint8_t *peer_nmi); +int esp_nan_construct_scia_ndp_resp(uint8_t *frm, uint8_t ndp_id, + const uint8_t *peer_nmi); + +int esp_nan_get_ndp_req_shared_key_desc(uint8_t *buf, size_t buf_len, + uint8_t ndp_id, const uint8_t *peer_nmi); +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); +int esp_nan_get_ndp_confirm_shared_key_desc(uint8_t *buf, size_t buf_len, + uint8_t ndp_id, const uint8_t *peer_nmi); +int esp_nan_get_ndp_security_install_key_desc(uint8_t *buf, size_t buf_len, + uint8_t ndp_id, const uint8_t *peer_nmi); + +int esp_nan_capture_m1_auth_token(const uint8_t *m1_body, size_t body_len, + uint8_t ndp_id, const uint8_t *peer_nmi); + +int esp_nan_update_ndp_resp_mic(uint8_t *m2_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); +int esp_nan_update_ndp_confirm_mic(uint8_t *m3_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); +int esp_nan_update_ndp_security_install_mic(uint8_t *m4_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); + +int esp_nan_verify_ndp_resp_mic(uint8_t *m2_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); +int esp_nan_verify_ndp_confirm_mic(uint8_t *m3_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); +int esp_nan_verify_ndp_security_install_mic(uint8_t *m4_body, size_t body_len, + uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi); + +void esp_nan_parse_ndp_csia(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param); +void esp_nan_parse_ndp_scia(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param); +void esp_nan_parse_ndp_key_desc(void *frm, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi); + +esp_err_t esp_nan_parse_publish_security(const uint8_t *attrs, size_t attrs_len, + wifi_nan_discovery_security_params_t *security); + +/* Helpers defined in nan_security.c, used by nan_app.c */ + +/* + * Apply any pending CSIA/SCIA/M1 state captured by the NDP key-desc parser + * onto the freshly-created NDL. Called from nan_app_ndp_indication_cb once + * (ndp_id, peer_nmi, peer_ndi, p_own_svc) are all known. + */ +void nan_security_apply_pending(struct ndl_info *ndl, + struct own_svc_info *p_own_svc, + uint8_t pub_id, + const uint8_t *peer_nmi, + const uint8_t *peer_ndi); + +/* PMK / PMKID derivation entry point used by the publish path. */ +esp_err_t nan_derive_security_params(wifi_nan_publish_cfg_t *cfg); + +/* Blob-side gate query: returns ndl->security_ctx.csid_bitmap for the NDP + * keyed on (ndp_id, peer_nmi), or 0 if no NDL match. ndp_id=0 is valid for + * the initiator pre-claim window (peer-only lookup). */ +uint16_t nan_get_ndp_security_csid(uint8_t ndp_id, const uint8_t *peer_nmi); + +/* Subscribe path: cache security_cfg + service name for PMKID verification on match. */ +void nan_security_cache_subscriber_params(const char *service_name, + const wifi_nan_discovery_security_params_t *security_cfg); + +/* Subscribe path: populate the initiator NDL's security_ctx (cipher + pair-PMKID + + * ND-PMK) from the cached subscriber params, so the blob's CSIA/SCIA/Shared-Key + * Descriptor builder callbacks find security keyed by (ndp_id, peer_nmi). + * No-op if subscriber didn't request encrypted datapath. */ +esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, + const uint8_t *peer_nmi); + +/* + * Compare locally derived ND-PMKID (subscriber passphrase, publisher NMI) to + * peer discovery security params. publisher_nmi is the publisher's NAN MAC + * from the service-match callback. + */ +bool nan_security_service_match(const uint8_t *publisher_nmi, + const wifi_nan_discovery_security_params_t *peer_sec); +#else +static inline esp_err_t nan_derive_security_params(wifi_nan_publish_cfg_t *cfg) +{ + (void)cfg; + return ESP_FAIL; +} +#endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ + +#ifdef __cplusplus +} +#endif 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 new file mode 100644 index 00000000000..a0f1c4504c0 --- /dev/null +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -0,0 +1,2133 @@ +/* + * SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + * + * NAN encrypted-datapath (NDP) security: PMK/PMKID derivation, 4-way + * handshake key descriptor build/parse, MIC computation, and CSIA/SCIA + * attribute construction. Whole file is gated on CONFIG_ESP_WIFI_NAN_SECURITY. + */ + +#include "sdkconfig.h" + +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + +#include +#include +#include "esp_wifi.h" +#include "esp_private/wifi.h" +#include "esp_log.h" +#include "esp_check.h" +#include "esp_mac.h" +#include "os.h" +#include "esp_nan.h" +#include "utils/common.h" +#include "crypto/sha256.h" +#include "nan_i.h" + +static const char *TAG = "nan_sec"; + +#define NAN_PMK_NAME_LABEL "NAN PMK Name" +#define NAN_PMK_NAME_LABEL_LEN 12 + +/* Attribute IDs used in this file (mirrors values used by the Wi-Fi blob) */ +#define NAN_ATTR_ID_CIPHER_SUITE_INFO 0x22 +#define NAN_ATTR_ID_SECURITY_CONTEXT 0x23 +#define NAN_ATTR_ID_SHARED_KEY_DESC 0x24 + +/*------------------------------------------------------------------------- + * Pending state captured by the NDP key-desc parser before the indication + * callback fires. Consumed by nan_security_apply_pending(). + *-----------------------------------------------------------------------*/ + +/* Pending M1 (ANonce + replay counter) */ +static struct { + bool valid; + uint8_t pub_id; + uint8_t anonce[NAN_NONCE_LEN]; + uint8_t rx_replay_counter[NAN_REPLAY_COUNTER_LEN]; + uint8_t key_rsc[NAN_KEY_RSC_LEN]; +} s_pending_m1; + +/* Pending PMKID and CSID from CSIA/SCIA. csid_bitmap accumulates every + * spec-valid CSID advertised in the CSIA (a publisher may list more than + * one cipher suite). */ +static struct { + bool has_pmkid; + bool has_csid; + uint8_t pub_id; + uint8_t pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN]; + uint16_t csid_bitmap; +} s_pending_scia; + +/* 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) + +/* Cached subscribe-side discovery security (passphrase / PMK, service name). */ +static struct { + bool valid; + char service_name[ESP_WIFI_MAX_SVC_NAME_LEN]; + wifi_nan_discovery_security_params_t security_cfg; +} s_nan_subscriber_sec_cache; + +/*------------------------------------------------------------------------- + * Static helpers + *-----------------------------------------------------------------------*/ + +static const uint8_t *nan_find_attr(const uint8_t *attrs, size_t attrs_len, + uint8_t attr_id, uint16_t *out_len) +{ + size_t offset = 0; + while (offset + 3 <= attrs_len) { + uint8_t id = attrs[offset]; + uint16_t len = attrs[offset + 1] | (attrs[offset + 2] << 8); + + if (offset + 3 + len > attrs_len) { + break; + } + + if (id == attr_id) { + if (out_len) { + *out_len = len; + } + return &attrs[offset + 3]; + } + + offset += 3 + len; + } + return NULL; +} + +/* + * Pick the highest-priority cipher suite the local device implements from + * the negotiated CSIA bitmap. Only NCS-SK-128 is wired through the rest of + * the M1–M4 path today (PTK length, MIC length, KCK/KEK lengths in + * nan_i.h are 128-bit-specific); 256-bit and PK suites are intentionally + * not selected here — return 0 to signal "no supported suite" so callers + * fail negotiation rather than dispatching to a half-implemented path. + */ +static uint8_t nan_get_first_csid(uint16_t csid_bitmap) +{ + if (csid_bitmap & WIFI_NAN_CSID_BIT_NCS_SK_128) { + return WIFI_NAN_CSID_NCS_SK_128; + } + return 0; +} + +/* + * Service ID = first 6 bytes of SHA256(lowercase(service_name)) + * per Wi-Fi Aware v4.0 §5.1.5 (Service Name and Service ID). + */ +static bool nan_compute_service_id(const char *service_name, uint8_t service_id[6]) +{ + if (!service_name || !g_wifi_default_wpa_crypto_funcs.sha256_vector) { + return false; + } + size_t name_len = strlen(service_name); + char *lower = os_malloc(name_len + 1); + if (!lower) { + return false; + } + strlcpy(lower, service_name, name_len + 1); + for (char *p = lower; *p; p++) { + *p = tolower((unsigned char) * p); + } + uint8_t hash[32]; + const uint8_t *addr[1] = {(const uint8_t *)lower}; + size_t len[1] = {name_len}; + int ret = g_wifi_default_wpa_crypto_funcs.sha256_vector(1, addr, len, hash); + os_free(lower); + if (ret != 0) { + return false; + } + memcpy(service_id, hash, 6); + return true; +} + +#define NAN_SERVICE_ID_LEN 6 + +/* + * ND-PMK from passphrase: salt = 0 || CSID || Service_ID || peer NMI (publisher NMI on both sides). + * PBKDF2 uses HMAC-SHA256 for all supported cipher suite IDs (no SHA-384 path). + */ +static int nan_derive_nd_pmk_from_passphrase(const char *pwd, uint8_t csid, + const uint8_t *service_id, + const uint8_t *peer_nmi, + uint8_t *nd_pmk) +{ + uint8_t salt[14]; + + if (!pwd || !service_id || !peer_nmi || !nd_pmk || csid == 0) { + return -1; + } + + salt[0] = 0; + salt[1] = csid; + os_memcpy(salt + 2, service_id, NAN_SERVICE_ID_LEN); + os_memcpy(salt + 2 + NAN_SERVICE_ID_LEN, peer_nmi, ETH_ALEN); + + /* NCS-SK-128 is the only suite supported by the rest of the handshake + * (PTK / MIC / KCK / KEK lengths in nan_i.h are 128-bit-specific). + * NCS-PK-PASN-128 reuses the same passphrase-based PMK derivation per + * Wi-Fi Aware v4.0 §7.1.3.6. Anything else fails negotiation here. */ + switch (csid) { + case WIFI_NAN_CSID_NCS_SK_128: + case WIFI_NAN_CSID_NCS_PK_PASN_128: + return pbkdf2_sha256(pwd, salt, sizeof(salt), 4096, nd_pmk, ESP_WIFI_NAN_NDP_PMK_LEN); + default: + return -1; + } +} + +/* + * Spec: ND-PMKID = L(HMAC-Hash(ND-PMK, + * "NAN PMK Name" || NDP Initiator NMI || NDP Responder NMI || Service ID), + * 0, 128) + * Uses NMI addresses (not NDI) for both initiator and responder. + */ +static bool nan_derive_ndp_request_pmkid(const uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN], + const uint8_t initiator_nmi[6], + const uint8_t responder_nmi[6], + const uint8_t service_id[6], + uint8_t pmkid_out[ESP_WIFI_NAN_NDP_PMKID_LEN]) +{ + uint8_t hash[32]; + + if (!service_id || !initiator_nmi || !responder_nmi || + !g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector) { + return false; + } + + const unsigned char *addr_pmkid[4] = {(const unsigned char *)NAN_PMK_NAME_LABEL, + initiator_nmi, responder_nmi, service_id + }; + int len_pmkid[4] = {NAN_PMK_NAME_LABEL_LEN, 6, 6, 6}; + if (g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(pmk, ESP_WIFI_NAN_NDP_PMK_LEN, + 4, addr_pmkid, len_pmkid, hash) != 0) { + return false; + } + memcpy(pmkid_out, hash, ESP_WIFI_NAN_NDP_PMKID_LEN); + return true; +} + +/* + * Match peer's ND-PMKID (from NDP Request/SCIA) against our PMK cache. + * + * Rule: Do NOT compare NDP Request PMKID to Publish-PMKID — they differ by design. + * - Publish SCIA PMKID uses IAddr = FF:FF:FF:FF:FF:FF (initiator-independent). + * - NDP Request SCIA PMKID uses Initiator NMI and Responder NMI (pair-specific). + */ +static bool nan_match_pmkid(struct own_svc_info *p_svc, + const uint8_t peer_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN], + uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN], + const uint8_t *peer_nmi, + const uint8_t *peer_ndi) +{ + (void)peer_ndi; + + if (!p_svc || !peer_pmkid) { + return false; + } + + if (!peer_nmi || !p_svc->svc_name[0]) { + goto no_match; + } + + uint8_t our_nmi[6]; + if (esp_wifi_get_mac(WIFI_IF_NAN, our_nmi) != ESP_OK) { + goto no_match; + } + + uint8_t service_id[6]; + if (!nan_compute_service_id(p_svc->svc_name, service_id)) { + goto no_match; + } + + ESP_LOGD(TAG, " Service Name: %s", p_svc->svc_name); + ESP_LOG_BUFFER_HEXDUMP(TAG, service_id, 6, ESP_LOG_DEBUG); + ESP_LOGD(TAG, " Initiator NMI (peer): "MACSTR, MAC2STR(peer_nmi)); + ESP_LOGD(TAG, " Responder NMI (us): "MACSTR, MAC2STR(our_nmi)); + + /* Cap loop at array bound; guards against unsanitized num_pmkids. */ + uint8_t cached = p_svc->security_cfg.num_pmkids; + if (cached > ESP_WIFI_NAN_MAX_PMKIDS) { + cached = ESP_WIFI_NAN_MAX_PMKIDS; + } + for (int i = 0; i < cached; i++) { + uint8_t expected_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN]; + + /* Spec order: Initiator NMI, Responder NMI */ + if (nan_derive_ndp_request_pmkid(p_svc->pmk_cache[i], peer_nmi, our_nmi, + service_id, expected_pmkid)) { + if (memcmp(expected_pmkid, peer_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) { + if (pmk) { + memcpy(pmk, p_svc->pmk_cache[i], ESP_WIFI_NAN_NDP_PMK_LEN); + } + return true; + } + } + + /* Fallback: try reversed order in case peer implementation differs */ + if (nan_derive_ndp_request_pmkid(p_svc->pmk_cache[i], our_nmi, peer_nmi, + service_id, expected_pmkid)) { + if (memcmp(expected_pmkid, peer_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) { + if (pmk) { + memcpy(pmk, p_svc->pmk_cache[i], ESP_WIFI_NAN_NDP_PMK_LEN); + } + return true; + } + } + } + +no_match: + ESP_LOGW(TAG, "PMKID not found in our cache (no match)"); + return false; +} + +/* + * RSNA NCS-SK-KDF (Wi-Fi Aware v4.0 §7.1.3.5) — alias of PRF-Length in + * IEEE 802.11-2020 §12.7.1.7.2 with HMAC-SHA-256 as the underlying + * pseudo-random function. Used to derive NAN PTK (KCK || KEK || TK). + * PRF-Length(Key, Label, Data) -> buf_len bytes. + */ +static int nan_sha256_prf(const uint8_t *key, size_t key_len, const char *label, + const uint8_t *data, size_t data_len, + uint8_t *buf, size_t buf_len) +{ + const unsigned char *addr[4]; + int len_arr[4]; + uint8_t counter_le[2]; + uint8_t length_le[2]; + uint16_t counter = 1; + size_t pos = 0; + const size_t hash_len = 32; + uint8_t hash[32]; + + if (!g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector || !key || !buf) { + return -1; + } + + length_le[0] = (uint8_t)((buf_len * 8) & 0xFF); + length_le[1] = (uint8_t)((buf_len * 8) >> 8); + + addr[0] = counter_le; + len_arr[0] = 2; + addr[1] = (const unsigned char *)label; + len_arr[1] = (int)strlen(label); + addr[2] = data; + len_arr[2] = (int)data_len; + addr[3] = length_le; + len_arr[3] = 2; + + while (pos < buf_len) { + counter_le[0] = (uint8_t)(counter & 0xFF); + counter_le[1] = (uint8_t)((counter >> 8) & 0xFF); + if (pos + hash_len <= buf_len) { + if (g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(key, (int)key_len, 4, addr, len_arr, &buf[pos]) != 0) { + return -1; + } + pos += hash_len; + } else { + if (g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(key, (int)key_len, 4, addr, len_arr, hash) != 0) { + return -1; + } + memcpy(&buf[pos], hash, buf_len - pos); + pos = buf_len; + } + counter++; + } + return 0; +} + +/* + * Derive ND-PTK (KCK, KEK, TK) for NCS-SK-128 (Wi-Fi Aware v4.0 §7.1.3.5): + * PRF-Length(ND-PMK, "NAN Pairwise key expansion", + * IAddr || RAddr || INonce || RNonce) + * Per spec, IAddr/RAddr are the NDP Initiator/Responder Data Interface + * addresses (NDI) — caller must pass NDIs, not NMIs. INonce=ANonce, + * RNonce=SNonce. + */ +static int nan_ndp_ptk_derive(const uint8_t *pmk, const uint8_t *i_addr, const uint8_t *r_addr, + const uint8_t *anonce, const uint8_t *snonce, + uint8_t *kck_out, uint8_t *kek_out, uint8_t *tk_out) +{ + uint8_t seed[6 + 6 + NAN_NONCE_LEN + NAN_NONCE_LEN]; + uint8_t ptk[NAN_NCS_SK_128_PTK_LEN]; + size_t off = 0; + + memcpy(seed + off, i_addr, 6); + off += 6; + memcpy(seed + off, r_addr, 6); + off += 6; + memcpy(seed + off, anonce, NAN_NONCE_LEN); + off += NAN_NONCE_LEN; + memcpy(seed + off, snonce, NAN_NONCE_LEN); + + if (nan_sha256_prf(pmk, ESP_WIFI_NAN_NDP_PMK_LEN, "NAN Pairwise key expansion", + seed, sizeof(seed), ptk, sizeof(ptk)) != 0) { + return -1; + } + memcpy(kck_out, ptk, NAN_NCS_SK_128_KCK_LEN); + memcpy(kek_out, ptk + NAN_NCS_SK_128_KCK_LEN, NAN_NCS_SK_128_KEK_LEN); + memcpy(tk_out, ptk + NAN_NCS_SK_128_KCK_LEN + NAN_NCS_SK_128_KEK_LEN, NAN_NCS_SK_128_TK_LEN); + return 0; +} + +/* + * Lazy initiator PTK derivation. No-op if ndl->ptk_set is already 1. + * Caller MUST hold NAN_DATA_LOCK. On success, ndl->nd_kck/kek/tk and + * the corresponding *_len fields are populated and ptk_set=1. + * + * Spec §7.1.3.5: PTK PRF takes Data Interface addresses. Local NDI is + * obtained via esp_wifi_get_mac(WIFI_IF_NAN); peer NDI lives in + * ndl->peer_ndi, populated by ndp_response_indication on M2 RX. + * + * Returns 0 on success, -1 on failure. + */ +static int ndl_ensure_ptk(struct ndl_info *ndl) +{ + if (ndl->ptk_set) { + return 0; + } + uint8_t our_mac[6]; + if (esp_wifi_get_mac(WIFI_IF_NAN, our_mac) != ESP_OK) { + ESP_LOGE(TAG, "ndl_ensure_ptk: get our MAC failed"); + return -1; + } + if (nan_ndp_ptk_derive(ndl->security_ctx.nd_pmk, + our_mac, /* IAddr = Initiator NDI (us) */ + ndl->peer_ndi, /* RAddr = Responder NDI (peer) */ + ndl->anonce, ndl->snonce, + ndl->nd_kck, ndl->nd_kek, ndl->nd_tk) != 0) { + ESP_LOGE(TAG, "ndl_ensure_ptk: PTK derivation failed"); + return -1; + } + ndl->kck_len = NAN_NCS_SK_128_KCK_LEN; + ndl->kek_len = NAN_NCS_SK_128_KEK_LEN; + ndl->tk_len = NAN_NCS_SK_128_TK_LEN; + ndl->ptk_set = 1; + return 0; +} + +/* Find NDL by publisher_id (e.g. when parsing key desc and ndp_id/peer_nmi unknown). */ +static struct ndl_info *nan_find_ndl_by_pub_id(uint8_t pub_id) +{ + struct ndl_info *ndl_in_progress = NULL; + struct ndl_info *ndl_any = NULL; + + for (int i = 0; i < ESP_WIFI_NAN_DATAPATH_MAX_PEERS; i++) { + struct ndl_info *ndl = &s_nan_ctx.ndl[i]; + if (ndl->ndp_id == 0) { + continue; + } + if (ndl->publisher_id != pub_id) { + continue; + } + ndl_any = ndl; + if (ndl->handshake_state != NAN_HANDSHAKE_IDLE) { + ndl_in_progress = ndl; + break; + } + } + return ndl_in_progress ? ndl_in_progress : ndl_any; +} + +/* + * Claim a free NDL slot for (pub_id, peer_nmi) when parsing CSIA/SCIA/key + * desc before NDP/NDL. Caller runs in wifi task (no NAN_DATA_LOCK). + * Later nan_record_new_ndl will find this by pub_id+peer and update ndp_id. + */ +static struct ndl_info *nan_ndl_claim_for_pub_peer(uint8_t pub_id, const uint8_t *peer_nmi, uint8_t ndp_id) +{ + if (!peer_nmi) { + return NULL; + } + for (int i = 0; i < ESP_WIFI_NAN_DATAPATH_MAX_PEERS; i++) { + struct ndl_info *ndl = &s_nan_ctx.ndl[i]; + if (ndl->ndp_id == 0) { + ndl->publisher_id = pub_id; + MACADDR_COPY(ndl->peer_nmi, peer_nmi); + ndl->ndp_id = ndp_id; + return ndl; + } + } + return NULL; +} + +/* + * Build RSNA Key Descriptor payload (95-byte EAPOL-Key layout, see + * IEEE 802.11-2020 §12.7.2). NAN carries this body inside the NAN + * Shared Key Descriptor attribute (Wi-Fi Aware v4.0 §9.5.21). + * + * Mirrors hostap nan_sec_add_key_attrs argument shape (instance_id is carried + * by the outer attribute, so this helper only populates the key descriptor + * body): caller supplies the variable bits (MIC/SECURE/INSTALL/ACK) and an + * optional nonce. KEY_TYPE (Pairwise) is always set; Version stays 0 + * (NCS-SK uses AES-CMAC). MIC is left as zero for the caller to fill. + */ +static int nan_build_rsna_key_descriptor(uint8_t *kd, uint16_t key_info_flags, + const uint8_t *nonce, + const uint8_t *replay_counter, + const uint8_t *key_rsc) +{ + if (!kd || !replay_counter || !key_rsc) { + return 0; + } + + kd[NAN_KEY_DESC_TYPE_OFF] = NAN_KEY_DESC_TYPE_RSN; + + uint16_t key_info = key_info_flags | NAN_KEY_INFO_KEY_TYPE; + kd[NAN_KEY_DESC_KEY_INFO_OFF] = (key_info >> 8) & 0xFF; + kd[NAN_KEY_DESC_KEY_INFO_OFF + 1] = (key_info >> 0) & 0xFF; + + kd[NAN_KEY_DESC_KEY_LEN_OFF] = 0; + kd[NAN_KEY_DESC_KEY_LEN_OFF + 1] = 0; + + memcpy(&kd[NAN_KEY_DESC_REPLAY_OFF], replay_counter, NAN_REPLAY_COUNTER_LEN); + + if (nonce) { + memcpy(&kd[NAN_KEY_DESC_NONCE_OFF], nonce, NAN_NONCE_LEN); + } else { + memset(&kd[NAN_KEY_DESC_NONCE_OFF], 0, NAN_NONCE_LEN); + } + + memset(&kd[NAN_KEY_DESC_IV_OFF], 0, 16); + memcpy(&kd[NAN_KEY_DESC_RSC_OFF], key_rsc, NAN_KEY_RSC_LEN); + memset(&kd[NAN_KEY_DESC_KEY_ID_OFF], 0, 8); + memset(&kd[NAN_KEY_DESC_MIC_OFF], 0, NAN_NCS_SK_128_MIC_LEN); + + kd[NAN_KEY_DESC_DATA_LEN_OFF] = 0; + kd[NAN_KEY_DESC_DATA_LEN_OFF + 1] = 0; + + return NAN_KEY_DESC_MIN_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. + */ +static int nan_build_scia_attr(uint8_t *frm, uint8_t pub_id, + const uint8_t (*pmkids)[ESP_WIFI_NAN_NDP_PMKID_LEN], + uint8_t num_pmkids) +{ + if (!frm || !pmkids || !num_pmkids) { + return 0; + } + + uint8_t *p = frm; + + *p++ = NAN_ATTR_ID_SECURITY_CONTEXT; + + uint16_t attr_len = 20 * num_pmkids; + *p++ = attr_len & 0xFF; + *p++ = (attr_len >> 8) & 0xFF; + + for (uint8_t i = 0; i < num_pmkids; i++) { + ESP_LOG_BUFFER_HEXDUMP(TAG, pmkids[i], ESP_WIFI_NAN_NDP_PMKID_LEN, ESP_LOG_DEBUG); + + uint16_t val_len = ESP_WIFI_NAN_NDP_PMKID_LEN; + *p++ = val_len & 0xFF; + *p++ = (val_len >> 8) & 0xFF; + *p++ = NAN_SEC_CTX_TYPE_ND_PMKID; + *p++ = pub_id; + memcpy(p, pmkids[i], ESP_WIFI_NAN_NDP_PMKID_LEN); + p += ESP_WIFI_NAN_NDP_PMKID_LEN; + } + + return (int)(p - frm); +} + +/* + * Compute Key MIC over a NAN action frame body and patch the Shared Key + * Descriptor attribute in place. Mirrors hostap nan_sec_pre_tx() (single + * entry point for M2/M3/M4 MIC); separated only because the dispatch by + * frame subtype lives in the WiFi blob, not here. + * + * If `include_auth_token` is true, the HMAC scope is `Auth_Token || body` + * per Wi-Fi Aware v4.0 §7.1.3.5 M3 construction; Auth_Token is taken from + * ndl->auth_token (captured during M1 TX/RX). Otherwise the scope is + * `body` only (M2/M4 construction). + */ +static int nan_update_kd_mic(uint8_t *body, size_t body_len, uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi, const char *label, + bool include_auth_token) +{ + if (!body || !key_desc_attr || !peer_nmi || body_len == 0) { + ESP_LOGE(TAG, "NDP %s Update MIC: invalid parameters", label); + return -1; + } + + if (!g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector) { + ESP_LOGE(TAG, "NDP %s Update MIC: hmac_sha256_vector not registered", label); + return -1; + } + + if (key_desc_attr < body || key_desc_attr >= (body + body_len)) { + ESP_LOGE(TAG, "NDP %s Update MIC: key_desc_attr outside body", label); + ESP_LOGE(TAG, " body=%p, body_len=%zu, end=%p", (void *)body, body_len, + (void *)(body + body_len)); + ESP_LOGE(TAG, " key_desc_attr=%p, offset=%td", (void *)key_desc_attr, + key_desc_attr - body); + ESP_LOG_BUFFER_HEXDUMP(TAG, body, body_len, ESP_LOG_ERROR); + return -1; + } + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP %s Update MIC: no NDL for ndp_id=%d", label, ndp_id); + return -1; + } + + if (!ndl->ptk_set) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP %s Update MIC: PTK not set for ndp_id=%d", label, ndp_id); + return -1; + } + + if (include_auth_token && ndl->auth_token_len == 0) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP %s Update MIC: Auth_Token not captured (call esp_nan_capture_m1_auth_token after M1 assembly)", label); + return -1; + } + + if ((key_desc_attr + 3) > (body + body_len)) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP %s Update MIC: attr header out of bounds", label); + return -1; + } + + /* Skip attribute header (3) + pub_id (1) to reach the 95-byte key desc */ + uint8_t *key_desc = key_desc_attr + 3 + 1; + + if ((key_desc + NAN_KEY_DESC_MIC_OFF + NAN_NCS_SK_128_MIC_LEN) > (body + body_len)) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP %s Update MIC: MIC field out of bounds", label); + return -1; + } + + /* MIC field must be zeroed before HMAC computation + * (Wi-Fi Aware v4.0 §7.1.3.5 / IEEE 802.11-2020 §12.7.2). */ + memset(&key_desc[NAN_KEY_DESC_MIC_OFF], 0, NAN_NCS_SK_128_MIC_LEN); + + ESP_LOGD(TAG, "NDP %s Update MIC: body (%zu bytes, MIC zeroed) for ndp_id=%d:", + label, body_len, ndp_id); + ESP_LOG_BUFFER_HEXDUMP(TAG, body, body_len, ESP_LOG_DEBUG); + + uint8_t mic_buf[32]; + const unsigned char *addr[2]; + int len_arr[2]; + int n_vec; + if (include_auth_token) { + addr[0] = ndl->auth_token; + len_arr[0] = (int)ndl->auth_token_len; + addr[1] = body; + len_arr[1] = (int)body_len; + n_vec = 2; + } else { + addr[0] = body; + len_arr[0] = (int)body_len; + n_vec = 1; + } + + if (g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(ndl->nd_kck, + NAN_NCS_SK_128_KCK_LEN, + n_vec, addr, len_arr, + mic_buf) != 0) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP %s Update MIC: HMAC-SHA256 failed", label); + return -1; + } + + memcpy(&key_desc[NAN_KEY_DESC_MIC_OFF], mic_buf, NAN_NCS_SK_128_MIC_LEN); + + ESP_LOGD(TAG, "NDP %s Update MIC: KCK:", label); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->nd_kck, NAN_NCS_SK_128_KCK_LEN, ESP_LOG_DEBUG); + ESP_LOGD(TAG, "NDP %s Update MIC: computed MIC%s:", + label, include_auth_token ? " (scope = Auth_Token || body)" : ""); + ESP_LOG_BUFFER_HEXDUMP(TAG, mic_buf, NAN_NCS_SK_128_MIC_LEN, ESP_LOG_DEBUG); + + NAN_DATA_UNLOCK(); + return 0; +} + +/* + * Map RSNA Key Info bits (IEEE 802.11-2020 §12.7.2 Table 12-7) to + * NDP 4-way handshake message number (1..4) per Wi-Fi Aware v4.0 + * §7.1.3.5 NCS-SK construction. + * Returns 0 for an unrecognized combination. + * + * Hostap dispatches by NAN Action subtype directly; we infer here because the + * subtype isn't plumbed into this entry point. Keeping the inference in one + * place mirrors hostap's centralized validation in nan_sec_rx(). + */ +static uint8_t nan_keyinfo_to_msg_type(uint16_t key_info) +{ + bool has_mic = (key_info & NAN_KEY_INFO_MIC) != 0; + bool has_secure = (key_info & NAN_KEY_INFO_SECURE) != 0; + bool has_install = (key_info & NAN_KEY_INFO_INSTALL) != 0; + bool has_ack = (key_info & NAN_KEY_INFO_ACK) != 0; + + if (!has_mic && !has_secure) { + return 1; + } + if (has_mic && !has_secure) { + return 2; + } + if (has_mic && has_secure && !has_install) { + return 3; + } + if (has_mic && has_secure && has_install) { + /* + * Interop mapping observed in Android captures: + * - M3: KeyInfo=0x03c8 (Install=1, ACK=1, MIC=1, Secure=1, Pairwise=1) + * - M4: KeyInfo=0x0348 (Install=1, ACK=0, MIC=1, Secure=1, Pairwise=1) + */ + return has_ack ? 3 : 4; + } + return 0; +} + +/*------------------------------------------------------------------------- + * Public API: PMK/PMKID derivation for publish + *-----------------------------------------------------------------------*/ + +esp_err_t nan_derive_security_params(wifi_nan_publish_cfg_t *cfg) +{ + uint8_t pmk[32]; + uint8_t service_id[6]; + uint8_t pmkid[16]; + uint8_t hash[32]; + esp_err_t ret = ESP_FAIL; + + /* 1. Service ID = SHA256(lowercase service name)[:6] */ + if (!nan_compute_service_id(cfg->service_name, service_id)) { + ESP_LOGE(TAG, "Service ID derivation failed"); + goto cleanup; + } + + ESP_LOGD(TAG, "Security derivation: svc='%s'", cfg->service_name); + ESP_LOG_BUFFER_HEXDUMP(TAG, service_id, 6, ESP_LOG_DEBUG); + + /* 2. PMK from passphrase (or directly provided) */ + if (cfg->security_cfg.use_pmk) { + memcpy(pmk, cfg->security_cfg.pmk, 32); + } else { + uint8_t publisher_nmi[ETH_ALEN]; + uint8_t csid = nan_get_first_csid(cfg->security_cfg.csid_bitmap); + + if (csid == 0) { + ESP_LOGE(TAG, "No cipher suite in bitmap"); + goto cleanup; + } + esp_wifi_get_mac(WIFI_IF_NAN, publisher_nmi); + if (nan_derive_nd_pmk_from_passphrase(cfg->security_cfg.passphrase, csid, + service_id, publisher_nmi, pmk) != 0) { + ESP_LOGE(TAG, "PBKDF2 ND-PMK derivation failed"); + goto cleanup; + } + } + + /* 3. Publish PMKID uses IAddr = ff:ff:ff:ff:ff:ff (initiator-independent). + * Pair-specific PMKID for NDP Request is derived separately. */ + uint8_t i_addr[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + uint8_t r_addr[6]; + esp_wifi_get_mac(WIFI_IF_NAN, r_addr); + + if (!g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector) { + ESP_LOGE(TAG, "hmac_sha256_vector not registered"); + goto cleanup; + } + + const unsigned char *addr_pmkid[4] = {(const unsigned char *)NAN_PMK_NAME_LABEL, + i_addr, r_addr, service_id + }; + int len_pmkid[4] = {NAN_PMK_NAME_LABEL_LEN, 6, 6, 6}; + + g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(pmk, 32, 4, addr_pmkid, len_pmkid, hash); + memcpy(pmkid, hash, 16); + + memcpy(cfg->security_cfg.pmkids[0], pmkid, 16); + cfg->security_cfg.num_pmkids = 1; + memcpy(cfg->security_cfg.pmk, pmk, ESP_WIFI_NAN_NDP_PMK_LEN); + ret = ESP_OK; + +cleanup: + forced_memzero(pmk, sizeof(pmk)); + return ret; +} + +/* + * Blob-side gate query: blob calls this to learn whether an NDP is + * secured (and which cipher) without touching ndl->security_ctx + * directly. Lookup honors the initiator pre-claim convention: ndp_id=0 + * matches the slot keyed by peer_nmi alone, used between + * esp_nan_internal_datapath_req() entry and the blob assigning the + * real ndp_id. + */ +uint16_t nan_get_ndp_security_csid(uint8_t ndp_id, const uint8_t *peer_nmi) +{ + if (!peer_nmi) { + return 0; + } + NAN_DATA_LOCK(); + 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 csid; +} + +void nan_security_cache_subscriber_params(const char *service_name, + const wifi_nan_discovery_security_params_t *security_cfg) +{ + if (!service_name || !security_cfg) { + s_nan_subscriber_sec_cache.valid = false; + return; + } + strlcpy(s_nan_subscriber_sec_cache.service_name, service_name, + sizeof(s_nan_subscriber_sec_cache.service_name)); + memcpy(&s_nan_subscriber_sec_cache.security_cfg, security_cfg, + sizeof(wifi_nan_discovery_security_params_t)); + s_nan_subscriber_sec_cache.valid = true; +} + +bool nan_security_service_match(const uint8_t *publisher_nmi, + const wifi_nan_discovery_security_params_t *peer_sec) +{ + uint8_t pmk[32]; + uint8_t service_id[6]; + uint8_t pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN]; + uint8_t hash[32]; + bool matched = false; + + if (!s_nan_subscriber_sec_cache.valid || !publisher_nmi || !peer_sec) { + goto cleanup; + } + if (peer_sec->num_pmkids == 0) { + goto cleanup; + } + if (!nan_compute_service_id(s_nan_subscriber_sec_cache.service_name, service_id)) { + goto cleanup; + } + + const wifi_nan_discovery_security_params_t *cfg = &s_nan_subscriber_sec_cache.security_cfg; + + if (cfg->use_pmk) { + memcpy(pmk, cfg->pmk, sizeof(pmk)); + } else { + uint8_t csid = nan_get_first_csid(cfg->csid_bitmap); + + if (csid == 0) { + goto cleanup; + } + if (nan_derive_nd_pmk_from_passphrase(cfg->passphrase, csid, service_id, + publisher_nmi, pmk) != 0) { + goto cleanup; + } + } + + if (!g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector) { + goto cleanup; + } + + uint8_t i_addr[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + const unsigned char *addr_pmkid[4] = {(const unsigned char *)NAN_PMK_NAME_LABEL, + i_addr, publisher_nmi, service_id + }; + int len_pmkid[4] = {NAN_PMK_NAME_LABEL_LEN, 6, 6, 6}; + + g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(pmk, 32, 4, addr_pmkid, len_pmkid, hash); + memcpy(pmkid, hash, ESP_WIFI_NAN_NDP_PMKID_LEN); + + for (unsigned int i = 0; i < peer_sec->num_pmkids && i < ESP_WIFI_NAN_MAX_PMKIDS; i++) { + if (memcmp(pmkid, peer_sec->pmkids[i], ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) { + ESP_LOGD(TAG, "ND-PMKID match with publisher " MACSTR, MAC2STR(publisher_nmi)); + ESP_LOG_BUFFER_HEXDUMP(TAG, pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN, ESP_LOG_INFO); + matched = true; + break; + } + } + +cleanup: + forced_memzero(pmk, sizeof(pmk)); + return matched; +} + +/* + * Populate the initiator NDL's security_ctx from cached subscriber params so + * the blob's CSIA / SCIA / Shared-Key Descriptor builder callbacks can find + * the cipher + pair-PMKID + ND-PMK keyed by (ndp_id, peer_nmi). Writing into + * the NDL (rather than wifi_nan_datapath_req_t) keeps key material off the + * public API surface -- callers never see ND-PMK or PMKID. + * + * Computes the pair-specific PMKID using L(HMAC(PMK, "NAN PMK Name" || + * Initiator NMI || Responder NMI || Service_ID), 0, 128), distinct from the + * publish-time discovery PMKID (which uses broadcast IAddr). + */ +esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, + const uint8_t *peer_nmi) +{ + if (!ndl || !peer_nmi) { + return ESP_FAIL; + } + if (!s_nan_subscriber_sec_cache.valid) { + return ESP_OK; /* subscriber didn't cache security; open NDP */ + } + const wifi_nan_discovery_security_params_t *cfg = &s_nan_subscriber_sec_cache.security_cfg; + if (cfg->csid_bitmap == 0) { + return ESP_OK; /* subscriber didn't request encrypted datapath */ + } + + uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; + uint8_t service_id[6]; + uint8_t our_nmi[6]; + esp_err_t ret = ESP_FAIL; + + if (esp_wifi_get_mac(WIFI_IF_NAN, our_nmi) != ESP_OK) { + ESP_LOGE(TAG, "NDP req security: get own NMI failed"); + goto cleanup; + } + if (!nan_compute_service_id(s_nan_subscriber_sec_cache.service_name, service_id)) { + ESP_LOGE(TAG, "NDP req security: service id derivation failed"); + goto cleanup; + } + + if (cfg->use_pmk) { + memcpy(pmk, cfg->pmk, ESP_WIFI_NAN_NDP_PMK_LEN); + } else { + uint8_t csid = nan_get_first_csid(cfg->csid_bitmap); + if (csid == 0) { + goto cleanup; + } + if (nan_derive_nd_pmk_from_passphrase(cfg->passphrase, csid, service_id, + peer_nmi, pmk) != 0) { + ESP_LOGE(TAG, "NDP req security: passphrase->PMK failed"); + goto cleanup; + } + } + + uint8_t pair_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN]; + if (!nan_derive_ndp_request_pmkid(pmk, our_nmi, peer_nmi, service_id, pair_pmkid)) { + ESP_LOGE(TAG, "NDP req security: pair PMKID derivation failed"); + goto cleanup; + } + + ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; + ndl->security_ctx.csid_bitmap = cfg->csid_bitmap; + 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); + + ESP_LOGD(TAG, "NDP req security: derived ND-PMK + pair PMKID for peer "MACSTR, MAC2STR(peer_nmi)); + ESP_LOG_BUFFER_HEXDUMP(TAG, pair_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN, ESP_LOG_DEBUG); + ret = ESP_OK; + +cleanup: + forced_memzero(pmk, sizeof(pmk)); + return ret; +} + +/*------------------------------------------------------------------------- + * Public API: CSIA / SCIA / Shared Key Descriptor construction + *-----------------------------------------------------------------------*/ + +int esp_nan_construct_csia(uint8_t *frm, uint8_t pub_id, uint16_t own_csid_bitmap, uint16_t peer_csid_bitmap) +{ + uint16_t csid_bitmap = peer_csid_bitmap ? (own_csid_bitmap & peer_csid_bitmap) : own_csid_bitmap; + csid_bitmap &= NAN_CSID_VALID_BITMAP; + + ESP_LOGD(TAG, "Constructing CSIA: pub_id=%d, own=0x%04x, peer=0x%04x, effective=0x%04x", + pub_id, own_csid_bitmap, peer_csid_bitmap, csid_bitmap); + if (!frm || !csid_bitmap) { + return 0; + } + + uint8_t num_csids = __builtin_popcount(csid_bitmap); + uint8_t *p = frm; + + *p++ = NAN_ATTR_ID_CIPHER_SUITE_INFO; + + uint16_t attr_len = 1 + 2 * num_csids; + *p++ = attr_len & 0xFF; + *p++ = (attr_len >> 8) & 0xFF; + + /* Capabilities byte (currently 0) */ + *p++ = 0; + + /* Cipher Suite ID list: [CSID(1)] [Publish ID(1)] */ + for (uint8_t csid = 1; csid <= 8; csid++) { + if (csid_bitmap & (1 << csid)) { + *p++ = csid; + *p++ = pub_id; + } + } + + return (int)(p - frm); +} + +int esp_nan_construct_scia_publish(uint8_t *frm, uint8_t pub_id, + uint8_t num_pmkids, + const uint8_t pmkids[][ESP_WIFI_NAN_NDP_PMKID_LEN]) +{ + ESP_LOGD(TAG, "Constructing SCIA (Publish SDF): pub_id=%d, num_pmkids=%d", pub_id, num_pmkids); + return nan_build_scia_attr(frm, pub_id, pmkids, num_pmkids); +} + +int esp_nan_construct_scia_ndp_resp(uint8_t *frm, uint8_t ndp_id, const uint8_t *peer_nmi) +{ + if (!frm || !peer_nmi) { + return 0; + } + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + /* Initiator path: blob calls SCIA builder before the key-desc builder + * has had a chance to patch the pre-claimed NDL's ndp_id (still 0). + * Fall back to peer-only lookup so we find the NDL that + * esp_wifi_nan_datapath_req() pre-claimed and seeded with security + * context. The key-desc builder will assign the real ndp_id shortly + * after. */ + ndl = nan_find_ndl(0, (uint8_t *)peer_nmi); + if (!ndl) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP SCIA: no NDL for ndp_id=%d peer="MACSTR, ndp_id, MAC2STR(peer_nmi)); + return 0; + } + } + + 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) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP Response SCIA: PMKID not set for ndp_id=%d", ndp_id); + return 0; + } + + ESP_LOGD(TAG, "Constructing SCIA (NDP Response M2): ndp_id=%d, pub_id=%d", ndp_id, ndl->publisher_id); + ESP_LOGD(TAG, "Using pair-specific PMKID from M1 (not Publish PMKID):"); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->security_ctx.nd_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN, ESP_LOG_DEBUG); + + uint8_t pub_id = ndl->publisher_id; + uint8_t pmkid_one[1][ESP_WIFI_NAN_NDP_PMKID_LEN]; + memcpy(pmkid_one[0], ndl->security_ctx.nd_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN); + + NAN_DATA_UNLOCK(); + return nan_build_scia_attr(frm, pub_id, + (const uint8_t (*)[ESP_WIFI_NAN_NDP_PMKID_LEN])pmkid_one, 1); +} + +uint32_t esp_nan_get_csia_len(uint16_t own_csid_bitmap, uint16_t peer_csid_bitmap) +{ + /* ID(1) + Len(2) + Capabilities(1) + 2 bytes per CSID entry. Empty bitmap + * → 0 so the caller does not reserve a 4-byte hole the constructor refuses + * to fill. Mask matches what construct_csia actually emits. */ + uint16_t csid_bitmap = peer_csid_bitmap ? (own_csid_bitmap & peer_csid_bitmap) : own_csid_bitmap; + csid_bitmap &= NAN_CSID_VALID_BITMAP; + if (!csid_bitmap) { + return 0; + } + 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); + return len; +} + +uint32_t esp_nan_get_scia_len(uint8_t num_pmkids) +{ + /* ID(1) + Len(2) + 20 bytes per SCID entry. With zero PMKIDs the + * constructor (nan_build_scia_attr) writes nothing, so report 0 + * here too — otherwise the caller reserves 3 header bytes that + * never get filled and uninitialized memory ends up in the SDF. */ + if (!num_pmkids) { + return 0; + } + uint32_t len = 3 + 20 * num_pmkids; + ESP_LOGD(TAG, "GET SCIA LEN: %lu (num_pmkids=%d)", len, num_pmkids); + return len; +} + +uint32_t esp_nan_get_shared_key_desc_attr_len(uint16_t key_data_len) +{ + uint32_t body_len = 1 + NAN_KEY_DESC_MIN_LEN + key_data_len; + uint32_t total = 3 + body_len; + ESP_LOGD(TAG, "GET Shared Key Desc Attr LEN: %lu (key_data_len=%u)", + (unsigned long)total, (unsigned int)key_data_len); + return total; +} + +int esp_nan_ndp_security_install_get_shared_desc_len(void) +{ + return (int)esp_nan_get_shared_key_desc_attr_len(0); +} + +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) +{ + const uint32_t attr_len = esp_nan_get_shared_key_desc_attr_len(0); + + if (!buf || buf_len < attr_len || !peer_nmi) { + return 0; + } + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP Resp Key Desc: no NDL for ndp_id=%d", ndp_id); + return 0; + } + if (ndl->handshake_state != NAN_HANDSHAKE_M1_RCVD) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP Resp Key Desc: NDL not in M1_RCVD (state=%d)", ndl->handshake_state); + return 0; + } + + /* Resolve PMK from publish when: NDL not encrypted, or encrypted but nd_pmk not set */ + { + static const uint8_t zero_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN] = {0}; + static const uint8_t zero_pmk[ESP_WIFI_NAN_NDP_PMK_LEN] = {0}; + bool have_peer_pmkid = (memcmp(ndl->security_ctx.nd_pmkid, zero_pmkid, + ESP_WIFI_NAN_NDP_PMKID_LEN) != 0); + bool need_pmk = (ndl->security_ctx.type != WIFI_NAN_SECURITY_ENCRYPTED) || + (memcmp(ndl->security_ctx.nd_pmk, zero_pmk, ESP_WIFI_NAN_NDP_PMK_LEN) == 0); + + if (need_pmk && have_peer_pmkid) { + struct own_svc_info *p_svc = nan_find_own_svc(ndl->publisher_id); + uint8_t matched_pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; + if (p_svc && nan_match_pmkid(p_svc, ndl->security_ctx.nd_pmkid, matched_pmk, + ndl->peer_nmi, ndl->peer_ndi)) { + ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; + ndl->security_ctx.csid_bitmap = p_svc->security_cfg.csid_bitmap; + memcpy(ndl->security_ctx.nd_pmk, matched_pmk, ESP_WIFI_NAN_NDP_PMK_LEN); + ESP_LOGD(TAG, "NDP Resp Key Desc: resolved PMK from publish (PMKID match)"); + forced_memzero(matched_pmk, sizeof(matched_pmk)); + } else if (ndl->security_ctx.type != WIFI_NAN_SECURITY_ENCRYPTED) { + forced_memzero(matched_pmk, sizeof(matched_pmk)); + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP Resp Key Desc: NDL not encrypted; send NDP Response without Shared Key Descriptor"); + return 0; + } else { + forced_memzero(matched_pmk, sizeof(matched_pmk)); + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP Resp Key Desc: no PMK for peer PMKID; ensure same passphrase on both devices"); + return 0; + } + } else if (ndl->security_ctx.type != WIFI_NAN_SECURITY_ENCRYPTED) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP Resp Key Desc: NDL not encrypted; send NDP Response without Shared Key Descriptor"); + return 0; + } + } + + /* Generate SNonce and derive PTK if not yet done */ + if (!ndl->ptk_set) { + if (os_get_random(ndl->snonce, NAN_NONCE_LEN) != 0) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP Resp Key Desc: SNonce random failed"); + return 0; + } + /* M2 echoes M1's replay counter unchanged (per RSNA 4-way handshake). */ + memcpy(ndl->tx_replay_counter, ndl->rx_replay_counter, NAN_REPLAY_COUNTER_LEN); + + uint8_t our_mac[6]; + if (esp_wifi_get_mac(WIFI_IF_NAN, our_mac) != ESP_OK) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP Resp Key Desc: get our MAC failed"); + return 0; + } + if (nan_ndp_ptk_derive(ndl->security_ctx.nd_pmk, + ndl->peer_ndi, /* IAddr = Initiator NDI (Wi-Fi Aware v4.0 §7.1.3.5) */ + our_mac, /* RAddr = Responder NDI */ + ndl->anonce, ndl->snonce, + ndl->nd_kck, ndl->nd_kek, ndl->nd_tk) != 0) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP Resp Key Desc: PTK derivation failed"); + return 0; + } + ndl->kck_len = NAN_NCS_SK_128_KCK_LEN; + ndl->kek_len = NAN_NCS_SK_128_KEK_LEN; + ndl->tk_len = NAN_NCS_SK_128_TK_LEN; + ndl->ptk_set = 1; + + ESP_LOGD(TAG, "=== PTK Derivation Debug (M2 Responder) ==="); + ESP_LOGD(TAG, "ND-PMK:"); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->security_ctx.nd_pmk, ESP_WIFI_NAN_NDP_PMK_LEN, ESP_LOG_DEBUG); + ESP_LOGD(TAG, "ANonce:"); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->anonce, NAN_NONCE_LEN, ESP_LOG_DEBUG); + ESP_LOGD(TAG, "SNonce:"); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->snonce, NAN_NONCE_LEN, ESP_LOG_DEBUG); + ESP_LOGD(TAG, "KCK:"); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->nd_kck, NAN_NCS_SK_128_KCK_LEN, ESP_LOG_DEBUG); + ESP_LOGD(TAG, "KEK:"); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->nd_kek, NAN_NCS_SK_128_KEK_LEN, ESP_LOG_DEBUG); + ESP_LOGD(TAG, "TK:"); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, ESP_LOG_DEBUG); + } + + 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; + } + *p++ = ndl->publisher_id; + + int n = nan_build_rsna_key_descriptor(p, NAN_KEY_INFO_MIC, + ndl->snonce, + ndl->tx_replay_counter, + ndl->key_rsc); + if (n <= 0) { + NAN_DATA_UNLOCK(); + return 0; + } + + n = 3 + 1 + n; + + NAN_DATA_UNLOCK(); + ESP_LOGD(TAG, "NDP Resp Key Desc: built (MIC=0; driver must call esp_nan_update_ndp_resp_mic) for ndp_id=%d", ndp_id); + return n; +} + +int esp_nan_get_ndp_security_install_key_desc(uint8_t *buf, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi) +{ + const uint32_t attr_len = esp_nan_get_shared_key_desc_attr_len(0); + + if (!buf || buf_len < attr_len || !peer_nmi) { + return 0; + } + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP M4 Key Desc: no NDL for ndp_id=%d", ndp_id); + return 0; + } + + if (ndl->handshake_state != NAN_HANDSHAKE_M3_RCVD) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP M4 Key Desc: NDL not in M3_RCVD (state=%d)", ndl->handshake_state); + return 0; + } + + if (!ndl->ptk_set) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP M4 Key Desc: PTK not set for ndp_id=%d", ndp_id); + return 0; + } + + /* M4 echoes M3 replay counter unchanged */ + memcpy(ndl->tx_replay_counter, ndl->rx_replay_counter, NAN_REPLAY_COUNTER_LEN); + + 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; + } + *p++ = ndl->publisher_id; + + int n = nan_build_rsna_key_descriptor(p, + NAN_KEY_INFO_MIC | + NAN_KEY_INFO_SECURE | + NAN_KEY_INFO_INSTALL, + NULL, /* M4 nonce = 0 */ + ndl->tx_replay_counter, + ndl->key_rsc); + if (n <= 0) { + NAN_DATA_UNLOCK(); + return 0; + } + + n = 3 + 1 + n; + + NAN_DATA_UNLOCK(); + ESP_LOGD(TAG, "NDP M4 Key Desc: built for ndp_id=%d", ndp_id); + return n; +} + +int esp_nan_update_ndp_resp_mic(uint8_t *m2_body, size_t body_len, uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi) +{ + return nan_update_kd_mic(m2_body, body_len, key_desc_attr, ndp_id, peer_nmi, "M2", false); +} + +int esp_nan_update_ndp_security_install_mic(uint8_t *m4_body, size_t body_len, uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi) +{ + return nan_update_kd_mic(m4_body, body_len, key_desc_attr, ndp_id, peer_nmi, "M4", false); +} + +/*------------------------------------------------------------------------- + * Public API: parsers (CSIA / SCIA / Shared Key Descriptor / Publish SDF) + *-----------------------------------------------------------------------*/ + +void esp_nan_parse_ndp_csia(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param) +{ + if (!frm || !param || buf_len < 3) { + return; + } + + uint8_t *p = (uint8_t *)frm; + if (p[0] != NAN_ATTR_ID_CIPHER_SUITE_INFO) { + ESP_LOGW(TAG, "Invalid CSIA ID: 0x%02x (expected 0x%02x)", p[0], NAN_ATTR_ID_CIPHER_SUITE_INFO); + return; + } + uint16_t len = p[1] | (p[2] << 8); + if ((size_t)3 + len > buf_len) { + ESP_LOGW(TAG, "CSIA: encoded len %u exceeds buf %zu", len, buf_len); + return; + } + uint8_t *body = p + 3; + + /* body[0] = Capabilities; pairs of [CSID(1) PubID(1)] follow. Iterate all + * entries so a peer advertising multiple cipher suites is fully captured. */ + if (len >= 3) { + uint16_t accum = 0; + uint8_t pub_id = 0; + for (uint16_t off = 1; off + 2 <= len; off += 2) { + uint8_t csid = body[off]; + if (csid >= WIFI_NAN_CSID_NCS_SK_128 && csid <= WIFI_NAN_CSID_NCS_PK_PASN_256) { + accum |= (uint16_t)(1u << csid); + pub_id = body[off + 1]; + } + } + if (accum) { + param->csid_bitmap = accum; + param->type = WIFI_NAN_SECURITY_ENCRYPTED; + s_pending_scia.has_csid = true; + s_pending_scia.pub_id = pub_id; + s_pending_scia.csid_bitmap = accum; + } + } +} + +void esp_nan_parse_ndp_scia(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param) +{ + if (!frm || !param || buf_len < 3) { + return; + } + + uint8_t *p = (uint8_t *)frm; + if (p[0] != NAN_ATTR_ID_SECURITY_CONTEXT) { + ESP_LOGW(TAG, "Invalid SCIA ID: 0x%02x (expected 0x%02x)", p[0], NAN_ATTR_ID_SECURITY_CONTEXT); + return; + } + uint16_t len = p[1] | (p[2] << 8); + if ((size_t)3 + len > buf_len) { + ESP_LOGW(TAG, "SCIA: encoded len %u exceeds buf %zu", len, buf_len); + return; + } + uint8_t *body = p + 3; + + /* size_t offset prevents (4 + entry_len) wrapping to 0 on a peer-supplied + * entry_len near 0xFFFF; per-entry layout is Len(2)||Type(1)||PubID(1)||Value. */ + size_t offset = 0; + while (offset + 4 <= len) { + uint16_t entry_len = body[offset] | (body[offset + 1] << 8); + uint8_t type = body[offset + 2]; + uint8_t pub_id = body[offset + 3]; + uint8_t *val = &body[offset + 4]; + + if (offset + 4 + entry_len > len) { + break; + } + + if (type == 1 && entry_len == ESP_WIFI_NAN_NDP_PMKID_LEN) { + memcpy(param->nd_pmkid, val, ESP_WIFI_NAN_NDP_PMKID_LEN); + + s_pending_scia.has_pmkid = true; + s_pending_scia.pub_id = pub_id; + memcpy(s_pending_scia.pmkid, val, ESP_WIFI_NAN_NDP_PMKID_LEN); + break; + } + offset += 4 + entry_len; + } +} + +void esp_nan_parse_ndp_key_desc(void *frm, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi) +{ + if (!frm || buf_len < 3) { + return; + } + + uint8_t *p = (uint8_t *)frm; + if (p[0] != NAN_ATTR_ID_SHARED_KEY_DESC) { + ESP_LOGW(TAG, "Invalid Key Desc ID: 0x%02x (expected 0x%02x)", p[0], NAN_ATTR_ID_SHARED_KEY_DESC); + return; + } + uint16_t attr_len = p[1] | (p[2] << 8); + if ((size_t)3 + attr_len > buf_len) { + ESP_LOGW(TAG, "Key Desc: encoded len %u exceeds buf %zu", attr_len, buf_len); + return; + } + uint8_t *body = p + 3; + + if (attr_len < 1) { + return; + } + + uint8_t pub_id = body[0]; + uint8_t *key_desc = body + 1; + uint16_t key_desc_len = attr_len - 1; + + if (key_desc_len < NAN_KEY_DESC_MIN_LEN) { + ESP_LOGW(TAG, "Key Descriptor too short (%d bytes)", key_desc_len); + return; + } + + /* Key Info is big-endian on the wire per IEEE 802.11-2020 §12.7.2. */ + uint16_t key_info = (key_desc[NAN_KEY_DESC_KEY_INFO_OFF] << 8) | key_desc[NAN_KEY_DESC_KEY_INFO_OFF + 1]; + uint8_t *replay_counter = &key_desc[NAN_KEY_DESC_REPLAY_OFF]; + uint8_t *key_nonce = &key_desc[NAN_KEY_DESC_NONCE_OFF]; + uint8_t *key_rsc = &key_desc[NAN_KEY_DESC_RSC_OFF]; + uint16_t key_data_len = (key_desc[NAN_KEY_DESC_DATA_LEN_OFF] << 8) | key_desc[NAN_KEY_DESC_DATA_LEN_OFF + 1]; + + bool has_enc_key = (key_info & NAN_KEY_INFO_ENC_KEY) != 0; + uint8_t msg_type = nan_keyinfo_to_msg_type(key_info); + + if ((key_info & NAN_KEY_INFO_INSTALL) && (key_info & NAN_KEY_INFO_ACK)) { + ESP_LOGD(TAG, "NDP Key Desc: Install+ACK bits set (KeyInfo=0x%04x)", key_info); + } + + switch (msg_type) { + case 1: + ESP_LOGD(TAG, "NDP Key Desc: M1 (NDP Request) - ANonce + replay counter"); + break; + case 2: + ESP_LOGD(TAG, "NDP Key Desc: M2 (NDP Response) - SNonce + replay counter"); + break; + case 3: + ESP_LOGD(TAG, "NDP Key Desc: M3 (NDP Confirm); key data len=%d", key_data_len); + break; + case 4: + ESP_LOGD(TAG, "NDP Key Desc: M4 (Security Install); key data len=%d", key_data_len); + break; + default: + ESP_LOGD(TAG, "NDP Key Desc: unknown message (KeyInfo=0x%04x)", key_info); + break; + } + + /* + * Called from wifi task context; must not block on NAN_DATA_LOCK to avoid + * deadlock (app may hold lock while waiting for wifi). + */ + struct ndl_info *ndl = NULL; + if (ndp_id != 0 || peer_nmi != NULL) { + ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + } + if (ndl == NULL && peer_nmi != NULL) { + ndl = nan_find_ndl_by_pub_id_and_peer(pub_id, peer_nmi); + } + if (ndl == NULL && peer_nmi != NULL) { + ndl = nan_ndl_claim_for_pub_peer(pub_id, peer_nmi, ndp_id); + } + if (ndl == NULL) { + ndl = nan_find_ndl_by_pub_id(pub_id); + } + + if (ndl) { + memcpy(ndl->rx_replay_counter, replay_counter, NAN_REPLAY_COUNTER_LEN); + ndl->rx_replay_counter_set = 1; + + if (msg_type == 1) { + memcpy(ndl->anonce, key_nonce, NAN_NONCE_LEN); + ndl->handshake_state = NAN_HANDSHAKE_M1_RCVD; + ESP_LOGD(TAG, "NDP Key Desc: stored ANonce + replay counter (M1)"); + } else if (msg_type == 2) { + memcpy(ndl->snonce, key_nonce, NAN_NONCE_LEN); + ndl->handshake_state = NAN_HANDSHAKE_M2_RCVD; + ESP_LOGD(TAG, "NDP Key Desc: stored SNonce + replay counter (M2)"); + } else if (msg_type == 3) { + /* Responder receives M3 — leave the MIC verification (which has + * the Auth_Token||body scope) to esp_nan_verify_ndp_confirm_mic + * before promoting to M3_RCVD. */ + ndl->handshake_state = NAN_HANDSHAKE_M3_PENDING_VERIFY; + ESP_LOGD(TAG, "NDP Key Desc: handshake_state=M3_PENDING_VERIFY (responder)"); + } else if (msg_type == 4) { + /* Initiator receives M4 — gate final COMPLETE on MIC verify in + * esp_nan_verify_ndp_security_install_mic. */ + ndl->handshake_state = NAN_HANDSHAKE_M4_RCVD; + ESP_LOGD(TAG, "NDP Key Desc: handshake_state=M4_RCVD (initiator)"); + } + + 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]; + 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) { + 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; + + 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; + 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); + } + } 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); + } + } + } + kd_offset += 2 + kde_len; + } + } + } else if (msg_type == 1) { + /* M1 received but NDL not created yet — store as pending */ + s_pending_m1.valid = true; + s_pending_m1.pub_id = pub_id; + memcpy(s_pending_m1.anonce, key_nonce, NAN_NONCE_LEN); + memcpy(s_pending_m1.rx_replay_counter, replay_counter, NAN_REPLAY_COUNTER_LEN); + memcpy(s_pending_m1.key_rsc, key_rsc, NAN_KEY_RSC_LEN); + ESP_LOGI(TAG, "NDP Key Desc: M1 stored as pending for pub_id=%d", pub_id); + } else { + ESP_LOGI(TAG, "NDP Key Desc: no matching NDL (ndp_id=%d, pub_id=%d), key desc not applied", + ndp_id, pub_id); + } +} + +esp_err_t esp_nan_parse_publish_security(const uint8_t *attrs, size_t attrs_len, + wifi_nan_discovery_security_params_t *security) +{ + uint16_t csia_len = 0; + const uint8_t *csia = NULL; + uint16_t scia_len = 0; + const uint8_t *scia = NULL; + + if (!attrs || !security) { + return ESP_ERR_INVALID_ARG; + } + + memset(security, 0, sizeof(*security)); + + /* 1. CSIA — Cipher Suite IDs */ + csia = nan_find_attr(attrs, attrs_len, NAN_ATTR_ID_CIPHER_SUITE_INFO, &csia_len); + if (csia && csia_len >= 1) { + 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 */ + size_t offset = 1; + while (offset + 2 <= csia_len) { + uint8_t csid = csia[offset]; + + /* Spec Table 121: csid is 1..8. Skip out-of-range to avoid UB / truncation. */ + if (csid >= WIFI_NAN_CSID_NCS_SK_128 && csid <= WIFI_NAN_CSID_NCS_PK_PASN_256) { + security->csid_bitmap |= (uint16_t)(1u << csid); + } + + offset += 2; + } + } + + /* 2. SCIA — PMKIDs */ + scia = nan_find_attr(attrs, attrs_len, NAN_ATTR_ID_SECURITY_CONTEXT, &scia_len); + if (scia && scia_len > 0) { + ESP_LOGD(TAG, "Found SCIA in Publish SDF, len=%d", scia_len); + ESP_LOG_BUFFER_HEXDUMP(TAG, scia, scia_len, ESP_LOG_DEBUG); + + size_t offset = 0; + while (offset + 4 <= scia_len && security->num_pmkids < ESP_WIFI_NAN_MAX_PMKIDS) { + uint16_t val_len = scia[offset] | (scia[offset + 1] << 8); + uint8_t type = scia[offset + 2]; + + if (offset + 4 + val_len > scia_len) { + break; + } + + if (type == 1 && val_len == ESP_WIFI_NAN_NDP_PMKID_LEN) { + memcpy(security->pmkids[security->num_pmkids], + &scia[offset + 4], ESP_WIFI_NAN_NDP_PMKID_LEN); + ESP_LOGD(TAG, " Parsed PMKID[%d]:", security->num_pmkids); + ESP_LOG_BUFFER_HEXDUMP(TAG, security->pmkids[security->num_pmkids], + ESP_WIFI_NAN_NDP_PMKID_LEN, ESP_LOG_DEBUG); + security->num_pmkids++; + } + + offset += 4 + val_len; + } + } + + if (security->csid_bitmap == 0 && security->num_pmkids == 0) { + return ESP_ERR_NOT_FOUND; + } + + return ESP_OK; +} + +/*------------------------------------------------------------------------- + * Cross-file accessor: drain pending CSIA/SCIA/M1 onto NDL. + * Called from nan_app_ndp_indication_cb once peer info is known. + *-----------------------------------------------------------------------*/ + +void nan_security_apply_pending(struct ndl_info *ndl, + struct own_svc_info *p_own_svc, + uint8_t pub_id, + const uint8_t *peer_nmi, + const uint8_t *peer_ndi) +{ + if (ndl && s_pending_scia.pub_id == pub_id && + (s_pending_scia.has_csid || s_pending_scia.has_pmkid)) { + + if (s_pending_scia.has_csid) { + ndl->security_ctx.csid_bitmap = s_pending_scia.csid_bitmap; + ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; + } + + if (s_pending_scia.has_pmkid) { + memcpy(ndl->security_ctx.nd_pmkid, s_pending_scia.pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN); + uint8_t matched_pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; + if (nan_match_pmkid(p_own_svc, s_pending_scia.pmkid, matched_pmk, peer_nmi, peer_ndi)) { + ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; + memcpy(ndl->security_ctx.nd_pmk, matched_pmk, ESP_WIFI_NAN_NDP_PMK_LEN); + ESP_LOGD(TAG, "NDP Indication: PMKID validated, security=ENCRYPTED"); + } else { + ESP_LOGW(TAG, "NDP Indication: PMKID validation failed"); + } + forced_memzero(matched_pmk, sizeof(matched_pmk)); + } + + memset(&s_pending_scia, 0, sizeof(s_pending_scia)); + } + + if (s_pending_m1.valid && s_pending_m1.pub_id == pub_id && ndl) { + memcpy(ndl->anonce, s_pending_m1.anonce, NAN_NONCE_LEN); + memcpy(ndl->rx_replay_counter, s_pending_m1.rx_replay_counter, NAN_REPLAY_COUNTER_LEN); + ndl->rx_replay_counter_set = 1; + memcpy(ndl->key_rsc, s_pending_m1.key_rsc, NAN_KEY_RSC_LEN); + ndl->handshake_state = NAN_HANDSHAKE_M1_RCVD; + ESP_LOGD(TAG, "NDP Indication: applied pending M1 to NDL"); + s_pending_m1.valid = false; + } +} + +/* Replay counter helper (used by M3 initiator builder). */ +static void nan_replay_counter_increment(uint8_t *counter) +{ + int i; + for (i = NAN_REPLAY_COUNTER_LEN - 1; i >= 0; i--) { + counter[i]++; + if (counter[i] != 0) { + break; + } + } +} + +/* + * --------------------------------------------------------------------------- + * Initiator-side NDP security (M1 TX, M2 verify, M3 TX, M4 verify) and + * responder-side M3 MIC verification. These complete the NCS-SK 4-way + * handshake by adding the symmetric counterparts to the responder M2/M4 + * builders above. + * --------------------------------------------------------------------------- + */ + +/** + * @brief Verify MIC in Shared Key Descriptor of an inbound NDP message. + * + * Computes HMAC-SHA256(KCK, [prefix ||] body_with_MIC_zeroed) and compares + * the first 16 bytes against the saved MIC. Restores the MIC field on + * return so the caller buffer is left intact. + * + * For M2/M4 the scope is body only — pass prefix=NULL. For M3 (responder + * side) the scope is Auth_Token || body per Wi-Fi Aware v4.0 §7.1.3.5; + * pass prefix=Auth_Token, prefix_len=Auth_Token_len. + * + * @return 0 on MIC pass, -1 on parameter error or MIC mismatch. + */ +static int nan_verify_kck_mic(uint8_t *body, size_t body_len, + uint8_t *key_desc_attr, + const uint8_t *kck, size_t kck_len, + const uint8_t *prefix, size_t prefix_len, + const char *tag_label) +{ + if (!body || !key_desc_attr || body_len == 0 || !kck) { + ESP_LOGE(TAG, "%s Verify MIC: invalid parameters", tag_label); + return -1; + } + if (!g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector) { + ESP_LOGE(TAG, "%s Verify MIC: hmac_sha256_vector not registered", tag_label); + return -1; + } + if (key_desc_attr < body || key_desc_attr >= (body + body_len)) { + ESP_LOGE(TAG, "%s Verify MIC: key_desc_attr outside body", tag_label); + return -1; + } + uint8_t *key_desc = key_desc_attr + 3 + 1; /* skip attr hdr (3) + pub_id (1) */ + if ((key_desc + NAN_KEY_DESC_MIC_OFF + NAN_NCS_SK_128_MIC_LEN) > (body + body_len)) { + ESP_LOGE(TAG, "%s Verify MIC: MIC field out of bounds", tag_label); + return -1; + } + + uint8_t saved_mic[NAN_NCS_SK_128_MIC_LEN]; + memcpy(saved_mic, &key_desc[NAN_KEY_DESC_MIC_OFF], NAN_NCS_SK_128_MIC_LEN); + memset(&key_desc[NAN_KEY_DESC_MIC_OFF], 0, NAN_NCS_SK_128_MIC_LEN); + + uint8_t mic_buf[32]; + const unsigned char *addr[2]; + int len_arr[2]; + int n_vec; + if (prefix && prefix_len > 0) { + addr[0] = prefix; + len_arr[0] = (int)prefix_len; + addr[1] = body; + len_arr[1] = (int)body_len; + n_vec = 2; + } else { + addr[0] = body; + len_arr[0] = (int)body_len; + n_vec = 1; + } + int rc = g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(kck, (int)kck_len, + n_vec, addr, len_arr, mic_buf); + memcpy(&key_desc[NAN_KEY_DESC_MIC_OFF], saved_mic, NAN_NCS_SK_128_MIC_LEN); + + if (rc != 0) { + ESP_LOGE(TAG, "%s Verify MIC: HMAC-SHA256 failed", tag_label); + return -1; + } + if (memcmp(mic_buf, saved_mic, NAN_NCS_SK_128_MIC_LEN) != 0) { + ESP_LOGE(TAG, "%s Verify MIC: mismatch", tag_label); + ESP_LOGD(TAG, "%s expected MIC:", tag_label); + ESP_LOG_BUFFER_HEXDUMP(TAG, mic_buf, NAN_NCS_SK_128_MIC_LEN, ESP_LOG_DEBUG); + ESP_LOGD(TAG, "%s received MIC:", tag_label); + ESP_LOG_BUFFER_HEXDUMP(TAG, saved_mic, NAN_NCS_SK_128_MIC_LEN, ESP_LOG_DEBUG); + return -1; + } + ESP_LOGD(TAG, "%s Verify MIC: pass", tag_label); + return 0; +} + +/** + * @brief Construct SCIA for outbound NDP Request (M1) — initiator TX path. + * + * Identical wire format to esp_nan_construct_scia_ndp_resp(): the SCIA body + * carries the pair-specific PMKID stored in ndl->security_ctx.nd_pmkid. The + * field is pre-populated by the host before M1 TX; both initiator and + * responder paths converge on the same NDL field, so we delegate. + */ +int esp_nan_construct_scia_ndp_req(uint8_t *frm, uint8_t ndp_id, const uint8_t *peer_nmi) +{ + ESP_LOGD(TAG, "Constructing SCIA (NDP Request M1) -> delegating to scia_ndp_resp body"); + return esp_nan_construct_scia_ndp_resp(frm, ndp_id, peer_nmi); +} + +/** + * @brief Build Shared Key Descriptor attribute for outbound NDP Request (M1). + * + * Initiator-side analogue of esp_nan_get_ndp_resp_shared_key_desc(). Looks + * up the pre-claimed initiator NDL by ndp_id/peer (created in + * esp_wifi_nan_datapath_req() with own_role=INITIATOR + security_ctx.nd_pmk + * populated). Generates a random ANonce, sets the TX replay counter to all + * zeros (per RSNA convention for initial M1), zeroes the Key RSC, and emits + * the attribute header + body. + * + * No MIC is set — M1 is unauthenticated. After the blob assembles the full + * M1 body, it must call esp_nan_capture_m1_auth_token() so the host can + * store Auth_Token = SHA256(M1_body)[0:16] for use during M3 MIC. + */ +int esp_nan_get_ndp_req_shared_key_desc(uint8_t *buf, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi) +{ + const uint32_t attr_len = esp_nan_get_shared_key_desc_attr_len(0); + + if (!buf || buf_len < attr_len || !peer_nmi) { + return 0; + } + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + /* Pre-claimed initiator NDL may have ndp_id=0 (assigned by blob during + * this call). Fall back to peer-only lookup; capture the real ndp_id + * once we have the NDL so subsequent ndp_id-keyed lookups (parser, + * MIC verifier) find it. */ + ndl = nan_find_ndl(0, (uint8_t *)peer_nmi); + if (ndl && ndl->ndp_id == 0 && ndp_id != 0) { + ndl->ndp_id = ndp_id; + ESP_LOGD(TAG, "NDP Req Key Desc: assigned ndp_id=%d to pre-claimed NDL for peer="MACSTR, + ndp_id, MAC2STR(peer_nmi)); + } + } + if (!ndl) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP Req Key Desc: no NDL for ndp_id=%d peer="MACSTR, ndp_id, MAC2STR(peer_nmi)); + return 0; + } + if (ndl->own_role != ESP_WIFI_NDP_ROLE_INITIATOR) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP Req Key Desc: NDL not initiator role (role=%d)", ndl->own_role); + return 0; + } + if (ndl->handshake_state != NAN_HANDSHAKE_IDLE) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP Req Key Desc: NDL not in IDLE (state=%d)", ndl->handshake_state); + return 0; + } + if (ndl->security_ctx.type != WIFI_NAN_SECURITY_ENCRYPTED) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP Req Key Desc: NDL not configured for encrypted datapath"); + return 0; + } + + if (os_get_random(ndl->anonce, NAN_NONCE_LEN) != 0) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP Req Key Desc: ANonce random failed"); + return 0; + } + memset(ndl->tx_replay_counter, 0, NAN_REPLAY_COUNTER_LEN); + memset(ndl->key_rsc, 0, NAN_KEY_RSC_LEN); + /* Auth token must be re-captured by esp_nan_capture_m1_auth_token() after assembly */ + ndl->auth_token_len = 0; + + 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; + } + *p++ = ndl->publisher_id; + + int n = nan_build_rsna_key_descriptor(p, NAN_KEY_INFO_ACK, + ndl->anonce, + ndl->tx_replay_counter, + ndl->key_rsc); + if (n <= 0) { + NAN_DATA_UNLOCK(); + return 0; + } + + n = 3 + 1 + n; + + NAN_DATA_UNLOCK(); + /* State transition to M1_SENT happens in the host TX-confirm hook */ + ESP_LOGD(TAG, "NDP Req Key Desc: built (M1, no MIC) for ndp_id=%d", ndp_id); + return n; +} + +/** + * @brief Capture Auth_Token = SHA256(M1_body)[0:16] for use during M3 MIC. + * + * Per Wi-Fi Aware v4.0 §7.1.3.5 (M3 construction), the M3 MIC scope is + * Auth_Token || Body_of_M3, + * where Auth_Token is computed over the entire as-transmitted M1 body + * (CSIA, SCIA, Shared-Key Descriptor, NDPE attributes — everything the + * blob assembled). Driver must call after assembly, before TX. + */ +int esp_nan_capture_m1_auth_token(const uint8_t *m1_body, size_t body_len, + uint8_t ndp_id, const uint8_t *peer_nmi) +{ + if (!m1_body || body_len == 0 || !peer_nmi) { + ESP_LOGE(TAG, "Auth_Token capture: invalid parameters"); + return -1; + } + if (!g_wifi_default_wpa_crypto_funcs.sha256_vector) { + ESP_LOGE(TAG, "Auth_Token capture: sha256_vector not registered"); + return -1; + } + + uint8_t hash[32]; + const uint8_t *addr[1] = { m1_body }; + size_t len[1] = { body_len }; + if (g_wifi_default_wpa_crypto_funcs.sha256_vector(1, addr, len, hash) != 0) { + ESP_LOGE(TAG, "Auth_Token capture: SHA-256 failed"); + return -1; + } + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "Auth_Token capture: no NDL for ndp_id=%d", ndp_id); + return -1; + } + /* NCS-SK-128: Auth_Token length is 16 bytes (NCS-SK-HASH = SHA-256, L() takes 128 bits). */ + memcpy(ndl->auth_token, hash, NAN_NCS_SK_128_MIC_LEN); + ndl->auth_token_len = NAN_NCS_SK_128_MIC_LEN; + ESP_LOGD(TAG, "Auth_Token captured (16 bytes) for ndp_id=%d, body_len=%u", ndp_id, (unsigned)body_len); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->auth_token, NAN_NCS_SK_128_MIC_LEN, ESP_LOG_DEBUG); + NAN_DATA_UNLOCK(); + return 0; +} + +/** + * @brief Build Shared Key Descriptor attribute for outbound NDP Confirm (M3). + * + * Initiator-side. Looks up NDL (must be in M2_RCVD with stored ANonce + SNonce + * + nd_pmk). Derives PTK if not yet done, increments the TX replay counter, + * and emits the attribute header + body with MIC=0. + * + * Caller must subsequently call esp_nan_update_ndp_confirm_mic() to + * compute MIC = HMAC-SHA256(KCK, Auth_Token || M3_body). + */ +int esp_nan_get_ndp_confirm_shared_key_desc(uint8_t *buf, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi) +{ + const uint32_t attr_len = esp_nan_get_shared_key_desc_attr_len(0); + + if (!buf || buf_len < attr_len || !peer_nmi) { + return 0; + } + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP M3 Key Desc: no NDL for ndp_id=%d", ndp_id); + return 0; + } + if (ndl->own_role != ESP_WIFI_NDP_ROLE_INITIATOR) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP M3 Key Desc: NDL not initiator role (role=%d)", ndl->own_role); + return 0; + } + if (ndl->handshake_state != NAN_HANDSHAKE_M2_RCVD) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP M3 Key Desc: NDL not in M2_RCVD (state=%d)", ndl->handshake_state); + return 0; + } + if (ndl->security_ctx.type != WIFI_NAN_SECURITY_ENCRYPTED) { + NAN_DATA_UNLOCK(); + ESP_LOGW(TAG, "NDP M3 Key Desc: NDL not encrypted"); + return 0; + } + + if (ndl_ensure_ptk(ndl) != 0) { + NAN_DATA_UNLOCK(); + return 0; + } + + /* Initiator increments its own TX replay counter for M3 (M1 was zero). */ + nan_replay_counter_increment(ndl->tx_replay_counter); + + 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; + } + *p++ = ndl->publisher_id; + + int n = nan_build_rsna_key_descriptor(p, + NAN_KEY_INFO_MIC | + NAN_KEY_INFO_SECURE | + NAN_KEY_INFO_ACK, + NULL, /* M3 nonce = 0 */ + ndl->tx_replay_counter, + ndl->key_rsc); + if (n <= 0) { + NAN_DATA_UNLOCK(); + return 0; + } + + n = 3 + 1 + n; + + 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; +} + +/** + * @brief Verify M2 MIC on the initiator side after the parser has stored SNonce. + * + * Derives PTK if not yet done (using stored ANonce + just-parsed SNonce, with + * i_addr=our_NMI for initiator), then verifies HMAC-SHA256(KCK, M2_body) against + * the MIC field in the descriptor. + */ +int esp_nan_verify_ndp_resp_mic(uint8_t *m2_body, size_t body_len, uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi) +{ + if (!m2_body || !key_desc_attr || !peer_nmi || body_len == 0) { + ESP_LOGE(TAG, "NDP M2 Verify MIC: invalid parameters"); + return -1; + } + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M2 Verify MIC: no NDL for ndp_id=%d", ndp_id); + return -1; + } + if (ndl->own_role != ESP_WIFI_NDP_ROLE_INITIATOR) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M2 Verify MIC: NDL not initiator role"); + return -1; + } + if (ndl->handshake_state != NAN_HANDSHAKE_M2_RCVD) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M2 Verify MIC: NDL not in M2_RCVD (state=%d)", ndl->handshake_state); + return -1; + } + + if (ndl_ensure_ptk(ndl) != 0) { + NAN_DATA_UNLOCK(); + return -1; + } + + int rc = nan_verify_kck_mic(m2_body, body_len, key_desc_attr, + ndl->nd_kck, NAN_NCS_SK_128_KCK_LEN, + NULL, 0, "NDP M2"); + NAN_DATA_UNLOCK(); + return rc; +} + +/** + * @brief Compute MIC for outbound NDP Confirm (M3) — initiator TX path. + * + * Per Wi-Fi Aware v4.0 §7.1.3.5, M3 MIC scope is `Auth_Token || Body_of_M3` (not body + * alone). Auth_Token must have been captured during M1 TX via + * esp_nan_capture_m1_auth_token(). Computes + * HMAC-SHA256(KCK, Auth_Token || M3_body_with_MIC_zeroed) + * and writes the first 16 bytes into the MIC field of the descriptor. + */ +int esp_nan_update_ndp_confirm_mic(uint8_t *m3_body, size_t body_len, uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi) +{ + return nan_update_kd_mic(m3_body, body_len, key_desc_attr, ndp_id, peer_nmi, "M3", true); +} + +/** + * @brief Verify M3 MIC on the responder side. Promotes handshake_state from + * M3_PENDING_VERIFY to M3_RCVD on a passing HMAC-SHA-256(KCK, + * Auth_Token || M3_body). On -1, abort the M4 TX path. + * + * Auth_Token is the SHA-256 of the M1 body the responder received, + * stored via esp_nan_capture_m1_auth_token() during M1 RX. + */ +int esp_nan_verify_ndp_confirm_mic(uint8_t *m3_body, size_t body_len, uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi) +{ + if (!m3_body || !key_desc_attr || !peer_nmi || body_len == 0) { + ESP_LOGE(TAG, "NDP M3 Verify MIC: invalid parameters"); + return -1; + } + if (key_desc_attr < m3_body || key_desc_attr >= (m3_body + body_len)) { + ESP_LOGE(TAG, "NDP M3 Verify MIC: key_desc_attr outside m3_body"); + return -1; + } + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M3 Verify MIC: no NDL for ndp_id=%d", ndp_id); + return -1; + } + if (ndl->own_role == ESP_WIFI_NDP_ROLE_INITIATOR) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M3 Verify MIC: NDL is initiator role; M3 verify is responder-only"); + return -1; + } + if (!ndl->ptk_set) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M3 Verify MIC: PTK not set for ndp_id=%d", ndp_id); + return -1; + } + if (ndl->auth_token_len == 0) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M3 Verify MIC: Auth_Token not captured (call esp_nan_capture_m1_auth_token from M1 RX path)"); + return -1; + } + if (ndl->handshake_state != NAN_HANDSHAKE_M3_PENDING_VERIFY) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M3 Verify MIC: NDL not in M3_PENDING_VERIFY (state=%d)", ndl->handshake_state); + return -1; + } + + int rc = nan_verify_kck_mic(m3_body, body_len, key_desc_attr, + ndl->nd_kck, NAN_NCS_SK_128_KCK_LEN, + ndl->auth_token, ndl->auth_token_len, + "NDP M3"); + if (rc == 0) { + ndl->handshake_state = NAN_HANDSHAKE_M3_RCVD; + ESP_LOGD(TAG, "NDP M3 Verify MIC: pass; handshake_state=M3_RCVD (ready to TX M4)"); + } + NAN_DATA_UNLOCK(); + return rc; +} + +/** + * @brief Verify M4 MIC on the initiator side. Promotes handshake_state from + * M4_RCVD to COMPLETE on a passing HMAC-SHA256(KCK, M4_body). On -1, tear + * the NDP down rather than installing the TK. + */ +int esp_nan_verify_ndp_security_install_mic(uint8_t *m4_body, size_t body_len, uint8_t *key_desc_attr, + uint8_t ndp_id, const uint8_t *peer_nmi) +{ + if (!m4_body || !key_desc_attr || !peer_nmi || body_len == 0) { + ESP_LOGE(TAG, "NDP M4 Verify MIC: invalid parameters"); + return -1; + } + + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (!ndl) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M4 Verify MIC: no NDL for ndp_id=%d", ndp_id); + return -1; + } + if (ndl->own_role != ESP_WIFI_NDP_ROLE_INITIATOR) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M4 Verify MIC: NDL not initiator role"); + return -1; + } + if (!ndl->ptk_set) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M4 Verify MIC: PTK not set for ndp_id=%d", ndp_id); + return -1; + } + if (ndl->handshake_state != NAN_HANDSHAKE_M4_RCVD) { + NAN_DATA_UNLOCK(); + ESP_LOGE(TAG, "NDP M4 Verify MIC: NDL not in M4_RCVD (state=%d)", ndl->handshake_state); + return -1; + } + + int rc = nan_verify_kck_mic(m4_body, body_len, key_desc_attr, + ndl->nd_kck, NAN_NCS_SK_128_KCK_LEN, + NULL, 0, "NDP M4"); + if (rc == 0) { + ndl->handshake_state = NAN_HANDSHAKE_COMPLETE; + ESP_LOGD(TAG, "NDP M4 Verify MIC: handshake_state=COMPLETE (initiator)"); + } + NAN_DATA_UNLOCK(); + return rc; +} + +#endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ From 0244cb362bfb9a404053306266721719e8589196 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Mon, 11 May 2026 16:18:30 +0530 Subject: [PATCH 3/8] feat(wifi): NAN encrypted NDP example apps nan_publisher / nan_subscriber demonstrate the secure NDP path: publisher acts as the responder + UDP echo server, subscriber as the initiator. Both support passphrase and pre-shared PMK modes via menuconfig; PMK hex strings are decoded with the same helper on both sides so endpoints derive matching PMKIDs. --- .../nan_publisher/main/Kconfig.projbuild | 34 ++++++++ .../nan_publisher/main/publisher_main.c | 40 +++++++++- .../nan_subscriber/main/Kconfig.projbuild | 34 ++++++++ .../nan_subscriber/main/subscriber_main.c | 79 +++++++++++++++---- 4 files changed, 169 insertions(+), 18 deletions(-) diff --git a/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild b/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild index 1e410615f6a..55dd37874c5 100644 --- a/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild +++ b/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild @@ -23,4 +23,38 @@ menu "Example Configuration" help Send a reply to the Follow Up sent by a Subscriber + menu "Security Configuration" + config EXAMPLE_NAN_SECURITY_ENABLED + bool "Enable NAN Security" + depends on ESP_WIFI_NAN_SECURITY + default y + + choice EXAMPLE_NAN_SECURITY_METHOD + prompt "Security Method" + depends on EXAMPLE_NAN_SECURITY_ENABLED + default EXAMPLE_NAN_SEC_METHOD_PASSPHRASE + help + Choose the method for NAN discovery security. + + config EXAMPLE_NAN_SEC_METHOD_PASSPHRASE + bool "Passphrase-based (Password)" + config EXAMPLE_NAN_SEC_METHOD_PMK + bool "Direct PMK (32-byte hex)" + endchoice + + config EXAMPLE_NAN_PASSPHRASE + string "Passphrase" + depends on EXAMPLE_NAN_SEC_METHOD_PASSPHRASE + default "password" + help + Passphrase for NAN Discovery security. + + config EXAMPLE_NAN_PMK + string "Direct PMK (Hex)" + depends on EXAMPLE_NAN_SEC_METHOD_PMK + default "ee3585063056d164d15454ad39010d4e2640b0d82fb24a2d6899862d273c68bf" + help + 32-byte hex string representing the PMK. + endmenu + endmenu 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 fefb6956528..bf87c8b863d 100644 --- a/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c +++ b/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c @@ -11,6 +11,7 @@ software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ +#include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -37,6 +38,23 @@ static EventGroupHandle_t nan_event_group; static const char *TAG = "publisher"; +#ifdef CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK +static bool decode_hex_string(const char *hex, uint8_t *out, size_t out_len) +{ + if (!hex || !out || strlen(hex) != out_len * 2) { + return false; + } + for (size_t i = 0; i < out_len; i++) { + unsigned int byte; + if (sscanf(hex + 2 * i, "%2x", &byte) != 1) { + return false; + } + out[i] = (uint8_t)byte; + } + return true; +} +#endif + static int NAN_RECEIVE = BIT0; uint8_t g_peer_inst_id; static uint8_t g_peer_mac[ETH_ALEN]; @@ -105,10 +123,30 @@ void wifi_nan_publish(void) .matching_filter = EXAMPLE_NAN_MATCHING_FILTER, .single_replied_event = 1, /* 0 - All incoming NDP requests will be internally accepted, - 1 - All incoming NDP requests raise NDP_INDICATION event and require esp_wifi_nan_datapath_resp to accept or reject. */ + 1 - All incoming NDP requests raise NDP_INDICATION event and require esp_wifi_nan_datapath_resp to accept or reject. + This example uses 1 to exercise nan_ndp_indication_event_handler(). */ .ndp_resp_needed = 1, .datapath_reqd = 1, +#ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED + .security_reqd = 1, + .security_cfg = { + .csid_bitmap = WIFI_NAN_CSID_BIT_NCS_SK_128, +#ifdef CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK + .use_pmk = true, +#else + .use_pmk = false, + .passphrase = CONFIG_EXAMPLE_NAN_PASSPHRASE, +#endif + }, +#endif }; +#if defined(CONFIG_EXAMPLE_NAN_SECURITY_ENABLED) && defined(CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK) + if (!decode_hex_string(CONFIG_EXAMPLE_NAN_PMK, publish_cfg.security_cfg.pmk, + sizeof(publish_cfg.security_cfg.pmk))) { + ESP_LOGE(TAG, "Failed to decode CONFIG_EXAMPLE_NAN_PMK"); + return; + } +#endif pub_id = esp_wifi_nan_publish_service(&publish_cfg); if (pub_id == 0) { diff --git a/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild b/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild index 0bba7d93381..be176366843 100644 --- a/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild +++ b/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild @@ -33,4 +33,38 @@ menu "Example Configuration" help Send a message to the Publisher using NAN Follow Up + menu "Security Configuration" + config EXAMPLE_NAN_SECURITY_ENABLED + bool "Enable NAN Security" + depends on ESP_WIFI_NAN_SECURITY + default y + + choice EXAMPLE_NAN_SECURITY_METHOD + prompt "Security Method" + depends on EXAMPLE_NAN_SECURITY_ENABLED + default EXAMPLE_NAN_SEC_METHOD_PASSPHRASE + help + Choose the method for NAN discovery security. + + config EXAMPLE_NAN_SEC_METHOD_PASSPHRASE + bool "Passphrase-based (Password)" + config EXAMPLE_NAN_SEC_METHOD_PMK + bool "Direct PMK (32-byte hex)" + endchoice + + config EXAMPLE_NAN_PASSPHRASE + string "Passphrase" + depends on EXAMPLE_NAN_SEC_METHOD_PASSPHRASE + default "password" + help + Passphrase for NAN Discovery security. Must match the publisher. + + config EXAMPLE_NAN_PMK + string "Direct PMK (Hex)" + depends on EXAMPLE_NAN_SEC_METHOD_PMK + default "ee3585063056d164d15454ad39010d4e2640b0d82fb24a2d6899862d273c68bf" + help + 32-byte hex string representing the PMK. Must match the publisher. + endmenu + endmenu 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 2e72d1bcbde..e8caea7eb61 100644 --- a/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c +++ b/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c @@ -37,6 +37,29 @@ static const char *TAG = "subscriber"; +#ifdef CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK +/* Decode lowercase hex string into a fixed-size byte buffer. Returns true on + * success. Mirrors the helper in the publisher example so both endpoints + * derive the same PMK from the same hex input. */ +static bool decode_hex_string(const char *hex, uint8_t *out, size_t out_len) +{ + if (!hex || !out) { + return false; + } + if (strlen(hex) != out_len * 2) { + return false; + } + for (size_t i = 0; i < out_len; i++) { + unsigned int byte; + if (sscanf(hex + 2 * i, "%2x", &byte) != 1) { + return false; + } + out[i] = (uint8_t)byte; + } + return true; +} +#endif + static EventGroupHandle_t nan_event_group; const int NAN_SERVICE_MATCH = BIT0; @@ -45,7 +68,7 @@ const int NDP_FAILED = BIT2; static wifi_event_nan_svc_match_t g_svc_match_evt; -static uint8_t s_ipv6_identifier[8] = {0}; +static uint8_t s_ipv6_identifier[NAN_IPV6_ADDR_ID_LEN] = {0}; static void nan_receive_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) @@ -60,6 +83,23 @@ static void nan_receive_event_handler(void *arg, esp_event_base_t event_base, #ifdef CONFIG_EXAMPLE_NAN_SEND_PING static uint8_t g_peer_ndi[ETH_ALEN]; +static void nan_ndp_confirmed_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) +{ + wifi_event_ndp_confirm_t *evt = (wifi_event_ndp_confirm_t *)event_data; + + if (evt->status == NDP_STATUS_REJECTED) { + ESP_LOGE(TAG, "NDP request to Peer "MACSTR" rejected [NDP ID - %d]", MAC2STR(evt->peer_nmi), evt->ndp_id); + xEventGroupSetBits(nan_event_group, NDP_FAILED); + } else { + memcpy(g_peer_ndi, evt->peer_ndi, sizeof(g_peer_ndi)); + memcpy(s_ipv6_identifier, evt->ipv6_identifier, sizeof(evt->ipv6_identifier)); + xEventGroupSetBits(nan_event_group, NDP_CONFIRMED); + } +} +#endif + +#ifdef CONFIG_EXAMPLE_NAN_SEND_PING static void cmd_ping_on_ping_success(esp_ping_handle_t hdl, void *args) { uint8_t ttl; @@ -103,21 +143,6 @@ static void cmd_ping_on_ping_end(esp_ping_handle_t hdl, void *args) esp_ping_delete_session(hdl); } -static void nan_ndp_confirmed_event_handler(void *arg, esp_event_base_t event_base, - int32_t event_id, void *event_data) -{ - wifi_event_ndp_confirm_t *evt = (wifi_event_ndp_confirm_t *)event_data; - - if (evt->status == NDP_STATUS_REJECTED) { - ESP_LOGE(TAG, "NDP request to Peer "MACSTR" rejected [NDP ID - %d]", MAC2STR(evt->peer_nmi), evt->ndp_id); - xEventGroupSetBits(nan_event_group, NDP_FAILED); - } else { - memcpy(g_peer_ndi, evt->peer_ndi, sizeof(g_peer_ndi)); - memcpy(s_ipv6_identifier, evt->ipv6_identifier, sizeof(evt->ipv6_identifier)); - xEventGroupSetBits(nan_event_group, NDP_CONFIRMED); - } -} - static void ping_nan_peer(esp_netif_t *netif) { esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG(); @@ -196,7 +221,26 @@ void wifi_nan_subscribe(void) #endif .matching_filter = EXAMPLE_NAN_MATCHING_FILTER, .single_match_event = 1, +#ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED + .security_reqd = 1, + .security_cfg = { + .csid_bitmap = WIFI_NAN_CSID_BIT_NCS_SK_128, +#ifdef CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK + .use_pmk = true, +#else + .use_pmk = false, + .passphrase = CONFIG_EXAMPLE_NAN_PASSPHRASE, +#endif + }, +#endif }; +#if defined(CONFIG_EXAMPLE_NAN_SECURITY_ENABLED) && defined(CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK) + if (!decode_hex_string(CONFIG_EXAMPLE_NAN_PMK, subscribe_cfg.security_cfg.pmk, + sizeof(subscribe_cfg.security_cfg.pmk))) { + ESP_LOGE(TAG, "Failed to decode CONFIG_EXAMPLE_NAN_PMK"); + return; + } +#endif sub_id = esp_wifi_nan_subscribe_service(&subscribe_cfg); if (sub_id == 0) { @@ -231,7 +275,8 @@ void wifi_nan_subscribe(void) memcpy(ndp_req.peer_mac, g_svc_match_evt.pub_if_mac, sizeof(ndp_req.peer_mac)); esp_wifi_nan_datapath_req(&ndp_req); - EventBits_t bits_2 = xEventGroupWaitBits(nan_event_group, NDP_CONFIRMED, pdFALSE, pdFALSE, portMAX_DELAY); + EventBits_t bits_2 = xEventGroupWaitBits(nan_event_group, NDP_CONFIRMED | NDP_FAILED, + pdFALSE, pdFALSE, portMAX_DELAY); if (bits_2 & NDP_CONFIRMED) { vTaskDelay(5000 / portTICK_PERIOD_MS); ping_nan_peer(nan_netif); From 2da41618f5a6aa1c77d4673a06bdf67d73fd873e Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Mon, 11 May 2026 16:18:38 +0530 Subject: [PATCH 4/8] test(wifi): NAN PMK derivation crypto test Unit-test the ND-PMK derivation path against known-good vectors using the wpa_supplicant-ported pbkdf2_sha256. --- .../test_apps/nan_crypto_test/CMakeLists.txt | 6 + .../test_apps/nan_crypto_test/README.md | 32 ++++ .../nan_crypto_test/main/CMakeLists.txt | 6 + .../nan_crypto_test/main/Kconfig.projbuild | 4 + .../nan_crypto_test/main/nan_pmk_test.c | 175 ++++++++++++++++++ .../nan_crypto_test/main/nan_pmk_test.h | 29 +++ .../nan_crypto_test/main/test_main.c | 47 +++++ .../nan_crypto_test/sdkconfig.defaults | 1 + 8 files changed, 300 insertions(+) create mode 100644 components/esp_wifi/test_apps/nan_crypto_test/CMakeLists.txt create mode 100644 components/esp_wifi/test_apps/nan_crypto_test/README.md create mode 100644 components/esp_wifi/test_apps/nan_crypto_test/main/CMakeLists.txt create mode 100644 components/esp_wifi/test_apps/nan_crypto_test/main/Kconfig.projbuild create mode 100644 components/esp_wifi/test_apps/nan_crypto_test/main/nan_pmk_test.c create mode 100644 components/esp_wifi/test_apps/nan_crypto_test/main/nan_pmk_test.h create mode 100644 components/esp_wifi/test_apps/nan_crypto_test/main/test_main.c create mode 100644 components/esp_wifi/test_apps/nan_crypto_test/sdkconfig.defaults diff --git a/components/esp_wifi/test_apps/nan_crypto_test/CMakeLists.txt b/components/esp_wifi/test_apps/nan_crypto_test/CMakeLists.txt new file mode 100644 index 00000000000..237a6ee6f9b --- /dev/null +++ b/components/esp_wifi/test_apps/nan_crypto_test/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) + +project(nan_crypto_test) diff --git a/components/esp_wifi/test_apps/nan_crypto_test/README.md b/components/esp_wifi/test_apps/nan_crypto_test/README.md new file mode 100644 index 00000000000..2555219da26 --- /dev/null +++ b/components/esp_wifi/test_apps/nan_crypto_test/README.md @@ -0,0 +1,32 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-S2 | ESP32-S3 | ESP32-S31 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | --------- | + +# NAN Crypto Test Application + +This test application validates the NAN ND-PMK derivation implementation against +official Wi-Fi Aware Specification v4.0 test vectors (Appendix M.1). + +## Building + +```bash +cd components/esp_wifi/test_apps/nan_crypto_test +idf.py build +``` + +## Running + +```bash +idf.py -p PORT flash monitor +``` + +## Test Vectors + +The application tests three official test vectors: +1. Passphrase: "NAN" +2. Passphrase: "NAN2" +3. Passphrase: "NAN-Testvector-Phrase" + +All use: +- Cipher ID: 0x01 (NCS-SK-128) +- Service ID: 2b9c450f6671 +- NMI: 02904c12d001 diff --git a/components/esp_wifi/test_apps/nan_crypto_test/main/CMakeLists.txt b/components/esp_wifi/test_apps/nan_crypto_test/main/CMakeLists.txt new file mode 100644 index 00000000000..a782b448b5d --- /dev/null +++ b/components/esp_wifi/test_apps/nan_crypto_test/main/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register(SRCS "nan_pmk_test.c" + "test_main.c" + INCLUDE_DIRS "." + REQUIRES unity esp_wifi nvs_flash esp_netif esp_event + PRIV_REQUIRES wpa_supplicant + PRIV_INCLUDE_DIRS ../../../../wpa_supplicant/src/) diff --git a/components/esp_wifi/test_apps/nan_crypto_test/main/Kconfig.projbuild b/components/esp_wifi/test_apps/nan_crypto_test/main/Kconfig.projbuild new file mode 100644 index 00000000000..dd26fda3451 --- /dev/null +++ b/components/esp_wifi/test_apps/nan_crypto_test/main/Kconfig.projbuild @@ -0,0 +1,4 @@ +# +# Automatically generated file; DO NOT EDIT. +# NAN Crypto Test Configuration +# diff --git a/components/esp_wifi/test_apps/nan_crypto_test/main/nan_pmk_test.c b/components/esp_wifi/test_apps/nan_crypto_test/main/nan_pmk_test.c new file mode 100644 index 00000000000..4af28f6d8ed --- /dev/null +++ b/components/esp_wifi/test_apps/nan_crypto_test/main/nan_pmk_test.c @@ -0,0 +1,175 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* + * NAN ND-PMK Derivation Test Vectors + * + * Test vectors from Wi-Fi Aware Specification v4.0 + * Appendix M.1 - Example test vectors for pass-phrase to ND-PMK conversion + */ + +#include +#include +#include "esp_log.h" +#include "esp_wifi.h" +#include "esp_wifi_types.h" +#include "utils/common.h" /* u8 typedef used by sha256.h */ +#include "crypto/sha256.h" /* mbedTLS-backed pbkdf2_sha256 from wpa_supplicant */ + +static const char *TAG = "nan_pmk_test"; + +typedef struct { + const char *passphrase; + uint8_t cipher_id; + uint8_t service_id[6]; + uint8_t nmi[6]; + uint8_t expected_pmk[32]; + bool use_pmk; + uint8_t input_pmk[32]; + const char *description; +} test_vector_t; + +/* Test Vector 1 */ +static const test_vector_t test_vector_1 = { + .passphrase = "NAN", + .cipher_id = 0x01, + .service_id = {0x2b, 0x9c, 0x45, 0x0f, 0x66, 0x71}, + .nmi = {0x02, 0x90, 0x4c, 0x12, 0xd0, 0x01}, + .expected_pmk = { + 0xee, 0x35, 0x85, 0x06, 0x30, 0x56, 0xd1, 0x64, + 0xd1, 0x54, 0x54, 0xad, 0x39, 0x01, 0x0d, 0x4e, + 0x26, 0x40, 0xb0, 0xd8, 0x2f, 0xb2, 0x4a, 0x2d, + 0x68, 0x99, 0x86, 0x2d, 0x27, 0x3c, 0x68, 0xbf + }, + .description = "Test Vector 1: Passphrase 'NAN'" +}; + +/* Test Vector 2 */ +static const test_vector_t test_vector_2 = { + .passphrase = "NAN2", + .cipher_id = 0x01, + .service_id = {0x2b, 0x9c, 0x45, 0x0f, 0x66, 0x71}, + .nmi = {0x02, 0x90, 0x4c, 0x12, 0xd0, 0x01}, + .expected_pmk = { + 0x87, 0x53, 0x4f, 0xa7, 0x74, 0xb1, 0x73, 0x2d, + 0xb0, 0x42, 0x66, 0xc4, 0x2c, 0x5d, 0x08, 0xd0, + 0x9e, 0x58, 0x63, 0xd1, 0xda, 0x11, 0xce, 0x25, + 0x76, 0xa8, 0xf1, 0x55, 0xfe, 0x26, 0xcd, 0x2a + }, + .description = "Test Vector 2: Passphrase 'NAN2'" +}; + +/* Test Vector 3 */ +static const test_vector_t test_vector_3 = { + .passphrase = "NAN-Testvector-Phrase", + .cipher_id = 0x01, + .service_id = {0x2b, 0x9c, 0x45, 0x0f, 0x66, 0x71}, + .nmi = {0x02, 0x90, 0x4c, 0x12, 0xd0, 0x01}, + .expected_pmk = { + 0x4d, 0xc8, 0x6c, 0xcd, 0xa8, 0x04, 0xf4, 0xe2, + 0xe1, 0x39, 0xfc, 0xa5, 0xdd, 0xd2, 0x1b, 0xa5, + 0xc0, 0xb1, 0xb6, 0xed, 0x31, 0xff, 0xd7, 0x00, + 0x5e, 0x2d, 0x56, 0xf1, 0xe7, 0xbf, 0x51, 0x87 + }, + .description = "Test Vector 3: Passphrase 'NAN-Testvector-Phrase'" +}; + +/* Test Vector 4: Direct PMK */ +static const test_vector_t test_vector_4 = { + .use_pmk = true, + .input_pmk = { + 0xee, 0x35, 0x85, 0x06, 0x30, 0x56, 0xd1, 0x64, + 0xd1, 0x54, 0x54, 0xad, 0x39, 0x01, 0x0d, 0x4e, + 0x26, 0x40, 0xb0, 0xd8, 0x2f, 0xb2, 0x4a, 0x2d, + 0x68, 0x99, 0x86, 0x2d, 0x27, 0x3c, 0x68, 0xbf + }, + .expected_pmk = { + 0xee, 0x35, 0x85, 0x06, 0x30, 0x56, 0xd1, 0x64, + 0xd1, 0x54, 0x54, 0xad, 0x39, 0x01, 0x0d, 0x4e, + 0x26, 0x40, 0xb0, 0xd8, 0x2f, 0xb2, 0x4a, 0x2d, + 0x68, 0x99, 0x86, 0x2d, 0x27, 0x3c, 0x68, 0xbf + }, + .description = "Test Vector 4: Direct PMK (using PMK from Vector 1)" +}; + +static void print_hex(const char *label, const uint8_t *data, size_t len) +{ + printf("%s: ", label); + for (size_t i = 0; i < len; i++) { + printf("%02x", data[i]); + } + printf("\n"); +} + +static bool run_test_vector(const test_vector_t *tv) +{ + uint8_t salt[14]; + uint8_t pmk[32]; + + ESP_LOGI(TAG, "\n=== %s ===", tv->description); + + if (tv->use_pmk) { + ESP_LOGI(TAG, "(Direct PMK Mode)"); + memcpy(pmk, tv->input_pmk, 32); + } else { + /* Construct Salt = Salt_Version || Cipher_Suite_ID || Service_ID || Publisher_NMI */ + salt[0] = 0x00; /* Salt Version */ + salt[1] = tv->cipher_id; + memcpy(&salt[2], tv->service_id, 6); + memcpy(&salt[8], tv->nmi, 6); + + print_hex("Passphrase", (const uint8_t *)tv->passphrase, strlen(tv->passphrase)); + print_hex("Salt", salt, sizeof(salt)); + + /* Derive PMK using PBKDF2-SHA256 */ + if (pbkdf2_sha256(tv->passphrase, salt, sizeof(salt), 4096, pmk, 32) != 0) { + ESP_LOGE(TAG, "PBKDF2-SHA256 derivation failed!"); + return false; + } + } + + print_hex("Derived PMK ", pmk, 32); + print_hex("Expected PMK ", tv->expected_pmk, 32); + + /* Compare with expected PMK */ + if (memcmp(pmk, tv->expected_pmk, 32) == 0) { + ESP_LOGI(TAG, "✓ TEST PASSED"); + return true; + } else { + ESP_LOGE(TAG, "✗ TEST FAILED - PMK mismatch!"); + return false; + } +} + +void nan_pmk_run_test_vectors(void) +{ + int passed = 0; + int total = 4; + + ESP_LOGI(TAG, "\n"); + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, " NAN ND-PMK Derivation Test Vectors"); + ESP_LOGI(TAG, " Wi-Fi Aware Spec v4.0 - Appendix M.1"); + ESP_LOGI(TAG, "========================================"); + + if (run_test_vector(&test_vector_1)) { + passed++; + } + if (run_test_vector(&test_vector_2)) { + passed++; + } + if (run_test_vector(&test_vector_3)) { + passed++; + } + if (run_test_vector(&test_vector_4)) { + passed++; + } + + ESP_LOGI(TAG, "\n"); + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, " Test Results: %d/%d PASSED", passed, total); + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, "\n"); +} diff --git a/components/esp_wifi/test_apps/nan_crypto_test/main/nan_pmk_test.h b/components/esp_wifi/test_apps/nan_crypto_test/main/nan_pmk_test.h new file mode 100644 index 00000000000..9313fcc8c9e --- /dev/null +++ b/components/esp_wifi/test_apps/nan_crypto_test/main/nan_pmk_test.h @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* + * NAN ND-PMK Test Vector Header + */ + +#ifndef NAN_PMK_TEST_H +#define NAN_PMK_TEST_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Run NAN ND-PMK derivation test vectors + * + * Tests the PBKDF2 derivation against official Wi-Fi Aware Specification + * test vectors (Appendix M.1) + */ +void nan_pmk_run_test_vectors(void); + +#ifdef __cplusplus +} +#endif + +#endif /* NAN_PMK_TEST_H */ diff --git a/components/esp_wifi/test_apps/nan_crypto_test/main/test_main.c b/components/esp_wifi/test_apps/nan_crypto_test/main/test_main.c new file mode 100644 index 00000000000..f70701752c9 --- /dev/null +++ b/components/esp_wifi/test_apps/nan_crypto_test/main/test_main.c @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* + * NAN Crypto Test Main + * + * Test application for NAN ND-PMK derivation + */ + +#include +#include "unity.h" +#include "esp_log.h" +#include "esp_wifi.h" +#include "nvs_flash.h" +#include "nan_pmk_test.h" + +static const char *TAG = "nan_test"; + +void app_main(void) +{ + /* Initialize NVS */ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + /* Initialize Wi-Fi (needed for crypto funcs) */ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, " NAN ND-PMK Derivation Test"); + ESP_LOGI(TAG, "========================================\n"); + + /* Run test vectors */ + nan_pmk_run_test_vectors(); + + ESP_LOGI(TAG, "\n========================================"); + ESP_LOGI(TAG, " All tests completed"); + ESP_LOGI(TAG, "========================================"); +} diff --git a/components/esp_wifi/test_apps/nan_crypto_test/sdkconfig.defaults b/components/esp_wifi/test_apps/nan_crypto_test/sdkconfig.defaults new file mode 100644 index 00000000000..3d8f4d46579 --- /dev/null +++ b/components/esp_wifi/test_apps/nan_crypto_test/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_ESP_WIFI_NAN_SYNC_ENABLE=y From cfecf60986b7113248971d97a27425b3e211ab16 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Thu, 14 May 2026 02:18:10 +0530 Subject: [PATCH 5/8] feat(wifi): NAN multi-credential discovery security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace scalar passphrase/PMK in wifi_nan_discovery_security_params_t with wifi_nan_credential_t[N] array (cap 4) per Wi-Fi Aware v4.0 §9.5.21.4. Drop public PMKID array and service-level csid_bitmap. Rename wifi_nan_datapath_security_params_t -> wifi_nan_security_params_t; add wifi_nan_peer_sdf_security_t for multi-PMKID peer-RX. The derive_security_params callback takes (svc_name, sec_cfg, out_derived[]) and runs once per credential on the WiFi task. Split nan_record_own_svc into claim/finalize so the WiFi-task callback finds the pending slot by name and mirrors derived material — no main-task PBKDF2. --- .../esp_wifi/include/esp_private/wifi.h | 55 ++- .../esp_wifi/include/esp_wifi_types_generic.h | 42 +- .../include/injected/esp_wifi_types_generic.h | 46 +- .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 151 +++++-- .../esp_wifi/wifi_apps/nan_app/src/nan_i.h | 41 +- .../wifi_apps/nan_app/src/nan_security.c | 417 +++++++++++------- .../nan_publisher/main/publisher_main.c | 17 +- .../nan_subscriber/main/subscriber_main.c | 17 +- 8 files changed, 528 insertions(+), 258 deletions(-) diff --git a/components/esp_wifi/include/esp_private/wifi.h b/components/esp_wifi/include/esp_private/wifi.h index d7c5752fc03..e957e40bc7a 100644 --- a/components/esp_wifi/include/esp_private/wifi.h +++ b/components/esp_wifi/include/esp_private/wifi.h @@ -48,19 +48,43 @@ typedef enum { } wifi_nan_security_type_t; /** - * @brief NAN Datapath security parameters (Spec 6.1.1 - Data Path Request/Response) + * @brief NAN single-PMKID security parameters * - * @note Shared between WiFi libraries and NAN app layer. + * @note Holds one derived ND-PMK + ND-PMKID per spec §7.1.3.5. Used both for + * the blob's per-service derived cache (own-side Publish SDF SCIA TX) + * and for the host's per-NDL handshake state (M1-M4). Single PMKID is + * sufficient because every spec key derivation step yields exactly one + * PMKID for a given (PMK, peer NMIs, Service ID) tuple. + * Shared between WiFi libraries and NAN app layer. */ typedef struct { wifi_nan_security_type_t type; /**< Security Type (Open/Encrypted) */ - uint16_t csid_bitmap; /**< Bitmap of Cipher Suite IDs (WIFI_NAN_CSID_BIT_*) */ - uint8_t nd_pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; /**< ND-PMK (Required for Datapath) */ + 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 reserved: 6; /**< Reserved */ -} wifi_nan_datapath_security_params_t; +} wifi_nan_security_params_t; + +/** + * @brief NAN peer security material parsed from Publish/Subscribe SDF SCIA + * + * @note Per Wi-Fi Aware v4.0 §7.1.3.5, a publisher SDF SCIA may advertise + * multiple ND-PMKIDs (one per cached PMK). The receiver stores them + * and matches against locally-derived PMKID at NDP-initiation time. + * Internal only — not part of the public API. Shared between WiFi + * libraries and NAN app layer. + */ +#define NAN_PEER_MAX_PMKIDS 2 /**< Internal cap; can grow without API impact */ +typedef struct { + uint16_t csid_bitmap; /**< Peer's advertised Cipher Suite ID bitmap */ + 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 reserved: 6; /**< Reserved */ +} wifi_nan_peer_sdf_security_t; /* NAN Peer info parsed from SDF */ struct nan_cb_peer_info { @@ -71,7 +95,7 @@ struct nan_cb_peer_info { uint8_t ssi_ver; /**< SSI version (service_match) */ uint8_t *ssi; /**< Service-specific information */ uint16_t ssi_len; /**< SSI length in bytes */ - wifi_nan_discovery_security_params_t *peer_security_params; /**< Peer's discovery security params (cipher / PMKIDs) */ + wifi_nan_peer_sdf_security_t *peer_security_params; /**< Peer's discovery security params parsed from SDF */ nan_vendor_ie_t *vendor_ie; /**< Vendor-specific IE, if any */ }; @@ -167,18 +191,23 @@ struct nan_secure_dp_funcs { uint8_t ndp_id, const uint8_t *peer_nmi); /* RX-path attribute parsers (CSIA / SCIA / key-desc). */ - void (*parse_ndp_csia)(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param); - void (*parse_ndp_scia)(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param); + void (*parse_ndp_csia)(void *frm, size_t buf_len, wifi_nan_security_params_t *param); + void (*parse_ndp_scia)(void *frm, size_t buf_len, wifi_nan_security_params_t *param); void (*parse_ndp_key_desc)(void *frm, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi); /* Publish-side security parser (called when blob processes inbound publish SDF) */ esp_err_t (*parse_publish_security)(const uint8_t *attrs, size_t attrs_len, - wifi_nan_discovery_security_params_t *security); + wifi_nan_peer_sdf_security_t *security); - /* Publish-init helper -- derives ND-PMK / ND-PMKID from the publish cfg - * passphrase and stores them on the host service record. Result is - * unused when CONFIG_ESP_WIFI_NAN_SECURITY=n. */ - esp_err_t (*derive_security_params)(wifi_nan_publish_cfg_t *cfg); + /* Derives ND-PMK + ND-PMKID for each credential in sec_cfg->creds[] and + * writes one entry per credential into out_derived[0..num_credentials). + * Caller passes service_name + the original publish/subscribe security + * cfg. out_derived must point at an array of at least + * ESP_WIFI_NAN_MAX_CREDS_PER_SVC entries. All inputs are treated as + * read-only. Returns ESP_FAIL or NULL when CONFIG_ESP_WIFI_NAN_SECURITY=n. */ + esp_err_t (*derive_security_params)(const char *service_name, + const wifi_nan_discovery_security_params_t *sec_cfg, + wifi_nan_security_params_t *out_derived); /* NDP security gate: cipher-suite bitmap for (ndp_id, peer_nmi), or 0 for open. */ uint16_t (*get_ndp_security_csid)(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 4dc5a26a5d4..a28544220f5 100644 --- a/components/esp_wifi/include/esp_wifi_types_generic.h +++ b/components/esp_wifi/include/esp_wifi_types_generic.h @@ -868,7 +868,7 @@ typedef struct { #define ESP_WIFI_NAN_NDP_PMK_LEN 32 /**< Length of NAN Datapath PMK */ #define ESP_WIFI_NAN_NDP_PMKID_LEN 16 /**< Length of NAN Datapath PMKID */ -#define ESP_WIFI_NAN_MAX_PMKIDS 2 /**< Maximum number of PMKIDs supported */ +#define ESP_WIFI_NAN_MAX_CREDS_PER_SVC 4 /**< Maximum number of NAN security credentials per service (passphrase/PMK entries) */ #define ESP_WIFI_MAX_SVC_NAME_LEN 256 /**< Maximum length of NAN service name */ #define ESP_WIFI_MAX_FILTER_LEN 256 /**< Maximum length of NAN service filter */ @@ -912,7 +912,7 @@ typedef enum { } wifi_nan_service_type_t; /** - * @brief NAN Cipher Suite IDs (Spec 4.1.1 & 6.1.1) + * @brief NAN Cipher Suite IDs (Wi-Fi Aware v4.0 §4.1.1 & §6.1.1) * * @note Only WIFI_NAN_CSID_NCS_SK_128 is currently supported by the firmware. * The other values are reserved for future support; selecting any of @@ -936,19 +936,37 @@ typedef enum { #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_256 (1 << WIFI_NAN_CSID_NCS_PK_PASN_256) /** - * @brief NAN Discovery security parameters (Spec 4.1.1 - Publish/Subscribe) + * @brief NAN security credential — one passphrase or raw PMK + the cipher it's bound to. * + * Per Wi-Fi Aware v4.0 §7.1.3.5 the PMKID derivation formula is cipher-specific + * (NCS-SK-128 uses HMAC-SHA-256; NCS-SK-256 uses HMAC-SHA-384), so each + * credential must carry the cipher it was provisioned for. */ typedef struct { - uint16_t csid_bitmap; /**< Bitmap of Supported Cipher Suite IDs (WIFI_NAN_CSID_BIT_*) */ - uint8_t num_pmkids; /**< Number of PMKIDs */ - uint8_t pmkids[ESP_WIFI_NAN_MAX_PMKIDS][ESP_WIFI_NAN_NDP_PMKID_LEN]; /**< ND-PMKIDs */ - 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 use_pmk: 1; /**< 0 - Use passphrase, 1 - Use PMK directly */ - uint8_t reserved: 5; /**< Reserved */ - char passphrase[MAX_PASSPHRASE_LEN]; /**< NCS-SK passphrase (use_pmk=0). NUL-terminated. */ - uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; /**< NCS-SK PMK (use_pmk=1). Raw bytes, not NUL-terminated. */ + uint8_t csid; /**< Cipher Suite ID this credential is for (wifi_nan_cipher_suite_id_t value) */ + uint8_t use_pmk: 1; /**< 0 - Use passphrase, 1 - Use PMK directly */ + uint8_t reserved: 7; /**< Reserved */ + char passphrase[MAX_PASSPHRASE_LEN]; /**< NCS-SK passphrase (use_pmk=0). NUL-terminated. */ + uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; /**< NCS-SK PMK (use_pmk=1). Raw bytes, not NUL-terminated. */ +} wifi_nan_credential_t; + +/** + * @brief NAN Discovery security parameters (Wi-Fi Aware v4.0 §4.1.1 - Publish/Subscribe) + * + * Per Wi-Fi Aware v4.0 §9.5.21.4 (SCIA) and §7.1.3.5 the Publish/Subscribe SDF + * may advertise multiple ND-PMKIDs (one per provisioned ND-PMK). Applications + * provide one or more credentials in @c creds; the stack derives PMK + PMKID + * per credential and emits the multi-SCID list. Subscriber-side, the library + * walks an incoming publisher's SCID list and matches against any of the + * locally-provisioned credentials. The CSIA cipher bitmap advertised on air + * 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 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. */ } wifi_nan_discovery_security_params_t; /** 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 6b0b143e397..a3ca98bd962 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 @@ -17,6 +17,8 @@ extern "C" { #endif #define WIFI_AP_DEFAULT_MAX_IDLE_PERIOD 292 /**< Default timeout for SoftAP BSS Max Idle. Unit: 1000TUs >**/ +#define MAX_SSID_LEN 32 /**< Maximum length of SSID */ +#define MAX_PASSPHRASE_LEN 64 /**< Maximum length of passphrase */ /** * @brief Wi-Fi mode type @@ -866,7 +868,7 @@ typedef struct { #define ESP_WIFI_NAN_NDP_PMK_LEN 32 /**< Length of NAN Datapath PMK */ #define ESP_WIFI_NAN_NDP_PMKID_LEN 16 /**< Length of NAN Datapath PMKID */ -#define ESP_WIFI_NAN_MAX_PMKIDS 2 /**< Maximum number of PMKIDs supported */ +#define ESP_WIFI_NAN_MAX_CREDS_PER_SVC 4 /**< Maximum number of NAN security credentials per service (passphrase/PMK entries) */ #define ESP_WIFI_MAX_SVC_NAME_LEN 256 /**< Maximum length of NAN service name */ #define ESP_WIFI_MAX_FILTER_LEN 256 /**< Maximum length of NAN service filter */ @@ -910,7 +912,7 @@ typedef enum { } wifi_nan_service_type_t; /** - * @brief NAN Cipher Suite IDs (Spec 4.1.1 & 6.1.1) + * @brief NAN Cipher Suite IDs (Wi-Fi Aware v4.0 §4.1.1 & §6.1.1) * * @note Only WIFI_NAN_CSID_NCS_SK_128 is currently supported by the firmware. * The other values are reserved for future support; selecting any of @@ -934,19 +936,37 @@ typedef enum { #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_256 (1 << WIFI_NAN_CSID_NCS_PK_PASN_256) /** - * @brief NAN Discovery security parameters (Spec 4.1.1 - Publish/Subscribe) + * @brief NAN security credential - one passphrase or raw PMK + the cipher it's bound to. * + * Per Wi-Fi Aware v4.0 §7.1.3.5 the PMKID derivation formula is cipher-specific + * (NCS-SK-128 uses HMAC-SHA-256; NCS-SK-256 uses HMAC-SHA-384), so each + * credential must carry the cipher it was provisioned for. */ typedef struct { - uint16_t csid_bitmap; /**< Bitmap of Supported Cipher Suite IDs (WIFI_NAN_CSID_BIT_*) */ - uint8_t num_pmkids; /**< Number of PMKIDs */ - uint8_t pmkids[ESP_WIFI_NAN_MAX_PMKIDS][ESP_WIFI_NAN_NDP_PMKID_LEN]; /**< ND-PMKIDs */ - 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 use_pmk: 1; /**< 0 - Use passphrase, 1 - Use PMK directly */ - uint8_t reserved: 5; /**< Reserved */ - char passphrase[MAX_PASSPHRASE_LEN]; /**< NCS-SK passphrase (use_pmk=0). NUL-terminated. */ - uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; /**< NCS-SK PMK (use_pmk=1). Raw bytes, not NUL-terminated. */ + uint8_t csid; /**< Cipher Suite ID this credential is for (wifi_nan_cipher_suite_id_t value) */ + uint8_t use_pmk: 1; /**< 0 - Use passphrase, 1 - Use PMK directly */ + uint8_t reserved: 7; /**< Reserved */ + char passphrase[MAX_PASSPHRASE_LEN]; /**< NCS-SK passphrase (use_pmk=0). NUL-terminated. */ + uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; /**< NCS-SK PMK (use_pmk=1). Raw bytes, not NUL-terminated. */ +} wifi_nan_credential_t; + +/** + * @brief NAN Discovery security parameters (Wi-Fi Aware v4.0 §4.1.1 - Publish/Subscribe) + * + * Per Wi-Fi Aware v4.0 §9.5.21.4 (SCIA) and §7.1.3.5 the Publish/Subscribe SDF + * may advertise multiple ND-PMKIDs (one per provisioned ND-PMK). Applications + * provide one or more credentials in @c creds; the stack derives PMK + PMKID + * per credential and emits the multi-SCID list. Subscriber-side, the library + * walks an incoming publisher's SCID list and matches against any of the + * locally-provisioned credentials. The CSIA cipher bitmap advertised on air + * 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 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. */ } wifi_nan_discovery_security_params_t; /** @@ -1290,8 +1310,6 @@ typedef enum { WPS_FAIL_REASON_MAX /**< Max WPS fail reason */ } wifi_event_sta_wps_fail_reason_t; -#define MAX_SSID_LEN 32 /**< Maximum length of SSID */ -#define MAX_PASSPHRASE_LEN 64 /**< Maximum length of passphrase */ #define MAX_WPS_AP_CRED 3 /**< Maximum number of AP credentials received from WPS handshake */ /** 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 67ee7db5e58..6e71672fa12 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 @@ -111,7 +111,7 @@ struct own_svc_info *nan_find_own_svc(uint8_t svc_id) return p_svc; } -static struct own_svc_info *nan_find_own_svc_by_name(const char *svc_name) +struct own_svc_info *nan_find_own_svc_by_name(const char *svc_name) { struct own_svc_info *p_svc = NULL; @@ -271,46 +271,68 @@ static bool nan_services_limit_reached(void) return true; } -static void nan_record_own_svc(uint8_t id, uint8_t type, const char svc_name[], - bool ndp_resp_needed, - const wifi_nan_discovery_security_params_t *security_cfg) +/* Pre-claim a slot for an upcoming publish/subscribe service. The slot is + * marked with a pending sentinel svc_id (0xFF) so nan_find_own_svc_by_name() + * can find it from the derive callback running on the WiFi task — that's how + * the host's per-credential derived_security[] mirror gets populated without + * a duplicate PBKDF2 pass on the app/main task (which would block IDLE0 and + * trip the watchdog). Real svc_id is stamped later by nan_finalize_own_svc(). */ +#define NAN_SVC_ID_PENDING 0xFF + +static struct own_svc_info *nan_claim_own_svc_slot(uint8_t type, const char svc_name[], + const wifi_nan_discovery_security_params_t *security_cfg) { struct own_svc_info *p_svc = NULL; - for (int i = 0; i < ESP_WIFI_NAN_MAX_SVC_SUPPORTED; i++) { if (s_nan_ctx.own_svc[i].svc_id == 0) { p_svc = &s_nan_ctx.own_svc[i]; break; } } - if (!p_svc) { - return; + return NULL; } - p_svc->svc_id = id; + p_svc->svc_id = NAN_SVC_ID_PENDING; p_svc->type = type; strlcpy(p_svc->svc_name, svc_name, ESP_WIFI_MAX_SVC_NAME_LEN); - SLIST_INIT(&(p_svc->peer_list)); - if (type == ESP_NAN_PUBLISH) { - p_svc->ndp_resp_needed = ndp_resp_needed; - } + SLIST_INIT(&p_svc->peer_list); #ifdef CONFIG_ESP_WIFI_NAN_SECURITY - /* Wipe to drop stale PMK material if this slot was previously used. */ - forced_memzero(&p_svc->security_cfg, sizeof(p_svc->security_cfg)); - forced_memzero(p_svc->pmk_cache, sizeof(p_svc->pmk_cache)); - + forced_memzero(&p_svc->user_cfg, sizeof(p_svc->user_cfg)); + forced_memzero(&p_svc->derived_security, sizeof(p_svc->derived_security)); if (security_cfg) { - memcpy(&p_svc->security_cfg, security_cfg, sizeof(wifi_nan_discovery_security_params_t)); - if (security_cfg->num_pmkids > 0) { - memcpy(p_svc->pmk_cache[0], security_cfg->pmk, ESP_WIFI_NAN_NDP_PMK_LEN); - /* Public struct has one pmk[32]; cap num_pmkids to the count we cached. */ - p_svc->security_cfg.num_pmkids = 1; - } + memcpy(&p_svc->user_cfg, security_cfg, sizeof(*security_cfg)); } #else (void)security_cfg; -#endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ +#endif + return p_svc; +} + +/* Stamp the real svc_id and per-publish flags after the blob accepts the + * service. Looked up by name since the WiFi-task derive callback may have + * already populated derived_security[] before this runs. */ +static void nan_finalize_own_svc(const char *svc_name, uint8_t id, bool ndp_resp_needed) +{ + struct own_svc_info *p_svc = nan_find_own_svc_by_name(svc_name); + if (!p_svc) { + return; + } + p_svc->svc_id = id; + if (p_svc->type == ESP_NAN_PUBLISH) { + p_svc->ndp_resp_needed = ndp_resp_needed; + } +} + +/* Release a pending slot when the blob's publish/subscribe call fails. */ +static void nan_abort_own_svc(const char *svc_name) +{ + struct own_svc_info *p_svc = nan_find_own_svc_by_name(svc_name); + if (!p_svc) { + return; + } + forced_memzero(p_svc, sizeof(*p_svc)); + /* svc_id back to 0 -> slot free */ } /* A slot is in use once nan_record_new_ndl/preclaim has stamped peer_nmi, @@ -566,9 +588,21 @@ void nan_app_service_match_cb(uint8_t sub_id, struct nan_cb_peer_info *peer_info ESP_LOGI(TAG, "Service matched with capabilities: 0x%04x", capab); #ifdef CONFIG_ESP_WIFI_NAN_SECURITY - if (peer_info->peer_security_params) { + /* Service-match security gate: + * 1. Subscriber configured no credentials (open subscribe) -> accept + * regardless of what the peer advertised. NDP will be open. + * 2. Subscriber configured credentials (secure subscribe) -> peer + * must have advertised PMKIDs AND at least one of them must match + * one of the local creds, else drop. */ + if (nan_security_subscriber_has_creds()) { + if (!peer_info->peer_security_params || + peer_info->peer_security_params->num_pmkids == 0) { + ESP_LOGW(TAG, "Secure subscribe: peer "MACSTR" advertises no PMKIDs", + MAC2STR(pub_mac)); + return; + } if (!nan_security_service_match(pub_mac, peer_info->peer_security_params)) { - ESP_LOGD(TAG, "PMKID mismatch with "MACSTR, MAC2STR(pub_mac)); + ESP_LOGW(TAG, "PMKID mismatch with "MACSTR, MAC2STR(pub_mac)); return; } } @@ -1382,10 +1416,20 @@ uint8_t esp_wifi_nan_publish_service(const wifi_nan_publish_cfg_t *publish_cfg) ESP_LOGE(TAG, "Encrypted datapath not enabled (CONFIG_ESP_WIFI_NAN_SECURITY)"); goto fail; #else - if (!(publish_cfg->security_cfg.csid_bitmap & WIFI_NAN_CSID_BIT_NCS_SK_128)) { - ESP_LOGE(TAG, "Unsupported cipher suite in csid_bitmap (only NCS-SK-128 is supported)"); + if (publish_cfg->security_cfg.num_credentials == 0 || + publish_cfg->security_cfg.num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { + ESP_LOGE(TAG, "security_cfg.num_credentials=%u must be 1..%u", + publish_cfg->security_cfg.num_credentials, + ESP_WIFI_NAN_MAX_CREDS_PER_SVC); goto fail; } + for (uint8_t i = 0; i < publish_cfg->security_cfg.num_credentials; i++) { + uint8_t csid = publish_cfg->security_cfg.creds[i].csid; + if (csid != WIFI_NAN_CSID_NCS_SK_128) { + ESP_LOGE(TAG, "creds[%u].csid=%u unsupported (only NCS-SK-128)", i, csid); + goto fail; + } + } #endif } @@ -1397,24 +1441,31 @@ uint8_t esp_wifi_nan_publish_service(const wifi_nan_publish_cfg_t *publish_cfg) } memcpy(cfg, publish_cfg, sizeof(*cfg)); - /* Security derivation (PMK, PMKID) now runs in WiFi task context — - * the WiFi library calls nan_derive_security_params(cfg) during publish processing. - * After esp_nan_internal_publish_service returns, cfg->security_cfg has - * the derived PMK and PMKID populated by the WiFi task. */ + /* Pre-claim host slot BEFORE calling into the blob. Reason: the blob + * invokes our derive_security_params callback on the WiFi task and looks + * up the host's slot by service name to mirror derived ND-PMK/ND-PMKID + * into p_svc->derived_security[]. Doing the derive on WiFi task (not the + * app/main task) keeps PBKDF2's hardware-SHA polling off IDLE0 and avoids + * tripping the task watchdog when num_credentials > 1. */ + if (!nan_claim_own_svc_slot(ESP_NAN_PUBLISH, publish_cfg->service_name, +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + &cfg->security_cfg +#else + NULL +#endif + )) { + ESP_LOGE(TAG, "No free service slot"); + goto fail; + } if (esp_nan_internal_publish_service(cfg, (uint8_t *) &pub_id, false) != ESP_OK) { ESP_LOGE(TAG, "Failed to publish service '%s'", publish_cfg->service_name); + nan_abort_own_svc(publish_cfg->service_name); goto fail; } ESP_LOGI(TAG, "Started Publishing %s [Service ID - %u]", publish_cfg->service_name, pub_id); - nan_record_own_svc(pub_id, ESP_NAN_PUBLISH, publish_cfg->service_name, publish_cfg->ndp_resp_needed, -#ifdef CONFIG_ESP_WIFI_NAN_SECURITY - &cfg->security_cfg -#else - NULL -#endif - ); + nan_finalize_own_svc(publish_cfg->service_name, pub_id, publish_cfg->ndp_resp_needed); os_free(cfg); NAN_DATA_UNLOCK(); @@ -1481,10 +1532,20 @@ uint8_t esp_wifi_nan_subscribe_service(const wifi_nan_subscribe_cfg_t *subscribe ESP_LOGE(TAG, "Encrypted datapath not enabled (CONFIG_ESP_WIFI_NAN_SECURITY)"); return 0; #else - if (!(subscribe_cfg->security_cfg.csid_bitmap & WIFI_NAN_CSID_BIT_NCS_SK_128)) { - ESP_LOGE(TAG, "Unsupported cipher suite in csid_bitmap (only NCS-SK-128 is supported)"); + if (subscribe_cfg->security_cfg.num_credentials == 0 || + subscribe_cfg->security_cfg.num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { + ESP_LOGE(TAG, "security_cfg.num_credentials=%u must be 1..%u", + subscribe_cfg->security_cfg.num_credentials, + ESP_WIFI_NAN_MAX_CREDS_PER_SVC); return 0; } + for (uint8_t i = 0; i < subscribe_cfg->security_cfg.num_credentials; i++) { + uint8_t csid = subscribe_cfg->security_cfg.creds[i].csid; + if (csid != WIFI_NAN_CSID_NCS_SK_128) { + ESP_LOGE(TAG, "creds[%u].csid=%u unsupported (only NCS-SK-128)", i, csid); + return 0; + } + } #endif } NAN_DATA_LOCK(); @@ -1502,14 +1563,22 @@ uint8_t esp_wifi_nan_subscribe_service(const wifi_nan_subscribe_cfg_t *subscribe goto fail; } + /* Pre-claim host slot BEFORE the blob's subscribe call; see comment on + * the publish path for the watchdog rationale. */ + if (!nan_claim_own_svc_slot(ESP_NAN_SUBSCRIBE, subscribe_cfg->service_name, + &subscribe_cfg->security_cfg)) { + ESP_LOGE(TAG, "No free service slot"); + goto fail; + } if (esp_nan_internal_subscribe_service(subscribe_cfg, (uint8_t*) &sub_id, false) != ESP_OK) { ESP_LOGE(TAG, "Failed to subscribe to service '%s'", subscribe_cfg->service_name); + nan_abort_own_svc(subscribe_cfg->service_name); goto fail; } ESP_LOGI(TAG, "Started Subscribing to %s [Service ID - %u]", subscribe_cfg->service_name, sub_id); - nan_record_own_svc((uint8_t) sub_id, ESP_NAN_SUBSCRIBE, subscribe_cfg->service_name, false, &subscribe_cfg->security_cfg); + nan_finalize_own_svc(subscribe_cfg->service_name, (uint8_t) sub_id, false); #ifdef CONFIG_ESP_WIFI_NAN_SECURITY nan_security_cache_subscriber_params(subscribe_cfg->service_name, &subscribe_cfg->security_cfg); #endif 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 94bf3bd7602..c89d0cb75f0 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 @@ -162,8 +162,12 @@ struct own_svc_info { bool ndp_resp_needed; #ifdef CONFIG_ESP_WIFI_NAN_SECURITY - wifi_nan_discovery_security_params_t security_cfg; - uint8_t pmk_cache[ESP_WIFI_NAN_MAX_PMKIDS][ESP_WIFI_NAN_NDP_PMK_LEN]; + /* App-provided input (creds[] / csid_bitmap / group prot flags). */ + wifi_nan_discovery_security_params_t user_cfg; + /* Derived material — one ND-PMK + ND-PMKID per credential. Mirrors the + * per-service array the blob caches in svc_entry->self_security_params[]. + * Valid entries: [0, user_cfg.num_credentials). */ + wifi_nan_security_params_t derived_security[ESP_WIFI_NAN_MAX_CREDS_PER_SVC]; #endif uint8_t num_peer_records; SLIST_HEAD(peer_list_t, peer_svc_info) peer_list; @@ -178,7 +182,7 @@ struct ndl_info { uint8_t own_role; uint32_t device_caps; #ifdef CONFIG_ESP_WIFI_NAN_SECURITY - wifi_nan_datapath_security_params_t security_ctx; + wifi_nan_security_params_t security_ctx; uint8_t anonce[NAN_NONCE_LEN]; uint8_t snonce[NAN_NONCE_LEN]; @@ -233,6 +237,7 @@ extern nan_ctx_t s_nan_ctx; /* Helpers defined in nan_app.c, used by nan_security.c */ struct own_svc_info *nan_find_own_svc(uint8_t svc_id); +struct own_svc_info *nan_find_own_svc_by_name(const char *svc_name); struct ndl_info *nan_find_ndl(uint8_t ndp_id, uint8_t peer_nmi[]); struct ndl_info *nan_find_ndl_by_pub_id_and_peer(uint8_t pub_id, const uint8_t *peer_nmi); @@ -291,12 +296,12 @@ int esp_nan_verify_ndp_security_install_mic(uint8_t *m4_body, size_t body_len, uint8_t *key_desc_attr, uint8_t ndp_id, const uint8_t *peer_nmi); -void esp_nan_parse_ndp_csia(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param); -void esp_nan_parse_ndp_scia(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param); +void esp_nan_parse_ndp_csia(void *frm, size_t buf_len, wifi_nan_security_params_t *param); +void esp_nan_parse_ndp_scia(void *frm, size_t buf_len, wifi_nan_security_params_t *param); void esp_nan_parse_ndp_key_desc(void *frm, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi); esp_err_t esp_nan_parse_publish_security(const uint8_t *attrs, size_t attrs_len, - wifi_nan_discovery_security_params_t *security); + wifi_nan_peer_sdf_security_t *security); /* Helpers defined in nan_security.c, used by nan_app.c */ @@ -311,8 +316,12 @@ void nan_security_apply_pending(struct ndl_info *ndl, const uint8_t *peer_nmi, const uint8_t *peer_ndi); -/* PMK / PMKID derivation entry point used by the publish path. */ -esp_err_t nan_derive_security_params(wifi_nan_publish_cfg_t *cfg); +/* PMK / PMKID derivation entry point used by the publish path. Derives from + * (service_name, sec_cfg) (passphrase or PMK) and writes the result into + * out_derived. */ +esp_err_t nan_derive_security_params(const char *service_name, + const wifi_nan_discovery_security_params_t *sec_cfg, + wifi_nan_security_params_t *out_derived); /* Blob-side gate query: returns ndl->security_ctx.csid_bitmap for the NDP * keyed on (ndp_id, peer_nmi), or 0 if no NDL match. ndp_id=0 is valid for @@ -336,13 +345,23 @@ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, * from the service-match callback. */ bool nan_security_service_match(const uint8_t *publisher_nmi, - const wifi_nan_discovery_security_params_t *peer_sec); + const wifi_nan_peer_sdf_security_t *peer_sec); + +/* Returns true if the local subscriber configured at least one credential + * via the most recent esp_wifi_nan_subscribe_service(). Used by the + * service-match gate to decide whether to PMKID-validate an incoming peer. */ +bool nan_security_subscriber_has_creds(void); #else -static inline esp_err_t nan_derive_security_params(wifi_nan_publish_cfg_t *cfg) +static inline esp_err_t nan_derive_security_params(const char *service_name, + const wifi_nan_discovery_security_params_t *sec_cfg, + wifi_nan_security_params_t *out_derived) { - (void)cfg; + (void)service_name; + (void)sec_cfg; + (void)out_derived; return ESP_FAIL; } +static inline bool nan_security_subscriber_has_creds(void) { return false; } #endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ #ifdef __cplusplus 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 a0f1c4504c0..654b3c19aa2 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 @@ -70,6 +70,71 @@ static struct { wifi_nan_discovery_security_params_t security_cfg; } s_nan_subscriber_sec_cache; +/* Per-peer subscriber match memory: at service-match time + * nan_security_service_match identifies which local credential matched the + * publisher; we remember (publisher NMI -> matched cred index) so that the + * later NDP-init path (nan_security_populate_initiator_ndl) derives M1's pair + * PMKID from the *correct* credential instead of always cred[0]. Sized at + * NAN_MAX_PEERS_RECORD to cover the realistic peer count; round-robin + * eviction on overflow. */ +#define NAN_SUB_MATCH_CACHE_SIZE NAN_MAX_PEERS_RECORD +static struct { + bool used; + uint8_t peer_nmi[ETH_ALEN]; + uint8_t matched_cred_idx; +} s_nan_subscriber_match_cache[NAN_SUB_MATCH_CACHE_SIZE]; +static uint8_t s_nan_subscriber_match_cursor; + +static void nan_subscriber_match_remember(const uint8_t *peer_nmi, uint8_t cred_idx) +{ + if (!peer_nmi) { + return; + } + /* Update existing entry for this peer if present. */ + for (uint8_t i = 0; i < NAN_SUB_MATCH_CACHE_SIZE; i++) { + if (s_nan_subscriber_match_cache[i].used && + memcmp(s_nan_subscriber_match_cache[i].peer_nmi, peer_nmi, ETH_ALEN) == 0) { + s_nan_subscriber_match_cache[i].matched_cred_idx = cred_idx; + return; + } + } + /* First free slot. */ + for (uint8_t i = 0; i < NAN_SUB_MATCH_CACHE_SIZE; i++) { + if (!s_nan_subscriber_match_cache[i].used) { + s_nan_subscriber_match_cache[i].used = true; + memcpy(s_nan_subscriber_match_cache[i].peer_nmi, peer_nmi, ETH_ALEN); + s_nan_subscriber_match_cache[i].matched_cred_idx = cred_idx; + return; + } + } + /* Full: round-robin evict. */ + uint8_t idx = s_nan_subscriber_match_cursor; + s_nan_subscriber_match_cursor = (uint8_t)((s_nan_subscriber_match_cursor + 1) % NAN_SUB_MATCH_CACHE_SIZE); + s_nan_subscriber_match_cache[idx].used = true; + memcpy(s_nan_subscriber_match_cache[idx].peer_nmi, peer_nmi, ETH_ALEN); + s_nan_subscriber_match_cache[idx].matched_cred_idx = cred_idx; +} + +static int nan_subscriber_match_recall(const uint8_t *peer_nmi) +{ + if (!peer_nmi) { + return -1; + } + for (uint8_t i = 0; i < NAN_SUB_MATCH_CACHE_SIZE; i++) { + if (s_nan_subscriber_match_cache[i].used && + memcmp(s_nan_subscriber_match_cache[i].peer_nmi, peer_nmi, ETH_ALEN) == 0) { + return (int)s_nan_subscriber_match_cache[i].matched_cred_idx; + } + } + return -1; +} + +static void nan_subscriber_match_clear(void) +{ + memset(s_nan_subscriber_match_cache, 0, sizeof(s_nan_subscriber_match_cache)); + s_nan_subscriber_match_cursor = 0; +} + /*------------------------------------------------------------------------- * Static helpers *-----------------------------------------------------------------------*/ @@ -98,22 +163,6 @@ static const uint8_t *nan_find_attr(const uint8_t *attrs, size_t attrs_len, return NULL; } -/* - * Pick the highest-priority cipher suite the local device implements from - * the negotiated CSIA bitmap. Only NCS-SK-128 is wired through the rest of - * the M1–M4 path today (PTK length, MIC length, KCK/KEK lengths in - * nan_i.h are 128-bit-specific); 256-bit and PK suites are intentionally - * not selected here — return 0 to signal "no supported suite" so callers - * fail negotiation rather than dispatching to a half-implemented path. - */ -static uint8_t nan_get_first_csid(uint16_t csid_bitmap) -{ - if (csid_bitmap & WIFI_NAN_CSID_BIT_NCS_SK_128) { - return WIFI_NAN_CSID_NCS_SK_128; - } - return 0; -} - /* * Service ID = first 6 bytes of SHA256(lowercase(service_name)) * per Wi-Fi Aware v4.0 §5.1.5 (Service Name and Service ID). @@ -211,25 +260,22 @@ static bool nan_derive_ndp_request_pmkid(const uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_ } /* - * Match peer's ND-PMKID (from NDP Request/SCIA) against our PMK cache. + * Match peer's ND-PMKID (from NDP Request/SCIA) against our per-credential + * PMK cache. Returns the index in p_svc->derived_security[] that matched, or + * -1 if no credential matches. * * Rule: Do NOT compare NDP Request PMKID to Publish-PMKID — they differ by design. * - Publish SCIA PMKID uses IAddr = FF:FF:FF:FF:FF:FF (initiator-independent). * - NDP Request SCIA PMKID uses Initiator NMI and Responder NMI (pair-specific). */ -static bool nan_match_pmkid(struct own_svc_info *p_svc, - const uint8_t peer_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN], - uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN], - const uint8_t *peer_nmi, - const uint8_t *peer_ndi) +static int nan_match_pmkid(struct own_svc_info *p_svc, + const uint8_t peer_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN], + const uint8_t *peer_nmi, + const uint8_t *peer_ndi) { (void)peer_ndi; - if (!p_svc || !peer_pmkid) { - return false; - } - - if (!peer_nmi || !p_svc->svc_name[0]) { + if (!p_svc || !peer_pmkid || !peer_nmi || !p_svc->svc_name[0]) { goto no_match; } @@ -248,40 +294,30 @@ static bool nan_match_pmkid(struct own_svc_info *p_svc, ESP_LOGD(TAG, " Initiator NMI (peer): "MACSTR, MAC2STR(peer_nmi)); ESP_LOGD(TAG, " Responder NMI (us): "MACSTR, MAC2STR(our_nmi)); - /* Cap loop at array bound; guards against unsanitized num_pmkids. */ - uint8_t cached = p_svc->security_cfg.num_pmkids; - if (cached > ESP_WIFI_NAN_MAX_PMKIDS) { - cached = ESP_WIFI_NAN_MAX_PMKIDS; - } - for (int i = 0; i < cached; i++) { + for (uint8_t i = 0; i < p_svc->user_cfg.num_credentials; i++) { uint8_t expected_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN]; + const uint8_t *own_pmk = p_svc->derived_security[i].nd_pmk; /* Spec order: Initiator NMI, Responder NMI */ - if (nan_derive_ndp_request_pmkid(p_svc->pmk_cache[i], peer_nmi, our_nmi, - service_id, expected_pmkid)) { - if (memcmp(expected_pmkid, peer_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) { - if (pmk) { - memcpy(pmk, p_svc->pmk_cache[i], ESP_WIFI_NAN_NDP_PMK_LEN); - } - return true; - } + if (nan_derive_ndp_request_pmkid(own_pmk, peer_nmi, our_nmi, + service_id, expected_pmkid) && + memcmp(expected_pmkid, peer_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) { + ESP_LOGD(TAG, "PMKID matched at credential slot %u", i); + return (int)i; } /* Fallback: try reversed order in case peer implementation differs */ - if (nan_derive_ndp_request_pmkid(p_svc->pmk_cache[i], our_nmi, peer_nmi, - service_id, expected_pmkid)) { - if (memcmp(expected_pmkid, peer_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) { - if (pmk) { - memcpy(pmk, p_svc->pmk_cache[i], ESP_WIFI_NAN_NDP_PMK_LEN); - } - return true; - } + if (nan_derive_ndp_request_pmkid(own_pmk, our_nmi, peer_nmi, + service_id, expected_pmkid) && + memcmp(expected_pmkid, peer_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) { + ESP_LOGD(TAG, "PMKID matched at credential slot %u (reversed)", i); + return (int)i; } } no_match: ESP_LOGW(TAG, "PMKID not found in our cache (no match)"); - return false; + return -1; } /* @@ -693,69 +729,101 @@ static uint8_t nan_keyinfo_to_msg_type(uint16_t key_info) * Public API: PMK/PMKID derivation for publish *-----------------------------------------------------------------------*/ -esp_err_t nan_derive_security_params(wifi_nan_publish_cfg_t *cfg) +esp_err_t nan_derive_security_params(const char *service_name, + const wifi_nan_discovery_security_params_t *sec_cfg, + wifi_nan_security_params_t *out_derived) { - uint8_t pmk[32]; + uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; uint8_t service_id[6]; - uint8_t pmkid[16]; uint8_t hash[32]; - esp_err_t ret = ESP_FAIL; - /* 1. Service ID = SHA256(lowercase service name)[:6] */ - if (!nan_compute_service_id(cfg->service_name, service_id)) { - ESP_LOGE(TAG, "Service ID derivation failed"); - goto cleanup; + if (!service_name || !sec_cfg || !out_derived) { + return ESP_ERR_INVALID_ARG; + } + if (sec_cfg->num_credentials == 0 || + sec_cfg->num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { + ESP_LOGE(TAG, "Invalid num_credentials=%u (cap %u)", + sec_cfg->num_credentials, ESP_WIFI_NAN_MAX_CREDS_PER_SVC); + return ESP_ERR_INVALID_ARG; } - ESP_LOGD(TAG, "Security derivation: svc='%s'", cfg->service_name); + memset(out_derived, 0, + sizeof(*out_derived) * ESP_WIFI_NAN_MAX_CREDS_PER_SVC); + + if (!nan_compute_service_id(service_name, service_id)) { + ESP_LOGE(TAG, "Service ID derivation failed"); + return ESP_FAIL; + } + if (!g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector) { + ESP_LOGE(TAG, "hmac_sha256_vector not registered"); + return ESP_FAIL; + } + + ESP_LOGD(TAG, "Security derivation: svc='%s' num_creds=%u", + service_name, sec_cfg->num_credentials); ESP_LOG_BUFFER_HEXDUMP(TAG, service_id, 6, ESP_LOG_DEBUG); - /* 2. PMK from passphrase (or directly provided) */ - if (cfg->security_cfg.use_pmk) { - memcpy(pmk, cfg->security_cfg.pmk, 32); - } else { - uint8_t publisher_nmi[ETH_ALEN]; - uint8_t csid = nan_get_first_csid(cfg->security_cfg.csid_bitmap); - - if (csid == 0) { - ESP_LOGE(TAG, "No cipher suite in bitmap"); - goto cleanup; - } - esp_wifi_get_mac(WIFI_IF_NAN, publisher_nmi); - if (nan_derive_nd_pmk_from_passphrase(cfg->security_cfg.passphrase, csid, - service_id, publisher_nmi, pmk) != 0) { - ESP_LOGE(TAG, "PBKDF2 ND-PMK derivation failed"); - goto cleanup; - } - } - - /* 3. Publish PMKID uses IAddr = ff:ff:ff:ff:ff:ff (initiator-independent). - * Pair-specific PMKID for NDP Request is derived separately. */ + /* Publish-side PMKID uses IAddr=ff:ff:ff:ff:ff:ff (initiator-independent); + * pair-specific PMKID for NDP Request derived separately. */ uint8_t i_addr[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; uint8_t r_addr[6]; esp_wifi_get_mac(WIFI_IF_NAN, r_addr); - if (!g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector) { - ESP_LOGE(TAG, "hmac_sha256_vector not registered"); - goto cleanup; + for (uint8_t i = 0; i < sec_cfg->num_credentials; i++) { + const wifi_nan_credential_t *cred = &sec_cfg->creds[i]; + + if (cred->csid == 0) { + ESP_LOGE(TAG, "Cred[%u] missing csid", i); + goto fail; + } + + if (cred->use_pmk) { + memcpy(pmk, cred->pmk, ESP_WIFI_NAN_NDP_PMK_LEN); + } else { + if (nan_derive_nd_pmk_from_passphrase(cred->passphrase, cred->csid, + service_id, r_addr, pmk) != 0) { + ESP_LOGE(TAG, "Cred[%u]: PBKDF2 ND-PMK derivation failed", i); + goto fail; + } + } + + const unsigned char *addr_pmkid[4] = {(const unsigned char *)NAN_PMK_NAME_LABEL, + i_addr, r_addr, service_id + }; + int len_pmkid[4] = {NAN_PMK_NAME_LABEL_LEN, 6, 6, 6}; + + g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(pmk, ESP_WIFI_NAN_NDP_PMK_LEN, + 4, addr_pmkid, len_pmkid, hash); + + out_derived[i].type = WIFI_NAN_SECURITY_ENCRYPTED; + out_derived[i].csid_bitmap = (uint16_t)(1u << cred->csid); + out_derived[i].group_data_prot = sec_cfg->group_data_prot; + out_derived[i].group_mgmt_prot = sec_cfg->group_mgmt_prot; + memcpy(out_derived[i].nd_pmk, pmk, ESP_WIFI_NAN_NDP_PMK_LEN); + memcpy(out_derived[i].nd_pmkid, hash, ESP_WIFI_NAN_NDP_PMKID_LEN); + + forced_memzero(pmk, sizeof(pmk)); } - const unsigned char *addr_pmkid[4] = {(const unsigned char *)NAN_PMK_NAME_LABEL, - i_addr, r_addr, service_id - }; - int len_pmkid[4] = {NAN_PMK_NAME_LABEL_LEN, 6, 6, 6}; + /* 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 + * nan_app.c — read derived ND-PMK + PMKID without a second PBKDF2 pass + * on the app/main task. Single derive, two destinations. */ + { + struct own_svc_info *p_svc = nan_find_own_svc_by_name(service_name); + if (p_svc) { + memcpy(p_svc->derived_security, out_derived, + sizeof(*out_derived) * sec_cfg->num_credentials); + } + } + return ESP_OK; - g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(pmk, 32, 4, addr_pmkid, len_pmkid, hash); - memcpy(pmkid, hash, 16); - - memcpy(cfg->security_cfg.pmkids[0], pmkid, 16); - cfg->security_cfg.num_pmkids = 1; - memcpy(cfg->security_cfg.pmk, pmk, ESP_WIFI_NAN_NDP_PMK_LEN); - ret = ESP_OK; - -cleanup: +fail: forced_memzero(pmk, sizeof(pmk)); - return ret; + memset(out_derived, 0, + sizeof(*out_derived) * ESP_WIFI_NAN_MAX_CREDS_PER_SVC); + return ESP_FAIL; } /* @@ -781,6 +849,10 @@ uint16_t nan_get_ndp_security_csid(uint8_t ndp_id, const uint8_t *peer_nmi) void nan_security_cache_subscriber_params(const char *service_name, const wifi_nan_discovery_security_params_t *security_cfg) { + /* New (or cleared) subscribe session: wipe any stale per-peer match + * memory from a previous subscriber. */ + nan_subscriber_match_clear(); + if (!service_name || !security_cfg) { s_nan_subscriber_sec_cache.valid = false; return; @@ -792,43 +864,38 @@ void nan_security_cache_subscriber_params(const char *service_name, s_nan_subscriber_sec_cache.valid = true; } -bool nan_security_service_match(const uint8_t *publisher_nmi, - const wifi_nan_discovery_security_params_t *peer_sec) +bool nan_security_subscriber_has_creds(void) { - uint8_t pmk[32]; + return s_nan_subscriber_sec_cache.valid && + s_nan_subscriber_sec_cache.security_cfg.num_credentials > 0; +} + +bool nan_security_service_match(const uint8_t *publisher_nmi, + const wifi_nan_peer_sdf_security_t *peer_sec) +{ + uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; uint8_t service_id[6]; uint8_t pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN]; uint8_t hash[32]; bool matched = false; if (!s_nan_subscriber_sec_cache.valid || !publisher_nmi || !peer_sec) { - goto cleanup; + return false; } if (peer_sec->num_pmkids == 0) { - goto cleanup; + return false; } if (!nan_compute_service_id(s_nan_subscriber_sec_cache.service_name, service_id)) { - goto cleanup; + return false; + } + if (!g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector) { + return false; } const wifi_nan_discovery_security_params_t *cfg = &s_nan_subscriber_sec_cache.security_cfg; - - if (cfg->use_pmk) { - memcpy(pmk, cfg->pmk, sizeof(pmk)); - } else { - uint8_t csid = nan_get_first_csid(cfg->csid_bitmap); - - if (csid == 0) { - goto cleanup; - } - if (nan_derive_nd_pmk_from_passphrase(cfg->passphrase, csid, service_id, - publisher_nmi, pmk) != 0) { - goto cleanup; - } - } - - if (!g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector) { - goto cleanup; + if (cfg->num_credentials == 0 || + cfg->num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { + return false; } uint8_t i_addr[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; @@ -837,19 +904,45 @@ bool nan_security_service_match(const uint8_t *publisher_nmi, }; int len_pmkid[4] = {NAN_PMK_NAME_LABEL_LEN, 6, 6, 6}; - g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(pmk, 32, 4, addr_pmkid, len_pmkid, hash); - memcpy(pmkid, hash, ESP_WIFI_NAN_NDP_PMKID_LEN); + /* Cross-product: each local credential vs each peer-advertised PMKID. First + * match wins. Both sides honor the spec PMKID formula with IAddr broadcast, + * RAddr = publisher_nmi, so a (local cred, peer SCID) match indicates a + * shared ND-PMK for this service. */ + for (uint8_t c = 0; c < cfg->num_credentials; c++) { + const wifi_nan_credential_t *cred = &cfg->creds[c]; + if (cred->csid == 0) { + continue; + } - for (unsigned int i = 0; i < peer_sec->num_pmkids && i < ESP_WIFI_NAN_MAX_PMKIDS; i++) { - if (memcmp(pmkid, peer_sec->pmkids[i], ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) { - ESP_LOGD(TAG, "ND-PMKID match with publisher " MACSTR, MAC2STR(publisher_nmi)); - ESP_LOG_BUFFER_HEXDUMP(TAG, pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN, ESP_LOG_INFO); - matched = true; - break; + if (cred->use_pmk) { + memcpy(pmk, cred->pmk, sizeof(pmk)); + } else { + if (nan_derive_nd_pmk_from_passphrase(cred->passphrase, cred->csid, + service_id, publisher_nmi, pmk) != 0) { + continue; + } + } + + g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector(pmk, ESP_WIFI_NAN_NDP_PMK_LEN, + 4, addr_pmkid, len_pmkid, hash); + memcpy(pmkid, hash, ESP_WIFI_NAN_NDP_PMKID_LEN); + + for (uint8_t i = 0; i < peer_sec->num_pmkids && i < NAN_PEER_MAX_PMKIDS; i++) { + if (memcmp(pmkid, peer_sec->pmkids[i], ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) { + ESP_LOGD(TAG, "ND-PMKID match: cred[%u] vs peer SCID[%u], publisher "MACSTR, + c, i, MAC2STR(publisher_nmi)); + ESP_LOG_BUFFER_HEXDUMP(TAG, pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN, ESP_LOG_INFO); + /* Remember which local cred matched this publisher so the + * later NDP-init path (nan_security_populate_initiator_ndl) + * uses the right cred for M1's pair-PMKID derivation. */ + nan_subscriber_match_remember(publisher_nmi, c); + matched = true; + goto done; + } } } -cleanup: +done: forced_memzero(pmk, sizeof(pmk)); return matched; } @@ -875,10 +968,24 @@ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, return ESP_OK; /* subscriber didn't cache security; open NDP */ } const wifi_nan_discovery_security_params_t *cfg = &s_nan_subscriber_sec_cache.security_cfg; - if (cfg->csid_bitmap == 0) { + if (cfg->num_credentials == 0) { return ESP_OK; /* subscriber didn't request encrypted datapath */ } + /* Use the credential that matched this publisher at discovery time + * (remembered by nan_security_service_match -> nan_subscriber_match_remember). + * Falls back to creds[0] only when no match memory exists for this peer + * (e.g., subscriber initiated NDP without a prior service match — atypical). */ + int matched_idx = nan_subscriber_match_recall(peer_nmi); + uint8_t cred_idx = (matched_idx >= 0) ? (uint8_t)matched_idx : 0; + if (cred_idx >= cfg->num_credentials) { + cred_idx = 0; + } + const wifi_nan_credential_t *cred = &cfg->creds[cred_idx]; + ESP_LOGD(TAG, "NDP req security: using cred[%u] for peer "MACSTR + " (matched_idx=%d, num_creds=%u)", + cred_idx, MAC2STR(peer_nmi), matched_idx, cfg->num_credentials); + uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; uint8_t service_id[6]; uint8_t our_nmi[6]; @@ -893,14 +1000,13 @@ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, goto cleanup; } - if (cfg->use_pmk) { - memcpy(pmk, cfg->pmk, ESP_WIFI_NAN_NDP_PMK_LEN); + if (cred->use_pmk) { + memcpy(pmk, cred->pmk, ESP_WIFI_NAN_NDP_PMK_LEN); } else { - uint8_t csid = nan_get_first_csid(cfg->csid_bitmap); - if (csid == 0) { + if (cred->csid == 0) { goto cleanup; } - if (nan_derive_nd_pmk_from_passphrase(cfg->passphrase, csid, service_id, + if (nan_derive_nd_pmk_from_passphrase(cred->passphrase, cred->csid, service_id, peer_nmi, pmk) != 0) { ESP_LOGE(TAG, "NDP req security: passphrase->PMK failed"); goto cleanup; @@ -914,7 +1020,7 @@ 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 = cfg->csid_bitmap; + ndl->security_ctx.csid_bitmap = (uint16_t)(1u << cred->csid); 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); @@ -1093,23 +1199,22 @@ int esp_nan_get_ndp_resp_shared_key_desc(uint8_t *buf, size_t buf_len, uint8_t n if (need_pmk && have_peer_pmkid) { struct own_svc_info *p_svc = nan_find_own_svc(ndl->publisher_id); - uint8_t matched_pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; - if (p_svc && nan_match_pmkid(p_svc, ndl->security_ctx.nd_pmkid, matched_pmk, - ndl->peer_nmi, ndl->peer_ndi)) { + int matched_idx = p_svc ? nan_match_pmkid(p_svc, ndl->security_ctx.nd_pmkid, + ndl->peer_nmi, ndl->peer_ndi) : -1; + if (matched_idx >= 0) { ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; - ndl->security_ctx.csid_bitmap = p_svc->security_cfg.csid_bitmap; - memcpy(ndl->security_ctx.nd_pmk, matched_pmk, ESP_WIFI_NAN_NDP_PMK_LEN); - ESP_LOGD(TAG, "NDP Resp Key Desc: resolved PMK from publish (PMKID match)"); - forced_memzero(matched_pmk, sizeof(matched_pmk)); + ndl->security_ctx.csid_bitmap = p_svc->derived_security[matched_idx].csid_bitmap; + memcpy(ndl->security_ctx.nd_pmk, + p_svc->derived_security[matched_idx].nd_pmk, + ESP_WIFI_NAN_NDP_PMK_LEN); + ESP_LOGD(TAG, "NDP Resp Key Desc: resolved PMK from cred slot %d", matched_idx); } else if (ndl->security_ctx.type != WIFI_NAN_SECURITY_ENCRYPTED) { - forced_memzero(matched_pmk, sizeof(matched_pmk)); NAN_DATA_UNLOCK(); ESP_LOGW(TAG, "NDP Resp Key Desc: NDL not encrypted; send NDP Response without Shared Key Descriptor"); return 0; } else { - forced_memzero(matched_pmk, sizeof(matched_pmk)); NAN_DATA_UNLOCK(); - ESP_LOGW(TAG, "NDP Resp Key Desc: no PMK for peer PMKID; ensure same passphrase on both devices"); + ESP_LOGW(TAG, "NDP Resp Key Desc: no PMK for peer PMKID; ensure matching credential on both devices"); return 0; } } else if (ndl->security_ctx.type != WIFI_NAN_SECURITY_ENCRYPTED) { @@ -1264,7 +1369,7 @@ int esp_nan_update_ndp_security_install_mic(uint8_t *m4_body, size_t body_len, u * Public API: parsers (CSIA / SCIA / Shared Key Descriptor / Publish SDF) *-----------------------------------------------------------------------*/ -void esp_nan_parse_ndp_csia(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param) +void esp_nan_parse_ndp_csia(void *frm, size_t buf_len, wifi_nan_security_params_t *param) { if (!frm || !param || buf_len < 3) { return; @@ -1304,7 +1409,7 @@ void esp_nan_parse_ndp_csia(void *frm, size_t buf_len, wifi_nan_datapath_securit } } -void esp_nan_parse_ndp_scia(void *frm, size_t buf_len, wifi_nan_datapath_security_params_t *param) +void esp_nan_parse_ndp_scia(void *frm, size_t buf_len, wifi_nan_security_params_t *param) { if (!frm || !param || buf_len < 3) { return; @@ -1522,7 +1627,7 @@ void esp_nan_parse_ndp_key_desc(void *frm, size_t buf_len, uint8_t ndp_id, const } esp_err_t esp_nan_parse_publish_security(const uint8_t *attrs, size_t attrs_len, - wifi_nan_discovery_security_params_t *security) + wifi_nan_peer_sdf_security_t *security) { uint16_t csia_len = 0; const uint8_t *csia = NULL; @@ -1562,7 +1667,7 @@ esp_err_t esp_nan_parse_publish_security(const uint8_t *attrs, size_t attrs_len, ESP_LOG_BUFFER_HEXDUMP(TAG, scia, scia_len, ESP_LOG_DEBUG); size_t offset = 0; - while (offset + 4 <= scia_len && security->num_pmkids < ESP_WIFI_NAN_MAX_PMKIDS) { + while (offset + 4 <= scia_len && security->num_pmkids < NAN_PEER_MAX_PMKIDS) { uint16_t val_len = scia[offset] | (scia[offset + 1] << 8); uint8_t type = scia[offset + 2]; @@ -1611,15 +1716,17 @@ void nan_security_apply_pending(struct ndl_info *ndl, if (s_pending_scia.has_pmkid) { memcpy(ndl->security_ctx.nd_pmkid, s_pending_scia.pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN); - uint8_t matched_pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; - if (nan_match_pmkid(p_own_svc, s_pending_scia.pmkid, matched_pmk, peer_nmi, peer_ndi)) { + int matched_idx = nan_match_pmkid(p_own_svc, s_pending_scia.pmkid, peer_nmi, peer_ndi); + if (matched_idx >= 0) { ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; - memcpy(ndl->security_ctx.nd_pmk, matched_pmk, ESP_WIFI_NAN_NDP_PMK_LEN); - ESP_LOGD(TAG, "NDP Indication: PMKID validated, security=ENCRYPTED"); + ndl->security_ctx.csid_bitmap = p_own_svc->derived_security[matched_idx].csid_bitmap; + memcpy(ndl->security_ctx.nd_pmk, + p_own_svc->derived_security[matched_idx].nd_pmk, + ESP_WIFI_NAN_NDP_PMK_LEN); + ESP_LOGD(TAG, "NDP Indication: PMKID validated via cred slot %d", matched_idx); } else { ESP_LOGW(TAG, "NDP Indication: PMKID validation failed"); } - forced_memzero(matched_pmk, sizeof(matched_pmk)); } memset(&s_pending_scia, 0, sizeof(s_pending_scia)); 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 bf87c8b863d..56c6995b5d2 100644 --- a/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c +++ b/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c @@ -130,19 +130,24 @@ void wifi_nan_publish(void) #ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED .security_reqd = 1, .security_cfg = { - .csid_bitmap = WIFI_NAN_CSID_BIT_NCS_SK_128, + .num_credentials = 1, + .creds = { + { + .csid = WIFI_NAN_CSID_NCS_SK_128, #ifdef CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK - .use_pmk = true, + .use_pmk = true, #else - .use_pmk = false, - .passphrase = CONFIG_EXAMPLE_NAN_PASSPHRASE, + .use_pmk = false, + .passphrase = CONFIG_EXAMPLE_NAN_PASSPHRASE, #endif + }, + }, }, #endif }; #if defined(CONFIG_EXAMPLE_NAN_SECURITY_ENABLED) && defined(CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK) - if (!decode_hex_string(CONFIG_EXAMPLE_NAN_PMK, publish_cfg.security_cfg.pmk, - sizeof(publish_cfg.security_cfg.pmk))) { + if (!decode_hex_string(CONFIG_EXAMPLE_NAN_PMK, publish_cfg.security_cfg.creds[0].pmk, + sizeof(publish_cfg.security_cfg.creds[0].pmk))) { ESP_LOGE(TAG, "Failed to decode CONFIG_EXAMPLE_NAN_PMK"); return; } 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 e8caea7eb61..c35c4ca042f 100644 --- a/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c +++ b/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c @@ -224,19 +224,24 @@ void wifi_nan_subscribe(void) #ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED .security_reqd = 1, .security_cfg = { - .csid_bitmap = WIFI_NAN_CSID_BIT_NCS_SK_128, + .num_credentials = 1, + .creds = { + { + .csid = WIFI_NAN_CSID_NCS_SK_128, #ifdef CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK - .use_pmk = true, + .use_pmk = true, #else - .use_pmk = false, - .passphrase = CONFIG_EXAMPLE_NAN_PASSPHRASE, + .use_pmk = false, + .passphrase = CONFIG_EXAMPLE_NAN_PASSPHRASE, #endif + }, + }, }, #endif }; #if defined(CONFIG_EXAMPLE_NAN_SECURITY_ENABLED) && defined(CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK) - if (!decode_hex_string(CONFIG_EXAMPLE_NAN_PMK, subscribe_cfg.security_cfg.pmk, - sizeof(subscribe_cfg.security_cfg.pmk))) { + if (!decode_hex_string(CONFIG_EXAMPLE_NAN_PMK, subscribe_cfg.security_cfg.creds[0].pmk, + sizeof(subscribe_cfg.security_cfg.creds[0].pmk))) { ESP_LOGE(TAG, "Failed to decode CONFIG_EXAMPLE_NAN_PMK"); return; } From caba5dcfea2c424876345594074b5464258e5a8f Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Thu, 14 May 2026 14:28:51 +0530 Subject: [PATCH 6/8] refactor(wifi): NAN datapath header cleanups and docs - Rename NAN_IPV6_ADDR_ID_LEN / IS_ZERO_NAN_ADDR_ID to ..._IPV6_IDENTIFIER so the names no longer read like MAC-address constants; drop the duplicate macro definition in esp_nan.h. - Replace literal lengths with named macros: WIFI_OUI_LEN for nan_vendor_ie_t.vendor_oui, and a new WIFI_MAC_ADDR_LEN (private) applied to the NAN-related structs and callbacks in esp_private/wifi.h. - Document each callback in struct nan_sync_callbacks and each helper in struct nan_secure_dp_funcs: when fired / when invoked, what each argument means, and the matching spec section where useful. - Note in wifi_nan_datapath_req_t / wifi_nan_datapath_resp_t that they cover the NCS-SK credential model only; pairing-based cipher suites (NCS-PK-PASN) install per-peer ND-PMKs via a separate pairing API without changing these structs. Record why per-NDP credential injection at response time is not viable: the responder's PMKID lookup happens at M1 receive time, before the indication event surfaces to the host. --- .../esp_wifi/include/esp_private/wifi.h | 159 ++++++++++++++++-- .../esp_wifi/include/esp_wifi_types_generic.h | 21 ++- .../include/injected/esp_wifi_types_generic.h | 19 ++- .../wifi_apps/nan_app/include/esp_nan.h | 12 +- .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 8 +- .../nan_subscriber/main/subscriber_main.c | 2 +- 6 files changed, 187 insertions(+), 34 deletions(-) diff --git a/components/esp_wifi/include/esp_private/wifi.h b/components/esp_wifi/include/esp_private/wifi.h index e957e40bc7a..077f1d1354b 100644 --- a/components/esp_wifi/include/esp_private/wifi.h +++ b/components/esp_wifi/include/esp_private/wifi.h @@ -34,6 +34,11 @@ extern "C" { #endif +#define WIFI_MAC_ADDR_LEN 6 /**< Length of an 802.11 MAC address in octets */ +#ifndef NAN_IPV6_IDENTIFIER_LEN +#define NAN_IPV6_IDENTIFIER_LEN 8 /**< Length of a NAN IPv6 interface identifier in octets */ +#endif + typedef struct { QueueHandle_t handle; /**< FreeRTOS queue handler */ void *storage; /**< storage for FreeRTOS queue */ @@ -88,7 +93,7 @@ typedef struct { /* NAN Peer info parsed from SDF */ struct nan_cb_peer_info { - uint8_t peer_mac[6]; /**< Peer NMI / interface MAC */ + uint8_t peer_mac[WIFI_MAC_ADDR_LEN]; /**< Peer NMI / interface MAC */ uint8_t peer_svc_id; /**< Peer's service instance ID */ uint16_t sdea; /**< SDEA control field bits */ uint32_t device_caps; /**< NAN device capabilities */ @@ -102,21 +107,66 @@ struct nan_cb_peer_info { /* NDP Peer info parsed from NAF. */ struct ndp_cb_peer_info { uint8_t ndp_id; - uint8_t peer_nmi[6]; - uint8_t peer_ndi[6]; + uint8_t peer_nmi[WIFI_MAC_ADDR_LEN]; + uint8_t peer_ndi[WIFI_MAC_ADDR_LEN]; uint8_t *ssi; uint16_t ssi_len; }; +/* Host-side callbacks the closed-source NAN blob fires up into nan_app. + * Registered once at nan_app init; all callbacks run in blob/WiFi-task + * context, so handlers must be non-blocking and avoid heavy work. */ struct nan_sync_callbacks { + /* Subscriber side: a Publish SDF matching the local subscribe was + * received from a peer. + * sub_id -- local subscribe service instance ID + * peer_info -- publisher details (see struct nan_cb_peer_info) */ void (* service_match)(uint8_t sub_id, struct nan_cb_peer_info *peer_info); + + /* Publisher side: a Solicited Publish SDF was just transmitted in + * response to a Subscribe match. Fires once per recipient. + * pub_id -- local publish service instance ID + * peer_info -- subscriber MAC and its service instance ID */ void (* replied)(uint8_t pub_id, struct nan_cb_peer_info *peer_info); + + /* Either side: a Follow-up SDF was received for an existing service + * match (post-discovery messaging). + * svc_id -- local service instance ID this Follow-up targets + * peer_info -- sender MAC and SSI payload */ void (* receive)(uint8_t svc_id, struct nan_cb_peer_info *peer_info); + + /* Publisher side (NDP Responder): an NDP Request (M1) was received. + * Host either auto-accepts or waits for esp_wifi_nan_datapath_resp() + * depending on the publish-time ndp_resp_needed flag. + * pub_id -- local publish instance bound to this request + * peer_info -- initiator's ndp_id, NMI, NDI, SSI + * device_caps -- initiator's NAN device capabilities */ void (* ndp_indication)(uint8_t pub_id, struct ndp_cb_peer_info *peer_info, uint32_t device_caps); + + /* Either side: NDP setup completed (success or rejection). + * status -- NDP_STATUS_ACCEPTED / NDP_STATUS_REJECTED + * peer_info -- peer ndp_id, NMI, NDI + * own_ndi -- locally-assigned NDI for this NDP + * ipv6_identifier -- 8-byte IPv6 interface identifier learnt from + * the peer; all-zero if the peer did not + * advertise one (host then derives it from the + * peer NDI). */ void (* ndp_confirm)(uint8_t status, struct ndp_cb_peer_info *peer_info, - uint8_t own_ndi[6], uint8_t ipv6_identifier[8]); - void (* ndp_terminated)(uint8_t reason, uint8_t ndp_id, uint8_t init_ndi[6]); + uint8_t own_ndi[WIFI_MAC_ADDR_LEN], + uint8_t ipv6_identifier[NAN_IPV6_IDENTIFIER_LEN]); + + /* Either side: an established NDP was torn down. + * reason -- termination reason code (peer-initiated, timeout, ...) + * ndp_id -- NDP identifier of the terminated path + * init_ndi -- initiator's NDI for this NDP */ + void (* ndp_terminated)(uint8_t reason, uint8_t ndp_id, + uint8_t init_ndi[WIFI_MAC_ADDR_LEN]); + + /* TX completion for a host-queued NAN action frame (Follow-up, etc.). + * context -- opaque tag matching the original TX request + * tx_status -- true on transmit success, false on failure */ void (* action_txdone)(uint32_t context, bool tx_status); + /* Initiator-side M2 RX indication. Blob fires this after parsing the * Responder NDI from the M2 NDP attribute (Wi-Fi Aware v4.0 §9.5.16.1 * Table 82) and before invoking esp_nan_verify_ndp_resp_mic, so the @@ -131,71 +181,146 @@ struct nan_sync_callbacks { struct nan_secure_dp_funcs { /* === Always-present helpers === */ - /* TX completion notification for M2/M4 frames */ + /* Fired by the blob after a host-built NDP setup frame (M2/M4) is + * transmitted, so the host can advance its NDP setup state machine. + * msg_type -- 0x01 = M2 (NDP Response), 0x02 = M4 (Security Install) + * tx_status -- true on TX success, false on failure */ void (*ndp_tx_done_cb)(uint8_t ndp_id, const uint8_t *peer_nmi, uint8_t msg_type, bool tx_status); - /* === Security-gated helpers (24 fields, NULL when SECURITY=n) === */ + /* === Security-gated helpers (NULL when SECURITY=n) === */ - /* Length getters */ + /* --- Length getters: callers use these to size TX buffers before + * invoking the matching construct_*() / get_*_key_desc helper. --- */ + + /* Byte length of the CSIA emitted for the union of the two cipher + * suite bitmaps (Wi-Fi Aware v4.0 §9.5.21.2). */ uint32_t (*get_csia_len)(uint16_t own_csid_bitmap, uint16_t peer_csid_bitmap); + + /* Byte length of a SCIA carrying @c num_pmkids 16-octet ND-PMKIDs + * (§9.5.21.4). */ uint32_t (*get_scia_len)(uint8_t num_pmkids); + + /* Byte length of a NAN Shared Key Descriptor attribute (§9.5.21.5) + * 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); - /* CSIA / SCIA construction (publish + NDP req/resp) */ + /* --- CSIA / SCIA construction. Each writes a complete NAN + * attribute (header + body) at @c frm and returns bytes + * written, or -1 on error. --- */ + + /* Build CSIA from the negotiated own/peer cipher suite bitmaps. */ int (*construct_csia)(uint8_t *frm, uint8_t pub_id, uint16_t own_csid_bitmap, uint16_t peer_csid_bitmap); + + /* Build SCIA for a Publish SDF: emits one SCID per provisioned + * ND-PMKID so subscribers can recognise themselves (§9.5.21.4). */ int (*construct_scia_publish)(uint8_t *frm, uint8_t pub_id, uint8_t num_pmkids, const uint8_t pmkids[][ESP_WIFI_NAN_NDP_PMKID_LEN]); + + /* Build SCIA for an NDP Request (M1): single SCID matching the + * (ndp_id, peer_nmi) credential the initiator selected. */ int (*construct_scia_ndp_req)(uint8_t *frm, uint8_t ndp_id, const uint8_t *peer_nmi); + + /* Build SCIA for an NDP Response (M2): echoes the SCID accepted + * by the responder. */ int (*construct_scia_ndp_resp)(uint8_t *frm, uint8_t ndp_id, const uint8_t *peer_nmi); - /* Shared Key Descriptor builders (M1 / M2 / M3 / M4) -- MIC left zeroed */ + /* --- Shared Key Descriptor builders (M1 / M2 / M3 / M4). + * Each writes the full NAN Shared Key Descriptor attribute + * (§9.5.21.5) into @c buf; the MIC field is left zeroed and + * must be populated by the matching update_*_mic helper after + * the rest of the frame body is in place. Return bytes + * written, -1 on error. --- */ + + /* M1 (NDP Request): Nonce = INonce, no MIC required. */ int (*get_ndp_req_shared_key_desc)(uint8_t *buf, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi); + + /* M2 (NDP Response): Nonce = RNonce, MIC over M2 body. */ int (*get_ndp_resp_shared_key_desc)(uint8_t *buf, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi); + + /* M3 (NDP Confirm): MIC over (Auth_Token || M3 body). */ int (*get_ndp_confirm_shared_key_desc)(uint8_t *buf, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi); + + /* M4 (Security Install): install bit set; carries any GTK / IGTK / + * BIGTK KDEs encrypted under ND-KEK. */ int (*get_ndp_security_install_key_desc)(uint8_t *buf, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi); - /* M1 Auth_Token capture (host stores SHA-256(M1_body)[0:16] for M3 MIC) */ + /* Compute and cache SHA-256(M1_body)[0:16] in the (ndp_id, peer_nmi) + * NDL slot. The cached Authentication Token is later prepended when + * computing/verifying the M3 MIC (Wi-Fi Aware v4.0 §7.1.3.5). */ int (*capture_m1_auth_token)(const uint8_t *m1_body, size_t body_len, uint8_t ndp_id, const uint8_t *peer_nmi); - /* MIC compute (TX path) -- fills MIC field in already-built key descriptor */ + /* --- MIC compute (TX path). Fill the Key MIC field in + * @c key_desc_attr (which sits inside @c m*_body) using + * ND-KCK derived for this NDP. Return 0 on success. --- */ + + /* M2 MIC: HMAC over the entire M2 body. */ int (*update_ndp_resp_mic)(uint8_t *m2_body, size_t body_len, uint8_t *key_desc_attr, uint8_t ndp_id, const uint8_t *peer_nmi); + + /* M3 MIC: HMAC over (cached M1 Auth_Token || M3 body). */ int (*update_ndp_confirm_mic)(uint8_t *m3_body, size_t body_len, uint8_t *key_desc_attr, uint8_t ndp_id, const uint8_t *peer_nmi); + + /* M4 MIC: HMAC over the entire M4 body. */ int (*update_ndp_security_install_mic)(uint8_t *m4_body, size_t body_len, uint8_t *key_desc_attr, uint8_t ndp_id, const uint8_t *peer_nmi); - /* MIC verify (RX path) -- 0 on pass, -1 on mismatch (caller tears down NDP) */ + /* --- MIC verify (RX path). Recompute the expected Key MIC over + * the same input as the matching update_* helper and compare + * against the value in @c key_desc_attr. Return 0 on pass, + * -1 on mismatch -- caller tears down the NDP on -1. --- */ + + /* M2 MIC verify (initiator). */ int (*verify_ndp_resp_mic)(uint8_t *m2_body, size_t body_len, uint8_t *key_desc_attr, uint8_t ndp_id, const uint8_t *peer_nmi); + + /* M3 MIC verify (responder; uses cached M1 Auth_Token). */ int (*verify_ndp_confirm_mic)(uint8_t *m3_body, size_t body_len, uint8_t *key_desc_attr, uint8_t ndp_id, const uint8_t *peer_nmi); + + /* M4 MIC verify (initiator). */ int (*verify_ndp_security_install_mic)(uint8_t *m4_body, size_t body_len, uint8_t *key_desc_attr, uint8_t ndp_id, const uint8_t *peer_nmi); - /* RX-path attribute parsers (CSIA / SCIA / key-desc). */ + /* --- RX-path attribute parsers. --- */ + + /* Parse CSIA from a received NDP setup frame and populate the + * negotiated cipher-suite bitmap in @c param. */ void (*parse_ndp_csia)(void *frm, size_t buf_len, wifi_nan_security_params_t *param); + + /* Parse SCIA from a received NDP setup frame and populate the + * selected ND-PMKID + Publish ID in @c param. */ void (*parse_ndp_scia)(void *frm, size_t buf_len, wifi_nan_security_params_t *param); + + /* Parse the NAN Shared Key Descriptor attribute from a received + * M2/M3/M4 frame: advances the NDL replay counter, captures the + * peer Nonce, decrypts KDE payloads, and installs GTK/IGTK/BIGTK + * as applicable. */ void (*parse_ndp_key_desc)(void *frm, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi); - /* Publish-side security parser (called when blob processes inbound publish SDF) */ + /* Parse security-related attributes from an inbound Publish SDF + * (CSIA + SCIA list) and populate @c security so the subscriber + * state machine can pick a compatible credential. */ esp_err_t (*parse_publish_security)(const uint8_t *attrs, size_t attrs_len, wifi_nan_peer_sdf_security_t *security); @@ -209,7 +334,9 @@ struct nan_secure_dp_funcs { const wifi_nan_discovery_security_params_t *sec_cfg, wifi_nan_security_params_t *out_derived); - /* NDP security gate: cipher-suite bitmap for (ndp_id, peer_nmi), or 0 for open. */ + /* 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. */ uint16_t (*get_ndp_security_csid)(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 a28544220f5..73af06265f8 100644 --- a/components/esp_wifi/include/esp_wifi_types_generic.h +++ b/components/esp_wifi/include/esp_wifi_types_generic.h @@ -936,7 +936,7 @@ typedef enum { #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_256 (1 << WIFI_NAN_CSID_NCS_PK_PASN_256) /** - * @brief NAN security credential — one passphrase or raw PMK + the cipher it's bound to. + * @brief NAN security credential - one passphrase or raw PMK + the cipher it's bound to. * * Per Wi-Fi Aware v4.0 §7.1.3.5 the PMKID derivation formula is cipher-specific * (NCS-SK-128 uses HMAC-SHA-256; NCS-SK-256 uses HMAC-SHA-384), so each @@ -988,9 +988,9 @@ typedef struct { * @brief NAN Vendor Specific Attribute format */ typedef struct { - uint8_t vendor_oui[3]; /**< Vendor identifier (OUI) */ - uint16_t body_len; /**< Length of body payload (max NAN_VENDOR_IE_MAX_BODY_LEN bytes) */ - uint8_t *body; /**< Vendor specific body payload */ + uint8_t vendor_oui[WIFI_OUI_LEN]; /**< Vendor identifier (OUI) */ + uint16_t body_len; /**< Length of body payload (max 255 bytes) */ + uint8_t *body; /**< Vendor specific body payload */ } nan_vendor_ie_t; /** @@ -1064,6 +1064,11 @@ typedef struct { * configuration and applies them to every NDP initiated against the * matched publisher. Per-NDP security parameters are not exposed on * this struct: the caller never handles raw key material. + * + * @note NCS-SK only. For pairing-based cipher suites (NCS-PK-PASN), the + * per-peer ND-PMK is derived from a cached NPKSA and will be + * installed via a separate pairing API; this struct will remain + * unchanged. */ typedef struct { uint8_t pub_id; /**< Publisher's service instance id */ @@ -1080,6 +1085,14 @@ typedef struct { * configuration and applies them to every NDP this responder * accepts. Per-NDP security parameters are not exposed on this * struct: the caller never handles raw key material. + * + * @note NCS-SK only. For pairing-based cipher suites (NCS-PK-PASN), the + * per-peer ND-PMK is derived from a cached NPKSA and will be + * installed via a separate pairing API; this struct will remain + * unchanged. The responder must already have the PMK cached at + * M1 receive time (PMKID lookup happens before the indication + * event), so per-NDP credential injection at response time is + * not viable. */ typedef struct { bool accept; /**< True - Accept incoming NDP, False - Reject it */ 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 a3ca98bd962..e020d0a79cf 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 @@ -988,9 +988,9 @@ typedef struct { * @brief NAN Vendor Specific Attribute format */ typedef struct { - uint8_t vendor_oui[3]; /**< Vendor identifier (OUI) */ - uint16_t body_len; /**< Length of body payload (max NAN_VENDOR_IE_MAX_BODY_LEN bytes) */ - uint8_t *body; /**< Vendor specific body payload */ + uint8_t vendor_oui[WIFI_OUI_LEN]; /**< Vendor identifier (OUI) */ + uint16_t body_len; /**< Length of body payload (max 255 bytes) */ + uint8_t *body; /**< Vendor specific body payload */ } nan_vendor_ie_t; /** @@ -1064,6 +1064,11 @@ typedef struct { * configuration and applies them to every NDP initiated against the * matched publisher. Per-NDP security parameters are not exposed on * this struct: the caller never handles raw key material. + * + * @note NCS-SK only. For pairing-based cipher suites (NCS-PK-PASN), the + * per-peer ND-PMK is derived from a cached NPKSA and will be + * installed via a separate pairing API; this struct will remain + * unchanged. */ typedef struct { uint8_t pub_id; /**< Publisher's service instance id */ @@ -1080,6 +1085,14 @@ typedef struct { * configuration and applies them to every NDP this responder * accepts. Per-NDP security parameters are not exposed on this * struct: the caller never handles raw key material. + * + * @note NCS-SK only. For pairing-based cipher suites (NCS-PK-PASN), the + * per-peer ND-PMK is derived from a cached NPKSA and will be + * installed via a separate pairing API; this struct will remain + * unchanged. The responder must already have the PMK cached at + * M1 receive time (PMKID lookup happens before the indication + * event), so per-NDP credential injection at response time is + * not viable. */ typedef struct { bool accept; /**< True - Accept incoming NDP, False - Reject it */ 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 d1edab16bba..1e48483178e 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 @@ -30,12 +30,12 @@ extern "C" { #define NAN_MAX_PEERS_RECORD 15 #define ESP_NAN_PUBLISH 2 #define ESP_NAN_SUBSCRIBE 1 -#define NAN_IPV6_ADDR_ID_LEN 8 +#ifndef NAN_IPV6_IDENTIFIER_LEN +#define NAN_IPV6_IDENTIFIER_LEN 8 +#endif -#define IS_ZERO_NAN_ADDR_ID(a) (!((a)[0] | (a)[1] | (a)[2] | (a)[3] | \ - (a)[4] | (a)[5] | (a)[6] | (a)[7])) - -#define NAN_IPV6_ADDR_ID_LEN 8 +#define IS_ZERO_NAN_IPV6_IDENTIFIER(a) (!((a)[0] | (a)[1] | (a)[2] | (a)[3] | \ + (a)[4] | (a)[5] | (a)[6] | (a)[7])) #define ESP_NAN_SET_IPV6_LINKLOCAL_FROM_IDENTIFIER(_target_addr, _identifier) \ do { \ @@ -44,7 +44,7 @@ extern "C" { (_target_addr).u_addr.ip6.addr[1] = 0; \ memcpy(&(_target_addr).u_addr.ip6.addr[2], \ (_identifier), \ - NAN_IPV6_ADDR_ID_LEN); \ + NAN_IPV6_IDENTIFIER_LEN); \ } while (0) /** Parameters of a peer service record */ 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 6e71672fa12..2ba982c12ee 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 @@ -596,7 +596,7 @@ void nan_app_service_match_cb(uint8_t sub_id, struct nan_cb_peer_info *peer_info * one of the local creds, else drop. */ if (nan_security_subscriber_has_creds()) { if (!peer_info->peer_security_params || - peer_info->peer_security_params->num_pmkids == 0) { + peer_info->peer_security_params->num_pmkids == 0) { ESP_LOGW(TAG, "Secure subscribe: peer "MACSTR" advertises no PMKIDs", MAC2STR(pub_mac)); return; @@ -997,12 +997,12 @@ void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer_info, MACADDR_COPY(evt->peer_ndi, peer_ndi); MACADDR_COPY(evt->own_ndi, own_ndi); - if (IS_ZERO_NAN_ADDR_ID(ipv6_identifier)) { + if (IS_ZERO_NAN_IPV6_IDENTIFIER(ipv6_identifier)) { ip6_addr_t linklocal; esp_wifi_nan_get_ipv6_linklocal_from_mac(&linklocal, peer_ndi); - memcpy(evt->ipv6_identifier, &linklocal.addr[2], NAN_IPV6_ADDR_ID_LEN); + memcpy(evt->ipv6_identifier, &linklocal.addr[2], NAN_IPV6_IDENTIFIER_LEN); } else { - memcpy(evt->ipv6_identifier, ipv6_identifier, NAN_IPV6_ADDR_ID_LEN); + memcpy(evt->ipv6_identifier, ipv6_identifier, NAN_IPV6_IDENTIFIER_LEN); } if (ssi && ssi_len) { memcpy(evt->ssi, ssi, ssi_len); 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 c35c4ca042f..9b6d4fcf81b 100644 --- a/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c +++ b/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c @@ -68,7 +68,7 @@ const int NDP_FAILED = BIT2; static wifi_event_nan_svc_match_t g_svc_match_evt; -static uint8_t s_ipv6_identifier[NAN_IPV6_ADDR_ID_LEN] = {0}; +static uint8_t s_ipv6_identifier[NAN_IPV6_IDENTIFIER_LEN] = {0}; static void nan_receive_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) From b3512ccf7d01cdd278901f0ae0a9f7b101932523 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Sun, 17 May 2026 06:49:28 +0530 Subject: [PATCH 7/8] change(wifi): NAN security_cfg as pointer security_cfg in wifi_nan_publish_cfg_t / wifi_nan_subscribe_cfg_t is a large struct; pass it by pointer so callers don't bloat their cfg copy. Driver deep-copies during publish/subscribe, so caller may free right after the API returns. Add NULL-check when security_reqd is set. Update both example apps to declare a local security_cfg and assign its address. Add help text on EXAMPLE_NAN_SECURITY_ENABLED. --- .../esp_wifi/include/esp_wifi_types_generic.h | 8 +++-- .../include/injected/esp_wifi_types_generic.h | 8 +++-- .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 36 +++++++++++-------- .../nan_publisher/main/Kconfig.projbuild | 6 ++++ .../nan_publisher/main/publisher_main.c | 35 +++++++++--------- .../nan_subscriber/main/Kconfig.projbuild | 6 ++++ .../nan_subscriber/main/subscriber_main.c | 35 +++++++++--------- 7 files changed, 84 insertions(+), 50 deletions(-) diff --git a/components/esp_wifi/include/esp_wifi_types_generic.h b/components/esp_wifi/include/esp_wifi_types_generic.h index 73af06265f8..4e7055fe36b 100644 --- a/components/esp_wifi/include/esp_wifi_types_generic.h +++ b/components/esp_wifi/include/esp_wifi_types_generic.h @@ -1014,7 +1014,9 @@ typedef struct { unsigned int ttl; /**< Run publish function for a given time interval in seconds. If ttl=0 and usd_discovery_flag is enabled, only one Publish message is transmitted */ wifi_nan_usd_config_t usd_publish_config; /**< USD configuration parameters. Relevant only when 'usd_discovery_flag' is set. */ - wifi_nan_discovery_security_params_t security_cfg; /**< Security configuration parameters */ + wifi_nan_discovery_security_params_t *security_cfg; /**< Security configuration parameters. Used when security_reqd is set, NULL otherwise. + The driver makes a private copy during esp_wifi_nan_publish_service(); + the caller may free this immediately after the call returns. */ nan_vendor_ie_t *vendor_ie; /**< Vendor specific IE to be added in publish frames */ } wifi_nan_publish_cfg_t; @@ -1038,7 +1040,9 @@ typedef struct { unsigned int ttl; /**< Run subscribe function for a given time interval in seconds. If ttl=0 and usd_discovery_flag is enabled, the subscriber listens until the first service match is reported. */ wifi_nan_usd_config_t usd_subscribe_config; /**< USD configuration parameters. Relevant only when 'usd_discovery_flag' is set. */ - wifi_nan_discovery_security_params_t security_cfg; /**< Security configuration parameters */ + wifi_nan_discovery_security_params_t *security_cfg; /**< Security configuration parameters. Used when security_reqd is set, NULL otherwise. + The driver makes a private copy during esp_wifi_nan_subscribe_service(); + the caller may free this immediately after the call returns. */ nan_vendor_ie_t *vendor_ie; /**< Vendor specific IE to be added in subscribe frames */ } wifi_nan_subscribe_cfg_t; 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 e020d0a79cf..5c934cc9659 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 @@ -1014,7 +1014,9 @@ typedef struct { unsigned int ttl; /**< Run publish function for a given time interval in seconds. If ttl=0 and usd_discovery_flag is enabled, only one Publish message is transmitted */ wifi_nan_usd_config_t usd_publish_config; /**< USD configuration parameters. Relevant only when 'usd_discovery_flag' is set. */ - wifi_nan_discovery_security_params_t security_cfg; /**< Security configuration parameters */ + wifi_nan_discovery_security_params_t *security_cfg; /**< Security configuration parameters. Used when security_reqd is set, NULL otherwise. + The driver makes a private copy during esp_wifi_nan_publish_service(); + the caller may free this immediately after the call returns. */ nan_vendor_ie_t *vendor_ie; /**< Vendor specific IE to be added in publish frames */ } wifi_nan_publish_cfg_t; @@ -1038,7 +1040,9 @@ typedef struct { unsigned int ttl; /**< Run subscribe function for a given time interval in seconds. If ttl=0 and usd_discovery_flag is enabled, the subscriber listens until the first service match is reported. */ wifi_nan_usd_config_t usd_subscribe_config; /**< USD configuration parameters. Relevant only when 'usd_discovery_flag' is set. */ - wifi_nan_discovery_security_params_t security_cfg; /**< Security configuration parameters */ + wifi_nan_discovery_security_params_t *security_cfg; /**< Security configuration parameters. Used when security_reqd is set, NULL otherwise. + The driver makes a private copy during esp_wifi_nan_subscribe_service(); + the caller may free this immediately after the call returns. */ nan_vendor_ie_t *vendor_ie; /**< Vendor specific IE to be added in subscribe frames */ } wifi_nan_subscribe_cfg_t; 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 2ba982c12ee..ab6f4765605 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 @@ -1416,15 +1416,19 @@ uint8_t esp_wifi_nan_publish_service(const wifi_nan_publish_cfg_t *publish_cfg) ESP_LOGE(TAG, "Encrypted datapath not enabled (CONFIG_ESP_WIFI_NAN_SECURITY)"); goto fail; #else - if (publish_cfg->security_cfg.num_credentials == 0 || - publish_cfg->security_cfg.num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { + if (!publish_cfg->security_cfg) { + ESP_LOGE(TAG, "security_reqd set but security_cfg is NULL"); + goto fail; + } + if (publish_cfg->security_cfg->num_credentials == 0 || + publish_cfg->security_cfg->num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { ESP_LOGE(TAG, "security_cfg.num_credentials=%u must be 1..%u", - publish_cfg->security_cfg.num_credentials, + publish_cfg->security_cfg->num_credentials, ESP_WIFI_NAN_MAX_CREDS_PER_SVC); goto fail; } - for (uint8_t i = 0; i < publish_cfg->security_cfg.num_credentials; i++) { - uint8_t csid = publish_cfg->security_cfg.creds[i].csid; + for (uint8_t i = 0; i < publish_cfg->security_cfg->num_credentials; i++) { + uint8_t csid = publish_cfg->security_cfg->creds[i].csid; if (csid != WIFI_NAN_CSID_NCS_SK_128) { ESP_LOGE(TAG, "creds[%u].csid=%u unsupported (only NCS-SK-128)", i, csid); goto fail; @@ -1449,7 +1453,7 @@ uint8_t esp_wifi_nan_publish_service(const wifi_nan_publish_cfg_t *publish_cfg) * tripping the task watchdog when num_credentials > 1. */ if (!nan_claim_own_svc_slot(ESP_NAN_PUBLISH, publish_cfg->service_name, #ifdef CONFIG_ESP_WIFI_NAN_SECURITY - &cfg->security_cfg + cfg->security_cfg #else NULL #endif @@ -1520,7 +1524,7 @@ uint8_t esp_wifi_nan_subscribe_service(const wifi_nan_subscribe_cfg_t *subscribe subscribe_cfg->service_name, sub_id); #ifdef CONFIG_ESP_WIFI_NAN_SECURITY nan_security_cache_subscriber_params(subscribe_cfg->service_name, - &subscribe_cfg->security_cfg); + subscribe_cfg->security_cfg); #endif return sub_id; } @@ -1532,15 +1536,19 @@ uint8_t esp_wifi_nan_subscribe_service(const wifi_nan_subscribe_cfg_t *subscribe ESP_LOGE(TAG, "Encrypted datapath not enabled (CONFIG_ESP_WIFI_NAN_SECURITY)"); return 0; #else - if (subscribe_cfg->security_cfg.num_credentials == 0 || - subscribe_cfg->security_cfg.num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { + if (!subscribe_cfg->security_cfg) { + ESP_LOGE(TAG, "security_reqd set but security_cfg is NULL"); + return 0; + } + if (subscribe_cfg->security_cfg->num_credentials == 0 || + subscribe_cfg->security_cfg->num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { ESP_LOGE(TAG, "security_cfg.num_credentials=%u must be 1..%u", - subscribe_cfg->security_cfg.num_credentials, + subscribe_cfg->security_cfg->num_credentials, ESP_WIFI_NAN_MAX_CREDS_PER_SVC); return 0; } - for (uint8_t i = 0; i < subscribe_cfg->security_cfg.num_credentials; i++) { - uint8_t csid = subscribe_cfg->security_cfg.creds[i].csid; + for (uint8_t i = 0; i < subscribe_cfg->security_cfg->num_credentials; i++) { + uint8_t csid = subscribe_cfg->security_cfg->creds[i].csid; if (csid != WIFI_NAN_CSID_NCS_SK_128) { ESP_LOGE(TAG, "creds[%u].csid=%u unsupported (only NCS-SK-128)", i, csid); return 0; @@ -1566,7 +1574,7 @@ uint8_t esp_wifi_nan_subscribe_service(const wifi_nan_subscribe_cfg_t *subscribe /* Pre-claim host slot BEFORE the blob's subscribe call; see comment on * the publish path for the watchdog rationale. */ if (!nan_claim_own_svc_slot(ESP_NAN_SUBSCRIBE, subscribe_cfg->service_name, - &subscribe_cfg->security_cfg)) { + subscribe_cfg->security_cfg)) { ESP_LOGE(TAG, "No free service slot"); goto fail; } @@ -1580,7 +1588,7 @@ uint8_t esp_wifi_nan_subscribe_service(const wifi_nan_subscribe_cfg_t *subscribe ESP_LOGI(TAG, "Started Subscribing to %s [Service ID - %u]", subscribe_cfg->service_name, sub_id); nan_finalize_own_svc(subscribe_cfg->service_name, (uint8_t) sub_id, false); #ifdef CONFIG_ESP_WIFI_NAN_SECURITY - nan_security_cache_subscriber_params(subscribe_cfg->service_name, &subscribe_cfg->security_cfg); + nan_security_cache_subscriber_params(subscribe_cfg->service_name, subscribe_cfg->security_cfg); #endif NAN_DATA_UNLOCK(); diff --git a/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild b/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild index 55dd37874c5..b336577c920 100644 --- a/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild +++ b/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild @@ -28,6 +28,12 @@ menu "Example Configuration" bool "Enable NAN Security" depends on ESP_WIFI_NAN_SECURITY default y + help + Enable encrypted NAN datapath (NDP Security) for this example. + When enabled the publisher advertises a secured service and + accepts only encrypted NDP setups from subscribers that share + the same credential (passphrase or PMK). + Disable to advertise an open (unencrypted) service. choice EXAMPLE_NAN_SECURITY_METHOD prompt "Security Method" 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 56c6995b5d2..47be5ab4c0b 100644 --- a/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c +++ b/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c @@ -129,25 +129,28 @@ void wifi_nan_publish(void) .datapath_reqd = 1, #ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED .security_reqd = 1, - .security_cfg = { - .num_credentials = 1, - .creds = { - { - .csid = WIFI_NAN_CSID_NCS_SK_128, -#ifdef CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK - .use_pmk = true, -#else - .use_pmk = false, - .passphrase = CONFIG_EXAMPLE_NAN_PASSPHRASE, -#endif - }, - }, - }, #endif }; +#ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED + wifi_nan_discovery_security_params_t security_cfg = { + .num_credentials = 1, + .creds = { + { + .csid = WIFI_NAN_CSID_NCS_SK_128, +#ifdef CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK + .use_pmk = true, +#else + .use_pmk = false, + .passphrase = CONFIG_EXAMPLE_NAN_PASSPHRASE, +#endif + }, + }, + }; + publish_cfg.security_cfg = &security_cfg; +#endif #if defined(CONFIG_EXAMPLE_NAN_SECURITY_ENABLED) && defined(CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK) - if (!decode_hex_string(CONFIG_EXAMPLE_NAN_PMK, publish_cfg.security_cfg.creds[0].pmk, - sizeof(publish_cfg.security_cfg.creds[0].pmk))) { + if (!decode_hex_string(CONFIG_EXAMPLE_NAN_PMK, security_cfg.creds[0].pmk, + sizeof(security_cfg.creds[0].pmk))) { ESP_LOGE(TAG, "Failed to decode CONFIG_EXAMPLE_NAN_PMK"); return; } diff --git a/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild b/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild index be176366843..36c87d4eaa7 100644 --- a/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild +++ b/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild @@ -38,6 +38,12 @@ menu "Example Configuration" bool "Enable NAN Security" depends on ESP_WIFI_NAN_SECURITY default y + help + Enable encrypted NAN datapath (NDP Security) for this example. + When enabled the subscriber only matches secured Publish SDFs + whose PMKID is derivable from the credential configured below + (passphrase or PMK) and sets up an encrypted NDP. + Disable to discover open (unencrypted) services. choice EXAMPLE_NAN_SECURITY_METHOD prompt "Security Method" 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 9b6d4fcf81b..870cc0e7cc9 100644 --- a/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c +++ b/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c @@ -223,25 +223,28 @@ void wifi_nan_subscribe(void) .single_match_event = 1, #ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED .security_reqd = 1, - .security_cfg = { - .num_credentials = 1, - .creds = { - { - .csid = WIFI_NAN_CSID_NCS_SK_128, -#ifdef CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK - .use_pmk = true, -#else - .use_pmk = false, - .passphrase = CONFIG_EXAMPLE_NAN_PASSPHRASE, -#endif - }, - }, - }, #endif }; +#ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED + wifi_nan_discovery_security_params_t security_cfg = { + .num_credentials = 1, + .creds = { + { + .csid = WIFI_NAN_CSID_NCS_SK_128, +#ifdef CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK + .use_pmk = true, +#else + .use_pmk = false, + .passphrase = CONFIG_EXAMPLE_NAN_PASSPHRASE, +#endif + }, + }, + }; + subscribe_cfg.security_cfg = &security_cfg; +#endif #if defined(CONFIG_EXAMPLE_NAN_SECURITY_ENABLED) && defined(CONFIG_EXAMPLE_NAN_SEC_METHOD_PMK) - if (!decode_hex_string(CONFIG_EXAMPLE_NAN_PMK, subscribe_cfg.security_cfg.creds[0].pmk, - sizeof(subscribe_cfg.security_cfg.creds[0].pmk))) { + if (!decode_hex_string(CONFIG_EXAMPLE_NAN_PMK, security_cfg.creds[0].pmk, + sizeof(security_cfg.creds[0].pmk))) { ESP_LOGE(TAG, "Failed to decode CONFIG_EXAMPLE_NAN_PMK"); return; } From a17b3e97c6449de40a6050a4ec2e409fb4ae6cd6 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Mon, 18 May 2026 12:32:35 +0530 Subject: [PATCH 8/8] fix(wifi): NAN per-service subscriber security state The singleton subscriber security cache was overwritten on each new subscribe, breaking PMKID match for all but the most recent service. Move security state to own_svc, matched cred index to peer_svc. --- .../esp_wifi/wifi_apps/nan_app/src/nan_app.c | 50 +++--- .../esp_wifi/wifi_apps/nan_app/src/nan_i.h | 35 +++-- .../wifi_apps/nan_app/src/nan_security.c | 148 ++++-------------- 3 files changed, 79 insertions(+), 154 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 ab6f4765605..fd5a7ea1c45 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 @@ -196,6 +196,9 @@ static bool nan_record_peer_svc(uint8_t own_svc_id, uint8_t peer_svc_id, uint8_t p_peer_svc->type = (p_own_svc->type == ESP_NAN_SUBSCRIBE) ? ESP_NAN_PUBLISH : ESP_NAN_SUBSCRIBE; MACADDR_COPY(p_peer_svc->peer_nmi, peer_nmi); p_peer_svc->device_caps = device_caps; +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + p_peer_svc->matched_cred_idx = 0xFF; /* NAN_NO_MATCHED_CRED — set by service_match if PMKID hits */ +#endif if (p_own_svc->num_peer_records < NAN_MAX_PEERS_RECORD) { SLIST_INSERT_HEAD(&(p_own_svc->peer_list), p_peer_svc, next); p_own_svc->num_peer_records++; @@ -588,21 +591,33 @@ void nan_app_service_match_cb(uint8_t sub_id, struct nan_cb_peer_info *peer_info ESP_LOGI(TAG, "Service matched with capabilities: 0x%04x", capab); #ifdef CONFIG_ESP_WIFI_NAN_SECURITY - /* Service-match security gate: - * 1. Subscriber configured no credentials (open subscribe) -> accept - * regardless of what the peer advertised. NDP will be open. - * 2. Subscriber configured credentials (secure subscribe) -> peer - * must have advertised PMKIDs AND at least one of them must match - * one of the local creds, else drop. */ - if (nan_security_subscriber_has_creds()) { + /* Service-match security gate, keyed by the local subscribe (sub_id): + * 1. This subscribe was created without credentials (open subscribe) -> + * accept regardless of what the peer advertised. NDP will be open. + * 2. This subscribe was created with credentials (secure subscribe) -> + * peer must have advertised PMKIDs AND at least one of them must + * match one of this subscribe's creds, else drop. + * Reading per-subscribe state from own_svc (instead of a singleton cache) + * disambiguates concurrent secure subscribes — each service uses its own + * service_name + creds[] in the PMKID derivation. */ + NAN_DATA_LOCK(); + struct own_svc_info *p_own_svc = nan_find_own_svc(sub_id); + struct peer_svc_info *p_peer_svc_sec = nan_find_peer_svc(sub_id, pub_id, pub_mac); + bool has_creds = p_own_svc && p_own_svc->user_cfg.num_credentials > 0; + NAN_DATA_UNLOCK(); + + if (has_creds) { if (!peer_info->peer_security_params || peer_info->peer_security_params->num_pmkids == 0) { - ESP_LOGW(TAG, "Secure subscribe: peer "MACSTR" advertises no PMKIDs", - MAC2STR(pub_mac)); + ESP_LOGW(TAG, "Secure subscribe '%s': peer "MACSTR" advertises no PMKIDs", + p_own_svc ? p_own_svc->svc_name : "?", MAC2STR(pub_mac)); return; } - if (!nan_security_service_match(pub_mac, peer_info->peer_security_params)) { - ESP_LOGW(TAG, "PMKID mismatch with "MACSTR, MAC2STR(pub_mac)); + if (!p_peer_svc_sec || + !nan_security_service_match(p_own_svc, p_peer_svc_sec, + pub_mac, peer_info->peer_security_params)) { + ESP_LOGW(TAG, "PMKID mismatch: svc='%s' peer="MACSTR, + p_own_svc ? p_own_svc->svc_name : "?", MAC2STR(pub_mac)); return; } } @@ -1522,10 +1537,6 @@ uint8_t esp_wifi_nan_subscribe_service(const wifi_nan_subscribe_cfg_t *subscribe } ESP_LOGI(TAG, "Started Subscribing to %s [Service ID - %u]", subscribe_cfg->service_name, sub_id); -#ifdef CONFIG_ESP_WIFI_NAN_SECURITY - nan_security_cache_subscriber_params(subscribe_cfg->service_name, - subscribe_cfg->security_cfg); -#endif return sub_id; } #endif /* CONFIG_ESP_WIFI_NAN_USD_ENABLE */ @@ -1587,9 +1598,6 @@ uint8_t esp_wifi_nan_subscribe_service(const wifi_nan_subscribe_cfg_t *subscribe ESP_LOGI(TAG, "Started Subscribing to %s [Service ID - %u]", subscribe_cfg->service_name, sub_id); nan_finalize_own_svc(subscribe_cfg->service_name, (uint8_t) sub_id, false); -#ifdef CONFIG_ESP_WIFI_NAN_SECURITY - nan_security_cache_subscriber_params(subscribe_cfg->service_name, subscribe_cfg->security_cfg); -#endif NAN_DATA_UNLOCK(); return sub_id; @@ -1786,7 +1794,11 @@ uint8_t esp_wifi_nan_datapath_req(wifi_nan_datapath_req_t *req) req->peer_mac, saved_device_caps); if (preclaimed) { - nan_security_populate_initiator_ndl(preclaimed, p_peer_svc->peer_nmi); + /* p_peer_svc carries the per-(svc, peer) cred index remembered at SDF + * RX; own_svc supplies svc_name + creds[] for this specific subscribe. */ + struct own_svc_info *p_own_svc_init = nan_find_own_svc(p_peer_svc->own_svc_id); + nan_security_populate_initiator_ndl(preclaimed, p_own_svc_init, p_peer_svc, + p_peer_svc->peer_nmi); } #endif 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 c89d0cb75f0..e5d0762a997 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 @@ -152,6 +152,13 @@ struct peer_svc_info { uint8_t type; uint8_t peer_nmi[MACADDR_LEN]; uint32_t device_caps; +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + /* Per-(own_svc, peer) result of subscriber-side PMKID match at SDF RX. + * 0xFF = no match; otherwise the index into own_svc->user_cfg.creds[] + * 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; +#endif }; /* Own (locally registered) service info */ @@ -328,29 +335,26 @@ esp_err_t nan_derive_security_params(const char *service_name, * the initiator pre-claim window (peer-only lookup). */ uint16_t nan_get_ndp_security_csid(uint8_t ndp_id, const uint8_t *peer_nmi); -/* Subscribe path: cache security_cfg + service name for PMKID verification on match. */ -void nan_security_cache_subscriber_params(const char *service_name, - const wifi_nan_discovery_security_params_t *security_cfg); - /* Subscribe path: populate the initiator NDL's security_ctx (cipher + pair-PMKID + - * ND-PMK) from the cached subscriber params, so the blob's CSIA/SCIA/Shared-Key - * Descriptor builder callbacks find security keyed by (ndp_id, peer_nmi). - * No-op if subscriber didn't request encrypted datapath. */ + * ND-PMK) from the local subscribe's own_svc and the SDF-time matched credential + * recorded on peer_svc. The blob's CSIA / SCIA / Shared-Key-Descriptor builder + * callbacks look up security keyed by (ndp_id, peer_nmi). No-op if the local + * subscribe didn't request encrypted datapath. */ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, + const struct own_svc_info *own_svc, + const struct peer_svc_info *peer_svc, const uint8_t *peer_nmi); /* * Compare locally derived ND-PMKID (subscriber passphrase, publisher NMI) to - * peer discovery security params. publisher_nmi is the publisher's NAN MAC - * from the service-match callback. + * peer discovery security params. own_svc identifies the local subscribe + * (service_name + creds[]) and peer_svc is updated with matched_cred_idx on + * success. Returns true if any local cred's PMKID matches a peer-advertised one. */ -bool nan_security_service_match(const uint8_t *publisher_nmi, +bool nan_security_service_match(const struct own_svc_info *own_svc, + struct peer_svc_info *peer_svc, + const uint8_t *publisher_nmi, const wifi_nan_peer_sdf_security_t *peer_sec); - -/* Returns true if the local subscriber configured at least one credential - * via the most recent esp_wifi_nan_subscribe_service(). Used by the - * service-match gate to decide whether to PMKID-validate an incoming peer. */ -bool nan_security_subscriber_has_creds(void); #else static inline esp_err_t nan_derive_security_params(const char *service_name, const wifi_nan_discovery_security_params_t *sec_cfg, @@ -361,7 +365,6 @@ static inline esp_err_t nan_derive_security_params(const char *service_name, (void)out_derived; return ESP_FAIL; } -static inline bool nan_security_subscriber_has_creds(void) { return false; } #endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ #ifdef __cplusplus 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 654b3c19aa2..43547e44c43 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 @@ -63,77 +63,8 @@ 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) -/* Cached subscribe-side discovery security (passphrase / PMK, service name). */ -static struct { - bool valid; - char service_name[ESP_WIFI_MAX_SVC_NAME_LEN]; - wifi_nan_discovery_security_params_t security_cfg; -} s_nan_subscriber_sec_cache; - -/* Per-peer subscriber match memory: at service-match time - * nan_security_service_match identifies which local credential matched the - * publisher; we remember (publisher NMI -> matched cred index) so that the - * later NDP-init path (nan_security_populate_initiator_ndl) derives M1's pair - * PMKID from the *correct* credential instead of always cred[0]. Sized at - * NAN_MAX_PEERS_RECORD to cover the realistic peer count; round-robin - * eviction on overflow. */ -#define NAN_SUB_MATCH_CACHE_SIZE NAN_MAX_PEERS_RECORD -static struct { - bool used; - uint8_t peer_nmi[ETH_ALEN]; - uint8_t matched_cred_idx; -} s_nan_subscriber_match_cache[NAN_SUB_MATCH_CACHE_SIZE]; -static uint8_t s_nan_subscriber_match_cursor; - -static void nan_subscriber_match_remember(const uint8_t *peer_nmi, uint8_t cred_idx) -{ - if (!peer_nmi) { - return; - } - /* Update existing entry for this peer if present. */ - for (uint8_t i = 0; i < NAN_SUB_MATCH_CACHE_SIZE; i++) { - if (s_nan_subscriber_match_cache[i].used && - memcmp(s_nan_subscriber_match_cache[i].peer_nmi, peer_nmi, ETH_ALEN) == 0) { - s_nan_subscriber_match_cache[i].matched_cred_idx = cred_idx; - return; - } - } - /* First free slot. */ - for (uint8_t i = 0; i < NAN_SUB_MATCH_CACHE_SIZE; i++) { - if (!s_nan_subscriber_match_cache[i].used) { - s_nan_subscriber_match_cache[i].used = true; - memcpy(s_nan_subscriber_match_cache[i].peer_nmi, peer_nmi, ETH_ALEN); - s_nan_subscriber_match_cache[i].matched_cred_idx = cred_idx; - return; - } - } - /* Full: round-robin evict. */ - uint8_t idx = s_nan_subscriber_match_cursor; - s_nan_subscriber_match_cursor = (uint8_t)((s_nan_subscriber_match_cursor + 1) % NAN_SUB_MATCH_CACHE_SIZE); - s_nan_subscriber_match_cache[idx].used = true; - memcpy(s_nan_subscriber_match_cache[idx].peer_nmi, peer_nmi, ETH_ALEN); - s_nan_subscriber_match_cache[idx].matched_cred_idx = cred_idx; -} - -static int nan_subscriber_match_recall(const uint8_t *peer_nmi) -{ - if (!peer_nmi) { - return -1; - } - for (uint8_t i = 0; i < NAN_SUB_MATCH_CACHE_SIZE; i++) { - if (s_nan_subscriber_match_cache[i].used && - memcmp(s_nan_subscriber_match_cache[i].peer_nmi, peer_nmi, ETH_ALEN) == 0) { - return (int)s_nan_subscriber_match_cache[i].matched_cred_idx; - } - } - return -1; -} - -static void nan_subscriber_match_clear(void) -{ - memset(s_nan_subscriber_match_cache, 0, sizeof(s_nan_subscriber_match_cache)); - s_nan_subscriber_match_cursor = 0; -} +/* Sentinel for peer_svc->matched_cred_idx: no PMKID match remembered yet. */ +#define NAN_NO_MATCHED_CRED 0xFF /*------------------------------------------------------------------------- * Static helpers @@ -846,31 +777,9 @@ uint16_t nan_get_ndp_security_csid(uint8_t ndp_id, const uint8_t *peer_nmi) return csid; } -void nan_security_cache_subscriber_params(const char *service_name, - const wifi_nan_discovery_security_params_t *security_cfg) -{ - /* New (or cleared) subscribe session: wipe any stale per-peer match - * memory from a previous subscriber. */ - nan_subscriber_match_clear(); - - if (!service_name || !security_cfg) { - s_nan_subscriber_sec_cache.valid = false; - return; - } - strlcpy(s_nan_subscriber_sec_cache.service_name, service_name, - sizeof(s_nan_subscriber_sec_cache.service_name)); - memcpy(&s_nan_subscriber_sec_cache.security_cfg, security_cfg, - sizeof(wifi_nan_discovery_security_params_t)); - s_nan_subscriber_sec_cache.valid = true; -} - -bool nan_security_subscriber_has_creds(void) -{ - return s_nan_subscriber_sec_cache.valid && - s_nan_subscriber_sec_cache.security_cfg.num_credentials > 0; -} - -bool nan_security_service_match(const uint8_t *publisher_nmi, +bool nan_security_service_match(const struct own_svc_info *own_svc, + struct peer_svc_info *peer_svc, + const uint8_t *publisher_nmi, const wifi_nan_peer_sdf_security_t *peer_sec) { uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; @@ -879,20 +788,20 @@ bool nan_security_service_match(const uint8_t *publisher_nmi, uint8_t hash[32]; bool matched = false; - if (!s_nan_subscriber_sec_cache.valid || !publisher_nmi || !peer_sec) { + if (!own_svc || !peer_svc || !publisher_nmi || !peer_sec) { return false; } if (peer_sec->num_pmkids == 0) { return false; } - if (!nan_compute_service_id(s_nan_subscriber_sec_cache.service_name, service_id)) { + if (!nan_compute_service_id(own_svc->svc_name, service_id)) { return false; } if (!g_wifi_default_wpa_crypto_funcs.hmac_sha256_vector) { return false; } - const wifi_nan_discovery_security_params_t *cfg = &s_nan_subscriber_sec_cache.security_cfg; + const wifi_nan_discovery_security_params_t *cfg = &own_svc->user_cfg; if (cfg->num_credentials == 0 || cfg->num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { return false; @@ -929,13 +838,13 @@ bool nan_security_service_match(const uint8_t *publisher_nmi, for (uint8_t i = 0; i < peer_sec->num_pmkids && i < NAN_PEER_MAX_PMKIDS; i++) { if (memcmp(pmkid, peer_sec->pmkids[i], ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) { - ESP_LOGD(TAG, "ND-PMKID match: cred[%u] vs peer SCID[%u], publisher "MACSTR, - c, i, MAC2STR(publisher_nmi)); + ESP_LOGD(TAG, "ND-PMKID match: svc='%s' cred[%u] vs peer SCID[%u], publisher "MACSTR, + own_svc->svc_name, c, i, MAC2STR(publisher_nmi)); ESP_LOG_BUFFER_HEXDUMP(TAG, pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN, ESP_LOG_INFO); - /* Remember which local cred matched this publisher so the - * later NDP-init path (nan_security_populate_initiator_ndl) - * uses the right cred for M1's pair-PMKID derivation. */ - nan_subscriber_match_remember(publisher_nmi, c); + /* Remember which local cred matched, so the initiator NDP-req + * path (nan_security_populate_initiator_ndl) derives M1's + * pair-PMKID from the right credential. */ + peer_svc->matched_cred_idx = c; matched = true; goto done; } @@ -959,32 +868,33 @@ done: * publish-time discovery PMKID (which uses broadcast IAddr). */ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, + const struct own_svc_info *own_svc, + const struct peer_svc_info *peer_svc, const uint8_t *peer_nmi) { - if (!ndl || !peer_nmi) { + if (!ndl || !own_svc || !peer_nmi) { return ESP_FAIL; } - if (!s_nan_subscriber_sec_cache.valid) { - return ESP_OK; /* subscriber didn't cache security; open NDP */ - } - const wifi_nan_discovery_security_params_t *cfg = &s_nan_subscriber_sec_cache.security_cfg; + const wifi_nan_discovery_security_params_t *cfg = &own_svc->user_cfg; if (cfg->num_credentials == 0) { return ESP_OK; /* subscriber didn't request encrypted datapath */ } - /* Use the credential that matched this publisher at discovery time - * (remembered by nan_security_service_match -> nan_subscriber_match_remember). - * Falls back to creds[0] only when no match memory exists for this peer - * (e.g., subscriber initiated NDP without a prior service match — atypical). */ - int matched_idx = nan_subscriber_match_recall(peer_nmi); - uint8_t cred_idx = (matched_idx >= 0) ? (uint8_t)matched_idx : 0; + /* Use the credential that matched this publisher at SDF RX (stored on the + * peer_svc by nan_security_service_match). Falls back to creds[0] only + * when no SDF-time match was recorded (atypical: subscriber initiated NDP + * without a prior service match). */ + uint8_t matched_idx = peer_svc ? peer_svc->matched_cred_idx : NAN_NO_MATCHED_CRED; + uint8_t cred_idx = (matched_idx != NAN_NO_MATCHED_CRED) ? matched_idx : 0; if (cred_idx >= cfg->num_credentials) { cred_idx = 0; } const wifi_nan_credential_t *cred = &cfg->creds[cred_idx]; - ESP_LOGD(TAG, "NDP req security: using cred[%u] for peer "MACSTR + ESP_LOGD(TAG, "NDP req security: svc='%s' using cred[%u] for peer "MACSTR " (matched_idx=%d, num_creds=%u)", - cred_idx, MAC2STR(peer_nmi), matched_idx, cfg->num_credentials); + own_svc->svc_name, cred_idx, MAC2STR(peer_nmi), + (matched_idx == NAN_NO_MATCHED_CRED) ? -1 : (int)matched_idx, + cfg->num_credentials); uint8_t pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; uint8_t service_id[6]; @@ -995,7 +905,7 @@ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, ESP_LOGE(TAG, "NDP req security: get own NMI failed"); goto cleanup; } - if (!nan_compute_service_id(s_nan_subscriber_sec_cache.service_name, service_id)) { + if (!nan_compute_service_id(own_svc->svc_name, service_id)) { ESP_LOGE(TAG, "NDP req security: service id derivation failed"); goto cleanup; }