feat(nan): persist NIK/NPK credentials in NVS

Replace nik/nik_valid in wifi_nan_sync_config_t with reset_current_nvs_creds
and use_nvs_for_caching. On NAN start, load the saved own NIK and peer
credentials from NVS (or erase them when reset is requested); generate and
persist a fresh own NIK only when none is valid and caching is enabled.

PASN reuses the SAE module (PWE/crypto and the comeback-token mechanism),
so define CONFIG_SAE whenever SoftAP-SAE or PASN is enabled. This fixes the
undefined references to check_comeback_token()/auth_build_token_req() when
SOFTAP config is disabled.
This commit is contained in:
Nachiket Kukade
2026-06-11 11:25:31 +05:30
committed by Jack
parent e70033e6a7
commit ebf7dc9b31
11 changed files with 139 additions and 34 deletions

View File

@@ -604,9 +604,8 @@ typedef struct {
uint8_t scan_time; /**< Scan time in seconds while searching for a NAN cluster */
uint16_t warm_up_sec; /**< Warm up time before assuming NAN Anchor Master role */
bool disable_random_mac;/**< Disable the MAC Randomisation in NAN */
uint8_t nik[ESP_WIFI_NAN_NIK_LEN]; /**< Optional NIK. Auto-generated when nik_valid is false. */
uint8_t nik_valid: 1; /**< NIK present in nik[] and should be used as-is. */
uint8_t reserved: 7; /**< Reserved for future use. */
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. */
} wifi_nan_sync_config_t;
/**
@@ -874,7 +873,9 @@ typedef struct {
#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_NPK_LEN ESP_WIFI_NAN_NDP_PMK_LEN /**< Length of NAN Pairwise Key (same as NDP PMK) */
#define ESP_WIFI_NAN_NDP_PMKID_LEN 16 /**< Length of NAN Datapath PMKID */
#define ESP_WIFI_NAN_MAX_PEER_CREDS 2 /**< <Maximum number of NAN peer credentials that can be stored */
#define ESP_WIFI_NAN_MAX_CREDS_PER_SVC 4 /**< Maximum number of NAN security credentials per service (passphrase/PMK entries) */
#define ESP_WIFI_MAX_SVC_NAME_LEN 256 /**< Maximum length of NAN service name */

View File

@@ -604,9 +604,8 @@ typedef struct {
uint8_t scan_time; /**< Scan time in seconds while searching for a NAN cluster */
uint16_t warm_up_sec; /**< Warm up time before assuming NAN Anchor Master role */
bool disable_random_mac;/**< Disable the MAC Randomisation in NAN */
uint8_t nik[ESP_WIFI_NAN_NIK_LEN]; /**< Optional NIK. Auto-generated when nik_valid is false. */
uint8_t nik_valid: 1; /**< NIK present in nik[] and should be used as-is. */
uint8_t reserved: 7; /**< Reserved for future use. */
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. */
} wifi_nan_sync_config_t;
/**
@@ -874,7 +873,9 @@ typedef struct {
#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_NPK_LEN ESP_WIFI_NAN_NDP_PMK_LEN /**< Length of NAN Pairwise Key (same as NDP PMK) */
#define ESP_WIFI_NAN_NDP_PMKID_LEN 16 /**< Length of NAN Datapath PMKID */
#define ESP_WIFI_NAN_MAX_PEER_CREDS 2 /**< Maximum number of NAN peer credentials that can be stored */
#define ESP_WIFI_NAN_MAX_CREDS_PER_SVC 4 /**< Maximum number of NAN security credentials per service (passphrase/PMK entries) */
#define ESP_WIFI_MAX_SVC_NAME_LEN 256 /**< Maximum length of NAN service name */

View File

@@ -22,7 +22,8 @@ extern "C" {
.scan_time = 3, \
.warm_up_sec = 5, \
.disable_random_mac = false, \
.nik_valid = false, \
.reset_current_nvs_creds = false, \
.use_nvs_for_caching = false, \
};
#define NDP_STATUS_ACCEPTED 1

View File

