Skip to content

subsystem: settings: its: Add ITS backend #87778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions boards/adi/max32657evkit/max32657evkit_max32657_ns.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ supported:
- counter
- pwm
- spi
- its
ram: 192
flash: 576
4 changes: 4 additions & 0 deletions include/zephyr/psa/key_ids.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ typedef uint32_t psa_key_id_t;
#define ZEPHYR_PSA_WIFI_CREDENTIALS_KEY_ID_RANGE_BEGIN (psa_key_id_t)0x20010000
#define ZEPHYR_PSA_WIFI_CREDENTIALS_KEY_ID_RANGE_SIZE 0x100 /* 256 */

/** PSA key ID range to be used by TF-M ITS */
#define ZEPHYR_PSA_SETTINGS_TFM_ITS_KEY_ID_RANGE_BEGIN (psa_key_id_t)0x20010100
#define ZEPHYR_PSA_SETTINGS_TFM_ITS_KEY_ID_RANGE_SIZE 0x10000 /* 64 Ki */

/** PSA key ID range to be used by the end-user application. */
#define ZEPHYR_PSA_APPLICATION_KEY_ID_RANGE_BEGIN (psa_key_id_t)0x30000000
#define ZEPHYR_PSA_APPLICATION_KEY_ID_RANGE_SIZE 0x100000 /* 1 Mi */
Expand Down
38 changes: 38 additions & 0 deletions subsys/settings/Kconfig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright (c) 2018 Nordic Semiconductor ASA
# Copyright (c) 2025 Analog Devices, Inc.
# SPDX-License-Identifier: Apache-2.0

menuconfig SETTINGS
Expand Down Expand Up @@ -36,6 +37,7 @@ choice SETTINGS_BACKEND
default SETTINGS_NVS if NVS
default SETTINGS_FCB if FCB
default SETTINGS_FILE if FILE_SYSTEM
default SETTINGS_TFM_ITS if TFM_PARTITION_INTERNAL_TRUSTED_STORAGE
default SETTINGS_NONE
help
Storage back-end to be used by the settings subsystem.
Expand Down Expand Up @@ -109,6 +111,19 @@ config SETTINGS_CUSTOM
help
Use a custom settings storage back-end.

config SETTINGS_TFM_ITS
bool "Internal Trusted Storage (ITS) settings backend"
depends on TFM_PARTITION_INTERNAL_TRUSTED_STORAGE
help
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A note about the RAM consumption is missing: #87778 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enables Internal Trusted Storage (ITS) Settings backend, which an app will be able
to use to store sensitive information.
ITS backend can be used for storing sensitive information such as keys, passwords,
or other secrets.
Note: ITS is intended to store sensitive data on-chip, such as cryptographic keys,
credentials and configuration data and is not intended for storing large amounts of data.
Note: This settings backend compacts settings data into as few ITS nodes as possible.
On every save, the entire settings array is written to ITS.

config SETTINGS_NONE
bool "NONE"
help
Expand Down Expand Up @@ -219,4 +234,27 @@ config SETTINGS_SHELL
size of the shell thread may need to be increased to accommodate this
feature.

if SETTINGS_TFM_ITS

config SETTINGS_TFM_ITS_NUM_ENTRIES
int "Maximum number of settings entries"
default 10
help
Configures the maximum number of settings possible to be stored to PSA ITS.
Note: This value determines the size of a statically allocated buffer to hold
all settings entries in memory at all times.

config SETTINGS_TFM_ITS_LAZY_PERSIST_DELAY_MS
int "Milliseconds delay before persisting settings"
default 500
help
ITS may block for a long period of time when writing to flash, which may be
unacceptable for time-sensitive events.
Data is always persisted to ITS using k_work_delayable, instead of happening in
the same context as settings_its_save, this option sets the delay.
The delay is useful in cases where a PSA ITS write may block a time sensitive
event, Bluetooth pairing for example, that requires a sequence of settings writes.

endif # SETTINGS_TFM_ITS

