mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-22 13:01:16 +03:00
feat(examples/security): Add example to demonstrate the usage of custom key storages with PSA
This commit is contained in:
@@ -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"]
|
||||
|
||||
9
examples/security/psa_its_custom_backend/CMakeLists.txt
Normal file
9
examples/security/psa_its_custom_backend/CMakeLists.txt
Normal file
@@ -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)
|
||||
112
examples/security/psa_its_custom_backend/README.md
Normal file
112
examples/security/psa_its_custom_backend/README.md
Normal file
@@ -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 <chip_name>
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "app_main.c" "custom_nvs_its_backend.c"
|
||||
INCLUDE_DIRS "."
|
||||
PRIV_REQUIRES mbedtls nvs_flash)
|
||||
165
examples/security/psa_its_custom_backend/main/app_main.c
Normal file
165
examples/security/psa_its_custom_backend/main/app_main.c
Normal file
@@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#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 ===");
|
||||
}
|
||||
@@ -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 <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user