ci(esp_tee): Add test-case for verifying the TEE Secure Storage encryption

This commit is contained in:
Laukik Hase
2026-06-12 10:04:07 +05:30
parent 93466c1fd3
commit 61b88163be
5 changed files with 212 additions and 30 deletions
@@ -77,3 +77,7 @@ secure_services:
type: custom
function: esp_tee_test_stack_underflow
args: 0
- id: 219
type: custom
function: esp_tee_test_read_sec_stg
args: 1
@@ -1,13 +1,21 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <stddef.h>
#include "esp_cpu.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_tee.h"
#include "esp_tee_flash.h"
#include "esp_tee_memory_utils.h"
#include "esp_tee_test.h"
#include "esp_attr.h"
#include "esp_flash_partitions.h"
static const char *TAG = "test_sec_srv";
/* Sample Trusted App */
@@ -54,3 +62,23 @@ uint32_t _ss_esp_tee_test_priv_mode_switch(uint32_t *a, uint32_t *b)
return c;
}
esp_err_t _ss_esp_tee_test_read_sec_stg(uint8_t *buf)
{
if (!esp_tee_buf_in_ree(buf, FLASH_SECTOR_SIZE)) {
return ESP_ERR_INVALID_ARG;
}
esp_partition_info_t pinfo;
esp_err_t err = esp_tee_flash_find_partition(PART_TYPE_DATA, PART_SUBTYPE_DATA_WIFI,
ESP_TEE_SEC_STG_PART_LABEL, &pinfo);
if (err != ESP_OK) {
return err;
}
if (pinfo.pos.size < FLASH_SECTOR_SIZE) {
return ESP_ERR_INVALID_SIZE;
}
return (esp_err_t)esp_tee_flash_read(pinfo.pos.offset, (uint32_t *)buf, FLASH_SECTOR_SIZE, false);
}
@@ -4,6 +4,7 @@
import base64
import csv
import os
import re
import shutil
import subprocess
import sys
@@ -290,6 +291,118 @@ class TEESerial(IdfSerial):
self.flash()
self.custom_erase_partition('secure_storage')
KEY_DEFS_ENCRYPTION_TEST: list[str] = [
'aes256_key0',
'aes256_key1',
'attest_key',
'ecdsa_p256_key0',
]
# TEE Secure Storage Development mode
# NVS XTS-AES keys: E-key=0x33*32 || T-key=0xCC*32
NVS_KEYS_DEV_B64 = 'MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzPMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzA=='
@property
def nvs_partition_gen(self) -> str:
return str(
Path(os.environ['IDF_PATH'])
/ 'components'
/ 'nvs_flash'
/ 'nvs_partition_generator'
/ 'nvs_partition_gen.py'
)
def derive_sec_stg_nvs_keys(self, out_path: Path) -> None:
out_path.parent.mkdir(parents=True, exist_ok=True)
if self.app.sdkconfig.get('SECURE_TEE_SEC_STG_MODE_RELEASE'):
hmac_key_src = self.TEST_KEYS_DIR / 'tee_sec_stg_hmac_key.bin'
self.run_command(
[
sys.executable,
self.nvs_partition_gen,
'generate-key',
'--key_protect_hmac',
'--kp_hmac_inputkey',
str(hmac_key_src),
'--keyfile',
out_path.name,
'--outdir',
str(out_path.parent),
]
)
(out_path.parent / 'keys' / out_path.name).replace(out_path)
else:
self.write_keys_to_file(self.NVS_KEYS_DEV_B64, out_path)
def decrypt_sec_stg_partition(self, dump_path: Path, keys_path: Path, decrypted_path: Path) -> None:
self.run_command(
[sys.executable, self.nvs_partition_gen, 'decrypt', str(dump_path), str(keys_path), str(decrypted_path)]
)
_SEC_STG_HEX_LINE_RE = re.compile(
rb'test_esp_tee_sec_storage:\s+((?:[0-9a-f]{2} ){0,15}[0-9a-f]{2})',
)
SEC_STG_DUMP_SZ = 4096
def capture_sec_stg_partition_dump(self, dut: Any, timeout: float = 60) -> bytes:
dut.expect_exact('SEC_STG_DUMP_BEGIN', timeout=timeout)
blob = dut.expect_exact('SEC_STG_DUMP_END', timeout=timeout, return_what_before_match=True)
raw = bytearray()
for match in self._SEC_STG_HEX_LINE_RE.finditer(blob):
for tok in match.group(1).split():
raw.append(int(tok, 16))
if len(raw) != self.SEC_STG_DUMP_SZ:
raise RuntimeError(
f'Hex dump parse mismatch: got {len(raw)} bytes, expected {self.SEC_STG_DUMP_SZ}.\n'
f'Blob (first 256 bytes): {blob[:256]!r}'
)
# Make sure the Unity case actually passed before we trust the dump.
m = dut.expect(re.compile(rb'(\d+) Tests (\d+) Failures (\d+) Ignored'), timeout=timeout)
if int(m.group(2)) != 0:
raise RuntimeError(f'Unity reported {m.group(2).decode()} failures while running encryption test')
return bytes(raw)
def _run_nvs_tool_minimal(self, partition_file: Path) -> subprocess.CompletedProcess:
nvs_tool = Path(os.environ['IDF_PATH']) / 'components' / 'nvs_flash' / 'nvs_partition_tool' / 'nvs_tool.py'
return subprocess.run(
[sys.executable, str(nvs_tool), '-d', 'minimal', '--color', 'never', str(partition_file)],
capture_output=True,
text=True,
)
def verify_tee_sec_stg_encryption(self, dut: Any) -> None:
tmp_dir = self.TMP_DIR / 'sec_stg_encryption'
tmp_dir.mkdir(parents=True, exist_ok=True)
raw_path = tmp_dir / 'tee_sec_stg_dump.bin'
keys_path = tmp_dir / self.NVS_KEYS_FILE
decrypted_path = tmp_dir / 'tee_sec_stg_decr.bin'
expected_key_ids = self.KEY_DEFS_ENCRYPTION_TEST
print('Verifying TEE Secure Storage NVS partition encryption (XTS-AES-512: 256-bit AES, 512-bit total key)')
try:
raw_bytes = self.capture_sec_stg_partition_dump(dut)
raw_path.write_bytes(raw_bytes)
self.derive_sec_stg_nvs_keys(keys_path)
self.decrypt_sec_stg_partition(raw_path, keys_path, decrypted_path)
print('Confirming key IDs are NOT present in the raw (encrypted) NVS dump')
raw_parse = self._run_nvs_tool_minimal(raw_path)
for key_id in expected_key_ids:
assert key_id not in raw_parse.stdout, f'{key_id!r} surfaced in raw dump (not encrypted)'
print('Confirming key IDs ARE present after XTS-AES decrypt with the derived NVS keys')
decrypted_parse = self._run_nvs_tool_minimal(decrypted_path)
assert decrypted_parse.returncode == 0, f'nvs_tool exit {decrypted_parse.returncode} on decrypted dump'
for key_id in expected_key_ids:
assert key_id in decrypted_parse.stdout, f'{key_id!r} missing after decrypt (wrong XTS-AES key?)'
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
KEY_DEFS: list[dict[str, Any]] = [
{'key': 'aes256_key0', 'type': 'aes256', 'input': None, 'write_once': True},
{
@@ -364,37 +477,17 @@ class TEESerial(IdfSerial):
self.write_keys_to_file(entry['b64'], input_path)
entry['input'] = str(input_path)
idf_path = Path(os.environ['IDF_PATH'])
ESP_TEE_SEC_STG_KEYGEN = os.path.join(
idf_path, 'components', 'esp_tee', 'scripts', 'esp_tee_sec_stg_keygen', 'esp_tee_sec_stg_keygen.py'
)
NVS_PARTITION_GEN = os.path.join(
idf_path, 'components', 'nvs_flash', 'nvs_partition_generator', 'nvs_partition_gen.py'
os.environ['IDF_PATH'],
'components',
'esp_tee',
'scripts',
'esp_tee_sec_stg_keygen',
'esp_tee_sec_stg_keygen.py',
)
nvs_keys = tmp_dir / self.NVS_KEYS_FILE
if self.app.sdkconfig.get('SECURE_TEE_SEC_STG_MODE_RELEASE'):
hmac_key_src = self.TEST_KEYS_DIR / 'tee_sec_stg_hmac_key.bin'
self.run_command(
[
sys.executable,
NVS_PARTITION_GEN,
'generate-key',
'--key_protect_hmac',
'--kp_hmac_inputkey',
str(hmac_key_src),
'--keyfile',
self.NVS_KEYS_FILE,
'--outdir',
str(tmp_dir),
]
)
nvs_keys = tmp_dir / 'keys' / self.NVS_KEYS_FILE
else:
NVS_KEYS_DEV_B64 = (
'MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzPMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzA=='
)
self.write_keys_to_file(NVS_KEYS_DEV_B64, nvs_keys)
self.derive_sec_stg_nvs_keys(nvs_keys)
cmds = [
[sys.executable, ESP_TEE_SEC_STG_KEYGEN, '-k', entry['type'], '-o', str(tmp_dir / f'{entry["key"]}.bin')]
@@ -410,7 +503,7 @@ class TEESerial(IdfSerial):
cmds.append(
[
sys.executable,
NVS_PARTITION_GEN,
self.nvs_partition_gen,
'encrypt',
str(csv_path),
str(nvs_bin),
@@ -426,7 +519,7 @@ class TEESerial(IdfSerial):
self.flash()
self.custom_erase_partition('secure_storage')
self.custom_write_partition('secure_storage', nvs_bin)
self.custom_write_partition('secure_storage', str(nvs_bin))
finally:
shutil.rmtree(tmp_dir)
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
@@ -364,6 +365,45 @@ TEST_CASE("Test TEE Secure Storage - Null Pointer and Zero Length", "[sec_storag
TEST_ESP_OK(esp_tee_sec_storage_clear_key(key_cfg.id));
}
TEST_CASE("Test TEE Secure Storage - Verify data encryption", "[sec_storage_encr]")
{
ESP_LOGI(TAG, "Populating NVS-based TEE Secure Storage; encrypted with XTS-AES-512");
static const struct {
const char *id;
esp_tee_sec_storage_type_t type;
uint32_t flags;
} key_cfgs[] = {
{ "aes256_key0", ESP_SEC_STG_KEY_AES256, SEC_STORAGE_FLAG_WRITE_ONCE },
{ "aes256_key1", ESP_SEC_STG_KEY_AES256, SEC_STORAGE_FLAG_NONE },
{ "attest_key", ESP_SEC_STG_KEY_ECDSA_SECP256R1, SEC_STORAGE_FLAG_WRITE_ONCE },
{ "ecdsa_p256_key0", ESP_SEC_STG_KEY_ECDSA_SECP256R1, SEC_STORAGE_FLAG_NONE },
};
for (size_t i = 0; i < sizeof(key_cfgs) / sizeof(key_cfgs[0]); i++) {
esp_tee_sec_storage_key_cfg_t cfg = {
.id = key_cfgs[i].id,
.type = key_cfgs[i].type,
.flags = key_cfgs[i].flags,
};
if ((cfg.flags & SEC_STORAGE_FLAG_WRITE_ONCE) == 0) {
esp_err_t err = esp_tee_sec_storage_clear_key(cfg.id);
TEST_ASSERT_TRUE(err == ESP_OK || err == ESP_ERR_NOT_FOUND);
}
TEST_ESP_OK(esp_tee_sec_storage_gen_key(&cfg));
}
const size_t dump_sz = 4096;
uint8_t *buf = heap_caps_malloc(dump_sz, MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
TEST_ASSERT_NOT_NULL(buf);
TEST_ESP_OK((esp_err_t)esp_tee_service_call(2, SS_ESP_TEE_TEST_READ_SEC_STG, buf));
printf("\nSEC_STG_DUMP_BEGIN\n");
ESP_LOG_BUFFER_HEX(TAG, buf, dump_sz);
printf("SEC_STG_DUMP_END\n");
free(buf);
}
TEST_CASE("Test TEE Secure Storage - WRITE_ONCE keys", "[sec_storage]")
{
const char *key_id = "key_id_test_wo";
@@ -483,6 +483,23 @@ def test_esp_tee_secure_storage_with_host_img(dut: IdfDut) -> None:
dut.run_all_single_board_cases(group='sec_storage_host_keygen')
@idf_parametrize(
'config, target, skip_autoflash, markers',
CONFIG_OTA_NO_AUTOFLASH,
indirect=['config', 'target', 'skip_autoflash'],
)
def test_esp_tee_secure_storage_encryption(dut: IdfDut) -> None:
dut.serial.custom_flash_with_empty_sec_stg()
# NOTE: In release mode (CONFIG_SECURE_TEE_SEC_STG_MODE_RELEASE), the test
# expects the eFuse-burned HMAC key used for TEE secure storage to be available
# at the path "test_keys/tee_sec_stg_hmac_key.bin"
dut.expect_exact('Press ENTER to see the list of tests')
dut.write('"Test TEE Secure Storage - Verify data encryption"')
dut.serial.verify_tee_sec_stg_encryption(dut)
# ---------------- TEE Attestation tests ----------------