endif # SETTINGS
1 change: 1 addition & 0 deletions subsys/settings/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ zephyr_sources_ifdef(CONFIG_SETTINGS_NVS settings_nvs.c)
zephyr_sources_ifdef(CONFIG_SETTINGS_NONE settings_none.c)
zephyr_sources_ifdef(CONFIG_SETTINGS_SHELL settings_shell.c)
zephyr_sources_ifdef(CONFIG_SETTINGS_ZMS settings_zms.c)
zephyr_sources_ifdef(CONFIG_SETTINGS_TFM_ITS settings_its.c)
284 changes: 284 additions & 0 deletions subsys/settings/src/settings_its.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
/*
* Copyright (c) 2019 Laczen
* Copyright (c) 2019 Nordic Semiconductor ASA
* Copyright (c) 2025 Analog Devices, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/

#include <psa/internal_trusted_storage.h>
#include <zephyr/settings/settings.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/psa/key_ids.h>

LOG_MODULE_DECLARE(settings, CONFIG_SETTINGS_LOG_LEVEL);

K_MUTEX_DEFINE(worker_mutex);
static struct k_work_delayable worker;

struct setting_entry {
char name[SETTINGS_MAX_NAME_LEN];
char value[SETTINGS_MAX_VAL_LEN];
size_t val_len;
};

static struct setting_entry entries[CONFIG_SETTINGS_TFM_ITS_NUM_ENTRIES];
static int entries_count;

static int settings_its_load(struct settings_store *cs, const struct settings_load_arg *arg);
static int settings_its_save(struct settings_store *cs, const char *name, const char *value,
size_t val_len);

static const struct settings_store_itf settings_its_itf = {
.csi_load = settings_its_load,
.csi_save = settings_its_save,
};

static struct settings_store default_settings_its = {.cs_itf = &settings_its_itf};

static int increment_uid(psa_storage_uid_t *uid)
{
if (*uid < ZEPHYR_PSA_SETTINGS_TFM_ITS_KEY_ID_RANGE_BEGIN ||
*uid >= (ZEPHYR_PSA_SETTINGS_TFM_ITS_KEY_ID_RANGE_BEGIN +
ZEPHYR_PSA_SETTINGS_TFM_ITS_KEY_ID_RANGE_SIZE) - 1) {
LOG_ERR("UID out of range!");
return -EINVAL;
}

(*uid)++;
return 0;
}

static int store_entries(void)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, IIUC you're writing all the entries to the ITS again every time a single value is updated. The flash won't like this, especially if there is any settings user which updates values frequently (which in my understanding there is).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your point however I'll add some justification:

As a reminder about implementation, I am compacting all settings into a single struct and trying to cram as many settings as possible into as few ITS nodes as possible. My priority is to optimize the number of ITS nodes I consume, as these are costly to allocate.

  • Each UID is 1024 bytes in size (by default), and each settings entry is much less than that. In a typical Bluetooth use-case, where the device is paired to one central, it won't even exceed 1 UID.

    • Since the smallest 'sector' I could possibly write at a time is 1 ITS node, in most use cases no matter what I do, I will be rewriting all ITS entries when a single value is updated (since all the entries fit into a single node, and I always must write at least 1 node).
  • In the case of there being many settings that do span multiple ITS nodes, it is possible that the bytes of a setting span multiple ITS nodes, which would require identifying this case and then rewriting only the two nodes effected

I think since this is already not a very likely case given a typical Bluetooth device, that I do not want to support this in this PR. If it becomes a performance issue in the future, could it instead be addressed as a future improvement?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the 1024-byte UID always the case? Doesn't an entry size depend on the TF-M configuration? (not sure)

Sure, this could be improved later. Maybe add a warning or a note regarding that e.g. in the Kconfig option help?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the TF-M configuration controls this. So if a user wants to adjust it to something bigger or smaller, that is part of their design.

I'll add a note about this behavior to the Kconfig help section.

{
int err;
psa_status_t status;
psa_storage_uid_t uid = ZEPHYR_PSA_SETTINGS_TFM_ITS_KEY_ID_RANGE_BEGIN;
size_t remaining = sizeof(entries);
size_t chunk_size = CONFIG_TFM_ITS_MAX_ASSET_SIZE;
const uint8_t *metadata_bytes = (const uint8_t *)&entries;

/*
* Each ITS UID is treated like a sector. Data is written to each ITS node until
* that node is full, before incrementing the UID. This is done to minimize the
* number of allocated ITS nodes and to avoid wasting allocated bytes.
*/
while (remaining > 0) {
size_t write_size = (remaining > chunk_size) ? chunk_size : remaining;

status = psa_its_set(uid, write_size, metadata_bytes, PSA_STORAGE_FLAG_NONE);
if (status) {
LOG_ERR("Error storing %d bytes of metadata! Bytes Remaining: %d, status: "
"%d",
write_size, remaining, status);
return status;
}

metadata_bytes += write_size;
remaining -= write_size;
err = increment_uid(&uid);
if (err) {
return err;
}
}

