test(mbedtls/persistent_storage_format): Add ESP ITS NVS format-stability test app

This commit is contained in:
harshal.patil
2026-05-15 09:12:20 +05:30
parent 4821f331fe
commit 83ebd475c3
14 changed files with 635 additions and 0 deletions
@@ -22,3 +22,17 @@ components/mbedtls/test_apps/mbedtls_ut:
- esp_hal_dma
- esp_mm
- esp_hw_support
components/mbedtls/test_apps/persistent_storage_format:
disable:
- if: CONFIG_NAME not in ["hmac", "ecdsa"]
reason: this app has no default config; only the hmac and ecdsa overlays are exercised
- if: CONFIG_NAME == "hmac" and IDF_TARGET != "esp32c3"
reason: nvs_encr_hmac runner (HMAC + RSA-DS persistent format consume) is esp32c3 only
- if: CONFIG_NAME == "ecdsa" and IDF_TARGET != "esp32h2"
reason: ECDSA persistent format consume runner is esp32h2 only
depends_components:
- mbedtls
- esp_security
- esp_hal_security
- nvs_flash
@@ -0,0 +1,35 @@
# Persistent storage format stability tests for the ESP PSA opaque drivers.
#
# This is a separate test project from `mbedtls_ut` because it depends on a
# pre-flashed NVS fixture image, has its own partition layout (NVS is reserved
# at a known offset), and is run by a dedicated pytest entry point.
cmake_minimum_required(VERSION 3.22)
set(EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/test_apps/components")
set(COMPONENTS main)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(persistent_storage_format_test)
# Hook the shared multi-driver NVS fixture into `idf.py flash` so every
# CI runner gets a pre-populated NVS partition without manual steps.
# The same .bin works across runners — each only references its driver's
# key id from the fixture.
set(NVS_FIXTURE
"${CMAKE_CURRENT_SOURCE_DIR}/fixtures/nvs_efuse_v1.bin")
if(EXISTS ${NVS_FIXTURE})
partition_table_get_partition_info(nvs_offset "--partition-name nvs" "offset")
partition_table_get_partition_info(nvs_size "--partition-name nvs" "size")
if(NOT nvs_offset OR NOT nvs_size)
message(FATAL_ERROR "persistent_storage_format: nvs partition not found "
"in partition table — fixture cannot be flashed.")
endif()
# NVS partition is plaintext-on-flash (IDF excludes it from auto flash
# encryption). Force plaintext writes even when SECURE_FLASH_ENC is on,
# otherwise the bytes get encrypted-on-write and NVS reads them as
# garbage at runtime.
esptool_py_flash_target_image(flash "nvs" ${nvs_offset} ${NVS_FIXTURE}
ALWAYS_PLAINTEXT)
message(STATUS "persistent_storage_format: will flash ${NVS_FIXTURE} "
"to nvs @ ${nvs_offset} (size ${nvs_size}).")
endif()
@@ -0,0 +1,2 @@
| Supported Targets | ESP32-C3 | ESP32-H2 |
| ----------------- | -------- | -------- |
@@ -0,0 +1,55 @@
# Persistent storage format fixtures
A single committed NVS partition image, pre-populated with three persistent eFuse PSA keys — one per driver. Every CI runner flashes the same `.bin`; each runner's consume test only references its driver's key id (RSA-DS / HMAC / ECDSA) and ignores the other two.
Cross-platform format drift is caught by every runner — three independent signals on the same regression.
## File
| File | Storage version | Contains |
|------|-----------------|----------|
| `nvs_efuse_v1.bin` | v1 | 3 persistent eFuse keys: RSA-DS (id `0x1ADA1`), HMAC (id `0x1ADA2`), ECDSA SECP256R1 (id `0x1ADA3`) |
The key ids and the eFuse block/key-id assignments are declared in `../main/test_persistent_format.h`. They align with the existing volatile tests in `mbedtls_ut`, so each runner's already-burned eFuse key serves the persistent path too.
KM-source fixtures are intentionally absent — the per-key `esp_key_mgr_key_recovery_info_t` blob is HUK-wrapped by the deploying chip and meaningless on any other device. KM persistence is verified by the runtime deploy-then-import tests in `mbedtls_ut`.
## Regenerating
You need any chip with all three drivers in the build (typically ESP32-C5 or ESP32-P4 with DS + HMAC + ECDSA enabled).
The capture build needs all three drivers compiled in, which neither `sdkconfig.ci.hmac` nor `sdkconfig.ci.ecdsa` provides on its own. Create your own local overlay file (any path; the example below uses `sdkconfig.capture`) with the following contents, then point `SDKCONFIG_DEFAULTS` at it. Do NOT name it `sdkconfig.ci.*` — that prefix is auto-discovered by the CI app manifest and would add a build target the runners can't use.
```
CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL=y
CONFIG_MBEDTLS_HARDWARE_ECDSA_SIGN=y
CONFIG_MBEDTLS_HARDWARE_ECDSA_VERIFY=y
```
```
cd components/mbedtls/test_apps/persistent_storage_format
# 1. Erase NVS so the capture starts deterministic
esptool --chip <CHIP> --port <PORT> erase_region --force <NVS_OFFSET> <NVS_SIZE>
# 2. Build, flash, and run ONLY the capture test
idf.py set-target <CHIP>
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.capture" reconfigure
idf.py -p <PORT> flash
idf.py -p <PORT> monitor # send `[fixture_capture]` to Unity, wait for PASS
# 3. Read the partition out
esptool --chip <CHIP> --port <PORT> read_flash <NVS_OFFSET> <NVS_SIZE> fixtures/nvs_efuse_v1.bin
# 4. Commit. If you bumped any storage version, rename to nvs_efuse_v2.bin
# and keep nvs_efuse_v1.bin around — the v1 consume tests then prove
# v2 firmware can still read v1 NVS blobs (backward compat).
```
NVS offset/size live in `partitions.csv` — currently `0xA000` / `0x6000` (24 KB).
## When to regenerate
- After bumping any driver's persistent storage struct version. Add a new fixture file (`v2`, `v3`, …) AND keep the older fixtures around with their consume tests, so older-on-disk -> newer-firmware compatibility is proven.
- After an mbedtls upgrade that changes PSA ITS framing. Rare; needs a release note.
- Never just "to refresh." The file is meant to stay frozen so it detects regressions.
@@ -0,0 +1,14 @@
idf_component_register(
SRC_DIRS "."
PRIV_INCLUDE_DIRS "."
PRIV_REQUIRES efuse cmock test_utils mbedtls esp_timer
unity spi_flash esp_security nvs_flash
WHOLE_ARCHIVE)
# The RSA-DS opaque driver is the unit under test for these format
# stability checks; the linker wraps below let the sign path complete
# without a real eFuse HMAC key on the runner. Mirrors mbedtls_ut.
target_link_options(
${COMPONENT_LIB} INTERFACE
"-Wl,--wrap=esp_ds_finish_sign,--wrap=esp_ds_start_sign,--wrap=esp_efuse_get_key_purpose"
)
@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_newlib.h"
#include "memory_checks.h"
#include "nvs_flash.h"
#include "unity.h"
void setUp(void)
{
test_utils_record_free_mem();
test_utils_set_leak_level(CONFIG_UNITY_CRITICAL_LEAK_LEVEL_GENERAL,
ESP_LEAK_TYPE_CRITICAL, ESP_COMP_LEAK_GENERAL);
test_utils_set_leak_level(CONFIG_UNITY_WARN_LEAK_LEVEL_GENERAL,
ESP_LEAK_TYPE_WARNING, ESP_COMP_LEAK_GENERAL);
}
void tearDown(void)
{
vTaskDelay(5);
esp_reent_cleanup();
TEST_ASSERT_MESSAGE(heap_caps_check_integrity(MALLOC_CAP_INVALID, true),
"The test has corrupted the heap");
test_utils_finish_and_evaluate_leaks(
test_utils_get_leak_level(ESP_LEAK_TYPE_WARNING, ESP_COMP_LEAK_ALL),
test_utils_get_leak_level(ESP_LEAK_TYPE_CRITICAL, ESP_COMP_LEAK_ALL));
}
static void test_task(void *pvParameters)
{
vTaskDelay(2);
unity_run_menu();
}
void app_main(void)
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
xTaskCreatePinnedToCore(test_task, "testTask",
CONFIG_UNITY_FREERTOS_STACK_SIZE, NULL,
CONFIG_UNITY_FREERTOS_PRIORITY, NULL,
CONFIG_UNITY_FREERTOS_CPU);
}
@@ -0,0 +1,341 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Shared scaffolding for persistent-storage format-stability tests.
*
* - One capture test that imports a persistent eFuse key for every PSA
* opaque driver compiled into the build (RSA-DS, HMAC, ECDSA), with
* fixed key ids. Run once on a chip that has all three drivers in the
* build; the resulting NVS partition is captured with esptool and
* committed as fixtures/nvs_efuse_v1.bin.
*
* - The committed fixture is then flashed by every CI runner; each runner
* only references its own driver's key id (see the per-driver consume
* files), so a single shared NVS image works across heterogeneous
* runners.
*/
#include <string.h>
#include "unity.h"
#include "sdkconfig.h"
#include "soc/soc_caps.h"
#include "esp_efuse.h"
#include "psa/crypto.h"
#ifdef CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL
#include "esp_ds.h"
#include "hal/hmac_types.h"
#include "psa_crypto_driver_esp_rsa_ds.h"
#endif
#ifdef ESP_HMAC_OPAQUE_DRIVER_ENABLED
#include "psa_crypto_driver_esp_hmac_opaque.h"
#include "psa_crypto_driver_esp_hmac_opaque_contexts.h"
#endif
#if CONFIG_MBEDTLS_HARDWARE_ECDSA_SIGN
#include "psa_crypto_driver_esp_ecdsa.h"
#include "psa_crypto_driver_esp_ecdsa_contexts.h"
#endif
#include "test_persistent_format.h"
/* --- Dynamic-purpose wrap shared across all drivers' import validation. */
static volatile esp_efuse_purpose_t s_purpose_override = ESP_EFUSE_KEY_PURPOSE_USER;
extern esp_efuse_purpose_t __real_esp_efuse_get_key_purpose(esp_efuse_block_t block);
esp_efuse_purpose_t __wrap_esp_efuse_get_key_purpose(esp_efuse_block_t block)
{
if (s_purpose_override != ESP_EFUSE_KEY_PURPOSE_USER) {
return s_purpose_override;
}
return __real_esp_efuse_get_key_purpose(block);
}
/* --- DS hardware wraps. Without these the DS sign path on the consume
* side would touch the DS peripheral and fail for lack of a real
* eFuse HMAC key. Compiled only when DS is in the build. */
#ifdef CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL
int __wrap_esp_ds_start_sign(const void *message, const esp_ds_data_t *data,
hmac_key_id_t key_id, esp_ds_context_t **esp_ds_ctx)
{
if (message == NULL || data == NULL || esp_ds_ctx == NULL) {
return ESP_ERR_INVALID_ARG;
}
*esp_ds_ctx = malloc(sizeof(esp_ds_context_t));
if (*esp_ds_ctx == NULL) {
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
int __wrap_esp_ds_finish_sign(void *sig, esp_ds_context_t *ctx)
{
free(ctx);
return 0;
}
/* RSA-DS storage embeds the encrypted key blob; the capture test below
* needs a syntactically-valid mock since the driver cross-validates
* rsa_length_bits against ds_data.rsa_length. */
static esp_ds_data_ctx_t *mock_ds_data_ctx(void)
{
esp_ds_data_ctx_t *ds = calloc(1, sizeof(esp_ds_data_ctx_t));
if (!ds) {
return NULL;
}
ds->esp_ds_data = calloc(1, sizeof(esp_ds_data_t));
if (!ds->esp_ds_data) {
free(ds);
return NULL;
}
ds->rsa_length_bits = 2048;
ds->efuse_key_id = ESP_PERSISTENT_FIXTURE_DS_EFUSE_KEY_ID;
ds->esp_ds_data->rsa_length = (ds->rsa_length_bits / 32) - 1;
return ds;
}
static void free_mock_ds_data_ctx(esp_ds_data_ctx_t *ds)
{
if (ds) {
free(ds->esp_ds_data);
free(ds);
}
}
#endif /* CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL */
/* --- Capture test. Imports one persistent eFuse key per compiled driver,
* using fixed ids. Run once; capture NVS via esptool; commit. */
TEST_CASE("efuse persistent fixture capture v1 (one-shot, all drivers)",
"[fixture_capture]")
{
/* Clean any leftover from prior runs so we capture a deterministic NVS. */
psa_destroy_key(ESP_PERSISTENT_FIXTURE_DS_KEY_ID);
psa_destroy_key(ESP_PERSISTENT_FIXTURE_HMAC_KEY_ID);
psa_destroy_key(ESP_PERSISTENT_FIXTURE_ECDSA_KEY_ID);
#ifdef CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL
{
esp_rsa_ds_opaque_key_t key = {0};
key.ds_data_ctx = mock_ds_data_ctx();
TEST_ASSERT_NOT_NULL(key.ds_data_ctx);
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attr, PSA_KEY_TYPE_RSA_KEY_PAIR);
psa_set_key_bits(&attr, key.ds_data_ctx->rsa_length_bits);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_HASH);
psa_set_key_algorithm(&attr, PSA_ALG_RSA_PKCS1V15_SIGN(PSA_ALG_SHA_256));
psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_ESP_RSA_DS);
psa_set_key_id(&attr, ESP_PERSISTENT_FIXTURE_DS_KEY_ID);
s_purpose_override = ESP_EFUSE_KEY_PURPOSE_HMAC_DOWN_DIGITAL_SIGNATURE;
psa_key_id_t kid;
TEST_ASSERT_EQUAL(PSA_SUCCESS,
psa_import_key(&attr, (const uint8_t *)&key, sizeof(key), &kid));
s_purpose_override = ESP_EFUSE_KEY_PURPOSE_USER;
TEST_ASSERT_EQUAL(ESP_PERSISTENT_FIXTURE_DS_KEY_ID, kid);
TEST_ASSERT_EQUAL(PSA_SUCCESS, psa_purge_key(kid));
free_mock_ds_data_ctx(key.ds_data_ctx);
psa_reset_key_attributes(&attr);
}
#endif
#ifdef ESP_HMAC_OPAQUE_DRIVER_ENABLED
{
esp_hmac_opaque_key_t key = {
.efuse_key_id = ESP_PERSISTENT_FIXTURE_HMAC_EFUSE_KEY_ID,
};
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attr, PSA_KEY_TYPE_HMAC);
psa_set_key_bits(&attr, 256);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE);
psa_set_key_algorithm(&attr, PSA_ALG_HMAC(PSA_ALG_SHA_256));
psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_ESP_HMAC);
psa_set_key_id(&attr, ESP_PERSISTENT_FIXTURE_HMAC_KEY_ID);
s_purpose_override = ESP_EFUSE_KEY_PURPOSE_HMAC_UP;
psa_key_id_t kid;
TEST_ASSERT_EQUAL(PSA_SUCCESS,
psa_import_key(&attr, (const uint8_t *)&key, sizeof(key), &kid));
s_purpose_override = ESP_EFUSE_KEY_PURPOSE_USER;
TEST_ASSERT_EQUAL(ESP_PERSISTENT_FIXTURE_HMAC_KEY_ID, kid);
TEST_ASSERT_EQUAL(PSA_SUCCESS, psa_purge_key(kid));
psa_reset_key_attributes(&attr);
}
#endif
#if CONFIG_MBEDTLS_HARDWARE_ECDSA_SIGN
{
esp_ecdsa_opaque_key_t key = {
.curve = ESP_ECDSA_CURVE_SECP256R1,
.efuse_block = ESP_PERSISTENT_FIXTURE_ECDSA_EFUSE_BLOCK,
};
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attr, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1));
psa_set_key_bits(&attr, 256);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_HASH);
psa_set_key_algorithm(&attr, PSA_ALG_ECDSA(PSA_ALG_SHA_256));
psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_ESP_ECDSA);
psa_set_key_id(&attr, ESP_PERSISTENT_FIXTURE_ECDSA_KEY_ID);
s_purpose_override = ESP_EFUSE_KEY_PURPOSE_ECDSA_KEY;
psa_key_id_t kid;
TEST_ASSERT_EQUAL(PSA_SUCCESS,
psa_import_key(&attr, (const uint8_t *)&key, sizeof(key), &kid));
s_purpose_override = ESP_EFUSE_KEY_PURPOSE_USER;
TEST_ASSERT_EQUAL(ESP_PERSISTENT_FIXTURE_ECDSA_KEY_ID, kid);
TEST_ASSERT_EQUAL(PSA_SUCCESS, psa_purge_key(kid));
psa_reset_key_attributes(&attr);
}
#endif
printf("\n*** Fixture written; capture NVS partition with esptool now. ***\n\n");
}
/* ====================================================================== *
* Per-driver consume tests.
*
* The CI runner flashes fixtures/nvs_efuse_v1.bin to the NVS partition.
* The persistent keys are already there with their fixed ids; each
* driver's consume test only references its own id and ignores the rest,
* so a single shared NVS image works across heterogeneous runners.
*
* If anything in the on-NVS format has drifted (storage struct, PSA
* framing, NVS encoding) the corresponding op call returns
* INVALID_ARGUMENT/DATA_INVALID and the test fails. Each driver gates
* its test on the same CONFIG flag that pulls its driver into the build.
* ====================================================================== */
#if CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL
TEST_CASE("rsa-ds persistent NVS fixture v1 sign",
"[persistent_format][rsa_ds]")
{
uint8_t hash[32] = {0};
size_t hash_length = 0;
uint8_t input[7] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
TEST_ASSERT_EQUAL(PSA_SUCCESS,
psa_hash_compute(PSA_ALG_SHA_256, input, sizeof(input),
hash, sizeof(hash), &hash_length));
uint8_t signature[256] = {0};
size_t signature_length = 0;
TEST_ASSERT_EQUAL_HEX32(PSA_SUCCESS,
psa_sign_hash(ESP_PERSISTENT_FIXTURE_DS_KEY_ID,
PSA_ALG_RSA_PKCS1V15_SIGN(PSA_ALG_SHA_256),
hash, hash_length,
signature, sizeof(signature), &signature_length));
TEST_ASSERT_EQUAL(256, signature_length);
/* v15 padding-shape sanity, same as the volatile sign test. */
TEST_ASSERT_EQUAL(0, memcmp(hash, signature + (256 - hash_length), hash_length));
TEST_ASSERT_EQUAL(hash_length, signature[256 - hash_length - 1]);
TEST_ASSERT_EQUAL(0x04, signature[256 - hash_length - 2]);
TEST_ASSERT_EQUAL(0x00, signature[0]);
TEST_ASSERT_EQUAL(PSA_SUCCESS, psa_purge_key(ESP_PERSISTENT_FIXTURE_DS_KEY_ID));
}
#endif /* CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL */
#ifdef ESP_HMAC_OPAQUE_DRIVER_ENABLED
/* The runner has an eFuse HMAC key burned in
* ESP_PERSISTENT_FIXTURE_HMAC_EFUSE_KEY_ID with HMAC_UP purpose; we do a
* real mac_compute + mac_verify roundtrip. */
static const uint8_t hmac_test_data[] = "Pretty long input message";
TEST_CASE("hmac efuse persistent NVS fixture v1 mac",
"[persistent_format][hmac_efuse_key]")
{
uint8_t mac[32] = {0};
size_t mac_length = 0;
TEST_ASSERT_EQUAL_HEX32(PSA_SUCCESS,
psa_mac_compute(ESP_PERSISTENT_FIXTURE_HMAC_KEY_ID,
PSA_ALG_HMAC(PSA_ALG_SHA_256),
hmac_test_data, sizeof(hmac_test_data) - 1,
mac, sizeof(mac), &mac_length));
TEST_ASSERT_EQUAL(32, mac_length);
TEST_ASSERT_EQUAL(PSA_SUCCESS,
psa_mac_verify(ESP_PERSISTENT_FIXTURE_HMAC_KEY_ID,
PSA_ALG_HMAC(PSA_ALG_SHA_256),
hmac_test_data, sizeof(hmac_test_data) - 1,
mac, mac_length));
TEST_ASSERT_EQUAL(PSA_SUCCESS, psa_purge_key(ESP_PERSISTENT_FIXTURE_HMAC_KEY_ID));
}
#endif /* ESP_HMAC_OPAQUE_DRIVER_ENABLED */
#if CONFIG_MBEDTLS_HARDWARE_ECDSA_SIGN
/* SECP256R1 public key matching the SECP256R1 ECDSA key burned in
* ESP_PERSISTENT_FIXTURE_ECDSA_EFUSE_BLOCK on the runner. Same constants
* as mbedtls_ut/test_psa_ecdsa.c uses for its eFuse-key volatile tests. */
static const uint8_t ecdsa256_pub_x[] = {
0xa2, 0x8f, 0x52, 0x60, 0x20, 0x9b, 0x54, 0x3c,
0x13, 0x2f, 0x51, 0xb1, 0x89, 0xbf, 0xc7, 0xfa,
0x84, 0x5c, 0x56, 0x96, 0x2a, 0x00, 0x67, 0xdd,
0x7c, 0x8c, 0x0f, 0x63, 0x8b, 0x76, 0x7f, 0xb9,
};
static const uint8_t ecdsa256_pub_y[] = {
0xf6, 0x4c, 0x87, 0x5b, 0x5a, 0x9b, 0x59, 0x0a,
0xc4, 0x53, 0x04, 0x72, 0x0d, 0x7c, 0xde, 0xac,
0x7e, 0xad, 0x49, 0x8c, 0xf7, 0x5c, 0xc3, 0x1c,
0x1e, 0x81, 0xf2, 0x47, 0x01, 0x74, 0x05, 0xd5,
};
/* The runner has a SECP256R1 ECDSA key burned in
* ESP_PERSISTENT_FIXTURE_ECDSA_EFUSE_BLOCK with ECDSA_KEY purpose. We
* sign with the persistent key, then verify the resulting signature
* with a freshly-imported transparent public key matching the burned
* eFuse private key. The verify pass is the mathematical "compare the
* signature" — it proves the persistent extract recovered the SAME
* private key as the runner has burned. We don't rely on HW pubkey
* export here, since some ECDSA-capable chips (e.g. esp32h2) lack it
* for opaque keys. */
TEST_CASE("ecdsa efuse persistent NVS fixture v1 sign and verify",
"[persistent_format][ecdsa_efuse_key]")
{
psa_algorithm_t alg = PSA_ALG_ECDSA(PSA_ALG_SHA_256);
uint8_t hash[32];
memset(hash, 0xA5, sizeof(hash));
/* Sign on the persistent eFuse key. */
uint8_t signature[64]; /* SECP256R1: r || s, 32 bytes each */
size_t signature_length = 0;
TEST_ASSERT_EQUAL_HEX32(PSA_SUCCESS,
psa_sign_hash(ESP_PERSISTENT_FIXTURE_ECDSA_KEY_ID,
alg, hash, sizeof(hash),
signature, sizeof(signature), &signature_length));
TEST_ASSERT_EQUAL(64, signature_length);
/* Import the matching transparent public key and verify. */
uint8_t pub[65];
pub[0] = 0x04; /* uncompressed point format */
memcpy(pub + 1, ecdsa256_pub_x, sizeof(ecdsa256_pub_x));
memcpy(pub + 1 + sizeof(ecdsa256_pub_x), ecdsa256_pub_y, sizeof(ecdsa256_pub_y));
psa_key_attributes_t pub_attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&pub_attr, PSA_KEY_TYPE_ECC_PUBLIC_KEY(PSA_ECC_FAMILY_SECP_R1));
psa_set_key_usage_flags(&pub_attr, PSA_KEY_USAGE_VERIFY_HASH);
psa_set_key_algorithm(&pub_attr, PSA_ALG_ECDSA(PSA_ALG_SHA_256));
psa_set_key_bits(&pub_attr, 256);
psa_key_id_t pub_kid = 0;
TEST_ASSERT_EQUAL(PSA_SUCCESS,
psa_import_key(&pub_attr, pub, sizeof(pub), &pub_kid));
TEST_ASSERT_EQUAL_HEX32(PSA_SUCCESS,
psa_verify_hash(pub_kid, PSA_ALG_ECDSA(PSA_ALG_SHA_256),
hash, sizeof(hash), signature, signature_length));
TEST_ASSERT_EQUAL(PSA_SUCCESS, psa_destroy_key(pub_kid));
psa_reset_key_attributes(&pub_attr);
TEST_ASSERT_EQUAL(PSA_SUCCESS, psa_purge_key(ESP_PERSISTENT_FIXTURE_ECDSA_KEY_ID));
}
#endif /* CONFIG_MBEDTLS_HARDWARE_ECDSA_SIGN */
@@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Fixed key ids and eFuse block/id assignments baked into the committed
* NVS fixture (fixtures/nvs_efuse_v1.bin). Bumping ANY of these values
* means regenerating the fixture and bumping its filename's version.
*
* The eFuse block/id values are chosen to align with the existing
* volatile tests on each driver's runner so the same physical eFuse
* key the runner already has burned for the volatile test path serves
* the persistent path too.
*/
#pragma once
/* Persistent PSA key ids (chosen arbitrarily, must be non-zero). */
#define ESP_PERSISTENT_FIXTURE_DS_KEY_ID 0x1ADA1U
#define ESP_PERSISTENT_FIXTURE_HMAC_KEY_ID 0x1ADA2U
#define ESP_PERSISTENT_FIXTURE_ECDSA_KEY_ID 0x1ADA3U
/* eFuse block / key-id assignments. The capture chip's import path uses
* the dynamic-purpose wrap to satisfy validation; on the consume runners
* these must match the eFuse blocks that runner has burned for the
* relevant peripheral. */
/* RSA-DS uses a distinct block from HMAC so the two persistent keys
* don't visually share an eFuse id. The DS HW is wrapped at op time, so
* the runner doesn't actually need anything burned at this block — any
* non-conflicting value works. */
#define ESP_PERSISTENT_FIXTURE_DS_EFUSE_KEY_ID 1 /* HMAC_KEY1; DS HW wrapped at op time */
#define ESP_PERSISTENT_FIXTURE_HMAC_EFUSE_KEY_ID 0 /* HMAC_KEY0 — matches the nvs_encr_hmac runner's real burned key */
#define ESP_PERSISTENT_FIXTURE_ECDSA_EFUSE_BLOCK 5 /* EFUSE_BLK_KEY1, matches SECP256R1_EFUSE_BLOCK in test_psa_ecdsa.c */
@@ -0,0 +1,9 @@
# Persistent-storage-format test partition layout.
# CONFIG_PARTITION_TABLE_CUSTOM=y is needed for this file to be picked up.
# The NVS partition is the target for the pre-flashed fixture images
# under fixtures/.
#
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0xA000, 0x6000,
esp_secure_cert, 0x3F, , 0x10000, 0x2000,
factory, app, factory, 0x20000, 1M,
1 # Persistent-storage-format test partition layout.
2 # CONFIG_PARTITION_TABLE_CUSTOM=y is needed for this file to be picked up.
3 # The NVS partition is the target for the pre-flashed fixture images
4 # under fixtures/.
5 #
6 # Name, Type, SubType, Offset, Size, Flags
7 nvs, data, nvs, 0xA000, 0x6000,
8 esp_secure_cert, 0x3F, , 0x10000, 0x2000,
9 factory, app, factory, 0x20000, 1M,
@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
#
# Per-runner pytest entry points for the persistent-storage format
# stability tests. Each runner flashes the SAME shared NVS fixture
# (fixtures/nvs_efuse_v1.bin) — pre-populated with three persistent
# eFuse keys, one per driver — and runs the consume tests for whichever
# drivers it has hardware for. The capture-side test is tagged
# [fixture_capture] (NOT [persistent_format]) so it is never picked up
# by the normal CI groups.
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
# nvs_encr_hmac runner (esp32c3) covers BOTH HMAC and RSA-DS:
# - real eFuse HMAC key burned in block 0 with HMAC_UP purpose →
# genuine psa_mac_compute roundtrip
# - DS peripheral on c3, DS HW wrapped → RSA-DS consume runs without
# needing a real HMAC-DOWN-DIGITAL-SIGNATURE eFuse key
# A single runner exercises both drivers' persistent extract paths.
@pytest.mark.nvs_encr_hmac
@pytest.mark.parametrize('config', ['hmac'], indirect=True)
@idf_parametrize('target', ['esp32c3'], indirect=['target'])
def test_persistent_storage_format_hmac_and_rsa_ds(dut: Dut) -> None:
dut.run_all_single_board_cases(group='persistent_format')
# ECDSA runner (esp32h2) — has a SECP256R1 ECDSA key burned in
# EFUSE_BLK_KEY1 with ECDSA_KEY purpose (matches mbedtls_ut's existing
# ecdsa_sign volatile tests).
@pytest.mark.ecdsa_efuse
@pytest.mark.parametrize('config', ['ecdsa'], indirect=True)
@idf_parametrize('target', ['esp32h2'], indirect=['target'])
def test_persistent_storage_format_ecdsa(dut: Dut) -> None:
dut.run_all_single_board_cases(group='ecdsa_efuse_key')
@@ -0,0 +1,5 @@
# ECDSA opaque persistent-format runner overlay (esp32h2).
# The runner has a SECP256R1 ECDSA key burned in EFUSE_BLK_KEY1 with
# ECDSA_KEY purpose. DS isn't on h2; HMAC opaque test runs on c3, not here.
CONFIG_MBEDTLS_HARDWARE_ECDSA_SIGN=y
CONFIG_MBEDTLS_HARDWARE_ECDSA_VERIFY=y
@@ -0,0 +1,21 @@
# HMAC + RSA-DS persistent-format runner overlay (nvs_encr_hmac runner,
# esp32c3). The runner has:
# - an eFuse HMAC key in block 0 with HMAC_UP purpose (used by the HMAC
# consume test for a real psa_mac_compute roundtrip)
# in test_persistent_format.c, so the RSA-DS consume test runs without
# needing a real HMAC-DOWN-DIGITAL-SIGNATURE eFuse key on this runner.
#
# A single runner therefore exercises both the HMAC and RSA-DS persistent
# extract paths against the same shared NVS fixture.
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y
CONFIG_SECURE_FLASH_REQUIRE_ALREADY_ENABLED=y
CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=y
CONFIG_SECURE_BOOT_ALLOW_JTAG=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_DEC=y
CONFIG_SECURE_FLASH_UART_BOOTLOADER_ALLOW_CACHE=y
CONFIG_PARTITION_TABLE_OFFSET=0x9000
CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL=y
@@ -0,0 +1,19 @@
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_OFFSET=0x9000
# NVS encryption disabled — fixture images are committed as plaintext NVS
# bytes so they're portable across devices. With CONFIG_NVS_ENCRYPTION=y the
# bootloader auto-routes through nvs_sec_provider which requires an eFuse
# HMAC key; that's outside the scope of these format-stability tests.
CONFIG_NVS_ENCRYPTION=n
# Common build sanity options (mirrored from mbedtls_ut)
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
CONFIG_COMPILER_WARN_WRITE_STRINGS=y
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
CONFIG_COMPILER_STACK_CHECK=y
CONFIG_ESP_TASK_WDT_EN=y
CONFIG_ESP_TASK_WDT_INIT=n