diff --git a/examples/security/.build-test-rules.yml b/examples/security/.build-test-rules.yml index fd3da7181ec..41eef1ba90d 100644 --- a/examples/security/.build-test-rules.yml +++ b/examples/security/.build-test-rules.yml @@ -52,6 +52,16 @@ examples/security/nvs_encryption_hmac: depends_filepatterns: - examples/security/nvs_encryption_hmac/**/* +examples/security/psa_its_custom_backend: + disable: + - if: IDF_TARGET not in ["esp32"] + reason: example is target-agnostic, one chip is enough for CI + depends_components: + - mbedtls + - nvs_flash + depends_filepatterns: + - examples/security/psa_its_custom_backend/**/* + examples/security/security_features_app: disable: - if: IDF_TARGET not in ["esp32c3", "esp32s3"] diff --git a/examples/security/psa_its_custom_backend/CMakeLists.txt b/examples/security/psa_its_custom_backend/CMakeLists.txt new file mode 100644 index 00000000000..3c2521763fe --- /dev/null +++ b/examples/security/psa_its_custom_backend/CMakeLists.txt @@ -0,0 +1,9 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) +project(psa_its_custom_backend) diff --git a/examples/security/psa_its_custom_backend/README.md b/examples/security/psa_its_custom_backend/README.md new file mode 100644 index 00000000000..9fe7620f639 --- /dev/null +++ b/examples/security/psa_its_custom_backend/README.md @@ -0,0 +1,112 @@ +| Supported Targets | ESP32 | +| ----------------- | ----- | + +# PSA ITS Custom Storage Backend + +## Overview + +This example demonstrates the PSA ITS (Internal Trusted Storage) custom storage backend feature, which lets the application route persistent PSA Crypto keys to a user-defined storage implementation based on the key ID range. + +By default, all persistent PSA keys are stored in NVS (Non-Volatile Storage) under the framework-owned `psa_its` namespace. With the custom storage backend enabled, a configurable range of key IDs is routed to user-registered callbacks where the application controls where and how those keys are stored. + +This example registers a single custom backend that handles one key ID range: + +| Key ID Range | Storage Backend | Description | +|---|---|---| +| `0x00000001` - `0x2FFFFFFF` | Default NVS (`psa_its`) | Handled by the framework, no custom code needed | +| `0x30000000` - `0x3FFFFFFF` | Custom NVS namespace (`psa_its_ext`) | Implemented in this example, stores only the raw key bytes — no PSA wrapper, no attributes, no metadata | + +Two persistent AES-256 keys are provisioned — one per storage path — and used for AES-CBC encrypt/decrypt to verify correct operation. + +### How the custom backend stores keys + +PSA Crypto hands the backend a fully-formatted PSA persistent key blob in `set()`. This example unpacks the blob with the upstream helper `psa_parse_key_data_from_storage()` to extract just the inner key bytes and persists *only those bytes* — no PSA wrapper, no attribute trailer, no metadata at all. On `get()`, the backend reads the key bytes back, synthesises a `psa_key_attributes_t` from hardcoded values, and rebuilds the PSA blob with `psa_format_key_data_for_storage()` before returning it to PSA Crypto. + +The two upstream helpers (`psa_parse_key_data_from_storage` / `psa_format_key_data_for_storage`) are the canonical way for custom backends to manipulate the PSA blob format without re-implementing it. + +This is the minimum on-disk footprint possible — but the trade-off is that the backend commits to a fixed key shape. The attributes it reports on read must match what the application uses when calling `psa_generate_key()` / `psa_import_key()` for keys in the custom UID range. In this example that's AES-256 / CBC / encrypt+decrypt, encoded as a few `#define`s in `custom_nvs_its_backend.c`. A backend that needs to serve heterogeneous keys would persist the attributes too — see the comment block at the top of the backend source for pointers. + +> Note: `PSA_STORAGE_FLAG_WRITE_ONCE` is intentionally not tracked in this minimal example. + +## How to use the example + +### Hardware Required + +This example can be executed on any ESP32 development board. + +### Configure the project + +Set the correct chip target: + +``` +idf.py set-target +``` + +The default configuration in `sdkconfig.defaults` enables the custom storage backend with the key ID range `0x30000000` - `0x3FFFFFFF`. These can be adjusted via `idf.py menuconfig`: + +- `Component Config` > `mbedTLS` > `Enable custom storage backend for PSA ITS` +- `Component Config` > `mbedTLS` > `Minimum UID for custom backend range` +- `Component Config` > `mbedTLS` > `Maximum UID for custom backend range` + +### Build and Flash + +``` +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type `Ctrl-]`.) + +### Example Output + +First boot (keys generated): + +```log +I (286) example: === PSA ITS Custom Storage Backend === +I (306) example: Custom ITS backend registered +I (306) example: --- Provisioning keys --- +I (416) example: [NVS] Generated persistent key 0x1 +I (436) example: [CUSTOM] Generated persistent key 0x30000001 +I (436) example: --- Testing encryption/decryption --- +I (446) example: [NVS] Encrypt/decrypt OK with key 0x1 +I (456) example: [CUSTOM] Encrypt/decrypt OK with key 0x30000001 +I (456) example: === All tests passed === +``` + +Subsequent boots (keys loaded from storage): + +```log +I (286) example: === PSA ITS Custom Storage Backend === +I (306) example: Custom ITS backend registered +I (306) example: --- Provisioning keys --- +I (306) example: [NVS] Key 0x1 already exists +I (316) example: [CUSTOM] Key 0x30000001 already exists +I (316) example: --- Testing encryption/decryption --- +I (326) example: [NVS] Encrypt/decrypt OK with key 0x1 +I (336) example: [CUSTOM] Encrypt/decrypt OK with key 0x30000001 +I (336) example: === All tests passed === +``` + +## Project Structure + +``` +main/ + app_main.c - Application entry point, registers the custom backend + custom_nvs_its_backend.c/h - Custom NVS namespace storage implementation +partitions_example.csv - Partition table +``` + +## Adding a New Storage Backend + +To replace or extend the custom backend with another storage (e.g., SPIFFS, a secure element, a filesystem on an external chip): + +1. Implement the four storage primitives (`set`, `get`, `get_info`, `remove`) so their signatures match `esp_psa_its_custom_ops_t`. +2. Bind them in `app_main.c` when filling the `esp_psa_its_custom_ops_t` instance. +3. Pass that instance to `esp_psa_its_register_custom_backend()`. + +The framework imposes no on-disk format on custom backends. A few common patterns: + +- **Store the PSA blob verbatim** — minimal code, ~32 bytes of PSA wrapper per entry. Suitable when storage size isn't a concern and the backend should be agnostic to key shape. +- **Unpack-store-repack with hardcoded attributes** (used by this example) — call `psa_parse_key_data_from_storage()` in `set()` to extract just the inner key bytes, persist only those, then call `psa_format_key_data_for_storage()` in `get()` to rebuild the PSA blob using attributes the backend knows in advance. Smallest on-disk footprint; the backend commits to a single key shape. +- **Unpack-store-repack with persisted attributes** — same as above but the backend also stores the attributes (e.g., as a small trailer or sidecar entry). Larger on-disk footprint than hardcoded, but the backend can serve arbitrary key shapes. + +If you want to dispatch a single registered custom backend across multiple internal storage targets, do the routing inside your `set`/`get`/`get_info`/`remove` implementations (e.g., switch on `uid` sub-ranges) — the framework only sees one set of callbacks. diff --git a/examples/security/psa_its_custom_backend/main/CMakeLists.txt b/examples/security/psa_its_custom_backend/main/CMakeLists.txt new file mode 100644 index 00000000000..819c6cbcd84 --- /dev/null +++ b/examples/security/psa_its_custom_backend/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "app_main.c" "custom_nvs_its_backend.c" + INCLUDE_DIRS "." + PRIV_REQUIRES mbedtls nvs_flash) diff --git a/examples/security/psa_its_custom_backend/main/app_main.c b/examples/security/psa_its_custom_backend/main/app_main.c new file mode 100644 index 00000000000..de66ae526d8 --- /dev/null +++ b/examples/security/psa_its_custom_backend/main/app_main.c @@ -0,0 +1,165 @@ +/* + * PSA ITS custom storage backend example + * + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + * + * Demonstrates the PSA ITS custom storage backend feature: + * + * Key IDs 0x00000001 .. 0x2FFFFFFF → default NVS backend (framework) + * Key IDs 0x30000000 .. 0x3FFFFFFF → user-registered custom backend + * (this example: a separate NVS + * namespace "psa_its_ext") + * + * The custom UID range is configured via Kconfig + * (CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_{MIN,MAX}). + */ + +#include +#include + +#include "esp_err.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "psa/crypto.h" + +#include "esp_psa_its.h" +#include "custom_nvs_its_backend.h" + +static const char *TAG = "example"; + +/* ---- Key IDs ---- */ + +/* Default NVS range (handled by the framework) */ +#define NVS_KEY_ID ((psa_key_id_t) 0x00000001) + +/* Custom range routed to the registered backend */ +#define CUSTOM_KEY_ID ((psa_key_id_t) 0x30000001) + +/* ---- Helpers ---- */ + +static psa_status_t provision_aes_key(psa_key_id_t key_id, const char *label) +{ + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t handle; + psa_status_t status; + + status = psa_get_key_attributes(key_id, &attr); + if (status == PSA_SUCCESS) { + ESP_LOGI(TAG, "[%s] Key 0x%lx already exists", label, (unsigned long)key_id); + psa_reset_key_attributes(&attr); + return PSA_SUCCESS; + } + + psa_reset_key_attributes(&attr); + psa_set_key_id(&attr, key_id); + psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_PERSISTENT); + psa_set_key_type(&attr, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attr, 256); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attr, PSA_ALG_CBC_NO_PADDING); + + status = psa_generate_key(&attr, &handle); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "[%s] Failed to generate key 0x%lx: %d", + label, (unsigned long)key_id, (int)status); + } else { + ESP_LOGI(TAG, "[%s] Generated persistent key 0x%lx", + label, (unsigned long)key_id); + } + + psa_reset_key_attributes(&attr); + return status; +} + +static psa_status_t test_encrypt_decrypt(psa_key_id_t key_id, const char *label) +{ + const uint8_t plaintext[32] = "Hello PSA ITS custom backend!!"; + uint8_t ciphertext[16 + sizeof(plaintext)]; + uint8_t decrypted[sizeof(plaintext)]; + size_t ciphertext_len = 0; + size_t decrypted_len = 0; + + psa_status_t status = psa_cipher_encrypt(key_id, PSA_ALG_CBC_NO_PADDING, + plaintext, sizeof(plaintext), + ciphertext, sizeof(ciphertext), + &ciphertext_len); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "[%s] Encrypt failed: %d", label, (int)status); + return status; + } + + status = psa_cipher_decrypt(key_id, PSA_ALG_CBC_NO_PADDING, + ciphertext, ciphertext_len, + decrypted, sizeof(decrypted), + &decrypted_len); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "[%s] Decrypt failed: %d", label, (int)status); + return status; + } + + if (decrypted_len != sizeof(plaintext) || + memcmp(decrypted, plaintext, sizeof(plaintext)) != 0) { + ESP_LOGE(TAG, "[%s] Decrypted data mismatch!", label); + return PSA_ERROR_CORRUPTION_DETECTED; + } + + ESP_LOGI(TAG, "[%s] Encrypt/decrypt OK with key 0x%lx", + label, (unsigned long)key_id); + return PSA_SUCCESS; +} + +/* ---- Application entry ---- */ + +void app_main(void) +{ + psa_status_t status; + + ESP_LOGI(TAG, "=== PSA ITS Custom Storage Backend ==="); + + /* Initialize NVS (used by both the default backend and our custom backend, + * which keep their entries in distinct namespaces). */ + 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()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK(err); + + /* Register the custom backend. The signatures of custom_nvs_its_* + * match esp_psa_its_custom_ops_t exactly, so they can be bound directly. */ + static const esp_psa_its_custom_ops_t custom_ops = { + .set = custom_nvs_its_set, + .get = custom_nvs_its_get, + .get_info = custom_nvs_its_get_info, + .remove = custom_nvs_its_remove, + .ctx = NULL, + }; + + status = esp_psa_its_register_custom_backend(&custom_ops); + if (status != PSA_SUCCESS) { + ESP_LOGE(TAG, "Failed to register custom backend: %d", (int)status); + return; + } + ESP_LOGI(TAG, "Custom ITS backend registered"); + + /* Provision and exercise one key per storage path. */ + ESP_LOGI(TAG, "--- Provisioning keys ---"); + if (provision_aes_key(NVS_KEY_ID, "NVS") != PSA_SUCCESS) { + return; + } + if (provision_aes_key(CUSTOM_KEY_ID, "CUSTOM") != PSA_SUCCESS) { + return; + } + + ESP_LOGI(TAG, "--- Testing encryption/decryption ---"); + if (test_encrypt_decrypt(NVS_KEY_ID, "NVS") != PSA_SUCCESS) { + return; + } + if (test_encrypt_decrypt(CUSTOM_KEY_ID, "CUSTOM") != PSA_SUCCESS) { + return; + } + + ESP_LOGI(TAG, "=== All tests passed ==="); +} diff --git a/examples/security/psa_its_custom_backend/main/custom_nvs_its_backend.c b/examples/security/psa_its_custom_backend/main/custom_nvs_its_backend.c new file mode 100644 index 00000000000..02ff6bc0662 --- /dev/null +++ b/examples/security/psa_its_custom_backend/main/custom_nvs_its_backend.c @@ -0,0 +1,233 @@ +/* + * Custom NVS storage implementation for PSA ITS blobs + * + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + * + * Demonstrates the most stripped-down on-disk format possible: only the raw + * key material is persisted in a separate NVS namespace ("psa_its_ext"). + * Nothing else — no PSA wrapper, no attribute trailer, no metadata. + */ + +#include +#include + +#include "custom_nvs_its_backend.h" +#include "psa/crypto.h" +#include "esp_psa_key_file.h" +#include "nvs.h" + +#define EXT_NVS_NAMESPACE "psa_its_ext" +#define EXT_NVS_KEY_LEN 14 + +/* Hardcoded attributes for keys served by this backend. They must match + * what the application uses when calling psa_generate_key() / psa_import_key() + * for keys in the custom UID range — PSA Crypto validates the attributes + * we report against the operation being requested. */ +#define BACKEND_KEY_LIFETIME PSA_KEY_LIFETIME_PERSISTENT +#define BACKEND_KEY_TYPE PSA_KEY_TYPE_AES +#define BACKEND_KEY_USAGE (PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT) +#define BACKEND_KEY_ALG PSA_ALG_CBC_NO_PADDING + +static void uid_to_nvs_key(psa_storage_uid_t uid, char *key) +{ + static const char base32[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + for (int i = 0; i < 13; i++) { + key[12 - i] = base32[uid & 0x1F]; + uid >>= 5; + } + key[13] = '\0'; +} + +/* Fill psa_key_attributes_t for a stored key whose material is N bytes long. + * Bits is derived from N; everything else is the backend's fixed contract. */ +static void fill_hardcoded_attributes(psa_key_attributes_t *attr, size_t key_data_len) +{ + psa_set_key_lifetime(attr, BACKEND_KEY_LIFETIME); + psa_set_key_type(attr, BACKEND_KEY_TYPE); + psa_set_key_bits(attr, key_data_len * 8); + psa_set_key_usage_flags(attr, BACKEND_KEY_USAGE); + psa_set_key_algorithm(attr, BACKEND_KEY_ALG); +} + +/* ---- Public storage operations ---- */ + +psa_status_t custom_nvs_its_set(void *ctx, psa_storage_uid_t uid, uint32_t data_length, + const void *p_data, psa_storage_create_flags_t create_flags) +{ + (void)ctx; + (void)create_flags; /* WRITE_ONCE not tracked in this minimal example */ + + /* Unpack the incoming PSA blob — we keep only the inner key material + * and drop the attributes, because we already know what kind of key + * this backend stores. */ + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + const uint8_t *key_data = NULL; + size_t key_data_len = 0; + psa_status_t status = esp_psa_key_file_unpack(p_data, data_length, + &attr, &key_data, &key_data_len); + psa_reset_key_attributes(&attr); + if (status != PSA_SUCCESS) { + return status; + } + + nvs_handle_t handle; + char nvs_key[EXT_NVS_KEY_LEN]; + uid_to_nvs_key(uid, nvs_key); + + esp_err_t err = nvs_open(EXT_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err != ESP_OK) { + return PSA_ERROR_STORAGE_FAILURE; + } + + err = nvs_set_blob(handle, nvs_key, key_data, key_data_len); + if (err == ESP_OK) { + err = nvs_commit(handle); + } + nvs_close(handle); + + return (err == ESP_OK) ? PSA_SUCCESS : PSA_ERROR_STORAGE_FAILURE; +} + +psa_status_t custom_nvs_its_get(void *ctx, psa_storage_uid_t uid, uint32_t data_offset, + uint32_t data_length, void *p_data, size_t *p_data_length) +{ + (void)ctx; + + nvs_handle_t handle; + char nvs_key[EXT_NVS_KEY_LEN]; + uid_to_nvs_key(uid, nvs_key); + + esp_err_t err = nvs_open(EXT_NVS_NAMESPACE, NVS_READONLY, &handle); + if (err == ESP_ERR_NVS_NOT_FOUND) { + return PSA_ERROR_DOES_NOT_EXIST; + } + if (err != ESP_OK) { + return PSA_ERROR_STORAGE_FAILURE; + } + + size_t key_data_len = 0; + err = nvs_get_blob(handle, nvs_key, NULL, &key_data_len); + if (err != ESP_OK) { + nvs_close(handle); + return (err == ESP_ERR_NVS_NOT_FOUND) ? PSA_ERROR_DOES_NOT_EXIST + : PSA_ERROR_STORAGE_FAILURE; + } + + uint8_t *key_data = calloc(1, key_data_len); + if (key_data == NULL) { + nvs_close(handle); + return PSA_ERROR_INSUFFICIENT_MEMORY; + } + + err = nvs_get_blob(handle, nvs_key, key_data, &key_data_len); + nvs_close(handle); + if (err != ESP_OK) { + free(key_data); + return PSA_ERROR_STORAGE_FAILURE; + } + + /* Reconstruct the PSA blob: hardcoded attributes + stored key bytes. */ + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + fill_hardcoded_attributes(&attr, key_data_len); + + size_t blob_size = esp_psa_key_file_size(key_data_len); + if (data_offset + data_length < data_offset || + data_offset + data_length > blob_size) { + free(key_data); + psa_reset_key_attributes(&attr); + return PSA_ERROR_INVALID_ARGUMENT; + } + + uint8_t *blob = calloc(1, blob_size); + if (blob == NULL) { + free(key_data); + psa_reset_key_attributes(&attr); + return PSA_ERROR_INSUFFICIENT_MEMORY; + } + + size_t written = 0; + psa_status_t status = esp_psa_key_file_pack(&attr, key_data, key_data_len, + blob, blob_size, &written); + if (status != PSA_SUCCESS) { + free(blob); + free(key_data); + psa_reset_key_attributes(&attr); + return status; + } + + if (data_length > 0 && p_data != NULL) { + memcpy(p_data, blob + data_offset, data_length); + } + if (p_data_length != NULL) { + *p_data_length = data_length; + } + + free(blob); + free(key_data); + psa_reset_key_attributes(&attr); + return PSA_SUCCESS; +} + +psa_status_t custom_nvs_its_get_info(void *ctx, psa_storage_uid_t uid, + struct psa_storage_info_t *p_info) +{ + (void)ctx; + + nvs_handle_t handle; + char nvs_key[EXT_NVS_KEY_LEN]; + uid_to_nvs_key(uid, nvs_key); + + esp_err_t err = nvs_open(EXT_NVS_NAMESPACE, NVS_READONLY, &handle); + if (err == ESP_ERR_NVS_NOT_FOUND) { + return PSA_ERROR_DOES_NOT_EXIST; + } + if (err != ESP_OK) { + return PSA_ERROR_STORAGE_FAILURE; + } + + size_t key_data_len = 0; + err = nvs_get_blob(handle, nvs_key, NULL, &key_data_len); + nvs_close(handle); + + if (err == ESP_ERR_NVS_NOT_FOUND) { + return PSA_ERROR_DOES_NOT_EXIST; + } + if (err != ESP_OK) { + return PSA_ERROR_STORAGE_FAILURE; + } + + /* PSA expects the size of the synthesized PSA blob, not the on-disk size. */ + p_info->size = (uint32_t)esp_psa_key_file_size(key_data_len); + p_info->flags = 0; /* WRITE_ONCE not tracked in this minimal example */ + return PSA_SUCCESS; +} + +psa_status_t custom_nvs_its_remove(void *ctx, psa_storage_uid_t uid) +{ + (void)ctx; + + nvs_handle_t handle; + char nvs_key[EXT_NVS_KEY_LEN]; + uid_to_nvs_key(uid, nvs_key); + + esp_err_t err = nvs_open(EXT_NVS_NAMESPACE, NVS_READWRITE, &handle); + if (err == ESP_ERR_NVS_NOT_FOUND) { + return PSA_ERROR_DOES_NOT_EXIST; + } + if (err != ESP_OK) { + return PSA_ERROR_STORAGE_FAILURE; + } + + err = nvs_erase_key(handle, nvs_key); + if (err == ESP_OK) { + err = nvs_commit(handle); + } + nvs_close(handle); + + if (err == ESP_ERR_NVS_NOT_FOUND) { + return PSA_ERROR_DOES_NOT_EXIST; + } + return (err == ESP_OK) ? PSA_SUCCESS : PSA_ERROR_STORAGE_FAILURE; +} diff --git a/examples/security/psa_its_custom_backend/main/custom_nvs_its_backend.h b/examples/security/psa_its_custom_backend/main/custom_nvs_its_backend.h new file mode 100644 index 00000000000..b846dbd9f26 --- /dev/null +++ b/examples/security/psa_its_custom_backend/main/custom_nvs_its_backend.h @@ -0,0 +1,44 @@ +/* + * NVS storage implementation for PSA ITS blobs (separate namespace) + * + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + * + * Signatures match esp_psa_its_custom_ops_t so these functions can be + * assigned directly to the ops struct in app_main.c. + */ + +#pragma once + +#include "psa/internal_trusted_storage.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Unpack the incoming PSA blob and persist only the inner key bytes under + * `uid` in the custom NVS namespace. create_flags is ignored — this example + * does not track PSA_STORAGE_FLAG_WRITE_ONCE. */ +psa_status_t custom_nvs_its_set(void *ctx, psa_storage_uid_t uid, + uint32_t data_length, const void *p_data, + psa_storage_create_flags_t create_flags); + +/* Load the stored key bytes for `uid`, synthesize a PSA blob using hardcoded + * attributes for that UID, and copy `data_length` bytes from `data_offset` + * into `p_data`. */ +psa_status_t custom_nvs_its_get(void *ctx, psa_storage_uid_t uid, + uint32_t data_offset, uint32_t data_length, + void *p_data, size_t *p_data_length); + +/* Report the size of the synthesized PSA blob for `uid`. flags is always + * reported as 0 since create_flags is not tracked. */ +psa_status_t custom_nvs_its_get_info(void *ctx, psa_storage_uid_t uid, + struct psa_storage_info_t *p_info); + +/* Remove the key bytes stored under `uid` from the custom NVS namespace. */ +psa_status_t custom_nvs_its_remove(void *ctx, psa_storage_uid_t uid); + +#ifdef __cplusplus +} +#endif diff --git a/examples/security/psa_its_custom_backend/partitions_example.csv b/examples/security/psa_its_custom_backend/partitions_example.csv new file mode 100644 index 00000000000..dc7a5d1dcff --- /dev/null +++ b/examples/security/psa_its_custom_backend/partitions_example.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, diff --git a/examples/security/psa_its_custom_backend/pytest_psa_its_custom_backend.py b/examples/security/psa_its_custom_backend/pytest_psa_its_custom_backend.py new file mode 100644 index 00000000000..1e2c73abbc9 --- /dev/null +++ b/examples/security/psa_its_custom_backend/pytest_psa_its_custom_backend.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +"""Pytest for PSA ITS custom storage backend example.""" + +import logging +import os + +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize + + +@pytest.mark.generic +@idf_parametrize('target', ['esp32'], indirect=['target']) +def test_psa_its_custom_backend(dut: Dut) -> None: + binary_file = os.path.join(dut.app.binary_path, 'psa_its_custom_backend.bin') + bin_size = os.path.getsize(binary_file) + logging.info('psa_its_custom_backend_bin_size: %dKB', bin_size // 1024) + + dut.expect(r'example: === PSA ITS Custom Storage Backend ===', timeout=60) + dut.expect(r'example: Custom ITS backend registered', timeout=60) + + dut.expect(r'example: --- Provisioning keys ---', timeout=60) + + # Default NVS-backed key + match = dut.expect( + r'example: \[NVS\] Generated persistent key 0x1|example: \[NVS\] Key 0x1 already exists', + timeout=60, + ) + if b'Generated' in match.group(0): + logging.info('NVS key generated for the first time') + else: + logging.info('NVS key already existed in storage') + + # Custom-backend key + match = dut.expect( + r'example: \[CUSTOM\] Generated persistent key 0x30000001|' + r'example: \[CUSTOM\] Key 0x30000001 already exists', + timeout=60, + ) + if b'Generated' in match.group(0): + logging.info('Custom-backend key generated for the first time') + else: + logging.info('Custom-backend key already existed in storage') + + dut.expect(r'example: --- Testing encryption/decryption ---', timeout=60) + dut.expect(r'example: \[NVS\] Encrypt/decrypt OK with key 0x1', timeout=60) + dut.expect(r'example: \[CUSTOM\] Encrypt/decrypt OK with key 0x30000001', timeout=60) + + dut.expect(r'example: === All tests passed ===', timeout=60) diff --git a/examples/security/psa_its_custom_backend/sdkconfig.defaults b/examples/security/psa_its_custom_backend/sdkconfig.defaults new file mode 100644 index 00000000000..aed5384ef9e --- /dev/null +++ b/examples/security/psa_its_custom_backend/sdkconfig.defaults @@ -0,0 +1,9 @@ +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_example.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions_example.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# Enable PSA ITS custom storage backend (Kconfig: MBEDTLS_PSA_ITS_*) +CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND=y +CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MIN=0x30000000 +CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MAX=0x3FFFFFFF