LOG_DBG("ITS entries stored successfully - bytes_saved: %d num_entries: %d max_uid: %lld",
sizeof(entries), entries_count, uid);

return 0;
}

static int load_entries(void)
{
int err;
psa_status_t status;
size_t bytes_read;
psa_storage_uid_t uid = ZEPHYR_PSA_SETTINGS_TFM_ITS_KEY_ID_RANGE_BEGIN;
size_t remaining = sizeof(entries);
size_t chunk_size = CONFIG_TFM_ITS_MAX_ASSET_SIZE;
uint8_t *metadata_bytes = (uint8_t *)&entries;

/*
* Each ITS UID is treated like a sector. Data is written to each ITS node until
* that node is full, before incrementing the UID. This is done to minimize the
* number of allocated ITS nodes and to avoid wasting allocated bytes.
*/
while (remaining > 0) {
size_t to_read = (remaining > chunk_size) ? chunk_size : remaining;

status = psa_its_get(uid, 0, to_read, metadata_bytes, &bytes_read);
if (status) {
return status;
}

metadata_bytes += bytes_read;
remaining -= bytes_read;
err = increment_uid(&uid);
if (err) {
return err;
}
}

for (int i = 0; i < CONFIG_SETTINGS_TFM_ITS_NUM_ENTRIES; i++) {
if (strnlen(entries[i].name, SETTINGS_MAX_NAME_LEN) != 0) {
entries_count++;
}
}

LOG_DBG("ITS entries restored successfully - bytes_loaded: %d, num_entries: %d",
sizeof(entries), entries_count);

return 0;
}

/* void *back_end is the index of the entry in metadata entries struct */
static ssize_t settings_its_read_fn(void *back_end, void *data, size_t len)
{
LOG_DBG("ITS Read - index: %d", *(int *)back_end);

if (*(int *)back_end < 0 || *(int *)back_end >= CONFIG_SETTINGS_TFM_ITS_NUM_ENTRIES) {
LOG_ERR("Invalid index %d in ITS metadata", *(int *)back_end);
return 0;
}

memcpy(data, entries[*(int *)back_end].value, len);

/*
* Callback expects return value of the number of bytes read
*/
return entries[*(int *)back_end].val_len;
}

static int settings_its_load(struct settings_store *cs, const struct settings_load_arg *arg)
{
int ret;

for (int i = 0; i < entries_count; i++) {

if (strnlen(entries[i].name, SETTINGS_MAX_NAME_LEN) != 0) {

/*
* Pass the key to the settings handler with it's index as an argument,
* to be read during callback function later.
*/
ret = settings_call_set_handler(entries[i].name, entries[i].val_len,
settings_its_read_fn, (void *)&i,
(void *)arg);
if (ret) {
return ret;
}
}
}

return 0;
}

