From 83ebd475c378d67979f214330e2a5e76ad58f2a9 Mon Sep 17 00:00:00 2001 From: "harshal.patil" Date: Fri, 8 May 2026 10:30:54 +0530 Subject: [PATCH] test(mbedtls/persistent_storage_format): Add ESP ITS NVS format-stability test app --- .../mbedtls/test_apps/.build-test-rules.yml | 14 + .../persistent_storage_format/CMakeLists.txt | 35 ++ .../persistent_storage_format/README.md | 2 + .../fixtures/README.md | 55 +++ .../fixtures/nvs_efuse_v1.bin | Bin 0 -> 24576 bytes .../main/CMakeLists.txt | 14 + .../persistent_storage_format/main/app_main.c | 52 +++ .../main/test_persistent_format.c | 341 ++++++++++++++++++ .../main/test_persistent_format.h | 32 ++ .../persistent_storage_format/partitions.csv | 9 + .../pytest_persistent_storage_format.py | 36 ++ .../sdkconfig.ci.ecdsa | 5 + .../sdkconfig.ci.hmac | 21 ++ .../sdkconfig.defaults | 19 + 14 files changed, 635 insertions(+) create mode 100644 components/mbedtls/test_apps/persistent_storage_format/CMakeLists.txt create mode 100644 components/mbedtls/test_apps/persistent_storage_format/README.md create mode 100644 components/mbedtls/test_apps/persistent_storage_format/fixtures/README.md create mode 100644 components/mbedtls/test_apps/persistent_storage_format/fixtures/nvs_efuse_v1.bin create mode 100644 components/mbedtls/test_apps/persistent_storage_format/main/CMakeLists.txt create mode 100644 components/mbedtls/test_apps/persistent_storage_format/main/app_main.c create mode 100644 components/mbedtls/test_apps/persistent_storage_format/main/test_persistent_format.c create mode 100644 components/mbedtls/test_apps/persistent_storage_format/main/test_persistent_format.h create mode 100644 components/mbedtls/test_apps/persistent_storage_format/partitions.csv create mode 100644 components/mbedtls/test_apps/persistent_storage_format/pytest_persistent_storage_format.py create mode 100644 components/mbedtls/test_apps/persistent_storage_format/sdkconfig.ci.ecdsa create mode 100644 components/mbedtls/test_apps/persistent_storage_format/sdkconfig.ci.hmac create mode 100644 components/mbedtls/test_apps/persistent_storage_format/sdkconfig.defaults diff --git a/components/mbedtls/test_apps/.build-test-rules.yml b/components/mbedtls/test_apps/.build-test-rules.yml index ecca00833f6..9eceef9148a 100644 --- a/components/mbedtls/test_apps/.build-test-rules.yml +++ b/components/mbedtls/test_apps/.build-test-rules.yml @@ -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 diff --git a/components/mbedtls/test_apps/persistent_storage_format/CMakeLists.txt b/components/mbedtls/test_apps/persistent_storage_format/CMakeLists.txt new file mode 100644 index 00000000000..a0a6d29017b --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/CMakeLists.txt @@ -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() diff --git a/components/mbedtls/test_apps/persistent_storage_format/README.md b/components/mbedtls/test_apps/persistent_storage_format/README.md new file mode 100644 index 00000000000..9216dc57a96 --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/README.md @@ -0,0 +1,2 @@ +| Supported Targets | ESP32-C3 | ESP32-H2 | +| ----------------- | -------- | -------- | diff --git a/components/mbedtls/test_apps/persistent_storage_format/fixtures/README.md b/components/mbedtls/test_apps/persistent_storage_format/fixtures/README.md new file mode 100644 index 00000000000..dbaac7c4e1e --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/fixtures/README.md @@ -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 --port erase_region --force + +# 2. Build, flash, and run ONLY the capture test +idf.py set-target +idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.capture" reconfigure +idf.py -p flash +idf.py -p monitor # send `[fixture_capture]` to Unity, wait for PASS + +# 3. Read the partition out +esptool --chip --port read_flash 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. diff --git a/components/mbedtls/test_apps/persistent_storage_format/fixtures/nvs_efuse_v1.bin b/components/mbedtls/test_apps/persistent_storage_format/fixtures/nvs_efuse_v1.bin new file mode 100644 index 0000000000000000000000000000000000000000..da3f88204e66abe34bb1fbf35b15f13b93e60705 GIT binary patch literal 24576 zcmeIzu`feW6vy#%`>JUZ3$b7pIv9jeH1DZ|C}~m#Q)9G9r4s8K42_llL1G~>mBqk7 z3^soQ6N!;N=l1n++x`VjzPHJH?Y-xn(|(3uTTv8=u9n_=*YWI4^TJv0y#@MNjES=M zrI%WLx4L&&x2@XCbZSkImx)4k(oa^gv>9l{cUlYPKHt{-O4&Z}krpXed|6-FiJ$i* zG&RY}m`E-q8N2zcHAY{$D}{Cy7X%PMU@!%2M4D9-ZGGK7B=P9~<)h6yJr6t?Z=7wo z=gazY=k@HgvA>k$=d|v1)p7Jv+%9-|YPB$Jbnc|Nr^ti%(bX zc~Sc;Xq&sA%k5=)Z%_?@XNE8s9qK47p#x&Y#Mk9QF5)=exDMM*sl? z5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0 z009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{ I1O`Fi7g%Yv4FCWD literal 0 HcmV?d00001 diff --git a/components/mbedtls/test_apps/persistent_storage_format/main/CMakeLists.txt b/components/mbedtls/test_apps/persistent_storage_format/main/CMakeLists.txt new file mode 100644 index 00000000000..20ae1e57262 --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/main/CMakeLists.txt @@ -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" + ) diff --git a/components/mbedtls/test_apps/persistent_storage_format/main/app_main.c b/components/mbedtls/test_apps/persistent_storage_format/main/app_main.c new file mode 100644 index 00000000000..42463fd9382 --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/main/app_main.c @@ -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); +} diff --git a/components/mbedtls/test_apps/persistent_storage_format/main/test_persistent_format.c b/components/mbedtls/test_apps/persistent_storage_format/main/test_persistent_format.c new file mode 100644 index 00000000000..7bacb731156 --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/main/test_persistent_format.c @@ -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 +#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 */ diff --git a/components/mbedtls/test_apps/persistent_storage_format/main/test_persistent_format.h b/components/mbedtls/test_apps/persistent_storage_format/main/test_persistent_format.h new file mode 100644 index 00000000000..bdc53fc5795 --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/main/test_persistent_format.h @@ -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 */ diff --git a/components/mbedtls/test_apps/persistent_storage_format/partitions.csv b/components/mbedtls/test_apps/persistent_storage_format/partitions.csv new file mode 100644 index 00000000000..38f99eeb442 --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/partitions.csv @@ -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, diff --git a/components/mbedtls/test_apps/persistent_storage_format/pytest_persistent_storage_format.py b/components/mbedtls/test_apps/persistent_storage_format/pytest_persistent_storage_format.py new file mode 100644 index 00000000000..0cd1f70c596 --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/pytest_persistent_storage_format.py @@ -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') diff --git a/components/mbedtls/test_apps/persistent_storage_format/sdkconfig.ci.ecdsa b/components/mbedtls/test_apps/persistent_storage_format/sdkconfig.ci.ecdsa new file mode 100644 index 00000000000..95f68d57d64 --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/sdkconfig.ci.ecdsa @@ -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 diff --git a/components/mbedtls/test_apps/persistent_storage_format/sdkconfig.ci.hmac b/components/mbedtls/test_apps/persistent_storage_format/sdkconfig.ci.hmac new file mode 100644 index 00000000000..de2ed42e26e --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/sdkconfig.ci.hmac @@ -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 diff --git a/components/mbedtls/test_apps/persistent_storage_format/sdkconfig.defaults b/components/mbedtls/test_apps/persistent_storage_format/sdkconfig.defaults new file mode 100644 index 00000000000..6af4dd7be9b --- /dev/null +++ b/components/mbedtls/test_apps/persistent_storage_format/sdkconfig.defaults @@ -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