From 94f226d73b731e7d902de03899eb3c0f2d327152 Mon Sep 17 00:00:00 2001 From: Sarvesh Bodakhe Date: Mon, 11 May 2026 16:18:10 +0530 Subject: [PATCH] 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 */