Merge branch 'feat/psa_its_custom_backend_v6.1' into 'release/v6.1'

Support custom storage backend for persistent PSA keys (v6.1)

See merge request espressif/esp-idf!49151
This commit is contained in:
Mahavir Jain
2026-07-03 11:34:23 +05:30
20 changed files with 1389 additions and 34 deletions

View File

@@ -39,7 +39,8 @@ if(NOT ${IDF_TARGET} STREQUAL "linux")
endif()
endif()
set(mbedtls_srcs "port/esp_mem.c")
set(mbedtls_srcs "port/esp_mem.c"
"port/psa_crypto_storage/esp_psa_key_file.c")
set(mbedtls_include_dirs
"port/include"
"mbedtls/include"
@@ -60,6 +61,7 @@ endif()
list(APPEND mbedtls_include_dirs "${COMPONENT_DIR}/port/psa_driver/include")
list(APPEND mbedtls_include_dirs "${COMPONENT_DIR}/port/psa_crypto_storage/include")
idf_component_register(SRCS "${mbedtls_srcs}"
INCLUDE_DIRS "${mbedtls_include_dirs}"
@@ -243,6 +245,7 @@ if(NOT ${IDF_TARGET} STREQUAL "linux")
target_link_libraries(tfpsacrypto PRIVATE "$<$<TARGET_EXISTS:idf::nvs_flash>:idf::nvs_flash>")
# Define compile definition to indicate ESP-IDF PSA ITS implementation is available
target_compile_definitions(tfpsacrypto PUBLIC "$<$<TARGET_EXISTS:idf::nvs_flash>:ESP_PSA_ITS_AVAILABLE>")
target_include_directories(tfpsacrypto PRIVATE "${COMPONENT_DIR}/port/psa_crypto_storage/include")
else()
# For v1: check if component is in build before adding source and linking
idf_build_get_property(build_components BUILD_COMPONENTS)
@@ -251,6 +254,7 @@ if(NOT ${IDF_TARGET} STREQUAL "linux")
idf_component_get_property(nvs_flash_lib nvs_flash COMPONENT_LIB)
target_link_libraries(tfpsacrypto PRIVATE ${nvs_flash_lib})
target_compile_definitions(tfpsacrypto PUBLIC ESP_PSA_ITS_AVAILABLE)
target_include_directories(tfpsacrypto PRIVATE "${COMPONENT_DIR}/port/psa_crypto_storage/include")
endif()
endif()
endif()

View File

@@ -30,6 +30,38 @@ menu "mbedTLS"
which is added through vfs component for ESP32 based targets or by
the host system when the target is Linux.
config MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND
bool "Enable custom storage backend for PSA ITS"
default n
help
Enable support for registering a custom storage backend that
handles PSA ITS operations for keys in a reserved UID range.
When enabled, users can call esp_psa_its_register_custom_backend()
to route storage operations for UIDs in the configured range to
their own implementation. UIDs outside the range continue using
the default NVS backend.
config MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MIN
hex "Minimum UID for custom backend range"
depends on MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND
default 0x30000000
range 0x00000001 0x3FFFFFFF
help
The minimum UID value (inclusive) that will be routed to the
custom storage backend. Must be within the PSA user key ID
range (0x00000001 - 0x3FFFFFFF).
config MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MAX
hex "Maximum UID for custom backend range"
depends on MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND
default 0x3FFFFFFF
range MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MIN 0x3FFFFFFF
help
The maximum UID value (inclusive) that will be routed to the
custom storage backend. Must be >= the minimum UID and within
the PSA user key ID range (0x00000001 - 0x3FFFFFFF); the lower
bound is enforced by Kconfig.
config MBEDTLS_THREADING_C
bool "Enable the threading abstraction layer"
default y

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
@@ -23,8 +23,28 @@
#include "nvs_flash.h"
#include "esp_log.h"
#include "sdkconfig.h"
#if CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND
#include "esp_psa_its.h"
#endif
static const char *TAG = "esp_psa_its";
#if CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND
/* Single registered custom backend (NULL when none registered) */
static const esp_psa_its_custom_ops_t *s_custom_ops = NULL;
/**
* Check if a UID falls within the custom backend range.
*/
static inline bool uid_in_custom_range(psa_storage_uid_t uid)
{
return (uid >= CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MIN &&
uid <= CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MAX);
}
#endif /* CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND */
/* NVS namespace for PSA ITS */
#define PSA_ITS_NVS_NAMESPACE "psa_its"
@@ -106,6 +126,15 @@ psa_status_t psa_its_get_info(psa_storage_uid_t uid,
return PSA_ERROR_INVALID_ARGUMENT;
}
#if CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND
if (uid_in_custom_range(uid)) {
if (s_custom_ops == NULL || s_custom_ops->get_info == NULL) {
return PSA_ERROR_STORAGE_FAILURE;
}
return s_custom_ops->get_info(s_custom_ops->ctx, uid, p_info);
}
#endif
/* Convert UID to NVS key */
uid_to_nvs_key(uid, nvs_key);
@@ -188,6 +217,16 @@ psa_status_t psa_its_get(psa_storage_uid_t uid,
return PSA_ERROR_INVALID_ARGUMENT;
}
#if CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND
if (uid_in_custom_range(uid)) {
if (s_custom_ops == NULL || s_custom_ops->get == NULL) {
return PSA_ERROR_STORAGE_FAILURE;
}
return s_custom_ops->get(s_custom_ops->ctx, uid, data_offset,
data_length, p_data, p_data_length);
}
#endif
/* Convert UID to NVS key */
uid_to_nvs_key(uid, nvs_key);
@@ -297,6 +336,16 @@ psa_status_t psa_its_set(psa_storage_uid_t uid,
return PSA_ERROR_INVALID_ARGUMENT;
}
#if CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND
if (uid_in_custom_range(uid)) {
if (s_custom_ops == NULL || s_custom_ops->set == NULL) {
return PSA_ERROR_STORAGE_FAILURE;
}
return s_custom_ops->set(s_custom_ops->ctx, uid, data_length,
p_data, create_flags);
}
#endif
/* Convert UID to NVS key */
uid_to_nvs_key(uid, nvs_key);
@@ -383,6 +432,15 @@ psa_status_t psa_its_remove(psa_storage_uid_t uid)
psa_its_entry_t *existing_entry = NULL;
psa_status_t status = PSA_ERROR_STORAGE_FAILURE;
#if CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND
if (uid_in_custom_range(uid)) {
if (s_custom_ops == NULL || s_custom_ops->remove == NULL) {
return PSA_ERROR_STORAGE_FAILURE;
}
return s_custom_ops->remove(s_custom_ops->ctx, uid);
}
#endif
/* Convert UID to NVS key */
uid_to_nvs_key(uid, nvs_key);
@@ -451,3 +509,30 @@ exit:
nvs_close(handle);
return status;
}
#if CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND
psa_status_t esp_psa_its_register_custom_backend(const esp_psa_its_custom_ops_t *ops)
{
if (ops == NULL || ops->set == NULL || ops->get == NULL ||
ops->get_info == NULL || ops->remove == NULL) {
return PSA_ERROR_INVALID_ARGUMENT;
}
if (s_custom_ops != NULL) {
return PSA_ERROR_NOT_PERMITTED;
}
s_custom_ops = ops;
return PSA_SUCCESS;
}
psa_status_t esp_psa_its_unregister_custom_backend(void)
{
if (s_custom_ops == NULL) {
return PSA_ERROR_DOES_NOT_EXIST;
}
s_custom_ops = NULL;
return PSA_SUCCESS;
}
#endif

View File

@@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "esp_psa_key_file.h"
#include "psa/crypto.h"
#include "psa_crypto_storage.h"
size_t esp_psa_key_file_size(const size_t material_len)
{
return psa_persistent_key_storage_blob_size(material_len);
}
psa_status_t esp_psa_key_file_pack(const psa_key_attributes_t *attrs,
const uint8_t *material,
const size_t material_len,
uint8_t *out_buf,
const size_t out_buf_size,
size_t *out_len)
{
if (attrs == NULL || out_buf == NULL || out_len == NULL ||
(material == NULL && material_len != 0)) {
return PSA_ERROR_INVALID_ARGUMENT;
}
const size_t total = psa_persistent_key_storage_blob_size(material_len);
if (out_buf_size < total) {
return PSA_ERROR_BUFFER_TOO_SMALL;
}
psa_format_key_data_for_storage(material, material_len, attrs, out_buf);
*out_len = total;
return PSA_SUCCESS;
}
psa_status_t esp_psa_key_file_unpack(const uint8_t *blob,
const size_t blob_len,
psa_key_attributes_t *attrs,
const uint8_t **material,
size_t *material_len)
{
if (blob == NULL || attrs == NULL || material == NULL || material_len == NULL) {
return PSA_ERROR_INVALID_ARGUMENT;
}
uint8_t *upstream_material = NULL;
size_t upstream_material_len = 0;
psa_status_t status = psa_parse_key_data_from_storage(blob, blob_len,
&upstream_material,
&upstream_material_len,
attrs);
if (status != PSA_SUCCESS) {
return status;
}
/* Upstream allocates and copies the material into a fresh buffer.
* Discard the copy and return a pointer into the caller's blob — the
* material section starts immediately after the fixed-size header, so
* the bytes are identical. Preserves the zero-copy public contract. */
*material = (upstream_material_len == 0) ? NULL
: blob + (blob_len - upstream_material_len);
*material_len = upstream_material_len;
psa_free_persistent_key_data(upstream_material, upstream_material_len);
return PSA_SUCCESS;
}

View File

@@ -0,0 +1,122 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* PSA ITS custom storage backend API.
*
* When CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND is enabled, users can register
* a custom storage backend for a reserved range of PSA key IDs. UIDs within
* the configured range are routed to the registered backend; all other UIDs
* continue using the default NVS backend.
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
#include "psa/internal_trusted_storage.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Custom storage backend operations for PSA ITS.
*
* Implement this structure to provide a custom storage backend for PSA ITS
* keys in the configured UID range. Callback signatures mirror the PSA ITS
* API with an additional user-provided context pointer.
*/
typedef struct {
/**
* @brief Store data for the given UID.
*
* @param ctx User-provided context pointer
* @param uid Storage UID (key ID)
* @param data_length Length of the data to store
* @param p_data Pointer to the data buffer
* @param create_flags Storage flags (e.g., PSA_STORAGE_FLAG_WRITE_ONCE)
* @return PSA status code
*/
psa_status_t (*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);
/**
* @brief Retrieve data for the given UID.
*
* @param ctx User-provided context pointer
* @param uid Storage UID (key ID)
* @param data_offset Byte offset within the stored data
* @param data_length Number of bytes to retrieve
* @param p_data Output buffer for the data
* @param p_data_length On success, set to the number of bytes written
* @return PSA status code
*/
psa_status_t (*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);
/**
* @brief Retrieve metadata for the given UID.
*
* @param ctx User-provided context pointer
* @param uid Storage UID (key ID)
* @param p_info Output structure for size and flags
* @return PSA status code
*/
psa_status_t (*get_info)(void *ctx,
const psa_storage_uid_t uid,
struct psa_storage_info_t *p_info);
/**
* @brief Remove data for the given UID.
*
* @param ctx User-provided context pointer
* @param uid Storage UID (key ID)
* @return PSA status code
*/
psa_status_t (*remove)(void *ctx,
const psa_storage_uid_t uid);
/** User-provided context pointer, passed as the first argument to all callbacks. */
void *ctx;
} esp_psa_its_custom_ops_t;
/**
* @brief Register a custom storage backend for the configured UID range.
*
* Only one custom backend may be registered at a time. The UID range is
* determined by CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MIN (inclusive) and
* CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MAX (inclusive).
*
* @param ops Pointer to the operations structure. Must remain valid for
* the lifetime of the registration. All four function pointers
* (set, get, get_info, remove) must be non-NULL.
*
* @return PSA_SUCCESS on success
* @return PSA_ERROR_INVALID_ARGUMENT if ops or any callback is NULL
* @return PSA_ERROR_NOT_PERMITTED if a backend is already registered
*/
psa_status_t esp_psa_its_register_custom_backend(const esp_psa_its_custom_ops_t *ops);
/**
* @brief Unregister the currently registered custom storage backend.
*
* After this call, UIDs in the custom range will return
* PSA_ERROR_STORAGE_FAILURE until a new backend is registered.
*
* @return PSA_SUCCESS on success
* @return PSA_ERROR_DOES_NOT_EXIST if no backend is registered
*/
psa_status_t esp_psa_its_unregister_custom_backend(void);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*
* Custom PSA ITS backends that synthesize blobs on read, or that strip the
* header on write to save space, use these helpers to convert between PSA
* attributes + raw key material and the documented blob format without
* depending on tf-psa-crypto internal functions.
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "psa/crypto.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Total blob size produced by esp_psa_key_file_pack() for a given key
* material length (i.e. fixed header size + @p material_len).
*/
size_t esp_psa_key_file_size(const size_t material_len);
/**
* @brief Pack key attributes and material into a key file blob.
*
* @param[in] attrs Source attributes (lifetime, type, bits, policy).
* @param[in] material Key material. For transparent keys, the
* psa_export_key() output. For opaque keys, the
* driver-specific opaque blob.
* @param[in] material_len Length of @p material in bytes.
* @param[out] out_buf Destination buffer.
* @param[in] out_buf_size Size of @p out_buf. Must be at least
* esp_psa_key_file_size(material_len).
* @param[out] out_len Set to the number of bytes written.
*
* @retval PSA_SUCCESS
* @retval PSA_ERROR_INVALID_ARGUMENT Null pointer.
* @retval PSA_ERROR_BUFFER_TOO_SMALL @p out_buf_size is too small.
*/
psa_status_t esp_psa_key_file_pack(const psa_key_attributes_t *attrs,
const uint8_t *material,
const size_t material_len,
uint8_t *out_buf,
const size_t out_buf_size,
size_t *out_len);
/**
* @brief Parse a key file blob into key attributes and material.
*
* The returned @p material pointer points into @p blob and is valid only as
* long as @p blob itself is. No allocation occurs.
*
* Attribute fields described in the blob (lifetime, type, bits, usage,
* algorithm, enrollment algorithm) are set on @p attrs on success. Other
* fields (notably the key id) are left untouched, so the caller can set the
* id beforehand if needed.
*
* @param[in] blob Blob to parse.
* @param[in] blob_len Length of @p blob in bytes.
* @param[out] attrs Filled with parsed attributes on success.
* @param[out] material Set to point to the material section within @p blob,
* or NULL if the blob declares zero-length material.
* @param[out] material_len Length of the material section in bytes.
*
* @retval PSA_SUCCESS
* @retval PSA_ERROR_INVALID_ARGUMENT Null pointer, or @p blob_len smaller than
* ESP_PSA_KEY_FILE_HEADER_SIZE.
* @retval PSA_ERROR_DATA_INVALID Bad magic, unsupported version, or the
* declared material length does not match
* the blob size (the spec rejects trailing
* data on load).
*/
psa_status_t esp_psa_key_file_unpack(const uint8_t *blob,
const size_t blob_len,
psa_key_attributes_t *attrs,
const uint8_t **material,
size_t *material_len);
#ifdef __cplusplus
}
#endif

