diff --git a/components/esp_wifi/include/esp_private/wifi.h b/components/esp_wifi/include/esp_private/wifi.h index 9331077f919..6047ad67a1f 100644 --- a/components/esp_wifi/include/esp_private/wifi.h +++ b/components/esp_wifi/include/esp_private/wifi.h @@ -67,8 +67,8 @@ typedef struct { uint16_t csid_bitmap; /**< Selected Cipher Suite ID bit (WIFI_NAN_CSID_BIT_*) */ uint8_t nd_pmk[ESP_WIFI_NAN_NDP_PMK_LEN]; /**< ND-PMK */ uint8_t nd_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN]; /**< ND-PMKID */ - uint8_t group_data_prot: 1; /**< Group addressed data frame protection. Reserved: not supported right now. */ - uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection. Reserved: not supported right now. */ + uint8_t group_data_prot: 1; /**< Group addressed data frame protection (GTKSA): distribute a GTK on the secured NDP so multicast data frames are protected. */ + uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection (IGTKSA/BIGTKSA): BIP-protect multicast SDFs and Beacons. */ uint8_t reserved: 6; /**< Reserved */ } wifi_nan_security_params_t; @@ -89,10 +89,11 @@ typedef struct { uint8_t num_pmkids; /**< Number of parsed PMKIDs */ uint8_t pmkids[NAN_PEER_MAX_PMKIDS][ESP_WIFI_NAN_NDP_PMKID_LEN]; /**< Parsed ND-PMKIDs */ uint8_t group_data_prot: 1; /**< Peer advertises group data frame protection */ - uint8_t group_mgmt_prot: 1; /**< Peer advertises group mgmt frame protection */ + uint8_t group_mgmt_prot: 1; /**< Peer advertises IGTKSA (CSIA caps bits 1-2 != 0) */ uint8_t pairing_setup: 1; /**< Pairing setup: 0 - disabled, 1 - enabled */ uint8_t npk_nik_caching: 1; /**< NPK/NIK caching: 0 - disabled, 1 - enabled (valid if pairing_setup) */ - uint8_t reserved: 4; /**< Reserved */ + uint8_t group_bigtk_prot: 1; /**< Peer advertises BIGTKSA (CSIA caps bits 1-2 == 10) */ + uint8_t reserved: 3; /**< Reserved */ } wifi_nan_peer_sdf_security_t; /* NAN Peer info parsed from SDF */ @@ -230,9 +231,11 @@ struct nan_secure_dp_funcs { * with the given Key Data field length. */ uint32_t (*get_shared_key_desc_attr_len)(uint16_t key_data_len); - /* Byte length of the M4 Shared Key Descriptor including any - * encrypted KDE payload (GTK/IGTK/BIGTK). */ - int (*ndp_security_install_get_shared_desc_len)(void); + /* Exact byte length of the M4 (NDP Security Install) Shared Key Descriptor + * attribute for this NDP, including any encrypted KDE payload (GTK/IGTK/BIGTK). + * @ndp_id + @peer_nmi identify the NDL so the host returns the exact length the + * builder will write (no over-reservation / on-air zero-padding). */ + int (*ndp_security_install_get_shared_desc_len)(uint8_t ndp_id, const uint8_t *peer_nmi); uint8_t (*get_ndp_resp_num_pmkids)(uint8_t ndp_id, const uint8_t *peer_nmi); uint32_t (*get_ndp_resp_shared_key_desc_len)(uint8_t ndp_id, const uint8_t *peer_nmi); @@ -362,10 +365,21 @@ struct nan_secure_dp_funcs { const wifi_nan_discovery_security_params_t *sec_cfg, wifi_nan_security_params_t *out_derived); - /* Returns the cipher-suite bitmap negotiated for an in-flight NDP, - * or 0 if the NDP is open. Used by RX/TX paths to decide whether - * to apply CCMP/GCMP and which key length to use. */ + /* Returns the full cipher-suite bitmap negotiated for an in-flight NDP + * (including the group cipher NCS-GTK when group-addressed data protection + * is in use), or 0 if the NDP is open. The blob treats this as an opaque + * value passed straight to the host CSIA callbacks (construct_csia / + * get_csia_len) to build the on-air M1/M3 CSIA own-bitmap; it must NOT + * interpret individual CSID bits. Pairwise cipher selection and key install + * are host-driven and independent of this value. */ uint16_t (*get_ndp_security_csid)(uint8_t ndp_id, const uint8_t *peer_nmi); + + /* Exact byte length of the M3 (NDP Confirm) Shared Key Descriptor attribute for + * this NDP, including any encrypted KDE payload (GTK/IGTK/BIGTK). @ndp_id + + * @peer_nmi identify the NDL. Mirrors ndp_security_install_get_shared_desc_len + * for the initiator path. Appended at the end of the struct to preserve the + * layout of the fields above. */ + int (*ndp_confirm_get_shared_desc_len)(uint8_t ndp_id, const uint8_t *peer_nmi); }; /** @@ -1195,7 +1209,7 @@ esp_err_t esp_wifi_disconnect_internal(void); uint32_t esp_nan_get_nira_len(void); /** - * @brief Construct dummy NAN Identity Resolution Attribute (NIRA) + * @brief Construct NAN Identity Resolution Attribute (NIRA) * * @param[out] frm Buffer to write the attribute to * diff --git a/components/esp_wifi/include/esp_wifi_netif.h b/components/esp_wifi/include/esp_wifi_netif.h index 7dfa724b066..ca054e8a6e1 100644 --- a/components/esp_wifi/include/esp_wifi_netif.h +++ b/components/esp_wifi/include/esp_wifi_netif.h @@ -81,6 +81,42 @@ bool esp_wifi_is_if_ready_when_started(wifi_netif_driver_t ifx); */ esp_err_t esp_wifi_register_if_rxcb(wifi_netif_driver_t ifx, esp_netif_receive_t fn, void * arg); +/** + * @brief Derive an IPv6 link-local address from a link-layer (MAC) address + * + * Computes fe80::/64 combined with the EUI-64 form of the given MAC (the 802 + * group bit complemented) into an esp_ip6_addr_t (zone 0). Interface-agnostic. + * + * @param[out] ip6 destination, set to the derived IPv6 link-local address + * @param[in] mac source link-layer (MAC) address (6 bytes) + */ +void esp_wifi_netif_get_ip6_linklocal_from_mac(esp_ip6_addr_t *ip6, const uint8_t mac[6]); + +#if CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES +/** + * @brief Pin (or remove) a static IPv6 link-local neighbor mapping on a wifi netif + * + * Installs a fixed link-local IPv6 -> MAC mapping for a peer reachable on the + * given wifi interface so that traffic to the peer bypasses Neighbor Discovery + * (no NS/NA exchanged), or removes a previously installed one. This layer owns + * both the netif lookup (from the interface type) and the derivation of the + * peer's link-local address from its MAC, so the caller only supplies the + * interface and the peer MAC. Only available when lwIP static ND6 entries are + * enabled. + * + * @param[in] wifi_if wifi interface the peer is reachable on + * @param[in] mac peer's link-layer (MAC) address + * @param[in] add true to add the mapping, false to remove it + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if mac is NULL or wifi_if is out of range + * - ESP_ERR_INVALID_STATE if the interface's netif is not up + * - error code from the underlying esp_netif call otherwise + */ +esp_err_t esp_wifi_netif_set_static_neighbor(wifi_interface_t wifi_if, const uint8_t mac[6], bool add); +#endif /* CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES */ + #ifdef __cplusplus } #endif diff --git a/components/esp_wifi/include/esp_wifi_types_generic.h b/components/esp_wifi/include/esp_wifi_types_generic.h index 1a92443aa5a..cebdcb45497 100644 --- a/components/esp_wifi/include/esp_wifi_types_generic.h +++ b/components/esp_wifi/include/esp_wifi_types_generic.h @@ -606,6 +606,7 @@ typedef struct { bool disable_random_mac;/**< Disable the MAC Randomisation in NAN */ bool reset_current_nvs_creds; /**< Erase all NAN credentials (own NIK and cached peer NIK/NPK entries) saved in NVS before starting. */ bool use_nvs_for_caching; /**< Persist newly-learned peer credentials (NIK/NPK) to NVS so they survive across reboots. */ + bool group_mgmt_prot; /**< Device-global group management protection (IGTKSA/BIGTKSA): BIP-protect Beacons + multicast SDFs. Forces GTKSA on all secured services (CSIA caps cannot encode IGTK/BIGTK without GTKSA). */ } wifi_nan_sync_config_t; /** @@ -932,9 +933,9 @@ typedef enum { WIFI_NAN_CSID_NCS_SK_256 = 2, /**< NCS-SK-256 (PSK/Passphrase). Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_2WDH_128 = 3, /**< NCS-PK-2WDH-128. Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_2WDH_256 = 4, /**< NCS-PK-2WDH-256. Reserved: not supported right now. */ - WIFI_NAN_CSID_NCS_GTK_CCM_128 = 5, - WIFI_NAN_CSID_NCS_GTK_GCM_256 = 6, - WIFI_NAN_CSID_NCS_PK_PASN_128 = 7, /**< NCS-PK-PASN-128. Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_GTK_CCMP_128 = 5, /**< NCS-GTK-CCMP-128, the group-data cipher (GTKSA). Selected internally when group_data_prot is set; not user-selectable via csid_bitmap. */ + WIFI_NAN_CSID_NCS_GTK_GCMP_256 = 6, /**< NCS-GTK-GCMP-256. Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_PK_PASN_128 = 7, /**< NCS-PK-PASN-128 (NAN Pairing). Requires CONFIG_ESP_WIFI_NAN_PAIRING and the Wi-Fi Aware component (esp-wifi-apps); not usable with stand-alone ESP-IDF. */ WIFI_NAN_CSID_NCS_PK_PASN_256 = 8, /**< NCS-PK-PASN-256. Reserved: not supported right now. */ } wifi_nan_cipher_suite_id_t; @@ -942,8 +943,8 @@ typedef enum { #define WIFI_NAN_CSID_BIT_NCS_SK_256 (1 << WIFI_NAN_CSID_NCS_SK_256) #define WIFI_NAN_CSID_BIT_NCS_PK_2WDH_128 (1 << WIFI_NAN_CSID_NCS_PK_2WDH_128) #define WIFI_NAN_CSID_BIT_NCS_PK_2WDH_256 (1 << WIFI_NAN_CSID_NCS_PK_2WDH_256) -#define WIFI_NAN_CSID_BIT_NCS_GTK_CCM_128 (1 << WIFI_NAN_CSID_NCS_GTK_CCM_128) -#define WIFI_NAN_CSID_BIT_NCS_GTK_GCM_256 (1 << WIFI_NAN_CSID_NCS_GTK_GCM_256) +#define WIFI_NAN_CSID_BIT_NCS_GTK_CCMP_128 (1 << WIFI_NAN_CSID_NCS_GTK_CCMP_128) +#define WIFI_NAN_CSID_BIT_NCS_GTK_GCMP_256 (1 << WIFI_NAN_CSID_NCS_GTK_GCMP_256) #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_128 (1 << WIFI_NAN_CSID_NCS_PK_PASN_128) #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_256 (1 << WIFI_NAN_CSID_NCS_PK_PASN_256) @@ -974,8 +975,8 @@ typedef struct { * is computed by the stack as the union of each credential's @c csid. */ typedef struct { - uint8_t group_data_prot: 1; /**< Group addressed data frame protection. Reserved: not supported right now. */ - uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection. Reserved: not supported right now. */ + uint8_t group_data_prot: 1; /**< Group addressed data frame protection (GTKSA): distribute a GTK on the secured NDP so multicast data frames are protected. */ + uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection (IGTKSA/BIGTKSA): BIP-protect multicast SDFs and Beacons. */ uint8_t reserved: 6; /**< Reserved */ uint8_t num_credentials; /**< Number of valid entries in @c creds (0..ESP_WIFI_NAN_MAX_CREDS_PER_SVC). 0 = open service. */ wifi_nan_credential_t creds[ESP_WIFI_NAN_MAX_CREDS_PER_SVC]; /**< Credentials list. */ diff --git a/components/esp_wifi/lib b/components/esp_wifi/lib index 5c5338ebbd3..c9950ae98a8 160000 --- a/components/esp_wifi/lib +++ b/components/esp_wifi/lib @@ -1 +1 @@ -Subproject commit 5c5338ebbd32c72fcde1a46d28f0a2ce17b8b29b +Subproject commit c9950ae98a8a422a98cc80726c5e91d8b191b319 diff --git a/components/esp_wifi/remote/include/injected/esp_wifi_netif.h b/components/esp_wifi/remote/include/injected/esp_wifi_netif.h index 7dfa724b066..ca054e8a6e1 100644 --- a/components/esp_wifi/remote/include/injected/esp_wifi_netif.h +++ b/components/esp_wifi/remote/include/injected/esp_wifi_netif.h @@ -81,6 +81,42 @@ bool esp_wifi_is_if_ready_when_started(wifi_netif_driver_t ifx); */ esp_err_t esp_wifi_register_if_rxcb(wifi_netif_driver_t ifx, esp_netif_receive_t fn, void * arg); +/** + * @brief Derive an IPv6 link-local address from a link-layer (MAC) address + * + * Computes fe80::/64 combined with the EUI-64 form of the given MAC (the 802 + * group bit complemented) into an esp_ip6_addr_t (zone 0). Interface-agnostic. + * + * @param[out] ip6 destination, set to the derived IPv6 link-local address + * @param[in] mac source link-layer (MAC) address (6 bytes) + */ +void esp_wifi_netif_get_ip6_linklocal_from_mac(esp_ip6_addr_t *ip6, const uint8_t mac[6]); + +#if CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES +/** + * @brief Pin (or remove) a static IPv6 link-local neighbor mapping on a wifi netif + * + * Installs a fixed link-local IPv6 -> MAC mapping for a peer reachable on the + * given wifi interface so that traffic to the peer bypasses Neighbor Discovery + * (no NS/NA exchanged), or removes a previously installed one. This layer owns + * both the netif lookup (from the interface type) and the derivation of the + * peer's link-local address from its MAC, so the caller only supplies the + * interface and the peer MAC. Only available when lwIP static ND6 entries are + * enabled. + * + * @param[in] wifi_if wifi interface the peer is reachable on + * @param[in] mac peer's link-layer (MAC) address + * @param[in] add true to add the mapping, false to remove it + * + * @return + * - ESP_OK on success + * - ESP_ERR_INVALID_ARG if mac is NULL or wifi_if is out of range + * - ESP_ERR_INVALID_STATE if the interface's netif is not up + * - error code from the underlying esp_netif call otherwise + */ +esp_err_t esp_wifi_netif_set_static_neighbor(wifi_interface_t wifi_if, const uint8_t mac[6], bool add); +#endif /* CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES */ + #ifdef __cplusplus } #endif diff --git a/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h b/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h index 3980683f70a..4b6bb15b336 100644 --- a/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h +++ b/components/esp_wifi/remote/include/injected/esp_wifi_types_generic.h @@ -606,6 +606,7 @@ typedef struct { bool disable_random_mac;/**< Disable the MAC Randomisation in NAN */ bool reset_current_nvs_creds; /**< Erase all NAN credentials (own NIK and cached peer NIK/NPK entries) saved in NVS before starting. */ bool use_nvs_for_caching; /**< Persist newly-learned peer credentials (NIK/NPK) to NVS so they survive across reboots. */ + bool group_mgmt_prot; /**< Device-global group management protection (IGTKSA/BIGTKSA): BIP-protect Beacons + multicast SDFs. Forces GTKSA on all secured services (CSIA caps cannot encode IGTK/BIGTK without GTKSA). */ } wifi_nan_sync_config_t; /** @@ -932,9 +933,9 @@ typedef enum { WIFI_NAN_CSID_NCS_SK_256 = 2, /**< NCS-SK-256 (PSK/Passphrase). Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_2WDH_128 = 3, /**< NCS-PK-2WDH-128. Reserved: not supported right now. */ WIFI_NAN_CSID_NCS_PK_2WDH_256 = 4, /**< NCS-PK-2WDH-256. Reserved: not supported right now. */ - WIFI_NAN_CSID_NCS_GTK_CCM_128 = 5, - WIFI_NAN_CSID_NCS_GTK_GCM_256 = 6, - WIFI_NAN_CSID_NCS_PK_PASN_128 = 7, /**< NCS-PK-PASN-128. Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_GTK_CCMP_128 = 5, /**< NCS-GTK-CCMP-128, the group-data cipher (GTKSA). Selected internally when group_data_prot is set; not user-selectable via csid_bitmap. */ + WIFI_NAN_CSID_NCS_GTK_GCMP_256 = 6, /**< NCS-GTK-GCMP-256. Reserved: not supported right now. */ + WIFI_NAN_CSID_NCS_PK_PASN_128 = 7, /**< NCS-PK-PASN-128 (NAN Pairing). Requires CONFIG_WIFI_RMT_NAN_PAIRING and the Wi-Fi Aware component (esp-wifi-apps); not usable with stand-alone ESP-IDF. */ WIFI_NAN_CSID_NCS_PK_PASN_256 = 8, /**< NCS-PK-PASN-256. Reserved: not supported right now. */ } wifi_nan_cipher_suite_id_t; @@ -942,8 +943,8 @@ typedef enum { #define WIFI_NAN_CSID_BIT_NCS_SK_256 (1 << WIFI_NAN_CSID_NCS_SK_256) #define WIFI_NAN_CSID_BIT_NCS_PK_2WDH_128 (1 << WIFI_NAN_CSID_NCS_PK_2WDH_128) #define WIFI_NAN_CSID_BIT_NCS_PK_2WDH_256 (1 << WIFI_NAN_CSID_NCS_PK_2WDH_256) -#define WIFI_NAN_CSID_BIT_NCS_GTK_CCM_128 (1 << WIFI_NAN_CSID_NCS_GTK_CCM_128) -#define WIFI_NAN_CSID_BIT_NCS_GTK_GCM_256 (1 << WIFI_NAN_CSID_NCS_GTK_GCM_256) +#define WIFI_NAN_CSID_BIT_NCS_GTK_CCMP_128 (1 << WIFI_NAN_CSID_NCS_GTK_CCMP_128) +#define WIFI_NAN_CSID_BIT_NCS_GTK_GCMP_256 (1 << WIFI_NAN_CSID_NCS_GTK_GCMP_256) #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_128 (1 << WIFI_NAN_CSID_NCS_PK_PASN_128) #define WIFI_NAN_CSID_BIT_NCS_PK_PASN_256 (1 << WIFI_NAN_CSID_NCS_PK_PASN_256) @@ -974,8 +975,8 @@ typedef struct { * is computed by the stack as the union of each credential's @c csid. */ typedef struct { - uint8_t group_data_prot: 1; /**< Group addressed data frame protection. Reserved: not supported right now. */ - uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection. Reserved: not supported right now. */ + uint8_t group_data_prot: 1; /**< Group addressed data frame protection (GTKSA): distribute a GTK on the secured NDP so multicast data frames are protected. */ + uint8_t group_mgmt_prot: 1; /**< Group addressed management frame protection (IGTKSA/BIGTKSA): BIP-protect multicast SDFs and Beacons. */ uint8_t reserved: 6; /**< Reserved */ uint8_t num_credentials; /**< Number of valid entries in @c creds (0..ESP_WIFI_NAN_MAX_CREDS_PER_SVC). 0 = open service. */ wifi_nan_credential_t creds[ESP_WIFI_NAN_MAX_CREDS_PER_SVC]; /**< Credentials list. */ diff --git a/components/esp_wifi/src/wifi_default.c b/components/esp_wifi/src/wifi_default.c index 66ed536ac8a..c73f4294abc 100644 --- a/components/esp_wifi/src/wifi_default.c +++ b/components/esp_wifi/src/wifi_default.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2019-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2019-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -184,6 +184,11 @@ static void wifi_default_action_nan_started(void *arg, esp_event_base_t base, in if (s_wifi_netifs[WIFI_IF_NAN] != NULL) { wifi_start(s_wifi_netifs[WIFI_IF_NAN], base, event_id, data); esp_nan_action_start(s_wifi_netifs[WIFI_IF_NAN]); + /* Bring the netif up before esp_netif_create_ip6_linklocal() (a no-op unless + * netif_is_up()). esp_netif_up() is private, so use the public action handler; + * NAN is non-DHCP, so it only calls esp_netif_up() and ignores the event args. */ + esp_netif_action_connected(s_wifi_netifs[WIFI_IF_NAN], NULL, 0, NULL); + esp_netif_create_ip6_linklocal(s_wifi_netifs[WIFI_IF_NAN]); } } diff --git a/components/esp_wifi/src/wifi_netif.c b/components/esp_wifi/src/wifi_netif.c index 96d4135dd21..418b249479d 100644 --- a/components/esp_wifi/src/wifi_netif.c +++ b/components/esp_wifi/src/wifi_netif.c @@ -3,6 +3,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ +#include #include "esp_wifi.h" #include "esp_netif.h" #include "esp_log.h" @@ -181,3 +182,45 @@ esp_err_t esp_wifi_register_if_rxcb(wifi_netif_driver_t ifx, esp_netif_receive_t } return ESP_OK; } + +void esp_wifi_netif_get_ip6_linklocal_from_mac(esp_ip6_addr_t *ip6, const uint8_t mac[6]) +{ + if (ip6 == NULL || mac == NULL) { + return; + } + + /* fe80::/64 + EUI-64 of the MAC (802 group bit complemented), laid out in + * network byte order straight into esp_ip6_addr_t. Zone stays 0. */ + const uint8_t linklocal[16] = { + 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + (uint8_t)(mac[0] ^ 0x02), mac[1], mac[2], 0xff, + 0xfe, mac[3], mac[4], mac[5], + }; + memset(ip6, 0, sizeof(*ip6)); + memcpy(ip6->addr, linklocal, sizeof(linklocal)); +} + +#if CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES +esp_err_t esp_wifi_netif_set_static_neighbor(wifi_interface_t wifi_if, const uint8_t mac[6], bool add) +{ + if (mac == NULL || wifi_if >= MAX_WIFI_IFS) { + return ESP_ERR_INVALID_ARG; + } + + /* The netif handle is owned by this layer (recorded when the interface's RX + * callback is registered), so callers only need to supply the interface and + * the peer MAC. */ + esp_netif_t *esp_netif = s_wifi_netifs[wifi_if]; + if (esp_netif == NULL) { + return ESP_ERR_INVALID_STATE; + } + + /* Derive the peer's link-local address; esp_netif copies only the address + * words for static neighbor entries, so no zone handling is needed here. */ + esp_ip6_addr_t addr6; + esp_wifi_netif_get_ip6_linklocal_from_mac(&addr6, mac); + + return add ? esp_netif_add_static_neighbor(esp_netif, &addr6, mac) + : esp_netif_remove_static_neighbor(esp_netif, &addr6); +} +#endif /* CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES */ diff --git a/components/esp_wifi/wifi_apps/nan_app/include/esp_nan.h b/components/esp_wifi/wifi_apps/nan_app/include/esp_nan.h index a1cdabb441b..0d947bb78f3 100644 --- a/components/esp_wifi/wifi_apps/nan_app/include/esp_nan.h +++ b/components/esp_wifi/wifi_apps/nan_app/include/esp_nan.h @@ -24,6 +24,7 @@ extern "C" { .disable_random_mac = false, \ .reset_current_nvs_creds = false, \ .use_nvs_for_caching = false, \ + .group_mgmt_prot = false, \ }; #define NDP_STATUS_ACCEPTED 1 diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c index f3c82d637f0..6dca0787e16 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_app.c @@ -264,13 +264,48 @@ static void nan_app_clear_one_peer_tks(const uint8_t *peer_nmi) key_rsc, sizeof(key_rsc), NULL, 0, NAN_KEY_ND_TK); + /* Drop the peer's RX GTK (bound to the peer NDI) and wipe local GTK + * state. The TX GTK keyed on the local NDI is released by the blob + * when the NDI/interface is torn down. */ + esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, + key_addr, ndl->gtk_keyid, 0, + key_rsc, sizeof(key_rsc), + NULL, 0, NAN_KEY_ND_GTK); + + /* Drop the peer's RX IGTK/BIGTK (BIP-CMAC-128, installed against the + * peer NMI at NDP confirm) so no stale BIP keys linger in the blob. */ + if (ndl->igtk_set) { + esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + (uint8_t *)peer_nmi, ndl->igtk_keyid, 0, + key_rsc, 6, + NULL, 0, NAN_KEY_ND_IGTK); + } + if (ndl->bigtk_set) { + esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + (uint8_t *)peer_nmi, ndl->bigtk_keyid, 0, + key_rsc, 6, + NULL, 0, NAN_KEY_ND_BIGTK); + } + forced_memzero(ndl->nd_tk, sizeof(ndl->nd_tk)); forced_memzero(ndl->nd_kck, sizeof(ndl->nd_kck)); forced_memzero(ndl->nd_kek, sizeof(ndl->nd_kek)); + forced_memzero(ndl->gtk, sizeof(ndl->gtk)); + forced_memzero(ndl->own_gtk, sizeof(ndl->own_gtk)); + forced_memzero(ndl->igtk, sizeof(ndl->igtk)); + forced_memzero(ndl->bigtk, sizeof(ndl->bigtk)); ndl->ptk_set = 0; ndl->tk_len = 0; ndl->kck_len = 0; ndl->kek_len = 0; + ndl->gtk_set = 0; + ndl->own_gtk_set = 0; + ndl->gtk_len = 0; + ndl->own_gtk_len = 0; + ndl->igtk_set = 0; + ndl->bigtk_set = 0; + ndl->igtk_len = 0; + ndl->bigtk_len = 0; } /* Fallback when no NDL slot tracks peer_ndi yet. */ @@ -337,20 +372,12 @@ void esp_wifi_nan_get_ipv6_linklocal_from_mac(ip6_addr_t *ip6, uint8_t *mac_addr if (ip6 == NULL || mac_addr == NULL) { return; } - /* Link-local prefix. */ - ip6->addr[0] = htonl(0xfe800000ul); - ip6->addr[1] = 0; - - /* Assume hwaddr is a 48-bit IEEE 802 MAC. Convert to EUI-64 address. Complement Group bit. */ - ip6->addr[2] = htonl((((uint32_t)(mac_addr[0] ^ 0x02)) << 24) | - ((uint32_t)(mac_addr[1]) << 16) | - ((uint32_t)(mac_addr[2]) << 8) | - (0xff)); - ip6->addr[3] = htonl((uint32_t)(0xfeul << 24) | - ((uint32_t)(mac_addr[3]) << 16) | - ((uint32_t)(mac_addr[4]) << 8) | - (mac_addr[5])); - + /* Reuse the interface-agnostic derivation in the esp_wifi netif layer, then + * copy the address words into the lwIP ip6_addr_t. The two structures share + * the same layout, which is how esp_netif converts between them. */ + esp_ip6_addr_t esp_ip6; + esp_wifi_netif_get_ip6_linklocal_from_mac(&esp_ip6, mac_addr); + memcpy(ip6->addr, esp_ip6.addr, sizeof(ip6->addr)); ip6->zone = IP6_NO_ZONE; } @@ -601,6 +628,18 @@ static struct own_svc_info *nan_claim_own_svc_slot(uint8_t type, const char svc_ forced_memzero(&p_svc->derived_security, sizeof(p_svc->derived_security)); if (security_cfg) { memcpy(&p_svc->user_cfg, security_cfg, sizeof(*security_cfg)); + /* Device-global group_mgmt_prot (IGTKSA/BIGTKSA) forces GTKSA on every + * secured service: the CSIA capability field has no "IGTK/BIGTK without + * GTKSA" encoding (§9.5.21.2 Table 122), so advertising group-management + * protection mandates advertising GTKSA. Warn and force it on if the app + * requested group_data_prot=0. Forcing support never blocks a peer that + * lacks protection — keys activate only after capability negotiation. */ + if (s_nan_ctx.group_mgmt_prot && !p_svc->user_cfg.group_data_prot) { + ESP_LOGW(TAG, "group_data_prot forced ON for '%s': group_mgmt_prot is " + "enabled device-wide; GTKSA cannot be advertised without it", + svc_name); + p_svc->user_cfg.group_data_prot = 1; + } } #ifdef CONFIG_ESP_WIFI_NAN_PAIRING if (pairing) { @@ -681,6 +720,7 @@ static void nan_record_new_ndl(uint8_t ndp_id, uint8_t publish_id, uint8_t peer_ if (ndl && reuse_slot) { ndl->ndp_id = ndp_id; ndl->own_role = own_role; + ndl->device_caps = device_caps; return; } if (ndl) { @@ -1024,7 +1064,6 @@ static void nan_app_replied_cb(uint8_t pub_id, struct nan_cb_peer_info *peer_inf evt->subscribe_id = sub_id; MACADDR_COPY(evt->sub_if_mac, sub_nmi); - ESP_LOGI(TAG, "Sent Publish to Peer "MACSTR" [Peer Subscribe id - %d]", MAC2STR(sub_nmi), sub_id); if (ssi && ssi_len) { memcpy(evt->ssi, ssi, ssi_len); evt->ssi_len = ssi_len; @@ -1036,8 +1075,8 @@ static void nan_app_replied_cb(uint8_t pub_id, struct nan_cb_peer_info *peer_inf } static void nan_app_receive_cb(uint8_t svc_id, struct nan_cb_peer_info *peer_info, - uint8_t *shared_key_attr, uint16_t shared_key_attr_buf_len, - struct nan_cb_npba_t *npba) + uint8_t *shared_key_attr, uint16_t shared_key_attr_buf_len, + struct nan_cb_npba_t *npba) { if (!peer_info) { return; @@ -1147,8 +1186,11 @@ static void nan_app_ndp_indication_cb(uint8_t pub_id, struct ndp_cb_peer_info *p nan_record_new_ndl(ndp_id, pub_id, peer_nmi, ESP_WIFI_NDP_ROLE_RESPONDER, device_caps); - if (!nan_find_peer_svc(pub_id, 0, peer_nmi)) { + struct peer_svc_info *p_peer_svc = nan_find_peer_svc(pub_id, 0, peer_nmi); + if (!p_peer_svc) { nan_record_peer_svc(pub_id, 0, peer_nmi, device_caps); + } else { + p_peer_svc->device_caps = device_caps; } struct ndl_info *ndl = nan_find_ndl(ndp_id, peer_nmi); @@ -1176,8 +1218,13 @@ static void nan_app_ndp_indication_cb(uint8_t pub_id, struct ndp_cb_peer_info *p uint8_t own_bssid[6]; esp_err_t err = esp_wifi_get_mac(WIFI_IF_NAN, own_bssid); if (err != ESP_OK) { + /* Cannot build the auto-response: free the NDL slot and deny the + * peer so it does not wait indefinitely. Send outside the lock. */ + ESP_LOGE(TAG, "get own NAN MAC failed, rc=0x%x; denying NDP ndp_id=%d", err, ndp_id); + nan_reset_ndl(ndp_id, false); NAN_DATA_UNLOCK(); - ESP_LOGE(TAG, "Cannot get own BSSID!"); + ndp_resp.accept = false; + esp_nan_internal_datapath_resp(&ndp_resp, (uint8_t *)&own_ipv6.u_addr.ip6.addr[2]); return; } esp_wifi_nan_get_ipv6_linklocal_from_mac(&own_ipv6.u_addr.ip6, own_bssid); @@ -1359,10 +1406,6 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer goto done; } -#ifndef NAN_KEY_ND_TK -#define NAN_KEY_ND_TK 0 -#endif - #ifdef CONFIG_ESP_WIFI_NAN_SECURITY if (ndl->security_ctx.type == WIFI_NAN_SECURITY_ENCRYPTED) { uint8_t key_rsc[8] = {0}; @@ -1375,12 +1418,103 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, NAN_KEY_ND_TK); + ESP_LOG_BUFFER_HEXDUMP("ND-TK", ndl->nd_tk, NAN_NCS_SK_128_TK_LEN, ESP_LOG_DEBUG); if (ret != 0) { - ESP_LOGE(TAG, "NDP confirm: failed to install NAN pairwise key (ndp_id=%d, ret=%d)", ndp_id, ret); + ESP_LOGE(TAG, "NDP confirm: failed to install ND-TK (ndp_id=%d, ret=%d)", ndp_id, ret); os_free(evt); nan_ndp_confirm_teardown(peer_nmi, ndp_id); goto done; } + ret = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, + peer_nmi, + 0, + 1, + key_rsc, + sizeof(key_rsc), + ndl->nd_tk, + NAN_NCS_SK_128_TK_LEN, + NAN_KEY_NM_TK); + if (ret != 0) { + ESP_LOGE(TAG, "NDP confirm: failed to install NM-TK (ndp_id=%d, ret=%d)", ndp_id, ret); + os_free(evt); + nan_ndp_confirm_teardown(peer_nmi, ndp_id); + goto done; + } + + /* Group keys (ND-GTK) exchanged during NDP setup (§7.1.3.2): our GTK + * is the TX key bound to the local NDI; the peer's GTK is the RX key + * bound to the peer NDI. Best-effort — a GTK install failure must not + * tear down the working unicast datapath. */ + if (ndl->own_gtk_set && ndl->own_gtk_len) { + int gret = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, + own_ndi, ndl->own_gtk_keyid, 1, + ndl->own_gtk_rsc, NAN_KEY_RSC_LEN, + ndl->own_gtk, ndl->own_gtk_len, + NAN_KEY_ND_GTK); + if (gret != 0) { + ESP_LOGW(TAG, "NDP confirm: own GTK (TX) install failed (ndp_id=%d, ret=%d)", ndp_id, gret); + } else { + ESP_LOGI(TAG, "NDP confirm: own GTK (TX) installed (keyid=%d)", ndl->own_gtk_keyid); + } + ESP_LOG_BUFFER_HEXDUMP("ND-GTK", ndl->own_gtk, ndl->own_gtk_len, ESP_LOG_DEBUG); + } + if (ndl->gtk_set && ndl->gtk_len) { + int gret = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_CCMP, + peer_ndi, ndl->gtk_keyid, 0, + ndl->gtk_rsc, NAN_KEY_RSC_LEN, + ndl->gtk, ndl->gtk_len, + NAN_KEY_ND_GTK); + if (gret != 0) { + ESP_LOGW(TAG, "NDP confirm: peer GTK (RX) install failed (ndp_id=%d, ret=%d)", ndp_id, gret); + } else { + ESP_LOGI(TAG, "NDP confirm: peer GTK (RX) installed (keyid=%d)", ndl->gtk_keyid); + } + } + + /* Own IGTK/BIGTK (TX) are installed once at NAN start (see + * nan_security_install_own_group_integrity_keys); not re-installed here, + * to preserve the blob's monotonic BIPN/IPN across the session. Only the + * peer RX keys are bound at NDP confirm. §7.1.3.3/§7.1.3.4; NMI==NDI today. + * Peer IGTK/BIGTK install RX-only against the peer NMI, seeding the BIP + * RX replay counter with the peer's advertised IPN/BIPN from the KDE. */ + if (ndl->igtk_set && ndl->igtk_len) { + if (ndl->igtk_len != NAN_ND_GTK_LEN) { + ESP_LOGW(TAG, "NDP confirm: peer IGTK len=%d unsupported (BIP-CMAC-128 only); skipping", + ndl->igtk_len); + } else { + int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + peer_nmi, ndl->igtk_keyid, 0, + ndl->igtk_ipn, 6, + ndl->igtk, ndl->igtk_len, + NAN_KEY_ND_IGTK); + if (r != 0) { + ESP_LOGW(TAG, "NDP confirm: peer IGTK (RX) install failed, rc=0x%x (ndp_id=%d)", r, ndp_id); + } else { + ESP_LOGI(TAG, "NDP confirm: peer IGTK (RX) installed (keyid=%d)", ndl->igtk_keyid); + } + /* Peer IGTK bytes for sniffer MIC cross-check vs the peer's multicast SDFs. */ + ESP_LOG_BUFFER_HEXDUMP("PEER ND-IGTK", ndl->igtk, ndl->igtk_len, ESP_LOG_DEBUG); + } + } + if (ndl->bigtk_set && ndl->bigtk_len) { + if (ndl->bigtk_len != NAN_ND_GTK_LEN) { + ESP_LOGW(TAG, "NDP confirm: peer BIGTK len=%d unsupported (BIP-CMAC-128 only); skipping", + ndl->bigtk_len); + } else { + int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + peer_nmi, ndl->bigtk_keyid, 0, + ndl->bigtk_ipn, 6, + ndl->bigtk, ndl->bigtk_len, + NAN_KEY_ND_BIGTK); + if (r != 0) { + ESP_LOGW(TAG, "NDP confirm: peer BIGTK (RX) install failed, rc=0x%x (ndp_id=%d)", r, ndp_id); + } else { + ESP_LOGI(TAG, "NDP confirm: peer BIGTK (RX) installed (keyid=%d)", ndl->bigtk_keyid); + } + /* Peer BIGTK bytes for sniffer MIC cross-check vs the peer's protected Beacons. */ + ESP_LOG_BUFFER_HEXDUMP("PEER ND-BIGTK", ndl->bigtk, ndl->bigtk_len, ESP_LOG_DEBUG); + } + } } #endif /* CONFIG_ESP_WIFI_NAN_SECURITY */ evt->status = status; @@ -1402,8 +1536,6 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ESP_LOG_BUFFER_HEXDUMP(TAG, ssi, ssi_len, ESP_LOG_DEBUG); } - esp_netif_action_connected(s_nan_ctx.nan_netif, WIFI_EVENT, WIFI_EVENT_NDP_CONFIRM, evt); - esp_netif_create_ip6_linklocal(s_nan_ctx.nan_netif); NAN_DATA_UNLOCK(); ip6_addr_t peer_ip6 = {0}; @@ -1414,6 +1546,20 @@ static void nan_app_ndp_confirm_cb(uint8_t status, struct ndp_cb_peer_info *peer ESP_LOGI(TAG, "NDP confirmed with Peer "MACSTR" [NDP ID - %d, Peer IPv6 - %s]", MAC2STR(peer_nmi), ndp_id, inet6_ntoa(peer_ip6)); +#if CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES + /* Pin the peer's link-local -> NDI mapping so traffic to the peer skips + * Neighbor Discovery (no NS/NA) on the NAN link. The esp_wifi netif layer + * owns the netif lookup and derives the peer's link-local from its NDI + * (the address the peer actually sources from). */ + esp_err_t nbr_err = esp_wifi_netif_set_static_neighbor(WIFI_IF_NAN, peer_ndi, true); + if (nbr_err != ESP_OK) { + ESP_LOGW(TAG, "static nbr ADD failed: %s", esp_err_to_name(nbr_err)); + } +#else + esp_netif_action_connected(s_nan_ctx.nan_netif, WIFI_EVENT, WIFI_EVENT_NDP_CONFIRM, evt); + esp_netif_create_ip6_linklocal(s_nan_ctx.nan_netif); +#endif + os_event_group_set_bits(nan_event_group, NDP_ACCEPTED); nan_app_post_event(WIFI_EVENT_NDP_CONFIRM, evt, evt_data_len); os_free(evt); @@ -1436,6 +1582,15 @@ static void nan_app_ndp_terminated_cb(uint8_t reason, uint8_t ndp_id, uint8_t in s_nan_ctx.event &= ~(NDP_INDICATION); NAN_DATA_UNLOCK(); +#if CONFIG_LWIP_ND6_SUPPORT_STATIC_ENTRIES + /* Drop the peer's static neighbor mapping added on NDP confirm. (It is also + * cleared automatically if the NAN netif goes down on the last datapath.) */ + esp_err_t nbr_err = esp_wifi_netif_set_static_neighbor(WIFI_IF_NAN, init_ndi, false); + if (nbr_err != ESP_OK) { + ESP_LOGW(TAG, "static nbr DEL failed: %s", esp_err_to_name(nbr_err)); + } +#endif + wifi_event_ndp_terminated_t *evt = (wifi_event_ndp_terminated_t *)os_zalloc(sizeof(wifi_event_ndp_terminated_t)); if (!evt) { ESP_LOGE(TAG, "Failed to allocate for event"); @@ -1506,6 +1661,7 @@ static struct nan_secure_dp_funcs s_nan_secure_dp_funcs = { .ndp_security_install_get_shared_desc_len = esp_nan_ndp_security_install_get_shared_desc_len, .get_ndp_resp_num_pmkids = esp_nan_get_ndp_resp_num_pmkids, .get_ndp_resp_shared_key_desc_len = esp_nan_get_ndp_resp_shared_key_desc_len, + .ndp_confirm_get_shared_desc_len = esp_nan_ndp_confirm_get_shared_desc_len, /* CSIA / SCIA construction */ .construct_csia = esp_nan_construct_csia, @@ -1686,6 +1842,16 @@ void esp_nan_action_start(esp_netif_t *nan_netif) }; esp_nan_internal_register_callbacks(&nan_cb); +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + /* s_nan_ctx.group_mgmt_prot (device-global IGTKSA/BIGTKSA, one per NMI) was + * captured from the user's start config in esp_wifi_nan_sync_start(). + * Install the device-global IGTK/BIGTK for TX now (when enabled) so Beacons + * (BIGTK) and group-addressed SDFs (IGTK) are BIP-protected from the first + * frame, like iOS. The blob gates beacon BIP-TX on an active BIGTK index + * only (no NDP state), so installing here is sufficient. */ + nan_security_install_own_group_integrity_keys(); +#endif + ESP_LOGI(TAG, "NAN Discovery started."); os_event_group_clear_bits(nan_event_group, NAN_STOPPED_BIT); os_event_group_set_bits(nan_event_group, NAN_STARTED_BIT); @@ -1713,6 +1879,13 @@ void esp_nan_action_stop(void) nan_app_clear_paired_peers(); #endif +#ifdef CONFIG_ESP_WIFI_NAN_SECURITY + /* Drop the device-global IGTK/BIGTK so the next start regenerates fresh + * keys instead of re-installing a stale key with IPN/BIPN=0 (which would + * reset the blob's replay counter) — see nan_security_reset_own_group_keys. */ + nan_security_reset_own_group_keys(); +#endif + esp_nan_internal_register_callbacks(NULL); os_event_group_clear_bits(nan_event_group, NAN_STARTED_BIT); os_event_group_set_bits(nan_event_group, NAN_STOPPED_BIT); @@ -1756,6 +1929,7 @@ esp_err_t esp_wifi_nan_sync_start(const wifi_nan_sync_config_t *nan_cfg) s_nan_ctx.num_peer_creds = 0; memset(s_nan_ctx.peer_creds, 0, sizeof(s_nan_ctx.peer_creds)); s_nan_ctx.use_nvs_for_caching = nan_cfg->use_nvs_for_caching; + s_nan_ctx.group_mgmt_prot = nan_cfg->group_mgmt_prot; s_nan_ctx.nik_lifetime = 0; if (nan_cfg->reset_current_nvs_creds) { @@ -2525,7 +2699,6 @@ esp_err_t esp_wifi_nan_datapath_resp(wifi_nan_datapath_resp_t *resp) ESP_LOGE(TAG, "Need NDP Indication before NDP Response can be sent"); goto fail; } - if (MACADDR_EQUAL(resp->peer_mac, null_mac)) { MACADDR_COPY(resp->peer_mac, ndl->peer_nmi); } diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h index c7ee0cd8a5a..7bbb4d8b0f0 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_i.h @@ -130,6 +130,40 @@ extern void *s_nan_data_lock; #define NAN_KEY_INFO_ENC_KEY BIT(12) #define NAN_KEY_INFO_KEY_TYPE BIT(3) /* 1=Pairwise, 0=Group */ +/* NAN KDE OUIs and Data Types carried in the Key Data field (Wi-Fi Aware + * v4.0 §9.5.21.5 Table 126; formats per 802.11 Fig 12-36/12-42/12-47). */ +#define NAN_KDE_OUI_RSN 0x000FACUL +#define NAN_KDE_OUI_WFA 0x506F9AUL +#define NAN_KDE_TYPE_GTK 1 /* 00-0F-AC GTK KDE */ +#define NAN_KDE_TYPE_MAC 3 /* 00-0F-AC MAC address KDE */ +#define NAN_KDE_TYPE_IGTK 9 /* 00-0F-AC IGTK KDE */ +#define NAN_KDE_TYPE_BIGTK 14 /* 00-0F-AC BIGTK KDE */ +#define NAN_KDE_TYPE_NIK 36 /* 50-6F-9A NIK KDE */ +#define NAN_KDE_TYPE_KEY_LIFE 37 /* 50-6F-9A NAN Key Lifetime KDE */ + +/* KDE inner-prefix lengths (bytes before the actual key material). */ +#define NAN_KDE_HDR_LEN 6 /* DD(1) + len(1) + OUI(3) + DataType(1) */ +#define NAN_GTK_KDE_PREFIX_LEN 2 /* KeyID/Tx(1) + Reserved(1) */ +#define NAN_IGTK_KDE_PREFIX_LEN 8 /* KeyID(2) + IPN(6) */ +#define NAN_BIGTK_KDE_PREFIX_LEN 8 /* KeyID(2) + BIPN(6) */ + +/* CSIA Capabilities group-SA support (§9.5.21.2 Table 122, bits 1-2, bit 2 high + * order). Mirrors hostap nan_defs.h NAN_CS_INFO_CAPA_GTK_SUPP_*. */ +#define NAN_CSIA_CAP_GTK_SUPP_POS 1 +#define NAN_CSIA_CAP_GTK_SUPP_MASK 0x06 +#define NAN_CSIA_CAP_GTK_SUPP_NONE 0 /* 00: no group SA */ +#define NAN_CSIA_CAP_GTK_SUPP_IGTK 1 /* 01: GTKSA + IGTKSA */ +#define NAN_CSIA_CAP_GTK_SUPP_ALL 2 /* 10: GTKSA + IGTKSA + BIGTKSA */ + +/* ND-GTK is the data-path group key (CCMP-128). */ +#define NAN_ND_GTK_LEN 16 +/* Host-side bound for the group Key Data scratch buffers (plaintext + AES-wrap), + * sized for GTK+IGTK+BIGTK: GTK 24B + IGTK 30B + BIGTK 30B + optional Key Lifetime + * KDE(s), padded to a multiple of 8, + 8B NIST AES Key Wrap overhead (~104B worst + * case). NOT the descriptor-len reservation — the getters now return the EXACT + * per-NDP length, so the blob allocates exactly what the builder writes. */ +#define NAN_GROUP_KEY_DATA_MAX 128 + /* NCS-SK-128 only for now (Table 21): KCK 128 bits, KEK 128 bits, TK 128 bits, MIC 16 bytes */ #define NAN_NCS_SK_128_KCK_LEN 16 #define NAN_NCS_SK_128_KEK_LEN 16 @@ -138,11 +172,16 @@ extern void *s_nan_data_lock; #define NAN_NCS_SK_128_PTK_LEN (NAN_NCS_SK_128_KCK_LEN + NAN_NCS_SK_128_KEK_LEN + NAN_NCS_SK_128_TK_LEN) /* Internal key-install constants matching esp_wifi_set_sta_key_internal semantics. */ -#define NAN_WIFI_WPA_ALG_CCMP 3 +#define NAN_WIFI_WPA_ALG_CCMP 3 +#define NAN_WIFI_WPA_ALG_BIP_CMAC_128 7 /* IGTK/BIGTK BIP = blob WIFI_WPA_ALG_IGTK; 4 is SMS4 */ #define NAN_KEY_FLAG_RX BIT(2) #define NAN_KEY_FLAG_TX BIT(3) #define NAN_KEY_FLAG_PAIRWISE BIT(5) +/* NAN key-type selector (nan_key_type_t: NAN_KEY_ND_TK / ND_GTK / NM_TK / ND_IGTK / + * ND_BIGTK), passed as the last arg of esp_wifi_set_nan_key_internal, is defined in + * esp_wifi_driver.h (included above) and shared with the blob. */ + /* Handshake state */ enum nan_handshake_state { NAN_HANDSHAKE_IDLE = 0, @@ -195,6 +234,13 @@ struct peer_svc_info { * whose ND-PMKID matched the publisher SCIA. The initiator NDP-req path * uses this to pick the right credential for M1's pair-PMKID. */ uint8_t matched_cred_idx; + /* Set at SDF match if the publisher advertised an NCS-GTK suite in its CSIA; + * the initiator NDP-req path uses it (with our own group_data_prot) to + * decide whether to distribute a GTK during NDP setup. */ + uint8_t peer_group_data_cap: 1; + uint8_t peer_group_mgmt_cap: 1; /* peer advertised IGTKSA (CSIA caps bits 1-2 != 0) */ + uint8_t peer_group_bigtk_cap: 1; /* peer advertised BIGTKSA (CSIA caps bits 1-2 == 10) */ + uint8_t peer_sec_reserved: 5; #endif #if CONFIG_ESP_WIFI_NAN_PAIRING /* Peer NIK / cipher version / lifetime extracted from a NAN Shared Key @@ -273,20 +319,38 @@ struct ndl_info { uint8_t handshake_state; - /* Group key state (unsupported -- pairwise-only M1-M4 flow today; - * fields kept to match the spec-defined RSNA key descriptor layout - * and stay forward-compatible). */ + /* Received peer group keys (RX GTKSA). GTK protects group-addressed data + * frames the peer transmits; installed against the peer NDI. IGTK/BIGTK + * protect group-addressed management/Beacon frames (NMI plane). */ uint8_t gtk[NAN_GTK_MAX_LEN]; uint8_t igtk[NAN_GTK_MAX_LEN]; uint8_t bigtk[NAN_GTK_MAX_LEN]; uint8_t gtk_len; uint8_t igtk_len; uint8_t bigtk_len; + uint8_t gtk_keyid; /* peer GTK Key ID (1 or 2) */ + uint8_t igtk_keyid; /* peer IGTK Key ID (4 or 5) */ + uint8_t bigtk_keyid; /* peer BIGTK Key ID (6 or 7) */ + uint8_t gtk_rsc[NAN_KEY_RSC_LEN]; /* peer GTK RSC from Key RSC field */ + uint8_t igtk_ipn[6]; /* peer IGTK IPN (seeds BIP RX replay counter) */ + uint8_t bigtk_ipn[6]; /* peer BIGTK BIPN (seeds BIP RX replay counter) */ uint8_t gtk_set: 1; uint8_t igtk_set: 1; uint8_t bigtk_set: 1; uint8_t group_keys_reserved: 5; + /* Own group key (TX GTKSA) distributed to the peer in M3 (initiator) or + * M4 (responder); installed against the local NDI as the TX group key. */ + uint8_t own_gtk[NAN_ND_GTK_LEN]; + uint8_t own_gtk_len; + uint8_t own_gtk_keyid; /* own GTK Key ID (1 or 2) */ + uint8_t own_gtk_rsc[NAN_KEY_RSC_LEN]; + uint8_t own_gtk_set: 1; + uint8_t gtk_required: 1; /* GTKSA negotiated for this NDP */ + uint8_t igtk_required: 1; /* IGTKSA negotiated for this NDP */ + uint8_t bigtk_required: 1; /* BIGTKSA negotiated for this NDP */ + uint8_t own_group_reserved: 4; + uint8_t key_rsc[NAN_KEY_RSC_LEN]; #endif }; @@ -304,6 +368,28 @@ typedef struct { #ifdef CONFIG_ESP_WIFI_NAN_SECURITY uint8_t own_nik[ESP_WIFI_NAN_NIK_LEN]; bool own_nik_valid; + /* Device-global IGTKSA/BIGTKSA (one per NMI, §7.1.3.3/§7.1.3.4). Generated + * once via os_get_random and reused across all secured NDPs; copied into + * each M3/M4 and installed against the local NMI. BIP-CMAC-128 (16-byte). + * On ESP the NMI and NDI are the same MAC today. */ + uint8_t own_igtk[NAN_ND_GTK_LEN]; + uint8_t own_igtk_ipn[6]; + uint8_t own_igtk_keyid; /* 4 or 5 */ + bool own_igtk_set; + uint8_t own_bigtk[NAN_ND_GTK_LEN]; + uint8_t own_bigtk_bipn[6]; + uint8_t own_bigtk_keyid; /* 6 or 7 */ + bool own_bigtk_set; + /* Device-global "support + use group management protection (IGTKSA/BIGTKSA)". + * One setting per NMI, not per service: Beacons are NMI-level and there is + * exactly one IGTKSA/BIGTKSA per NMI (§7.1.3.3/§7.1.3.4). Sourced from + * wifi_nan_sync_config_t.group_mgmt_prot at NAN start. When set it: forces + * GTKSA on every secured service (the CSIA caps field cannot encode IGTK/ + * BIGTK without GTKSA, §9.5.21.2 Table 122), generates own IGTK+BIGTK, + * advertises caps 0x04, and BIP-protects Beacons + multicast SDFs. + * Advertising never blocks a non-supporting peer — keys and protection are + * set up only after capability negotiation (§7.1.3.5). */ + bool group_mgmt_prot; uint8_t cached_nira_nonce[8]; uint8_t cached_nira_tag[8]; bool nira_cached; @@ -340,7 +426,8 @@ bool nan_compute_service_id(const char *service_name, uint8_t service_id[6]); uint32_t esp_nan_get_csia_len(uint16_t own_csid_bitmap, uint16_t peer_csid_bitmap); uint32_t esp_nan_get_scia_len(uint8_t num_pmkids); uint32_t esp_nan_get_shared_key_desc_attr_len(uint16_t key_data_len); -int esp_nan_ndp_security_install_get_shared_desc_len(void); +int esp_nan_ndp_security_install_get_shared_desc_len(uint8_t ndp_id, const uint8_t *peer_nmi); +int esp_nan_ndp_confirm_get_shared_desc_len(uint8_t ndp_id, const uint8_t *peer_nmi); uint8_t esp_nan_get_ndp_resp_num_pmkids(uint8_t ndp_id, const uint8_t *peer_nmi); uint32_t esp_nan_get_ndp_resp_shared_key_desc_len(uint8_t ndp_id, const uint8_t *peer_nmi); @@ -428,6 +515,17 @@ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, const struct peer_svc_info *peer_svc, const uint8_t *peer_nmi); +/* Generate (once) and TX-install the device-global IGTK/BIGTK so Beacons and + * group-addressed SDFs are BIP-protected from NAN start (§7.1.3.3/§7.1.3.4). + * Call once at NAN start; never per-NDP (would reset the blob's BIPN counter). */ +void nan_security_install_own_group_integrity_keys(void); + +/* Clear the device-global IGTK/BIGTK state on NAN stop so the next NAN start + * regenerates fresh keys. Prevents re-installing a stale key with IPN/BIPN=0 + * (which resets the blob's monotonic replay counter) and key reuse if the NMI + * is re-randomized on restart. */ +void nan_security_reset_own_group_keys(void); + /* * Match subscriber discovery security to a publisher's params. * NCS-SK: Compare locally derived ND-PMKID (subscriber passphrase, publisher NMI) to diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_pairing.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_pairing.c index de1c7e36a75..eaaa6da324b 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_pairing.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_pairing.c @@ -510,6 +510,11 @@ int esp_nan_construct_nira(uint8_t *frm) #define NAN_PASN_KDE_OUI_TYPE_LIFETIME 37 #define NAN_PASN_KEY_LIFETIME_NIK_BIT BIT(3) #define NAN_ATTR_ID_SHARED_KEY_DESC 0x24 +/* iOS sends its NIK follow-up ~2.5-2.6 s after we derive the NM-TK; a 2 s window + * fired first and destructively removed the peer, so the late NIK arrived after + * teardown and the follow-up went out unprotected -> peer never started the NDP. + * 5 s lets the NIK land in time, so the cancel in the store-NIK path keeps the + * peer SA intact. */ #define NAN_PAIRING_NIK_FUP_TIMEOUT_SEC 5 struct nan_pairing_fup_ctx { diff --git a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c index f25a2e70cd9..73b1a1a9d59 100644 --- a/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c +++ b/components/esp_wifi/wifi_apps/nan_app/src/nan_security.c @@ -24,6 +24,7 @@ #include "esp_nan.h" #include "utils/common.h" #include "crypto/sha256.h" +#include "crypto/aes_wrap.h" #include "nan_i.h" static const char *TAG = "nan_sec"; @@ -56,6 +57,8 @@ static struct { static struct { bool has_pmkid; bool has_csid; + bool peer_group_data; /* peer signalled group-data (GTK) support in its CSIA */ + uint8_t peer_caps; /* raw CSIA Capabilities byte; IGTK/BIGTK gating derives bits 1-2 */ uint8_t pub_id; uint8_t pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN]; uint16_t csid_bitmap; @@ -64,6 +67,12 @@ static struct { /* Spec Table 121: valid CSIDs are 1..8. Bit 0 and bits 9..15 are not assigned. */ #define NAN_CSID_VALID_BITMAP ((uint16_t)0x01FE) +/* NCS-GTK cipher-suite bits (group-addressed data). Advertised when a service + * sets group_data_prot; the basic default we generate/advertise is CCMP-128. */ +#define NAN_CSID_GTK_BITS (WIFI_NAN_CSID_BIT_NCS_GTK_CCMP_128 | \ + WIFI_NAN_CSID_BIT_NCS_GTK_GCMP_256) +#define NAN_CSID_GTK_DEFAULT WIFI_NAN_CSID_BIT_NCS_GTK_CCMP_128 + /* Sentinel for peer_svc->matched_cred_idx: no PMKID match remembered yet. */ #define NAN_NO_MATCHED_CRED 0xFF @@ -458,6 +467,34 @@ static bool nan_security_fill_from_paired_cache(struct ndl_info *ndl, const uint return false; } + /* Early-reject: if the NDP Request carried an ND-PMKID, our cached ND-PMK must reproduce it (§7.1.3.5) before we commit. */ + static const uint8_t zero_pmkid[ESP_WIFI_NAN_NDP_PMKID_LEN] = {0}; + if (memcmp(ndl->security_ctx.nd_pmkid, zero_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) != 0) { + uint8_t our_nmi[6]; + uint8_t service_id[6]; + struct own_svc_info *own_svc = nan_find_own_svc(ndl->publisher_id); + + if (esp_wifi_get_mac(WIFI_IF_NAN, our_nmi) != ESP_OK || !own_svc || + !own_svc->svc_name[0] || !nan_compute_service_id(own_svc->svc_name, service_id)) { + ESP_LOGW(TAG, "Paired ND-PMK: cannot verify peer ND-PMKID (own NMI/service unavailable for pub_id=%u), deferring to handshake MIC", + ndl->publisher_id); + } else { + uint8_t expected[ESP_WIFI_NAN_NDP_PMKID_LEN]; + /* Spec order (Initiator=peer, Responder=us), then reversed for interop, mirroring nan_match_pmkid(). */ + bool ok = (nan_derive_ndp_request_pmkid(paired->nd_pmk, peer_nmi, our_nmi, service_id, expected) && + memcmp(expected, ndl->security_ctx.nd_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) == 0) || + (nan_derive_ndp_request_pmkid(paired->nd_pmk, our_nmi, peer_nmi, service_id, expected) && + memcmp(expected, ndl->security_ctx.nd_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN) == 0); + if (!ok) { + ESP_LOGE(TAG, "Paired ND-PMK rejected for peer "MACSTR": NDP-Request ND-PMKID does not match cached pairing key (csid=%u), secured NDP setup aborted", + MAC2STR(peer_nmi), paired->ndp_csid); + ESP_LOG_BUFFER_HEXDUMP(TAG, ndl->security_ctx.nd_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN, ESP_LOG_WARN); + return false; + } + ESP_LOGD(TAG, "Paired ND-PMK: peer ND-PMKID verified for "MACSTR, MAC2STR(peer_nmi)); + } + } + ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; ndl->security_ctx.csid_bitmap = (uint16_t)(1u << paired->ndp_csid); memcpy(ndl->security_ctx.nd_pmk, paired->nd_pmk, ESP_WIFI_NAN_NDP_PMK_LEN); @@ -604,6 +641,357 @@ static int nan_build_rsna_key_descriptor(uint8_t *kd, uint16_t key_info_flags, return NAN_KEY_DESC_MIN_LEN; } +/*------------------------------------------------------------------------- + * Group Key Data (GTK/IGTK/BIGTK KDEs) — Wi-Fi Aware v4.0 §7.1.3.2/§7.1.3.5/ + * §9.5.21.5. Initiator distributes in M3, responder in M4; KDEs are always + * KEK-wrapped (NIST AES Key Wrap), never in clear. Structure mirrors hostap + * src/nan/nan_sec.c nan_sec_add_kdes(). + *-----------------------------------------------------------------------*/ + +/* True if a GTKSA was negotiated for this NDP. gtk_required is the explicit + * result of (our service has group_data_prot) AND (peer advertised NCS-GTK), + * computed at NDL finalize on both roles. We deliberately do NOT also gate on + * security_ctx.csid_bitmap: that field carries the advertised GTK bit on some + * paths and would over-fire when only one side enabled group protection. */ +static bool nan_ndl_gtk_negotiated(const struct ndl_info *ndl) +{ + return ndl->gtk_required; +} + +/* IGTK/BIGTK negotiation gates. Per §7.1.3.5: include the IGTK/BIGTK KDE iff BOTH + * peers advertise the capability in their CSIA. igtk_required/bigtk_required are + * set at NDL finalize on both roles from (our service has group_mgmt_prot) AND + * (peer advertised IGTKSA/BIGTKSA in its CSIA caps byte). */ +static bool nan_ndl_igtk_negotiated(const struct ndl_info *ndl) +{ + return ndl->igtk_required; +} + +static bool nan_ndl_bigtk_negotiated(const struct ndl_info *ndl) +{ + return ndl->bigtk_required; +} + +/* Lazily generate the local data-path GTK (CCMP-128). Fresh GTK starts at RSC 0. */ +static int nan_ensure_own_gtk(struct ndl_info *ndl) +{ + if (ndl->own_gtk_set) { + return 0; + } + if (os_get_random(ndl->own_gtk, NAN_ND_GTK_LEN) != 0) { + return -1; + } + ndl->own_gtk_len = NAN_ND_GTK_LEN; + ndl->own_gtk_keyid = 1; /* spec allows Key ID 1 or 2 */ + memset(ndl->own_gtk_rsc, 0, NAN_KEY_RSC_LEN); + ndl->own_gtk_set = 1; + return 0; +} + +/* Lazily generate the device-global IGTK/BIGTK (BIP-CMAC-128, §7.1.3.3/§7.1.3.4). + * Exactly one key per local NMI, reused across all secured NDPs; IPN/BIPN start + * at 0. Generated by this device (transmitter) per spec. */ +static int nan_ensure_own_igtk(void) +{ + if (s_nan_ctx.own_igtk_set) { + return 0; + } + if (os_get_random(s_nan_ctx.own_igtk, NAN_ND_GTK_LEN) != 0) { + return -1; + } + s_nan_ctx.own_igtk_keyid = 4; /* spec allows Key ID 4 or 5 */ + memset(s_nan_ctx.own_igtk_ipn, 0, sizeof(s_nan_ctx.own_igtk_ipn)); + s_nan_ctx.own_igtk_set = true; + return 0; +} + +static int nan_ensure_own_bigtk(void) +{ + if (s_nan_ctx.own_bigtk_set) { + return 0; + } + if (os_get_random(s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN) != 0) { + return -1; + } + s_nan_ctx.own_bigtk_keyid = 6; /* spec allows Key ID 6 or 7 */ + memset(s_nan_ctx.own_bigtk_bipn, 0, sizeof(s_nan_ctx.own_bigtk_bipn)); + s_nan_ctx.own_bigtk_set = true; + return 0; +} + +/* Generate (once) and TX-install the device-global IGTK/BIGTK at NAN start, so + * Beacons (BIGTK) and group-addressed SDFs (IGTK) are BIP-protected from the + * first frame — matching peers (e.g. iOS) that protect from NAN start, not just + * after the first NDP. The keys are device-global per §7.1.3.3/§7.1.3.4 and the + * same bytes are later distributed KEK-wrapped in M3/M4. Install MUST happen + * only here (not per-NDP), else re-installing with IPN/BIPN=0 would reset the + * blob's monotonic replay counter mid-session. Best-effort: a failed install + * warns but does not block NAN start. */ +void nan_security_install_own_group_integrity_keys(void) +{ + uint8_t own_nmi[6]; + if (!s_nan_ctx.group_mgmt_prot) { + return; /* group-management protection disabled device-wide */ + } + if (esp_wifi_get_mac(WIFI_IF_NAN, own_nmi) != ESP_OK) { + ESP_LOGW(TAG, "NAN start: get NMI failed; own IGTK/BIGTK TX not installed"); + return; + } + if (nan_ensure_own_igtk() == 0 && s_nan_ctx.own_igtk_set) { + int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + own_nmi, s_nan_ctx.own_igtk_keyid, 1, + s_nan_ctx.own_igtk_ipn, sizeof(s_nan_ctx.own_igtk_ipn), + s_nan_ctx.own_igtk, NAN_ND_GTK_LEN, + NAN_KEY_ND_IGTK); + if (r != 0) { + ESP_LOGW(TAG, "NAN start: own IGTK (TX) install failed, rc=0x%x", r); + } else { + ESP_LOGI(TAG, "NAN start: own IGTK (TX) installed (keyid=%d)", s_nan_ctx.own_igtk_keyid); + } + ESP_LOG_BUFFER_HEXDUMP("ND-IGTK", s_nan_ctx.own_igtk, NAN_ND_GTK_LEN, ESP_LOG_DEBUG); + } + if (nan_ensure_own_bigtk() == 0 && s_nan_ctx.own_bigtk_set) { + int r = esp_wifi_set_nan_key_internal(NAN_WIFI_WPA_ALG_BIP_CMAC_128, + own_nmi, s_nan_ctx.own_bigtk_keyid, 1, + s_nan_ctx.own_bigtk_bipn, sizeof(s_nan_ctx.own_bigtk_bipn), + s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN, + NAN_KEY_ND_BIGTK); + if (r != 0) { + ESP_LOGW(TAG, "NAN start: own BIGTK (TX) install failed, rc=0x%x", r); + } else { + ESP_LOGI(TAG, "NAN start: own BIGTK (TX) installed (keyid=%d)", s_nan_ctx.own_bigtk_keyid); + } + ESP_LOG_BUFFER_HEXDUMP("ND-BIGTK", s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN, ESP_LOG_DEBUG); + } +} + +void nan_security_reset_own_group_keys(void) +{ + forced_memzero(s_nan_ctx.own_igtk, sizeof(s_nan_ctx.own_igtk)); + memset(s_nan_ctx.own_igtk_ipn, 0, sizeof(s_nan_ctx.own_igtk_ipn)); + s_nan_ctx.own_igtk_keyid = 0; + s_nan_ctx.own_igtk_set = false; + + forced_memzero(s_nan_ctx.own_bigtk, sizeof(s_nan_ctx.own_bigtk)); + memset(s_nan_ctx.own_bigtk_bipn, 0, sizeof(s_nan_ctx.own_bigtk_bipn)); + s_nan_ctx.own_bigtk_keyid = 0; + s_nan_ctx.own_bigtk_set = false; +} + +/* 00-0F-AC RSN OUI written into every NAN KDE header. */ +static const uint8_t nan_kde_rsn_oui[3] = {0x00, 0x0f, 0xac}; + +/* NAN KDE header (802.11 Fig 12-34: DD len OUI(3) DataType(1)); mirrors hostap + * nan_add_kde_hdr(). data_len excludes the DD/len/OUI/type bytes. */ +static uint8_t *nan_kde_put_hdr(uint8_t *p, uint8_t data_type, uint8_t data_len) +{ + *p++ = 0xDD; + *p++ = (uint8_t)(4 + data_len); /* OUI(3) + DataType(1) + data */ + memcpy(p, nan_kde_rsn_oui, sizeof(nan_kde_rsn_oui)); + p += sizeof(nan_kde_rsn_oui); + *p++ = data_type; + return p; +} + +/* IGTK KDE (§9.5.21.5: KeyID(2 LE) IPN(6) IGTK); mirrors hostap nan_sec_igtk_kde(). + * Carries the device-global IGTK (BIP-CMAC-128); the peer installs it RX-only to + * verify our group-addressed management frames. */ +static int nan_append_igtk_kde(struct ndl_info *ndl, uint8_t **p, const uint8_t *end) +{ + if (!nan_ndl_igtk_negotiated(ndl)) { + return 0; + } + if ((size_t)(end - *p) < NAN_KDE_HDR_LEN + NAN_IGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN) { + return -1; + } + uint8_t *q = nan_kde_put_hdr(*p, NAN_KDE_TYPE_IGTK, + NAN_IGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN); + *q++ = s_nan_ctx.own_igtk_keyid & 0xFF; *q++ = 0; /* KeyID (2, LE) — 4/5 */ + memcpy(q, s_nan_ctx.own_igtk_ipn, 6); q += 6; /* IPN (6) */ + memcpy(q, s_nan_ctx.own_igtk, NAN_ND_GTK_LEN); q += NAN_ND_GTK_LEN; + *p = q; + return 0; +} + +/* BIGTK KDE (§9.5.21.5: KeyID(2 LE) BIPN(6) BIGTK); mirrors hostap nan_sec_bigtk_kde(). + * Carries the device-global BIGTK (BIP-CMAC-128); the peer installs it RX-only to + * verify our protected Beacon frames. */ +static int nan_append_bigtk_kde(struct ndl_info *ndl, uint8_t **p, const uint8_t *end) +{ + if (!nan_ndl_bigtk_negotiated(ndl)) { + return 0; + } + if ((size_t)(end - *p) < NAN_KDE_HDR_LEN + NAN_BIGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN) { + return -1; + } + uint8_t *q = nan_kde_put_hdr(*p, NAN_KDE_TYPE_BIGTK, + NAN_BIGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN); + *q++ = s_nan_ctx.own_bigtk_keyid & 0xFF; *q++ = 0; /* KeyID (2, LE) — 6/7 */ + memcpy(q, s_nan_ctx.own_bigtk_bipn, 6); q += 6; /* BIPN (6) */ + memcpy(q, s_nan_ctx.own_bigtk, NAN_ND_GTK_LEN); q += NAN_ND_GTK_LEN; + *p = q; + return 0; +} + +/* GTK KDE (§7.1.3.2/§9.5.21.5, 802.11 Fig 12-36); mirrors hostap nan_sec_gtk_kde(). + * Tx bit stays 0: the peer installs this as an RX-only group key. */ +static int nan_append_gtk_kde(struct ndl_info *ndl, uint8_t **p, const uint8_t *end) +{ + if (!ndl->own_gtk_set || !ndl->own_gtk_len) { + return 0; + } + if (ndl->own_gtk_keyid < 1 || ndl->own_gtk_keyid > 2) { /* §7.1.3.2: GTK Key ID is 1 or 2 */ + ESP_LOGW(TAG, "GTK: invalid Key ID %u", ndl->own_gtk_keyid); + return -1; + } + if ((size_t)(end - *p) < NAN_KDE_HDR_LEN + NAN_GTK_KDE_PREFIX_LEN + ndl->own_gtk_len) { + return -1; + } + uint8_t *q = nan_kde_put_hdr(*p, NAN_KDE_TYPE_GTK, + NAN_GTK_KDE_PREFIX_LEN + ndl->own_gtk_len); + *q++ = ndl->own_gtk_keyid & 0x03; /* KeyID (b0-1); Tx (b2)=0; b3-7 reserved */ + *q++ = 0; /* Reserved */ + memcpy(q, ndl->own_gtk, ndl->own_gtk_len); + *p = q + ndl->own_gtk_len; + return 0; +} + +/* NIST AES Key Wrap of Key Data with the ND-KEK. Pads the plaintext to >=16 + * octets and a multiple of 8 (0xDD then 0x00, per §12.7.2). Cipher = padded+8. */ +static int nan_kek_wrap_key_data(struct ndl_info *ndl, const uint8_t *plain, + size_t plain_len, uint8_t *out, size_t out_cap, + size_t *out_len) +{ + uint8_t pad[NAN_GROUP_KEY_DATA_MAX]; + size_t padded = plain_len; + if (padded < 16) { + padded = 16; + } + if (padded % 8) { + padded = (padded + 7) & ~((size_t)7); + } + if (padded > sizeof(pad) || (padded + 8) > out_cap) { + return -1; + } + memcpy(pad, plain, plain_len); + if (padded > plain_len) { + pad[plain_len] = 0xDD; + memset(pad + plain_len + 1, 0, padded - plain_len - 1); + } + int rc = aes_wrap(ndl->nd_kek, ndl->kek_len, (int)(padded / 8), pad, out); + forced_memzero(pad, sizeof(pad)); /* scrub plaintext group keys */ + if (rc != 0) { + return -1; + } + *out_len = padded + 8; + return 0; +} + +/* NIST AES Key Unwrap of received Key Data with the ND-KEK. Plain = cipher-8. */ +static int nan_kek_unwrap_key_data(struct ndl_info *ndl, const uint8_t *cipher, + size_t cipher_len, uint8_t *out, size_t out_cap, + size_t *out_len) +{ + if (cipher_len < 24 || (cipher_len % 8) != 0) { /* >=16 plaintext + 8 wrap */ + return -1; + } + size_t plain_len = cipher_len - 8; + if (plain_len > out_cap) { + return -1; + } + if (aes_unwrap(ndl->nd_kek, ndl->kek_len, (int)(plain_len / 8), cipher, out) != 0) { + return -1; + } + *out_len = plain_len; + return 0; +} + +/* Build the local group Key Data (KDE order IGTK, BIGTK, GTK per upstream), + * KEK-wrap it, and patch Encrypted-Data bit + Key Data Length + Key RSC into + * the 95-byte descriptor in place. Returns the extended descriptor length, or + * NAN_KEY_DESC_MIN_LEN (pairwise-only) on negotiation-off or any error so the + * handshake still completes. Mirrors hostap nan_sec_add_kdes(); §7.1.3.5: KDEs + * are sent only KEK-wrapped, never in clear. */ +static int nan_append_own_group_kdes(struct ndl_info *ndl, uint8_t *key_desc, size_t desc_cap) +{ + bool want_gtk = nan_ndl_gtk_negotiated(ndl); + bool want_igtk = nan_ndl_igtk_negotiated(ndl); + bool want_bigtk = nan_ndl_bigtk_negotiated(ndl); + if (!want_gtk && !want_igtk && !want_bigtk) { + return NAN_KEY_DESC_MIN_LEN; + } + if (!ndl->ptk_set) { + ESP_LOGW(TAG, "Group KDEs [%s%s%s]: PTK/KEK not set; sending without group keys", + want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); + return NAN_KEY_DESC_MIN_LEN; + } + if (want_gtk && nan_ensure_own_gtk(ndl) != 0) { + ESP_LOGW(TAG, "GTK: own key generation failed; sending without group keys"); + return NAN_KEY_DESC_MIN_LEN; + } + if (want_igtk && nan_ensure_own_igtk() != 0) { + ESP_LOGW(TAG, "IGTK: own key generation failed; sending without group keys"); + return NAN_KEY_DESC_MIN_LEN; + } + if (want_bigtk && nan_ensure_own_bigtk() != 0) { + ESP_LOGW(TAG, "BIGTK: own key generation failed; sending without group keys"); + return NAN_KEY_DESC_MIN_LEN; + } + + uint8_t plain[NAN_GROUP_KEY_DATA_MAX]; + uint8_t *p = plain; + const uint8_t *end = plain + sizeof(plain); + size_t plain_len = 0; + size_t wrapped_len = 0; + uint16_t key_info = 0; + int ret = NAN_KEY_DESC_MIN_LEN; + + if (nan_append_igtk_kde(ndl, &p, end) < 0 || + nan_append_bigtk_kde(ndl, &p, end) < 0 || + nan_append_gtk_kde(ndl, &p, end) < 0) { + ESP_LOGW(TAG, "Group KDEs [%s%s%s]: assembly failed; sending without group keys", + want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); + goto out; + } + + plain_len = (size_t)(p - plain); + if (plain_len == 0) { + goto out; /* nothing negotiated to send */ + } + + if (nan_kek_wrap_key_data(ndl, plain, plain_len, + &key_desc[NAN_KEY_DESC_DATA_OFF], + desc_cap > NAN_KEY_DESC_DATA_OFF ? + desc_cap - NAN_KEY_DESC_DATA_OFF : 0, + &wrapped_len) != 0) { + ESP_LOGW(TAG, "Group KDEs [%s%s%s]: KEK wrap failed or no headroom; sending without group keys", + want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); + goto out; + } + + key_info = (key_desc[NAN_KEY_DESC_KEY_INFO_OFF] << 8) | + key_desc[NAN_KEY_DESC_KEY_INFO_OFF + 1]; + key_info |= NAN_KEY_INFO_ENC_KEY; + key_desc[NAN_KEY_DESC_KEY_INFO_OFF] = (key_info >> 8) & 0xFF; + key_desc[NAN_KEY_DESC_KEY_INFO_OFF + 1] = key_info & 0xFF; + key_desc[NAN_KEY_DESC_DATA_LEN_OFF] = (wrapped_len >> 8) & 0xFF; + key_desc[NAN_KEY_DESC_DATA_LEN_OFF + 1] = wrapped_len & 0xFF; + + /* Key RSC carries the GTK's RSC only when a GTK KDE is present (§7.1.3.5). */ + if (want_gtk) { + memcpy(&key_desc[NAN_KEY_DESC_RSC_OFF], ndl->own_gtk_rsc, NAN_KEY_RSC_LEN); + } + + ESP_LOGI(TAG, "Group Key Data wrapped: %u plain -> %u wrapped bytes, KDEs=[%s%s%s]", + (unsigned)plain_len, (unsigned)wrapped_len, + want_gtk ? "GTK " : "", want_igtk ? "IGTK " : "", want_bigtk ? "BIGTK " : ""); + ret = NAN_KEY_DESC_MIN_LEN + (int)wrapped_len; + +out: + forced_memzero(plain, sizeof(plain)); /* scrub assembled plaintext group keys */ + return ret; +} + /* * Internal SCIA builder shared by Publish SDF and NDP-Response paths. * Per-entry layout: Len(2) + Type(1) + PubID(1) + Value(PMKID_LEN) = 20 bytes. @@ -636,7 +1024,8 @@ static int nan_build_scia_attr(uint8_t *frm, uint8_t pub_id, p += ESP_WIFI_NAN_NDP_PMKID_LEN; } - return (int)(p - frm); + int written = (int)(p - frm); + return written; } /* @@ -871,10 +1260,24 @@ esp_err_t nan_derive_security_params(const char *service_name, } out_derived[i].type = WIFI_NAN_SECURITY_ENCRYPTED; out_derived[i].csid_bitmap = (uint16_t)(1u << cred->csid); + /* Advertise the basic-default NCS-GTK suite alongside the credential's + * PMK cipher when the service enables group-addressed data protection. + * The blob unions derived_security[].csid_bitmap into the on-air CSIA, + * so this is what makes us announce GTK support (§7.1.3.5). The bit is + * masked back out for the pairwise/link cipher query (see + * nan_get_ndp_security_csid). */ + if (sec_cfg->group_data_prot) { + out_derived[i].csid_bitmap |= NAN_CSID_GTK_DEFAULT; + } out_derived[i].group_data_prot = sec_cfg->group_data_prot; out_derived[i].group_mgmt_prot = sec_cfg->group_mgmt_prot; } + if (sec_cfg->group_data_prot) { + ESP_LOGI(TAG, "NAN GTK: advertising NCS-GTK-CCMP-128 (group_data_prot=1) for svc='%s'", + service_name); + } + /* Mirror the derived material into the host's own_svc_info slot for this * service (claimed before the blob's publish/subscribe call). This lets * host paths — nan_match_pmkid (responder M1) and the NDL seeding at @@ -913,6 +1316,16 @@ uint16_t nan_get_ndp_security_csid(uint8_t ndp_id, const uint8_t *peer_nmi) struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); uint16_t csid = ndl ? ndl->security_ctx.csid_bitmap : 0; NAN_DATA_UNLOCK(); + /* Return the FULL negotiated bitmap, INCLUDING NCS-GTK when group-addressed + * data protection is in use, so the on-air NDP Request/Response CSIA + * advertises the group cipher and the peer can detect our group-data support + * (required for symmetric GTK distribution and third-party/iOS/Android + * interop). Safe to include NCS-GTK here: the blob treats this value as an + * opaque bitmap that it passes straight to the host CSIA callbacks + * (construct_csia / get_csia_len) and never interprets individual bits. + * Pairwise/link cipher selection and ND-TK install are + * host-driven and do not read this field; the group key has its own install + * path (NAN_KEY_ND_GTK) and gtk_required gate. */ return csid; } @@ -933,6 +1346,19 @@ bool nan_security_service_match(const struct own_svc_info *own_svc, const wifi_nan_discovery_security_params_t *cfg = &own_svc->user_cfg; + /* Remember whether this publisher signalled group-data (GTK) support, so the + * initiator NDP-req path can gate GTK distribution on mutual support. The + * spec-correct signal is the CSIA Capabilities byte (parsed into + * group_data_prot by esp_nan_parse_publish_security); an NCS-GTK cipher-suite + * ID is the legacy Android/ESP signal and is OR'd in for interop. */ + peer_svc->peer_group_data_cap = (peer_sec->group_data_prot || + (peer_sec->csid_bitmap & NAN_CSID_GTK_BITS)) ? 1 : 0; + /* IGTK/BIGTK (group management) support comes from the CSIA Capabilities + * byte (parsed into group_mgmt_prot/group_bigtk_prot by + * esp_nan_parse_publish_security): IGTKSA = bits 1-2 != 0, BIGTKSA = 10. */ + peer_svc->peer_group_mgmt_cap = peer_sec->group_mgmt_prot ? 1 : 0; + peer_svc->peer_group_bigtk_cap = peer_sec->group_bigtk_prot ? 1 : 0; + if (cfg->num_credentials == 0 || cfg->num_credentials > ESP_WIFI_NAN_MAX_CREDS_PER_SVC) { return false; @@ -1117,6 +1543,35 @@ esp_err_t nan_security_populate_initiator_ndl(struct ndl_info *ndl, ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; ndl->security_ctx.csid_bitmap = (uint16_t)(1u << ndp_csid); + /* Distribute a GTK in M3 only if we enabled group_data_prot AND the + * publisher advertised an NCS-GTK suite (recorded at SDF match). */ + ndl->gtk_required = (cfg->group_data_prot && peer_svc && + peer_svc->peer_group_data_cap) ? 1 : 0; + /* Advertise NCS-GTK in the NDP-Request CSIA so any responder (incl. + * third-party/iOS/Android) can detect our group-data support and + * reciprocate. Carried in ndl->security_ctx.csid_bitmap, which + * nan_get_ndp_security_csid() returns verbatim to the blob for the on-air + * M1/M2 CSIA. Pairwise cipher selection / ND-TK install do not read this + * field, so including the group bit here is safe. */ + if (ndl->gtk_required) { + ndl->security_ctx.csid_bitmap |= NAN_CSID_GTK_DEFAULT; + } + if (cfg->group_data_prot) { + ESP_LOGI(TAG, "NDP GTK: %s (initiator) - own group_prot=1, peer NCS-GTK=%d", + ndl->gtk_required ? "negotiated" : "NOT negotiated", + (peer_svc && peer_svc->peer_group_data_cap) ? 1 : 0); + } + /* IGTK distributed in M3 iff we enabled group_mgmt_prot AND the publisher + * advertised IGTKSA; BIGTK additionally requires BIGTKSA (§7.1.3.5). */ + bool peer_igtk = peer_svc && peer_svc->peer_group_mgmt_cap; + bool peer_bigtk = peer_svc && peer_svc->peer_group_bigtk_cap; + ndl->igtk_required = (s_nan_ctx.group_mgmt_prot && peer_igtk) ? 1 : 0; + ndl->bigtk_required = (s_nan_ctx.group_mgmt_prot && peer_bigtk) ? 1 : 0; + if (s_nan_ctx.group_mgmt_prot) { + ESP_LOGI(TAG, "NDP IGTK/BIGTK: igtk=%s bigtk=%s (initiator) - peer igtk=%d bigtk=%d", + ndl->igtk_required ? "yes" : "no", ndl->bigtk_required ? "yes" : "no", + peer_igtk, peer_bigtk); + } memcpy(ndl->security_ctx.nd_pmk, pmk, ESP_WIFI_NAN_NDP_PMK_LEN); memcpy(ndl->security_ctx.nd_pmkid, pair_pmkid, ESP_WIFI_NAN_NDP_PMKID_LEN); @@ -1153,8 +1608,27 @@ int esp_nan_construct_csia(uint8_t *frm, uint8_t pub_id, uint16_t own_csid_bitma *p++ = attr_len & 0xFF; *p++ = (attr_len >> 8) & 0xFF; - /* Capabilities byte (0x4 set for iOS interop) */ - *p++ = 0x4; + /* Capabilities group-SA bits (§9.5.21.2 Table 122) are strictly nested: + * 00 = none, 01 = GTKSA+IGTKSA, 10 = GTKSA+IGTKSA+BIGTKSA. + * There is no "IGTK/BIGTK without GTKSA" codepoint, so: + * - device-global group_mgmt_prot set -> 10 (BIGTK forces GTK+IGTK; we also + * force GTKSA on every secured service, see nan_claim_own_svc_slot); + * - else if this CSIA carries a GTK suite -> 01 (GTKSA mandates IGTKSA); + * - else -> 00. + * Advertising support never blocks a peer that lacks protection: the keys and + * frame protection are set up only AFTER mutual capability negotiation — the + * IGTK/BIGTK/GTK KDEs are exchanged iff BOTH peers advertise support + * (§7.1.3.5), and a non-supporting peer ignores the unknown MME elements and + * still connects. */ + uint8_t grp_caps; + if (s_nan_ctx.group_mgmt_prot) { + grp_caps = (uint8_t)(NAN_CSIA_CAP_GTK_SUPP_ALL << NAN_CSIA_CAP_GTK_SUPP_POS); /* 0x04 (10) */ + } else if (own_csid_bitmap & NAN_CSID_GTK_DEFAULT) { + grp_caps = (uint8_t)(NAN_CSIA_CAP_GTK_SUPP_IGTK << NAN_CSIA_CAP_GTK_SUPP_POS); /* 0x02 (01) */ + } else { + grp_caps = NAN_CSIA_CAP_GTK_SUPP_NONE; /* 0x00 (00) */ + } + *p++ = grp_caps; /* Cipher Suite ID list: [CSID(1)] [Publish ID(1)] */ for (uint8_t csid = 1; csid <= 8; csid++) { @@ -1164,7 +1638,8 @@ int esp_nan_construct_csia(uint8_t *frm, uint8_t pub_id, uint16_t own_csid_bitma } } - return (int)(p - frm); + int written = (int)(p - frm); + return written; } int esp_nan_construct_scia_publish(uint8_t *frm, uint8_t pub_id, @@ -1228,8 +1703,6 @@ uint32_t esp_nan_get_csia_len(uint16_t own_csid_bitmap, uint16_t peer_csid_bitma } uint8_t num_csids = __builtin_popcount(csid_bitmap); uint32_t len = 3 + 1 + 2 * num_csids; - ESP_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; } @@ -1256,9 +1729,112 @@ uint32_t esp_nan_get_shared_key_desc_attr_len(uint16_t key_data_len) return total; } -int esp_nan_ndp_security_install_get_shared_desc_len(void) +/* Which group KDEs a key descriptor carries (for sizing + logging). */ +#define NAN_KDE_PRESENT_GTK BIT(0) +#define NAN_KDE_PRESENT_IGTK BIT(1) +#define NAN_KDE_PRESENT_BIGTK BIT(2) + +/* Exact wrapped group Key Data length this NDL will emit — mirrors what + * nan_append_own_group_kdes() writes, with NO side effects (does not generate the + * GTK). Sets *kde_mask to the included KDEs. 0 = no group keys (pairwise-only). + * Caller holds the lock. Keep in lockstep with nan_append_{igtk,bigtk,gtk}_kde(). */ +static size_t nan_group_key_data_wrapped_len(const struct ndl_info *ndl, uint8_t *kde_mask) { - return (int)esp_nan_get_shared_key_desc_attr_len(0); + uint8_t mask = 0; + size_t plain = 0; + if (!ndl || !ndl->ptk_set) { + if (kde_mask) { + *kde_mask = 0; + } + return 0; /* no KEK yet -> nothing can be wrapped */ + } + /* Order matches the builder: IGTK, BIGTK, GTK. Each branch contributes iff + * its negotiation gate (igtk/bigtk/gtk_required) fired for this NDP. */ + if (nan_ndl_igtk_negotiated(ndl)) { + plain += NAN_KDE_HDR_LEN + NAN_IGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN; + mask |= NAN_KDE_PRESENT_IGTK; + } + if (nan_ndl_bigtk_negotiated(ndl)) { + plain += NAN_KDE_HDR_LEN + NAN_BIGTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN; + mask |= NAN_KDE_PRESENT_BIGTK; + } + if (nan_ndl_gtk_negotiated(ndl)) { + plain += NAN_KDE_HDR_LEN + NAN_GTK_KDE_PREFIX_LEN + NAN_ND_GTK_LEN; + mask |= NAN_KDE_PRESENT_GTK; + } + if (kde_mask) { + *kde_mask = mask; + } + if (plain == 0) { + return 0; + } + size_t padded = plain < 16 ? 16 : plain; /* NIST AES Key Wrap minimum */ + if (padded % 8) { + padded = (padded + 7) & ~((size_t)7); + } + return padded + 8; /* + AES-Key-Wrap overhead */ +} + +/* INFO log of the descriptor length and which group KDEs it carries, so on-device + * logs show what was sized/sent (not a silent length). */ +static void nan_log_group_desc_len(const char *msg, uint8_t ndp_id, int ret, + uint16_t key_data_len, uint8_t kde_mask, + bool found, bool ptk_set) +{ + if (!found) { + ESP_LOGW(TAG, "%s desc len: no NDL (ndp_id=%d) -> len=%d (no Key Data)", + msg, ndp_id, ret); + } else if (key_data_len == 0) { + ESP_LOGI(TAG, "%s desc len=%d (ndp_id=%d): no group KDEs (ptk_set=%d)", + msg, ret, ndp_id, ptk_set); + } else { + ESP_LOGI(TAG, "%s desc len=%d (ndp_id=%d): Key Data=%u KDEs=[%s%s%s]", + msg, ret, ndp_id, key_data_len, + (kde_mask & NAN_KDE_PRESENT_GTK) ? "GTK " : "", + (kde_mask & NAN_KDE_PRESENT_IGTK) ? "IGTK " : "", + (kde_mask & NAN_KDE_PRESENT_BIGTK) ? "BIGTK " : ""); + } +} + +/* Exact M4 (NDP Security Install) Shared Key Descriptor length for this NDP, so the + * blob allocates exactly what the builder writes (no on-air zero-padding). */ +int esp_nan_ndp_security_install_get_shared_desc_len(uint8_t ndp_id, const uint8_t *peer_nmi) +{ + uint16_t key_data_len = 0; + uint8_t kde_mask = 0; + bool found = false, ptk = false; + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (ndl) { + found = true; + ptk = ndl->ptk_set; + key_data_len = (uint16_t)nan_group_key_data_wrapped_len(ndl, &kde_mask); + } + NAN_DATA_UNLOCK(); + + int ret = (int)esp_nan_get_shared_key_desc_attr_len(key_data_len); + nan_log_group_desc_len("M4 Security Install", ndp_id, ret, key_data_len, kde_mask, found, ptk); + return ret; +} + +/* Exact M3 (NDP Confirm) length for the initiator path; same contract as M4. */ +int esp_nan_ndp_confirm_get_shared_desc_len(uint8_t ndp_id, const uint8_t *peer_nmi) +{ + uint16_t key_data_len = 0; + uint8_t kde_mask = 0; + bool found = false, ptk = false; + NAN_DATA_LOCK(); + struct ndl_info *ndl = nan_find_ndl(ndp_id, (uint8_t *)peer_nmi); + if (ndl) { + found = true; + ptk = ndl->ptk_set; + key_data_len = (uint16_t)nan_group_key_data_wrapped_len(ndl, &kde_mask); + } + NAN_DATA_UNLOCK(); + + int ret = (int)esp_nan_get_shared_key_desc_attr_len(key_data_len); + nan_log_group_desc_len("M3 Confirm", ndp_id, ret, key_data_len, kde_mask, found, ptk); + return ret; } int esp_nan_get_ndp_resp_shared_key_desc(uint8_t *buf, size_t buf_len, uint8_t ndp_id, const uint8_t *peer_nmi) @@ -1377,14 +1953,12 @@ int esp_nan_get_ndp_security_install_key_desc(uint8_t *buf, size_t buf_len, uint uint8_t *p = buf; *p++ = NAN_ATTR_ID_SHARED_KEY_DESC; - { - uint16_t body_len = 1 + NAN_KEY_DESC_MIN_LEN; - *p++ = body_len & 0xFF; - *p++ = (body_len >> 8) & 0xFF; - } + uint8_t *len_field = p; /* backpatched once Key Data length is known */ + p += 2; *p++ = ndl->publisher_id; - int n = nan_build_rsna_key_descriptor(p, + uint8_t *key_desc = p; + int n = nan_build_rsna_key_descriptor(key_desc, NAN_KEY_INFO_MIC | NAN_KEY_INFO_SECURE | NAN_KEY_INFO_INSTALL, @@ -1396,11 +1970,18 @@ int esp_nan_get_ndp_security_install_key_desc(uint8_t *buf, size_t buf_len, uint return 0; } - n = 3 + 1 + n; + /* Responder distributes its GTK in M4 (§7.1.3.2). Buffer headroom comes + * from esp_nan_ndp_security_install_get_shared_desc_len(); falls back to + * pairwise-only if the blob under-allocated the buffer. */ + n = nan_append_own_group_kdes(ndl, key_desc, buf_len - 4); + + uint16_t body_len = 1 + (uint16_t)n; + len_field[0] = body_len & 0xFF; + len_field[1] = (body_len >> 8) & 0xFF; NAN_DATA_UNLOCK(); - ESP_LOGD(TAG, "NDP M4 Key Desc: built for ndp_id=%d", ndp_id); - return n; + ESP_LOGD(TAG, "NDP M4 Key Desc: built (key_desc=%d bytes) for ndp_id=%d", n, ndp_id); + return 3 + 1 + n; } int esp_nan_update_ndp_resp_mic(uint8_t *m2_body, size_t body_len, uint8_t *key_desc_attr, @@ -1456,6 +2037,24 @@ void esp_nan_parse_ndp_csia(void *frm, size_t buf_len, wifi_nan_security_params_ s_pending_scia.has_csid = true; s_pending_scia.pub_id = pub_id; s_pending_scia.csid_bitmap = accum; + /* Peer wants group-data (GTK) protection if it advertises GTKSA + * support in the CSIA Capabilities byte (body[0] bits 1-2, + * §9.5.21.2 Table 122 — how iOS signals it) or lists an NCS-GTK + * cipher suite (how Android/ESP signal it). The Capabilities byte + * is the spec-correct indicator; an NCS-GTK CSID only selects WHICH + * group cipher and need not be present. */ + s_pending_scia.peer_group_data = + ((body[0] & NAN_CSIA_CAP_GTK_SUPP_MASK) || + (accum & NAN_CSID_GTK_BITS)) ? true : false; + if (s_pending_scia.peer_group_data) { + param->group_data_prot = 1; + } + /* Store the raw CSIA Capabilities byte; IGTK/BIGTK gating derives + * bits 1-2 (01=IGTKSA, 10=+BIGTKSA) independently at NDL finalize. */ + s_pending_scia.peer_caps = body[0]; + if (body[0] & NAN_CSIA_CAP_GTK_SUPP_MASK) { + param->group_mgmt_prot = 1; + } } } } @@ -1612,57 +2211,106 @@ void esp_nan_parse_ndp_key_desc(void *frm, size_t buf_len, uint8_t ndp_id, const memcpy(ndl->key_rsc, key_rsc, NAN_KEY_RSC_LEN); - /* Parse Key Data (KDEs) - only when not encrypted */ - if (key_data_len > 0 && (NAN_KEY_DESC_DATA_OFF + key_data_len <= key_desc_len) && !has_enc_key) { - uint8_t *key_data = &key_desc[NAN_KEY_DESC_DATA_OFF]; + /* Parse Key Data (KDEs). Per §7.1.3.5 group keys are always carried + * KEK-wrapped, so decrypt with the ND-KEK before walking. The plaintext + * may carry 0xDD/0x00 AES-Key-Wrap padding after the last KDE. */ + if (key_data_len > 0 && (NAN_KEY_DESC_DATA_OFF + key_data_len <= key_desc_len)) { + uint8_t plain[NAN_GROUP_KEY_DATA_MAX]; + const uint8_t *kde_buf = &key_desc[NAN_KEY_DESC_DATA_OFF]; + size_t kde_len = key_data_len; + + if (has_enc_key) { + size_t unwrapped = 0; + if (!ndl->ptk_set) { + ESP_LOGW(TAG, "NDP Key Desc: encrypted Key Data but PTK/KEK not set; skipping KDEs"); + kde_len = 0; + } else if (nan_kek_unwrap_key_data(ndl, &key_desc[NAN_KEY_DESC_DATA_OFF], + key_data_len, plain, sizeof(plain), + &unwrapped) != 0) { + ESP_LOGW(TAG, "NDP Key Desc: KEK unwrap of Key Data failed; skipping KDEs"); + kde_len = 0; + } else { + kde_buf = plain; + kde_len = unwrapped; + } + } else { + /* §7.1.3.5 / 802.11 §12.7.2: group KDEs are only ever carried + * KEK-wrapped; ignore any Key Data presented in the clear. */ + ESP_LOGW(TAG, "NDP Key Desc: Key Data not encrypted; ignoring KDEs"); + kde_len = 0; + } + uint16_t kd_offset = 0; - - while (kd_offset + 2 <= key_data_len) { - uint8_t kde_type = key_data[kd_offset]; - uint8_t kde_len = key_data[kd_offset + 1]; - - if (kd_offset + 2 + kde_len > key_data_len) { + while (kd_offset + 2 <= kde_len) { + uint8_t kde_id = kde_buf[kd_offset]; + uint8_t kde_flen = kde_buf[kd_offset + 1]; + /* All KDEs start with 0xDD; a 0xDD/0x00 pad (or any short + * element) terminates the meaningful list. */ + if (kde_id != 0xDD || kde_flen < 4 || + kd_offset + 2 + kde_flen > kde_len) { break; } - if (kde_type == 0xDD && kde_len >= 4) { - uint32_t oui = (key_data[kd_offset + 2] << 16) | (key_data[kd_offset + 3] << 8) | key_data[kd_offset + 4]; - uint8_t data_type = key_data[kd_offset + 5]; - uint8_t *data = &key_data[kd_offset + 6]; - uint8_t data_len = kde_len - 4; + uint32_t oui = (kde_buf[kd_offset + 2] << 16) | + (kde_buf[kd_offset + 3] << 8) | kde_buf[kd_offset + 4]; + uint8_t data_type = kde_buf[kd_offset + 5]; + const uint8_t *body = &kde_buf[kd_offset + 6]; + uint8_t body_len = kde_flen - 4; /* after OUI(3) + DataType(1) */ - if (oui == 0x000FAC) { /* WFA OUI */ - if (data_type == 1 && data_len <= NAN_GTK_MAX_LEN) { - memcpy(ndl->gtk, data, data_len); - ndl->gtk_len = data_len; + if (oui == NAN_KDE_OUI_RSN) { + if (data_type == NAN_KDE_TYPE_GTK && body_len > NAN_GTK_KDE_PREFIX_LEN) { + /* GTK KDE: KeyID/Tx(1) Reserved(1) GTK(var) */ + uint8_t gtk_len = body_len - NAN_GTK_KDE_PREFIX_LEN; + if (gtk_len <= NAN_GTK_MAX_LEN) { + ndl->gtk_keyid = body[0] & 0x03; + memcpy(ndl->gtk, body + NAN_GTK_KDE_PREFIX_LEN, gtk_len); + ndl->gtk_len = gtk_len; + memcpy(ndl->gtk_rsc, key_rsc, NAN_KEY_RSC_LEN); ndl->gtk_set = 1; - ESP_LOGI(TAG, "KDE: GTK stored (len=%d)", data_len); - } else if (data_type == 3 && data_len >= 6) { - ESP_LOGI(TAG, "KDE: MAC Address: " MACSTR, MAC2STR(data)); - } else if (data_type == 4 && data_len <= NAN_GTK_MAX_LEN) { - memcpy(ndl->igtk, data, data_len); - ndl->igtk_len = data_len; - ndl->igtk_set = 1; - ESP_LOGI(TAG, "KDE: IGTK stored (len=%d)", data_len); - } else if (data_type == 5 && data_len <= NAN_GTK_MAX_LEN) { - memcpy(ndl->bigtk, data, data_len); - ndl->bigtk_len = data_len; - ndl->bigtk_set = 1; - ESP_LOGI(TAG, "KDE: BIGTK stored (len=%d)", data_len); + ESP_LOGI(TAG, "KDE: peer GTK stored (keyid=%d, len=%d)", + ndl->gtk_keyid, gtk_len); } - } else if (oui == 0x506F9A) { /* NAN OUI */ - if (data_type == 36 && data_len >= 1) { - ESP_LOGI(TAG, "KDE: NIK (Cipher Ver: %d)", data[0]); - } else if (data_type == 37 && data_len >= 6) { - uint16_t key_bitmap = data[0] | (data[1] << 8); - uint32_t lifetime = (data[2] << 24) | (data[3] << 16) | (data[4] << 8) | data[5]; - ESP_LOGI(TAG, "KDE: NAN Key Lifetime: %lu s, Bitmap: 0x%04x", - (unsigned long)lifetime, key_bitmap); + } else if (data_type == NAN_KDE_TYPE_MAC && body_len >= 6) { + ESP_LOGI(TAG, "KDE: MAC Address: " MACSTR, MAC2STR(body)); + } else if (data_type == NAN_KDE_TYPE_IGTK && body_len > NAN_IGTK_KDE_PREFIX_LEN) { + /* IGTK KDE: KeyID(2 LE) IPN(6) IGTK(var) */ + uint8_t igtk_len = body_len - NAN_IGTK_KDE_PREFIX_LEN; + if (igtk_len <= NAN_GTK_MAX_LEN) { + ndl->igtk_keyid = body[0]; + memcpy(ndl->igtk_ipn, body + 2, 6); /* IPN follows KeyID(2) */ + memcpy(ndl->igtk, body + NAN_IGTK_KDE_PREFIX_LEN, igtk_len); + ndl->igtk_len = igtk_len; + ndl->igtk_set = 1; + ESP_LOGI(TAG, "KDE: peer IGTK stored (keyid=%d, len=%d)", + ndl->igtk_keyid, igtk_len); + } + } else if (data_type == NAN_KDE_TYPE_BIGTK && body_len > NAN_BIGTK_KDE_PREFIX_LEN) { + /* BIGTK KDE: KeyID(2 LE) BIPN(6) BIGTK(var) */ + uint8_t bigtk_len = body_len - NAN_BIGTK_KDE_PREFIX_LEN; + if (bigtk_len <= NAN_GTK_MAX_LEN) { + ndl->bigtk_keyid = body[0]; + memcpy(ndl->bigtk_ipn, body + 2, 6); /* BIPN follows KeyID(2) */ + memcpy(ndl->bigtk, body + NAN_BIGTK_KDE_PREFIX_LEN, bigtk_len); + ndl->bigtk_len = bigtk_len; + ndl->bigtk_set = 1; + ESP_LOGI(TAG, "KDE: peer BIGTK stored (keyid=%d, len=%d)", + ndl->bigtk_keyid, bigtk_len); } } + } else if (oui == NAN_KDE_OUI_WFA) { + if (data_type == NAN_KDE_TYPE_NIK && body_len >= 1) { + ESP_LOGI(TAG, "KDE: NIK (Cipher Ver: %d)", body[0]); + } else if (data_type == NAN_KDE_TYPE_KEY_LIFE && body_len >= 6) { + uint16_t key_bitmap = body[0] | (body[1] << 8); + uint32_t lifetime = (body[2] << 24) | (body[3] << 16) | + (body[4] << 8) | body[5]; + ESP_LOGI(TAG, "KDE: NAN Key Lifetime: %lu s, Bitmap: 0x%04x", + (unsigned long)lifetime, key_bitmap); + } } - kd_offset += 2 + kde_len; + kd_offset += 2 + kde_flen; } + forced_memzero(plain, sizeof(plain)); /* scrub decrypted group keys */ } } else if (msg_type == 1) { /* M1 received but NDL not created yet — store as pending */ @@ -1698,7 +2346,19 @@ esp_err_t esp_nan_parse_publish_security(const uint8_t *attrs, size_t attrs_len, ESP_LOGD(TAG, "Found CSIA in Publish SDF, len=%d", csia_len); ESP_LOG_BUFFER_HEXDUMP(TAG, csia, csia_len, ESP_LOG_DEBUG); - /* Skip Capabilities byte; iterate [CSID(1)] [Publish ID(1)] entries */ + /* Capabilities byte (csia[0]) bits 1-2 = GTKSA/IGTKSA/BIGTKSA support + * (§9.5.21.2 Table 122): 01 = GTKSA+IGTKSA, 10 = +BIGTKSA. This — not an + * NCS-GTK cipher-suite ID — is how a peer (notably iOS) advertises it. */ + uint8_t grp_supp = csia[0] & NAN_CSIA_CAP_GTK_SUPP_MASK; + if (grp_supp) { + security->group_data_prot = 1; + security->group_mgmt_prot = 1; /* IGTKSA */ + } + if (grp_supp == (NAN_CSIA_CAP_GTK_SUPP_ALL << NAN_CSIA_CAP_GTK_SUPP_POS)) { + security->group_bigtk_prot = 1; /* BIGTKSA */ + } + + /* iterate [CSID(1)] [Publish ID(1)] entries after the Capabilities byte */ size_t offset = 1; while (offset + 2 <= csia_len) { uint8_t csid = csia[offset]; @@ -1710,6 +2370,12 @@ esp_err_t esp_nan_parse_publish_security(const uint8_t *attrs, size_t attrs_len, offset += 2; } + + /* Android/ESP peers advertise group-data support by listing an NCS-GTK + * cipher suite instead of (or with) the Capabilities byte bits. */ + if (security->csid_bitmap & NAN_CSID_GTK_BITS) { + security->group_data_prot = 1; + } } /* 2. SCIA — PMKIDs */ @@ -1762,6 +2428,33 @@ void nan_security_apply_pending(struct ndl_info *ndl, (s_pending_scia.has_csid || s_pending_scia.has_pmkid)) { if (s_pending_scia.has_csid) { + /* GTK is exchanged only if both sides support it: our service must + * enable group_data_prot AND the peer must have signalled group-data + * support in its NDP-Request CSIA (Capabilities byte for iOS, or an + * NCS-GTK cipher suite for Android/ESP — captured in peer_group_data + * by esp_nan_parse_ndp_csia). */ + ndl->gtk_required = (p_own_svc && p_own_svc->user_cfg.group_data_prot && + s_pending_scia.peer_group_data) ? 1 : 0; + if (p_own_svc && p_own_svc->user_cfg.group_data_prot) { + ESP_LOGI(TAG, "NDP GTK: %s (responder) - own group_prot=1, peer group_data=%d", + ndl->gtk_required ? "negotiated" : "NOT negotiated", + s_pending_scia.peer_group_data); + } + /* IGTK distributed in M4 iff we enabled group_mgmt_prot AND the peer + * advertised IGTKSA; BIGTK additionally requires BIGTKSA (§7.1.3.5). */ + { + bool own_mgmt = s_nan_ctx.group_mgmt_prot; + uint8_t grp = s_pending_scia.peer_caps & NAN_CSIA_CAP_GTK_SUPP_MASK; + bool peer_igtk = grp != 0; + bool peer_bigtk = grp == (NAN_CSIA_CAP_GTK_SUPP_ALL << NAN_CSIA_CAP_GTK_SUPP_POS); + ndl->igtk_required = (own_mgmt && peer_igtk) ? 1 : 0; + ndl->bigtk_required = (own_mgmt && peer_bigtk) ? 1 : 0; + if (own_mgmt) { + ESP_LOGI(TAG, "NDP IGTK/BIGTK: igtk=%s bigtk=%s (responder) - peer caps=0x%02x", + ndl->igtk_required ? "yes" : "no", ndl->bigtk_required ? "yes" : "no", + s_pending_scia.peer_caps); + } + } ndl->security_ctx.csid_bitmap = s_pending_scia.csid_bitmap; ndl->security_ctx.type = WIFI_NAN_SECURITY_ENCRYPTED; } @@ -2105,14 +2798,12 @@ int esp_nan_get_ndp_confirm_shared_key_desc(uint8_t *buf, size_t buf_len, uint8_ uint8_t *p = buf; *p++ = NAN_ATTR_ID_SHARED_KEY_DESC; - { - uint16_t body_len = 1 + NAN_KEY_DESC_MIN_LEN; - *p++ = body_len & 0xFF; - *p++ = (body_len >> 8) & 0xFF; - } + uint8_t *len_field = p; /* backpatched once Key Data length is known */ + p += 2; *p++ = ndl->publisher_id; - int n = nan_build_rsna_key_descriptor(p, + uint8_t *key_desc = p; + int n = nan_build_rsna_key_descriptor(key_desc, NAN_KEY_INFO_MIC | NAN_KEY_INFO_SECURE | NAN_KEY_INFO_ACK, @@ -2124,12 +2815,19 @@ int esp_nan_get_ndp_confirm_shared_key_desc(uint8_t *buf, size_t buf_len, uint8_ return 0; } - n = 3 + 1 + n; + /* Initiator distributes its GTK in M3 (§7.1.3.2). Needs the blob to size + * the M3 buffer with GTK headroom (ndp_confirm_get_shared_desc_len); + * falls back to pairwise-only otherwise. */ + n = nan_append_own_group_kdes(ndl, key_desc, buf_len - 4); + + uint16_t body_len = 1 + (uint16_t)n; + len_field[0] = body_len & 0xFF; + len_field[1] = (body_len >> 8) & 0xFF; NAN_DATA_UNLOCK(); /* State transition to M3_SENT happens in host TX-confirm hook */ - ESP_LOGD(TAG, "NDP M3 Key Desc: built (MIC=0, driver must call esp_nan_update_ndp_confirm_mic) for ndp_id=%d", ndp_id); - return n; + ESP_LOGD(TAG, "NDP M3 Key Desc: built (key_desc=%d bytes, MIC=0; driver must call esp_nan_update_ndp_confirm_mic) for ndp_id=%d", n, ndp_id); + return 3 + 1 + n; } /** diff --git a/components/wpa_supplicant/esp_supplicant/src/esp_wifi_driver.h b/components/wpa_supplicant/esp_supplicant/src/esp_wifi_driver.h index 1ee7ff95827..e58fa811542 100644 --- a/components/wpa_supplicant/esp_supplicant/src/esp_wifi_driver.h +++ b/components/wpa_supplicant/esp_supplicant/src/esp_wifi_driver.h @@ -238,6 +238,8 @@ typedef enum { NAN_KEY_ND_TK = 0, NAN_KEY_ND_GTK, NAN_KEY_NM_TK, + NAN_KEY_ND_IGTK, /* 3 - NAN Integrity Group Temporal Key (BIP-CMAC-128) */ + NAN_KEY_ND_BIGTK, /* 4 - NAN Beacon Integrity Group Temporal Key (BIP-CMAC-128) */ } nan_key_type_t; typedef struct { diff --git a/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild b/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild index b336577c920..72c296bf369 100644 --- a/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild +++ b/examples/wifi/wifi_aware/nan_publisher/main/Kconfig.projbuild @@ -35,6 +35,16 @@ menu "Example Configuration" the same credential (passphrase or PMK). Disable to advertise an open (unencrypted) service. + config EXAMPLE_NAN_GROUP_DATA_PROT + bool "Protect group-addressed datapath traffic (ND-GTK)" + depends on EXAMPLE_NAN_SECURITY_ENABLED + default y + help + Negotiate and install an ND-GTK so group-addressed (multicast/ + broadcast) frames on the NAN datapath are encrypted. This is + required for IPv6 over the secured datapath (Neighbor Discovery, + MLD). Both peers must enable this for group keys to be exchanged. + choice EXAMPLE_NAN_SECURITY_METHOD prompt "Security Method" depends on EXAMPLE_NAN_SECURITY_ENABLED diff --git a/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c b/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c index 47be5ab4c0b..febbea48085 100644 --- a/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c +++ b/examples/wifi/wifi_aware/nan_publisher/main/publisher_main.c @@ -133,6 +133,9 @@ void wifi_nan_publish(void) }; #ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED wifi_nan_discovery_security_params_t security_cfg = { +#ifdef CONFIG_EXAMPLE_NAN_GROUP_DATA_PROT + .group_data_prot = 1, /* distribute/accept ND-GTK for group-addressed data */ +#endif .num_credentials = 1, .creds = { { diff --git a/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild b/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild index 36c87d4eaa7..82ec413ec45 100644 --- a/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild +++ b/examples/wifi/wifi_aware/nan_subscriber/main/Kconfig.projbuild @@ -45,6 +45,16 @@ menu "Example Configuration" (passphrase or PMK) and sets up an encrypted NDP. Disable to discover open (unencrypted) services. + config EXAMPLE_NAN_GROUP_DATA_PROT + bool "Protect group-addressed datapath traffic (ND-GTK)" + depends on EXAMPLE_NAN_SECURITY_ENABLED + default y + help + Negotiate and install an ND-GTK so group-addressed (multicast/ + broadcast) frames on the NAN datapath are encrypted. This is + required for IPv6 over the secured datapath (Neighbor Discovery, + MLD). Both peers must enable this for group keys to be exchanged. + choice EXAMPLE_NAN_SECURITY_METHOD prompt "Security Method" depends on EXAMPLE_NAN_SECURITY_ENABLED diff --git a/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c b/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c index 870cc0e7cc9..dfb01df751b 100644 --- a/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c +++ b/examples/wifi/wifi_aware/nan_subscriber/main/subscriber_main.c @@ -227,6 +227,9 @@ void wifi_nan_subscribe(void) }; #ifdef CONFIG_EXAMPLE_NAN_SECURITY_ENABLED wifi_nan_discovery_security_params_t security_cfg = { +#ifdef CONFIG_EXAMPLE_NAN_GROUP_DATA_PROT + .group_data_prot = 1, /* distribute/accept ND-GTK for group-addressed data */ +#endif .num_credentials = 1, .creds = { {