static int settings_its_save(struct settings_store *cs, const char *name, const char *value,
size_t val_len)
{
if (entries_count >= CONFIG_SETTINGS_TFM_ITS_NUM_ENTRIES) {
LOG_ERR("%s: Max settings reached: %d", __func__,
CONFIG_SETTINGS_TFM_ITS_NUM_ENTRIES);
return -ENOMEM;
}

if (val_len > SETTINGS_MAX_VAL_LEN) {
LOG_ERR("%s: Invalid settings size - val_len: %d", __func__, val_len);
return -EINVAL;
}

int index;
bool delete;

/* Find out if we are doing a delete */
delete = ((value == NULL) || (val_len == 0));

/* Lock mutex before manipulating settings array */
k_mutex_lock(&worker_mutex, K_FOREVER);

/*
* Search metadata to see if entry already exists. Array is compacted, so first blank entry
* signals end of settings.
*/
for (index = 0; index < CONFIG_SETTINGS_TFM_ITS_NUM_ENTRIES; index++) {
if (strncmp(entries[index].name, name, SETTINGS_MAX_NAME_LEN) == 0) {
break;
} else if (entries[index].val_len == 0) {

/* Setting already deleted */
if (delete) {
LOG_DBG("%s: %s Already deleted!", __func__, name);
k_mutex_unlock(&worker_mutex);
return 0;
}

/* New setting being entered */
entries_count++;
break;
}
}

LOG_DBG("ITS Save - index %d: name %s, val_len %d", index, name, val_len);

if (delete) {
/* Clear metadata */
memset(entries[index].name, 0, SETTINGS_MAX_NAME_LEN);
memset(entries[index].value, 0, SETTINGS_MAX_VAL_LEN);
entries[index].val_len = 0;

/* If setting not at end of array, shift entries */
if (index < entries_count - 1) {
memcpy(&entries[index], &entries[index + 1],
(entries_count - index - 1) * sizeof(struct setting_entry));
/* Remove duplicate entry at end of array */
memset(&entries[entries_count - 1], 0, sizeof(struct setting_entry));
}

entries_count--;
} else {
/* Update metadata */
strncpy(entries[index].name, name, SETTINGS_MAX_NAME_LEN);
memcpy(entries[index].value, value, val_len);
entries[index].val_len = val_len;
}

k_mutex_unlock(&worker_mutex);
k_work_schedule(&worker, K_MSEC(CONFIG_SETTINGS_TFM_ITS_LAZY_PERSIST_DELAY_MS));

return 0;
}

void worker_persist_entries_struct_fn(struct k_work *work)
{
k_mutex_lock(&worker_mutex, K_FOREVER);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this usage of the mutex is enough. It does not prevent entries[] to be modified (calling settings_its_save()) while store_entries() is executed and that can cause corruption problems.
You can keep mutex lock/unlock in this function, but you also need to use the same mutex in settings_its_save().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you

store_entries();
k_mutex_unlock(&worker_mutex);
}

int settings_backend_init(void)
{
psa_status_t status;

/* Load ITS metadata */
status = load_entries();

/* If resource DNE, we need to allocate it */
if (status == PSA_ERROR_DOES_NOT_EXIST) {
status = store_entries();
if (status) {
LOG_ERR("Error storing metadata in %s: (status %d)", __func__, status);
return -EIO;
}
} else if (status) {
LOG_ERR("Error loading metadata in %s: (status %d)", __func__, status);
return -EIO;
}

settings_dst_register(&default_settings_its);
settings_src_register(&default_settings_its);

k_work_init_delayable(&worker, worker_persist_entries_struct_fn);

return 0;
}
9 changes: 9 additions & 0 deletions tests/subsys/settings/its/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright (c) 2025 Analog Devices, Inc.
#
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(test_settings_its_raw)

add_subdirectory(./src its_test_bindir)
12 changes: 12 additions & 0 deletions tests/subsys/settings/its/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright (c) 2025 Analog Devices, Inc.
#
# SPDX-License-Identifier: Apache-2.0

CONFIG_ZTEST=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_TFM_PARTITION_INTERNAL_TRUSTED_STORAGE=y
CONFIG_TFM_ITS_MAX_ASSET_SIZE_OVERRIDE=y
CONFIG_TFM_ITS_MAX_ASSET_SIZE=1024
CONFIG_SETTINGS=y
CONFIG_SETTINGS_RUNTIME=y
Loading
Loading