View File

@@ -347,7 +347,7 @@ void esp_rsa_ds_release_ds_lock(void)
}
}
static int esp_rsa_ds_validate_opaque_key(const esp_rsa_ds_opaque_key_t *opaque_key)
static psa_status_t esp_rsa_ds_validate_opaque_key(const esp_rsa_ds_opaque_key_t *opaque_key)
{
if (opaque_key == NULL) {
return PSA_ERROR_INVALID_ARGUMENT;
@@ -392,6 +392,111 @@ static int esp_rsa_ds_validate_opaque_key(const esp_rsa_ds_opaque_key_t *opaque_
return PSA_SUCCESS;
}
/**
* Serialize an already-validated opaque key into the persistent inline
* storage layout (eFuse or Key Manager, selected from key_recovery_info).
* Internal helper — does not validate inputs.
*/
static psa_status_t rsa_ds_format_persistent_key_buffer_internal(
const esp_rsa_ds_opaque_key_t *opaque_key,
uint8_t *buf, size_t buf_size, size_t *out_len)
{
#if SOC_KEY_MANAGER_SUPPORTED
if (opaque_key->key_recovery_info) {
if (buf_size < sizeof(esp_rsa_ds_km_key_storage_t)) {
return PSA_ERROR_BUFFER_TOO_SMALL;
}
esp_rsa_ds_km_key_storage_t *storage = (esp_rsa_ds_km_key_storage_t *)buf;
memset(storage, 0, sizeof(*storage));
storage->metadata.version = ESP_RSA_DS_KEY_STORAGE_VERSION_V1;
storage->metadata.key_source = ESP_RSA_DS_KEY_SOURCE_KEY_MGR;
storage->rsa_length_bits = opaque_key->ds_data_ctx->rsa_length_bits;
memcpy(&storage->ds_data, opaque_key->ds_data_ctx->esp_ds_data, sizeof(esp_ds_data_t));
memcpy(&storage->key_recovery_info, opaque_key->key_recovery_info,
sizeof(esp_key_mgr_key_recovery_info_t));
*out_len = sizeof(esp_rsa_ds_km_key_storage_t);
return PSA_SUCCESS;
}
#endif /* SOC_KEY_MANAGER_SUPPORTED */
if (buf_size < sizeof(esp_rsa_ds_efuse_key_storage_t)) {
return PSA_ERROR_BUFFER_TOO_SMALL;
}
esp_rsa_ds_efuse_key_storage_t *storage = (esp_rsa_ds_efuse_key_storage_t *)buf;
memset(storage, 0, sizeof(*storage));
storage->metadata.version = ESP_RSA_DS_KEY_STORAGE_VERSION_V1;
storage->metadata.key_source = ESP_RSA_DS_KEY_SOURCE_EFUSE;
storage->efuse_key_id = opaque_key->ds_data_ctx->efuse_key_id;
storage->rsa_length_bits = opaque_key->ds_data_ctx->rsa_length_bits;
memcpy(&storage->ds_data, opaque_key->ds_data_ctx->esp_ds_data, sizeof(esp_ds_data_t));
*out_len = sizeof(esp_rsa_ds_efuse_key_storage_t);
return PSA_SUCCESS;
}
size_t esp_rsa_ds_persistent_key_buffer_size(const esp_rsa_ds_opaque_key_t *opaque_key)
{
if (opaque_key == NULL) {
return 0;
}
return esp_rsa_ds_get_storage_size(opaque_key, true);
}
psa_status_t esp_rsa_ds_format_persistent_key_buffer(const esp_rsa_ds_opaque_key_t *opaque_key,
uint8_t *buf, size_t buf_size,
size_t *out_len)
{
if (opaque_key == NULL || buf == NULL || out_len == NULL) {
return PSA_ERROR_INVALID_ARGUMENT;
}
psa_status_t ret = esp_rsa_ds_validate_opaque_key(opaque_key);
if (ret != PSA_SUCCESS) {
return ret;
}
return rsa_ds_format_persistent_key_buffer_internal(opaque_key, buf, buf_size, out_len);
}
psa_status_t esp_rsa_ds_parse_persistent_key_buffer(const uint8_t *buf, size_t buf_len,
esp_rsa_ds_opaque_key_t *out)
{
if (buf == NULL || out == NULL || out->ds_data_ctx == NULL) {
return PSA_ERROR_INVALID_ARGUMENT;
}
esp_rsa_ds_key_source_t key_source = ESP_RSA_DS_KEY_SOURCE_EFUSE;
uint16_t rsa_length_bits = 0;
const esp_ds_data_t *ds_data = NULL;
hmac_key_id_t hmac_id = 0;
#if SOC_KEY_MANAGER_SUPPORTED
esp_key_mgr_key_recovery_info_t *km_ri = NULL;
#endif
psa_status_t status = esp_rsa_ds_extract_storage(
buf, buf_len, /* is_persistent = */ true,
&key_source, &rsa_length_bits, &ds_data, &hmac_id
#if SOC_KEY_MANAGER_SUPPORTED
, &km_ri
#endif
);
if (status != PSA_SUCCESS) {
return status;
}
/* Aliases into buf — caller must not write through these pointers and
* must keep buf alive for as long as out is used. */
out->ds_data_ctx->esp_ds_data = (esp_ds_data_t *)ds_data;
out->ds_data_ctx->efuse_key_id = (uint8_t)hmac_id;
out->ds_data_ctx->rsa_length_bits = rsa_length_bits;
#if SOC_KEY_MANAGER_SUPPORTED
out->key_recovery_info = km_ri;
#else
(void)key_source;
#endif
return PSA_SUCCESS;
}
psa_status_t esp_rsa_ds_opaque_sign_hash_start(
esp_rsa_ds_opaque_sign_hash_operation_t *operation,
const psa_key_attributes_t *attributes,
@@ -678,7 +783,7 @@ psa_status_t esp_rsa_ds_opaque_import_key(
}
const esp_rsa_ds_opaque_key_t *opaque_key = (const esp_rsa_ds_opaque_key_t *)data;
int ret = esp_rsa_ds_validate_opaque_key(opaque_key);
psa_status_t ret = esp_rsa_ds_validate_opaque_key(opaque_key);
if (ret != PSA_SUCCESS) {
return ret;
}
@@ -709,36 +814,10 @@ psa_status_t esp_rsa_ds_opaque_import_key(
*key_buffer_length = sizeof(esp_rsa_ds_volatile_key_storage_t);
} else {
/* Persistent: deep-copy all data into self-contained storage struct */
#if SOC_KEY_MANAGER_SUPPORTED
if (key_source == ESP_RSA_DS_KEY_SOURCE_KEY_MGR) {
if (key_buffer_size < sizeof(esp_rsa_ds_km_key_storage_t)) {
return PSA_ERROR_BUFFER_TOO_SMALL;
}
esp_rsa_ds_km_key_storage_t *storage = (esp_rsa_ds_km_key_storage_t *)key_buffer;
storage->metadata.version = ESP_RSA_DS_KEY_STORAGE_VERSION_V1;
storage->metadata.key_source = ESP_RSA_DS_KEY_SOURCE_KEY_MGR;
storage->rsa_length_bits = opaque_key->ds_data_ctx->rsa_length_bits;
memcpy(&storage->ds_data, opaque_key->ds_data_ctx->esp_ds_data, sizeof(esp_ds_data_t));
memcpy(&storage->key_recovery_info, opaque_key->key_recovery_info,
sizeof(esp_key_mgr_key_recovery_info_t));
*key_buffer_length = sizeof(esp_rsa_ds_km_key_storage_t);
} else
#endif /* SOC_KEY_MANAGER_SUPPORTED */
{
if (key_buffer_size < sizeof(esp_rsa_ds_efuse_key_storage_t)) {
return PSA_ERROR_BUFFER_TOO_SMALL;
}
esp_rsa_ds_efuse_key_storage_t *storage = (esp_rsa_ds_efuse_key_storage_t *)key_buffer;
storage->metadata.version = ESP_RSA_DS_KEY_STORAGE_VERSION_V1;
storage->metadata.key_source = ESP_RSA_DS_KEY_SOURCE_EFUSE;
storage->efuse_key_id = opaque_key->ds_data_ctx->efuse_key_id;
storage->reserved = 0;
storage->rsa_length_bits = opaque_key->ds_data_ctx->rsa_length_bits;
memset(storage->reserved2, 0, sizeof(storage->reserved2));
memcpy(&storage->ds_data, opaque_key->ds_data_ctx->esp_ds_data, sizeof(esp_ds_data_t));
*key_buffer_length = sizeof(esp_rsa_ds_efuse_key_storage_t);
psa_status_t status = rsa_ds_format_persistent_key_buffer_internal(
opaque_key, key_buffer, key_buffer_size, key_buffer_length);
if (status != PSA_SUCCESS) {
return status;
}
}

View File

@@ -38,6 +38,89 @@ extern "C" {
PSA_KEY_PERSISTENCE_VOLATILE, \
PSA_KEY_LOCATION_ESP_RSA_DS)
/**
* @brief Buffer size needed to serialize an ESP-RSA DS key into the persistent
* storage layout used by this driver.
*
* The size depends on the key source: eFuse-sourced keys and Key Manager-sourced
* keys (KM-capable SoCs only) use different inline storage structs. The source
* is inferred from @p opaque_key — the Key Manager layout is selected when
* @p opaque_key->key_recovery_info is non-NULL.
*
* @param opaque_key User-facing opaque key (must be non-NULL).
* @return Storage buffer size in bytes, or 0 if @p opaque_key is NULL.
*/
size_t esp_rsa_ds_persistent_key_buffer_size(const esp_rsa_ds_opaque_key_t *opaque_key);
/**
* @brief Serialize an ESP-RSA DS key into the persistent storage layout that
* this driver expects when loading a persistent key from PSA storage.
*
* Custom PSA ITS backends that synthesize persistent RSA-DS key blobs at
* read time can use this helper to produce the @c key_data payload, then wrap
* it with esp_psa_its_pack_key_blob() to build the full PSA persistent key blob.
*
* The output layout (eFuse vs Key Manager) is selected automatically from
* @p opaque_key, mirroring the import path. Validation of @p opaque_key is
* performed before serialization.
*
* @param opaque_key User-facing opaque key.
* @param buf Output buffer (caller-allocated, sized via
* esp_rsa_ds_persistent_key_buffer_size()).
* @param buf_size Size of @p buf in bytes.
* @param[out] out_len Bytes written to @p buf on success.
*
* @return PSA_SUCCESS on success
* @return PSA_ERROR_INVALID_ARGUMENT if any required input is NULL or the
* opaque key fields are invalid
* @return PSA_ERROR_BUFFER_TOO_SMALL if @p buf_size is insufficient
*/
psa_status_t esp_rsa_ds_format_persistent_key_buffer(const esp_rsa_ds_opaque_key_t *opaque_key,
uint8_t *buf, size_t buf_size,
size_t *out_len);
/**
* @brief Parse an ESP-RSA DS persistent key buffer.
*
* Inverse of esp_rsa_ds_format_persistent_key_buffer(): validates the
* key-storage metadata (version + source) in @p buf, then fills @p out
* with the same opaque-key shape the format path consumes.
*
* The caller owns the storage for @p out, including the @c esp_ds_data_ctx_t
* it points to (@p out->ds_data_ctx must be non-NULL before the call).
* On success, scalar fields (@c efuse_key_id, @c rsa_length_bits) are filled
* into the caller's @c esp_ds_data_ctx_t, and the @c esp_ds_data pointer
* — together with @c out->key_recovery_info on KM-capable SoCs — aliases
* into @p buf. Those pointers remain valid only for as long as @p buf is,
* and must not be written through.
*
* The key source (eFuse vs Key Manager) is conveyed implicitly: on success,
* @c out->key_recovery_info is non-NULL iff the buffer was produced from a
* Key-Manager-backed key. This mirrors how the format path discriminates
* via the same field.
*
* Custom PSA ITS backends that accept writes (translating PSA-formatted
* blobs handed to psa_its_set() into a native storage format) can use this
* helper after first stripping the PSA wrapper with esp_psa_its_unpack_key_blob().
*
* @param buf Input buffer (as produced by esp_rsa_ds_format_persistent_key_buffer()
* or written by the driver's import path).
* @param buf_len Length of @p buf in bytes.
* @param[in,out] out Opaque key to fill. @p out->ds_data_ctx must point to a
* caller-allocated @c esp_ds_data_ctx_t.
*
* @return PSA_SUCCESS on success
* @return PSA_ERROR_INVALID_ARGUMENT if @p buf, @p out, or @p out->ds_data_ctx
* is NULL, or @p buf_len is too small, or
* the buffer is internally inconsistent
* (e.g. invalid RSA length)
* @return PSA_ERROR_DATA_INVALID if the metadata version or key source is
* unrecognized, or the embedded esp_ds_data_t
* does not match the declared key length
*/
psa_status_t esp_rsa_ds_parse_persistent_key_buffer(const uint8_t *buf, size_t buf_len,
esp_rsa_ds_opaque_key_t *out);
/**
* @brief Start the RSA DS opaque sign hash operation
*

View File

@@ -167,6 +167,114 @@ The new mbedTLS configuration system is organized into logical categories for ea
X.509 certificate parsing, validation, and certificate bundle management.
PSA ITS Custom Storage Backend
-------------------------------
ESP-IDF's PSA Internal Trusted Storage (ITS) implementation uses NVS as its default backend for storing persistent PSA Crypto keys. The custom storage backend feature allows routing a reserved range of PSA key IDs to a user-provided storage implementation, while all other keys continue using NVS.
This is useful when:
- Certain keys need to be stored on a different filesystem (FATFS, SPIFFS, littlefs)
- Keys require hardware-protected encryption (e.g., via TEE secure storage)
- Different storage partitions are needed for different key categories
Enabling the Custom Backend
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Enable the feature via ``menuconfig`` under ``Component Config`` > ``mbedTLS``:
- :ref:`CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND`: Enable the custom storage backend
- :ref:`CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MIN`: Start of the custom key ID range (default ``0x30000000``)
- :ref:`CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MAX`: End of the custom key ID range (default ``0x3FFFFFFF``)
PSA key IDs within the configured range are routed to the registered backend. All other key IDs (and internal PSA data such as the random seed) continue using the default NVS backend.
Implementing a Custom Backend
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Implement the ``esp_psa_its_custom_ops_t`` callback structure and register it before using PSA Crypto with keys in the custom range:
.. code-block:: c
#include "esp_psa_its.h"
static psa_status_t my_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)
{
/* Store the blob identified by uid */
}
static psa_status_t my_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)
{
/* Retrieve the blob identified by uid */
}
static psa_status_t my_get_info(void *ctx, const psa_storage_uid_t uid,
struct psa_storage_info_t *p_info)
{
/* Return size and flags for the blob identified by uid */
}
static psa_status_t my_remove(void *ctx, const psa_storage_uid_t uid)
{
/* Delete the blob identified by uid */
}
static esp_psa_its_custom_ops_t my_ops = {
.set = my_set,
.get = my_get,
.get_info = my_get_info,
.remove = my_remove,
.ctx = NULL, /* optional user context */
};
/* Register before using PSA keys in the custom range */
esp_psa_its_register_custom_backend(&my_ops);
The callback signatures mirror the PSA ITS API. Each callback receives the raw ``psa_storage_uid_t`` (not a string), allowing the implementation to make routing decisions based on the numeric key ID. The ``ctx`` pointer is passed as the first argument to every callback.
.. note::
Only persistent keys flow through the ITS layer. PSA requires ``psa_set_key_id()`` for persistent keys, so the application always controls which key IDs it assigns and thus which range they fall into.
The backend implementation is responsible for enforcing ``psa_storage_create_flags_t`` semantics if needed.
Working with the PSA Key Blob Format
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The byte stream that flows through ``psa_its_set()`` / ``psa_its_get()`` is the PSA persistent key blob format documented in the Mbed TLS `storage specification <https://github.com/Mbed-TLS/TF-PSA-Crypto/blob/development/docs/architecture/mbed-crypto-storage-specification.md>`__. Backends that store the blob verbatim do not need to look inside it. Backends that strip the header on write (to save space) or synthesise the blob on read (for example, to expose a pre-provisioned hardware key) need to construct or parse it themselves.
ESP-IDF provides two helpers in ``esp_psa_key_file.h`` for this:
- :cpp:func:`esp_psa_key_file_pack` — assemble a key blob from a ``psa_key_attributes_t`` structure and raw key material bytes.
- :cpp:func:`esp_psa_key_file_unpack` — parse a key blob back into attributes and a pointer into the key material section.
- :cpp:func:`esp_psa_key_file_size` — return the total blob size for a given material length.
These helpers implement the documented byte layout directly and do not depend on any internal Mbed TLS function. For example, a backend that stores only the inner key bytes can rebuild the blob on read:
.. code-block:: c
#include "esp_psa_key_file.h"
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_PERSISTENT);
psa_set_key_type(&attr, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attr, key_data_len * 8);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attr, PSA_ALG_CBC_NO_PADDING);
size_t blob_size = esp_psa_key_file_size(key_data_len);
uint8_t *blob = calloc(1, blob_size);
size_t written = 0;
esp_psa_key_file_pack(&attr, key_data, key_data_len, blob, blob_size, &written);
/* blob now holds the full PSA persistent key file; copy the requested
* window into p_data per psa_its_get()'s offset/length arguments. */
For a complete working example using a custom NVS namespace as the custom backend, refer to :example:`security/psa_its_custom_backend`.
Application Examples
--------------------

