feat(examples/security): Add example to demonstrate the usage of custom key storages with PSA

This commit is contained in:
harshal.patil
2026-07-02 16:00:23 +05:30
parent 6634a0b620
commit b3214f13c6
10 changed files with 577 additions and 0 deletions
@@ -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)
@@ -0,0 +1,110 @@
| 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 the PSA blob verbatim |
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 persists that blob verbatim and returns it unchanged on `get()`. The backend treats the blob as opaque bytes — no parsing, no attribute knowledge, no commitment to a particular key shape. This is the simplest possible backend; the PSA layer owns the format and the backend is purely a byte store.
Backends that need to optimise on-disk size, expose pre-provisioned hardware keys, or synthesise blobs on read can call `esp_psa_key_file_unpack()` / `esp_psa_key_file_pack()` (declared in `esp_psa_key_file.h`) to convert between PSA blobs and (attributes + key material). See the "Adding a New Storage Backend" section below for the trade-offs and an example.
> 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** (used by this example) — minimal code, 36 bytes of PSA wrapper per entry plus the key material. Suitable when storage size isn't a concern and the backend should be agnostic to key shape.
- **Unpack-store-repack with hardcoded attributes** — call `esp_psa_key_file_unpack()` in `set()` to extract just the inner key bytes, persist only those, then call `esp_psa_key_file_pack()` 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. The `esp_secure_cert_mgr` custom-backend example demonstrates this pattern with a pre-provisioned RSA-DS key synthesised from an eFuse-bound peripheral.
- **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)
@@ -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,173 @@
/*
* Custom NVS storage implementation for PSA ITS blobs
*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*
* Stores PSA blobs verbatim in a separate NVS namespace ("psa_its_ext").
*/
#include <string.h>
#include <stdlib.h>
#include "custom_nvs_its_backend.h"
#include "psa/crypto.h"
#include "nvs.h"
#define EXT_NVS_NAMESPACE "psa_its_ext"
#define EXT_NVS_KEY_LEN 14
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';
}
/* ---- Public storage operations ---- */
psa_status_t custom_nvs_its_set(void *ctx, const psa_storage_uid_t uid,
const uint32_t data_length, const void *p_data,
const psa_storage_create_flags_t create_flags)
{
(void)ctx;
(void)create_flags; /* WRITE_ONCE not tracked in this minimal example */
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, p_data, data_length);
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, const psa_storage_uid_t uid,
const uint32_t data_offset, const 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 blob_size = 0;
err = nvs_get_blob(handle, nvs_key, NULL, &blob_size);
if (err != ESP_OK) {
nvs_close(handle);
return (err == ESP_ERR_NVS_NOT_FOUND) ? PSA_ERROR_DOES_NOT_EXIST
: PSA_ERROR_STORAGE_FAILURE;
}
if (data_offset + data_length < data_offset ||
data_offset + data_length > blob_size) {
nvs_close(handle);
return PSA_ERROR_INVALID_ARGUMENT;
}
uint8_t *blob = calloc(1, blob_size);
if (blob == NULL) {
nvs_close(handle);
return PSA_ERROR_INSUFFICIENT_MEMORY;
}
err = nvs_get_blob(handle, nvs_key, blob, &blob_size);
nvs_close(handle);
if (err != ESP_OK) {
free(blob);
return PSA_ERROR_STORAGE_FAILURE;
}
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);
return PSA_SUCCESS;
}
psa_status_t custom_nvs_its_get_info(void *ctx, const 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 blob_size = 0;
err = nvs_get_blob(handle, nvs_key, NULL, &blob_size);
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;
}
p_info->size = (uint32_t)blob_size;
p_info->flags = 0; /* WRITE_ONCE not tracked in this minimal example */
return PSA_SUCCESS;
}
psa_status_t custom_nvs_its_remove(void *ctx, const 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,43 @@
/*
* 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
/* Persist the PSA blob verbatim 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, const psa_storage_uid_t uid,
const uint32_t data_length, const void *p_data,
const psa_storage_create_flags_t create_flags);
/* Copy `data_length` bytes from `data_offset` of the stored PSA blob for
* `uid` into `p_data`. */
psa_status_t custom_nvs_its_get(void *ctx, const psa_storage_uid_t uid,
const uint32_t data_offset, const uint32_t data_length,
void *p_data, size_t *p_data_length);
/* Report the size of the stored 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, const psa_storage_uid_t uid,
struct psa_storage_info_t *p_info);
/* Remove the blob stored under `uid` from the custom NVS namespace. */
psa_status_t custom_nvs_its_remove(void *ctx, const 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,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 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