@@ -1517,16 +1517,33 @@ esp_err_t esp_wifi_nan_sync_start(const wifi_nan_sync_config_t *nan_cfg)
return ESP_OK;
}
#ifdef CONFIG_ESP_WIFI_NAN_SECURITY
if (nan_cfg->nik_valid) {
memcpy(s_nan_ctx.own_nik, nan_cfg->nik, ESP_WIFI_NAN_NIK_LEN);
s_nan_ctx.own_nik_valid = true;
} else {
s_nan_ctx.own_nik_valid = false;
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;
if (nan_cfg->reset_current_nvs_creds) {
/* Start from a clean slate: drop every credential persisted in NVS. */
esp_wifi_nan_erase_all_creds();
} else if (esp_wifi_nan_load_saved_creds(s_nan_ctx.own_nik, &s_nan_ctx.own_nik_valid,
s_nan_ctx.peer_creds, &s_nan_ctx.num_peer_creds) != ESP_OK) {
ESP_LOGW(TAG, "Failed to load saved NAN credentials");
s_nan_ctx.own_nik_valid = false;
s_nan_ctx.num_peer_creds = 0;
}
if (!s_nan_ctx.own_nik_valid) {
if (os_get_random(s_nan_ctx.own_nik, ESP_WIFI_NAN_NIK_LEN) != 0) {
NAN_DATA_UNLOCK();
ESP_LOGE(TAG, "Failed to generate NAN NIK");
return ESP_FAIL;
}
s_nan_ctx.own_nik_valid = true;
/* Persist the freshly generated NIK only when NVS caching is enabled;
* otherwise the identity stays ephemeral for this session. */
if (s_nan_ctx.use_nvs_for_caching) {
esp_wifi_nan_save_own_nik(s_nan_ctx.own_nik);
}
}
/* Drop the cached NIRA tag; it was derived from the previous NIK. */
s_nan_ctx.nira_cached = false;

View File

@@ -17,6 +17,8 @@
#include "esp_wifi_types_generic.h"
#include "esp_private/wifi.h"
#include "esp_nan.h"
#include "utils/common.h" /* u8/u16 typedefs required by esp_wifi_driver.h */
#include "esp_wifi_driver.h" /* wifi_nan_peer_creds_t + NAN credential NVS APIs */
#include "os.h"
#ifdef __cplusplus
@@ -296,6 +298,12 @@ typedef struct {
uint8_t cached_nira_nonce[8];
uint8_t cached_nira_tag[8];
bool nira_cached;
/* Peer NIK/NPK credentials loaded from NVS at start and refreshed as peers
* are paired. Drives NIRA identity resolution; optionally persisted to NVS
* when @c use_nvs_for_caching is set. */
wifi_nan_peer_creds_t peer_creds[ESP_WIFI_NAN_MAX_PEER_CREDS];
uint8_t num_peer_creds;
bool use_nvs_for_caching;
#endif
#ifdef CONFIG_ESP_WIFI_PASN_SUPPORT
struct nan_pasn_data *nan_pasn_data;

View File

@@ -847,6 +847,39 @@ static void nan_pairing_key_installed_cb(const uint8_t *peer_nmi,
}
}
/* Insert or refresh a (peer NIK, NPK) entry in the in-RAM credential cache used
* for NIRA identity resolution. Caller holds NAN_DATA_LOCK. A @a npk of NULL
* stores a zeroed key. When the cache is full the oldest entry (slot 0) is
* reused. */
static void nan_app_update_peer_creds(const uint8_t *peer_nik, const uint8_t *npk)
{
wifi_nan_peer_creds_t *slot = NULL;
/* Reuse the slot already holding this NIK, if any. */
for (uint8_t i = 0; i < s_nan_ctx.num_peer_creds; i++) {
if (s_nan_ctx.peer_creds[i].is_valid &&
os_memcmp(s_nan_ctx.peer_creds[i].peer_nik, peer_nik, ESP_WIFI_NAN_NIK_LEN) == 0) {
slot = &s_nan_ctx.peer_creds[i];
break;
}
}
if (!slot) {
if (s_nan_ctx.num_peer_creds < ESP_WIFI_NAN_MAX_PEER_CREDS) {
slot = &s_nan_ctx.peer_creds[s_nan_ctx.num_peer_creds++];
} else {
slot = &s_nan_ctx.peer_creds[0];
}
}
memcpy(slot->peer_nik, peer_nik, ESP_WIFI_NAN_NIK_LEN);
if (npk) {
memcpy(slot->npk, npk, ESP_WIFI_NAN_NPK_LEN);
} else {
memset(slot->npk, 0, ESP_WIFI_NAN_NPK_LEN);
}
slot->is_valid = true;
}
void nan_app_receive_pairing_followup(uint8_t svc_id, uint8_t peer_svc_id,
const uint8_t *peer_mac,
const uint8_t *shared_key_attr,
@@ -891,6 +924,8 @@ void nan_app_receive_pairing_followup(uint8_t svc_id, uint8_t peer_svc_id,
bool already_had_nik = false;
bool pairing_completed = false;
uint8_t own_svc_id_to_disable = 0;
bool persist_creds = false;
uint8_t persist_npk[ESP_WIFI_NAN_NPK_LEN] = {0};
NAN_DATA_LOCK();
struct peer_svc_info *p_peer_svc = nan_find_peer_svc_exact(svc_id, peer_svc_id, peer_mac);
@@ -903,6 +938,15 @@ void nan_app_receive_pairing_followup(uint8_t svc_id, uint8_t peer_svc_id,
ESP_LOGI(TAG, "Stored peer NIK from " MACSTR " (cipher_ver=%u, lifetime=%u s)",
MAC2STR(peer_mac), cipher_ver, lifetime_sec);
/* Refresh the NIRA credential cache with this peer's NIK and, if the
* pairing record is available, its NPK. */
const struct nan_paired_peer *paired = nan_app_find_paired_peer(peer_mac);
if (paired) {
memcpy(persist_npk, paired->nd_pmk, ESP_WIFI_NAN_NPK_LEN);
}
nan_app_update_peer_creds(nik, paired ? paired->nd_pmk : NULL);
persist_creds = s_nan_ctx.use_nvs_for_caching;
if (!already_had_nik) {
pairing_completed = true;
own_svc_id_to_disable = p_peer_svc->own_svc_id;
@@ -910,6 +954,11 @@ void nan_app_receive_pairing_followup(uint8_t svc_id, uint8_t peer_svc_id,
}
NAN_DATA_UNLOCK();
/* Persist outside the lock; NVS writes can block. */
if (persist_creds) {
esp_wifi_nan_save_creds_for_peer(nik, persist_npk);
}
/* Invoke blocking calls outside NAN_DATA_LOCK to avoid deadlock. */
if (pairing_completed) {
wifi_event_nan_pairing_complete_t evt = {0};
@@ -959,8 +1008,7 @@ bool esp_nan_verify_nira(uint8_t *peer_mac, uint8_t *nira_attr, uint16_t nira_at
uint8_t expected_tag[NAN_NIRA_TAG_LEN];
const uint8_t *nonce;
const uint8_t *received_tag;
struct peer_svc_info *p_peer_svc;
bool match;
bool match = false;
if (!peer_mac || !nira_attr) {
return false;
@@ -975,26 +1023,28 @@ bool esp_nan_verify_nira(uint8_t *peer_mac, uint8_t *nira_attr, uint16_t nira_at
nonce = nira_attr + 4;
received_tag = nira_attr + 4 + NAN_NIRA_NONCE_LEN;
/* The peer derives its NIRA tag from one of its NIKs; the sending MAC may be
* randomised, so resolve the identity by trying every cached peer NIK. */
NAN_DATA_LOCK();
p_peer_svc = nan_find_peer_svc(0, 0, peer_mac);
if (!p_peer_svc || !p_peer_svc->has_nik) {
NAN_DATA_UNLOCK();
ESP_LOGD(TAG, "NIRA verify: no stored NIK for "MACSTR, MAC2STR(peer_mac));
return false;
}
if (nan_pairing_derive_nira_tag(p_peer_svc->peer_nik, peer_mac, nonce, expected_tag) != 0) {
NAN_DATA_UNLOCK();
ESP_LOGE(TAG, "NIRA verify: tag derivation failed for "MACSTR, MAC2STR(peer_mac));
return false;
for (uint8_t i = 0; i < s_nan_ctx.num_peer_creds; i++) {
if (!s_nan_ctx.peer_creds[i].is_valid) {
continue;
}
if (nan_pairing_derive_nira_tag(s_nan_ctx.peer_creds[i].peer_nik, peer_mac,
nonce, expected_tag) != 0) {
continue;
}
if (os_memcmp_const(expected_tag, received_tag, NAN_NIRA_TAG_LEN) == 0) {
match = true;
break;
}
}
NAN_DATA_UNLOCK();
match = (os_memcmp_const(expected_tag, received_tag, NAN_NIRA_TAG_LEN) == 0);
if (match) {
ESP_LOGD(TAG, "NIRA verify: OK for "MACSTR, MAC2STR(peer_mac));
} else {
ESP_LOGW(TAG, "NIRA verify: tag mismatch for "MACSTR, MAC2STR(peer_mac));
ESP_LOGW(TAG, "NIRA verify: no matching NIK for "MACSTR, MAC2STR(peer_mac));
}
return match;
}

View File

@@ -314,7 +314,11 @@ endif()
if(CONFIG_ESP_WIFI_ENABLE_SAE_H2E)
target_compile_definitions(${COMPONENT_LIB} PRIVATE CONFIG_SAE_H2E)
endif()
if(CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT)
# PASN reuses the SAE module (PWE/crypto + comeback-token mechanism), so
# CONFIG_SAE must be defined whenever SoftAP-SAE or PASN is enabled. AP-side SAE
# authentication paths are additionally gated on CONFIG_ESP_WIFI_SOFTAP_SUPPORT
# in the sources so they stay out of PASN-only (SoftAP-disabled) builds.
if(CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT OR CONFIG_ESP_WIFI_PASN_SUPPORT)
target_compile_definitions(${COMPONENT_LIB} PRIVATE CONFIG_SAE)
endif()
if(CONFIG_ESP_WIFI_WPA3_COMPATIBLE_SUPPORT)

View File

@@ -240,6 +240,12 @@ typedef enum {
NAN_KEY_NM_TK,
} nan_key_type_t;
typedef struct {
uint8_t peer_nik[ESP_WIFI_NAN_NIK_LEN]; /**< Peer's NAN Identity Key (16 bytes) */
uint8_t npk[ESP_WIFI_NAN_NPK_LEN]; /**< NAN Pairwise Key / NCS-SK PMK (32 bytes) */
bool is_valid;
} wifi_nan_peer_creds_t;
typedef wifi_scan_channel_bitmap_t channel_bitmap_t;
uint8_t *esp_wifi_ap_get_prof_pmk_internal(void);
@@ -335,5 +341,11 @@ uint8_t esp_wifi_op_class_supported_internal(uint8_t op_class, uint8_t min_chan,
bool esp_wifi_is_wpa3_compatible_mode_enabled(uint8_t if_index);
uint8_t esp_wifi_ap_get_owe_config_internal(void);
esp_err_t esp_nan_disable_pairing(uint8_t svc_id);
esp_err_t esp_wifi_nan_load_saved_creds(uint8_t own_nik[ESP_WIFI_NAN_NIK_LEN], bool *own_nik_valid,
wifi_nan_peer_creds_t peer_creds[ESP_WIFI_NAN_MAX_PEER_CREDS], uint8_t *num_peer_creds);
esp_err_t esp_wifi_nan_save_own_nik(const uint8_t own_nik[ESP_WIFI_NAN_NIK_LEN]) ;
esp_err_t esp_wifi_nan_save_creds_for_peer(const uint8_t peer_nik[ESP_WIFI_NAN_NIK_LEN],
const uint8_t npk[ESP_WIFI_NAN_NPK_LEN]);
esp_err_t esp_wifi_nan_erase_all_creds(void);
#endif /* _ESP_WIFI_DRIVER_H_ */

View File

@@ -441,7 +441,11 @@ void esp_wifi_unregister_wpa3_cb(void)
}
#endif /* CONFIG_WPA3_SAE */
#ifdef CONFIG_SAE
/* AP-side (hostap) SAE authentication. CONFIG_SAE may also be enabled for PASN
* with SoftAP disabled, but this block both defines and calls SoftAP-only
* primitives (esp_send_sae_auth_reply, handle_auth_sae), so additionally gate
* it on CONFIG_ESP_WIFI_SOFTAP_SUPPORT. */
#if defined(CONFIG_SAE) && defined(CONFIG_ESP_WIFI_SOFTAP_SUPPORT)
static TaskHandle_t g_wpa3_hostap_task_hdl = NULL;
static QueueHandle_t g_wpa3_hostap_evt_queue = NULL;
@@ -861,4 +865,4 @@ void esp_wifi_register_wpa3_ap_cb(struct wpa_funcs *wpa_cb)
wpa_cb->wpa3_hostap_handle_auth = wpa3_hostap_handle_auth;
}
#endif /* CONFIG_SAE */
#endif /* CONFIG_SAE && CONFIG_ESP_WIFI_SOFTAP_SUPPORT */

View File

@@ -6,6 +6,7 @@
#ifndef ESP_WPA3_H
#define ESP_WPA3_H
#include "sdkconfig.h"
#include "esp_wifi_driver.h"
#ifdef CONFIG_WPA3_SAE
@@ -27,7 +28,10 @@ static inline void esp_wpa3_free_sae_data(void)
#endif /* CONFIG_WPA3_SAE */
#ifdef CONFIG_SAE
/* AP-side (hostap) SAE definitions require SoftAP; CONFIG_SAE alone may be set
* for PASN. The #else branch provides a no-op esp_wifi_register_wpa3_ap_cb()
* stub so callers link in PASN-only (SoftAP-disabled) builds. */
#if defined(CONFIG_SAE) && defined(CONFIG_ESP_WIFI_SOFTAP_SUPPORT)
enum SIG_WPA3_TASK {
SIG_WPA3_RX_COMMIT,
SIG_WPA3_RX_CONFIRM,
@@ -58,12 +62,12 @@ void esp_wifi_register_wpa3_ap_cb(struct wpa_funcs *wpa_cb);
int wpa3_hostap_auth_init(void *data);
bool wpa3_hostap_auth_deinit(void);
#else /* CONFIG_SAE */
#else /* CONFIG_SAE && CONFIG_ESP_WIFI_SOFTAP_SUPPORT */
static inline void esp_wifi_register_wpa3_ap_cb(struct wpa_funcs *wpa_cb)
{
wpa_cb->wpa3_hostap_handle_auth = NULL;
}
#endif /* CONFIG_SAE */
#endif /* CONFIG_SAE && CONFIG_ESP_WIFI_SOFTAP_SUPPORT */
#endif /* ESP_WPA3_H */

View File

@@ -28,7 +28,10 @@
#define OWE_DH_GRP19 19
#endif
#ifdef CONFIG_SAE
/* AP-side SAE authentication. Requires SoftAP: CONFIG_SAE may also be enabled
* for PASN (SoftAP disabled), but this block calls SoftAP-only primitives
* (e.g. esp_send_sae_auth_reply), so gate it on CONFIG_ESP_WIFI_SOFTAP_SUPPORT. */
#if defined(CONFIG_SAE) && defined(CONFIG_ESP_WIFI_SOFTAP_SUPPORT)
static void sae_set_state(struct sta_info *sta, enum sae_state state,
const char *reason)
@@ -778,7 +781,7 @@ queued:
}
#endif /* CONFIG_SAE */
#endif /* CONFIG_SAE && CONFIG_ESP_WIFI_SOFTAP_SUPPORT */
u16 wpa_res_to_status_code(enum wpa_validate_result res)
{