View File

@@ -167,6 +167,114 @@ ESP-IDF 为 Mbed TLS 提供了基于预设的配置系统,用于简化设置
X.509 证书解析、验证和证书包管理。
PSA ITS 自定义存储后端
-----------------------
ESP-IDF 的 PSA 内部可信存储 (Internal Trusted Storage, ITS) 实现默认使用 NVS 作为持久化 PSA Crypto 密钥的存储后端。自定义存储后端功能允许将保留范围内的 PSA 密钥 ID 路由到用户提供的存储实现,而其他密钥继续使用 NVS。
适用场景包括:
- 某些密钥需要存储在不同的文件系统中(如 FATFS、SPIFFS、littlefs
- 密钥需要硬件保护的加密存储(例如通过 TEE 安全存储)
- 不同类别的密钥需要使用不同的存储分区
启用自定义后端
^^^^^^^^^^^^^^^
通过 ``menuconfig````Component Config`` > ``mbedTLS`` 中启用该功能:
- :ref:`CONFIG_MBEDTLS_PSA_ITS_CUSTOM_STORAGE_BACKEND`:启用自定义存储后端
- :ref:`CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MIN`:自定义密钥 ID 范围的起始值(默认 ``0x30000000``
- :ref:`CONFIG_MBEDTLS_PSA_ITS_CUSTOM_BACKEND_UID_MAX`:自定义密钥 ID 范围的结束值(默认 ``0x3FFFFFFF``
配置范围内的 PSA 密钥 ID 会被路由到已注册的后端。其他所有密钥 ID以及随机种子等 PSA 内部数据)继续使用默认的 NVS 后端。
实现自定义后端
^^^^^^^^^^^^^^^
实现 ``esp_psa_its_custom_ops_t`` 回调结构体,并在使用自定义范围内的 PSA Crypto 密钥之前进行注册:
.. code-block:: c
#include "esp_psa_its.h"
static psa_status_t my_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)
{
/* 存储 uid 对应的 blob */
}
static psa_status_t my_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)
{
/* 读取 uid 对应的 blob */
}
static psa_status_t my_get_info(void *ctx, const psa_storage_uid_t uid,
struct psa_storage_info_t *p_info)
{
/* 返回 uid 对应 blob 的大小和标志位 */
}
static psa_status_t my_remove(void *ctx, const psa_storage_uid_t uid)
{
/* 删除 uid 对应的 blob */
}
static esp_psa_its_custom_ops_t my_ops = {
.set = my_set,
.get = my_get,
.get_info = my_get_info,
.remove = my_remove,
.ctx = NULL, /* 可选的用户上下文 */
};
/* 在使用自定义范围内的 PSA 密钥之前注册 */
esp_psa_its_register_custom_backend(&my_ops);
回调函数的签名与 PSA ITS API 保持一致。每个回调接收原始的 ``psa_storage_uid_t``\ (而非字符串),允许实现根据数值型密钥 ID 进行路由决策。``ctx`` 指针会作为第一个参数传递给每个回调。
.. note::
只有持久化密钥会经过 ITS 层。PSA 要求持久化密钥必须调用 ``psa_set_key_id()``,因此应用程序始终可以控制分配哪些密钥 ID进而决定它们落在哪个范围内。
如有需要,后端实现需自行处理 ``psa_storage_create_flags_t`` 的语义。
处理 PSA 密钥文件格式
^^^^^^^^^^^^^^^^^^^^^^
通过 ``psa_its_set()`` / ``psa_its_get()`` 传输的字节流采用 Mbed TLS `存储规范 <https://github.com/Mbed-TLS/TF-PSA-Crypto/blob/development/docs/architecture/mbed-crypto-storage-specification.md>`__\ 中描述的 PSA 持久密钥文件格式。原样存储该 blob 的后端无需查看其内容;在写入时剥离头部以节省空间,或在读取时合成 blob 的后端(例如,将预配置的硬件密钥暴露为持久 PSA 密钥)则需要自行构造或解析。
为此ESP-IDF 在 ``esp_psa_key_file.h`` 中提供以下辅助函数:
- :cpp:func:`esp_psa_key_file_pack` —— 将 ``psa_key_attributes_t`` 结构和原始密钥字节封装为密钥 blob。
- :cpp:func:`esp_psa_key_file_unpack` —— 将密钥 blob 解析回属性以及指向密钥字节段的指针。
- :cpp:func:`esp_psa_key_file_size` —— 根据给定的密钥字节长度返回 blob 的总大小。
这些辅助函数直接实现规范文档中描述的字节布局,不依赖任何 Mbed TLS 内部函数。例如,仅存储内部密钥字节的后端可以在读取时重建 blob
.. code-block:: c
#include "esp_psa_key_file.h"
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_PERSISTENT);
psa_set_key_type(&attr, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attr, key_data_len * 8);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attr, PSA_ALG_CBC_NO_PADDING);
size_t blob_size = esp_psa_key_file_size(key_data_len);
uint8_t *blob = calloc(1, blob_size);
size_t written = 0;
esp_psa_key_file_pack(&attr, key_data, key_data_len, blob, blob_size, &written);
/* blob 现在包含完整的 PSA 持久密钥文件;按 psa_its_get() 的 offset/length
* 参数将所请求的窗口复制到 p_data。 */
完整的可运行示例(使用自定义 NVS 命名空间作为自定义后端)请参考 :example:`security/psa_its_custom_backend`
应用示例
--------

View File

@@ -51,6 +51,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"]

View 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)

View File

@@ -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.

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "app_main.c" "custom_nvs_its_backend.c"
INCLUDE_DIRS "."
PRIV_REQUIRES mbedtls nvs_flash)

View 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 ===");
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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