From 1e140476c8ba9fc17fd0c58d43ae6d740f140be0 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 18 Sep 2026 13:36:01 -0700 Subject: [PATCH 1/2] Add a generic secure application handoff with a measured boot record --- docs/secure_app_handoff.md | 97 ++++++++++++++++++++++++++++++ include/wolfboot/secure_handoff.h | 99 +++++++++++++++++++++++++++++++ include/wolfboot/wolfboot.h | 8 +++ options.mk | 23 +++++++ src/boot_arm.c | 15 ++++- src/update_flash.c | 29 +++++++++ tools/config.mk | 4 +- 7 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 docs/secure_app_handoff.md create mode 100644 include/wolfboot/secure_handoff.h diff --git a/docs/secure_app_handoff.md b/docs/secure_app_handoff.md new file mode 100644 index 0000000000..e51a65fb2b --- /dev/null +++ b/docs/secure_app_handoff.md @@ -0,0 +1,97 @@ +# Secure Application Handoff + +`WOLFBOOT_SECURE_APP` tells wolfBoot that the image it boots is a signed +**Secure-state** runtime (for example a TrustZone-M Secure application such as +wolfTrust) rather than a Non-secure application. wolfBoot authenticates and +measures the image, publishes a measured-boot record at a fixed secure-RAM +address, and then branches to the image's reset vector **without leaving Secure +state**. + +This is the wolfBoot analog of MCUboot's shared-data area consumed by TF-M: the +first stage measures what it launched and hands the measurement to the runtime. + +## Default TrustZone boot vs. the secure-app handoff + +| | Default TZ boot | `WOLFBOOT_SECURE_APP` | +| --- | --- | --- | +| Target state | Non-secure | stays Secure | +| Stack / entry | `msp_ns`, `bxns` to the NS vector | `msp`, branch to the Secure reset vector | +| Record | none | measured-boot record written before the jump | + +The Secure jump lives in `src/boot_arm.c` under `WOLFBOOT_SECURE_APP`: it turns +the wolfBoot MPU off, sets `MSP`, re-enables interrupts, and branches to the +image entry, leaving the runtime to install its own memory map from its +`Reset_Handler`. + +## The measured-boot record + +`include/wolfboot/secure_handoff.h` defines the record and a single builder, +`wolfBoot_secure_handoff_build()`. wolfBoot writes it from +`wolfBoot_prepare_secure_handoff()` in `src/update_flash.c` immediately before +`do_boot()`, and panics if the record cannot be built. + +| Field | Type | Meaning | +| --- | --- | --- | +| `magic` | `uint32_t` | `0x5742484F`, written **last** as the valid flag | +| `magic_inverse` | `uint32_t` | `~magic`, torn-write guard | +| `version` | `uint16_t` | record layout version (`1`) | +| `size` | `uint16_t` | `sizeof(record)` (56) | +| `lifecycle` | `uint32_t` | PSA lifecycle from `hal_attestation_get_lifecycle()` | +| `image_version` | `uint32_t` | version of the booted image | +| `hash_algorithm` | `uint16_t` | `1` = SHA-256 | +| `measurement_size` | `uint16_t` | digest length (32) | +| `measurement[32]` | `uint8_t` | SHA-256 of the booted image | + +The record lives at `WOLFBOOT_SECURE_HANDOFF_ADDRESS`, a secure-RAM address the +port reserves and the config supplies. The builder writes `magic_inverse` and +then `magic` last, fenced with `dmb`/`dsb`, so a consumer that observes `magic` +sees a complete record. + +A consumer validates `magic` and `magic_inverse`, checks `version`/`size`, uses +the fields, and then clears the record. + +## Enabling it + +``` +WOLFBOOT_SECURE_APP=1 +WOLFBOOT_SECURE_HANDOFF_ADDRESS=0x30020000 # secure-RAM address of the record +``` + +The handoff requires authenticated boot: `SIGN=NONE`/`WOLFBOOT_NO_SIGN` and +`WOLFBOOT_SKIP_BOOT_VERIFY` are rejected at configure time, and the record +currently requires `HASH=SHA256`. Setting `WOLFBOOT_SECURE_APP` without +`WOLFBOOT_SECURE_HANDOFF_ADDRESS` is a compile-time error. + +## Porting the handoff to a new target + +The handoff mechanism (record, measurement, Secure jump, build knob) is +target-independent. A new port becomes a clean secure-runtime loader by +supplying three things: + +1. **Config only** — `WOLFBOOT_SECURE_APP=1`, `WOLFBOOT_SECURE_HANDOFF_ADDRESS`, + and the usual `TZEN`/partition layout. No code. +2. **`hal_attestation_get_lifecycle()`** *(optional)* — returns the chip's PSA + lifecycle. A weak default in `hal/hal.c` reports "unknown", so a port links + and hands off without it; implement it for a real lifecycle value. +3. **TrustZone setup that keeps the secure app Secure across the jump** — the + port's own SAU / security-controller code must keep the secure app's flash + and RAM Secure and must not unsecure peripherals when `WOLFBOOT_SECURE_APP` + is set. Every SoC's security fabric differs (GTZC, AHBSC, TRDC, …), so this + is the one piece that is inherently per-port; it is the TrustZone init a port + already writes, guarded on `WOLFBOOT_SECURE_APP`. + +## STM32H5 reference + +`config/examples/stm32h5-tz-wolftrust.config` is the reference consumer. On the +STM32H5 the record lives at `0x30020000`, `hal_attestation_get_lifecycle()` +derives the PSA lifecycle from the flash product state and the debug +authentication status (`hal/stm32h5_lifecycle.h`), and `hal/stm32_tz.c` keeps +the whole SRAM1 window and the secure app flash Secure across the handoff. + +## Tests + +- `tools/unit-tests/unit-secure-handoff.c` — record layout, the builder, and the + STM32H5 lifecycle mapping. +- `tools/unit-tests/test-secure-handoff-config.sh` — the configure-time guards + (rejects `SIGN=NONE` and `WOLFBOOT_SKIP_BOOT_VERIFY`) and the STM32H5 stack + floors. diff --git a/include/wolfboot/secure_handoff.h b/include/wolfboot/secure_handoff.h new file mode 100644 index 0000000000..bb5990cd58 --- /dev/null +++ b/include/wolfboot/secure_handoff.h @@ -0,0 +1,99 @@ +/* secure_handoff.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#ifndef WOLFBOOT_SECURE_HANDOFF_H +#define WOLFBOOT_SECURE_HANDOFF_H + +#include +#include + +/* The record fixes the measurement to SHA-256, so consumers include this + * header freely. Only the bootloader build that copies boot->sha_hash must + * have wolfBoot configured for SHA-256. */ +#if defined(WOLFBOOT_SECURE_APP) && !defined(WOLFBOOT_HASH_SHA256) + #error "WOLFBOOT_SECURE_APP handoff currently requires SHA-256" +#endif + +#if defined(WOLFBOOT_SECURE_APP) && !defined(WOLFBOOT_SECURE_HANDOFF_ADDRESS) + #error "WOLFBOOT_SECURE_APP requires WOLFBOOT_SECURE_HANDOFF_ADDRESS (the secure-RAM address the port reserves for the boot handoff record)" +#endif + +#define WOLFBOOT_SECURE_HANDOFF_MAGIC 0x5742484Fu +#define WOLFBOOT_SECURE_HANDOFF_VERSION 1u +#define WOLFBOOT_SECURE_HANDOFF_HASH_SHA256 1u +#define WOLFBOOT_SECURE_HANDOFF_DIGEST_SIZE 32u + +#define WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_UNKNOWN 0x0000u +#define WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_ASSEMBLY_AND_TEST 0x1000u +#define WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_PSA_ROT_PROVISIONING 0x2000u +#define WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_SECURED 0x3000u +#define WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_NON_PSA_ROT_DEBUG 0x4000u +#define WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_RECOVERABLE_DEBUG 0x5000u + +typedef struct wolfBoot_secure_handoff { + uint32_t magic; + uint32_t magic_inverse; + uint16_t version; + uint16_t size; + uint32_t lifecycle; + uint32_t image_version; + uint16_t hash_algorithm; + uint16_t measurement_size; + uint8_t measurement[WOLFBOOT_SECURE_HANDOFF_DIGEST_SIZE]; +} wolfBoot_secure_handoff_t; + +static inline int wolfBoot_secure_handoff_build( + volatile wolfBoot_secure_handoff_t* handoff, + const uint8_t* measurement, uint32_t imageVersion, uint32_t lifecycle) +{ + uint32_t i; + + if ((handoff == NULL) || (measurement == NULL)) { + return -1; + } + + handoff->magic = 0u; + handoff->magic_inverse = UINT32_MAX; + handoff->version = WOLFBOOT_SECURE_HANDOFF_VERSION; + handoff->size = (uint16_t)sizeof(*handoff); + handoff->lifecycle = lifecycle; + handoff->image_version = imageVersion; + handoff->hash_algorithm = WOLFBOOT_SECURE_HANDOFF_HASH_SHA256; + handoff->measurement_size = WOLFBOOT_SECURE_HANDOFF_DIGEST_SIZE; + for (i = 0u; i < WOLFBOOT_SECURE_HANDOFF_DIGEST_SIZE; ++i) { + handoff->measurement[i] = measurement[i]; + } +#if defined(__arm__) || defined(__thumb__) + __asm volatile("dmb" ::: "memory"); +#else + __asm volatile("" ::: "memory"); +#endif + handoff->magic_inverse = ~WOLFBOOT_SECURE_HANDOFF_MAGIC; + handoff->magic = WOLFBOOT_SECURE_HANDOFF_MAGIC; +#if defined(__arm__) || defined(__thumb__) + __asm volatile("dsb" ::: "memory"); +#else + __asm volatile("" ::: "memory"); +#endif + + return 0; +} + +#endif /* WOLFBOOT_SECURE_HANDOFF_H */ diff --git a/include/wolfboot/wolfboot.h b/include/wolfboot/wolfboot.h index d86812d67f..20e22bd90d 100644 --- a/include/wolfboot/wolfboot.h +++ b/include/wolfboot/wolfboot.h @@ -265,6 +265,14 @@ extern "C" { #error "WOLFBOOT_SKIP_BOOT_VERIFY requires WOLFBOOT_SELF_UPDATE_MONOLITHIC" #endif +#if defined(WOLFBOOT_SKIP_BOOT_VERIFY) && defined(WOLFBOOT_SECURE_APP) +#error "WOLFBOOT_SECURE_APP handoff requires wolfBoot image verification" +#endif + +#if defined(WOLFBOOT_NO_SIGN) && defined(WOLFBOOT_SECURE_APP) +#error "WOLFBOOT_SECURE_APP handoff requires signed images" +#endif + #ifdef BIG_ENDIAN_ORDER # define WOLFBOOT_MAGIC 0x574F4C46 /* WOLF */ # define WOLFBOOT_MAGIC_TRAIL 0x424F4F54 /* BOOT */ diff --git a/options.mk b/options.mk index ce6915d098..1a77edeb3d 100644 --- a/options.mk +++ b/options.mk @@ -207,6 +207,9 @@ endif ## DSA Settings ifeq ($(SIGN),NONE) + ifeq ($(WOLFBOOT_SECURE_APP),1) + $(error SIGN=NONE is incompatible with the authenticated secure-app handoff) + endif $(warning SIGN=NONE / WOLFBOOT_NO_SIGN=1 disables firmware signature verification; images are NOT authenticated. Do not use in production.) SIGN_OPTIONS+=--no-sign ifeq ($(HASH),SHA384) @@ -976,6 +979,9 @@ ifeq ($(WOLFBOOT_REQUIRE_SIGNED_DTB),1) endif ifeq ($(WOLFBOOT_SKIP_BOOT_VERIFY),1) + ifeq ($(WOLFBOOT_SECURE_APP),1) + $(error WOLFBOOT_SKIP_BOOT_VERIFY=1 is incompatible with the authenticated secure-app handoff) + endif ifneq ($(WOLFBOOT_SELF_HEADER),1) $(error WOLFBOOT_SKIP_BOOT_VERIFY=1 requires WOLFBOOT_SELF_HEADER=1) endif @@ -1528,6 +1534,16 @@ endif CFLAGS+=$(CFLAGS_EXTRA) OBJS+=$(OBJS_EXTRA) +# The authenticated STM32H5 ECC256 secure-app path retains certificate parsing. +# Do not lower the larger stack thresholds selected by other signature schemes. +ifeq ($(WOLFBOOT_SECURE_APP),1) + ifeq ($(TARGET),stm32h5) + ifeq ($(SIGN),ECC256) + STACK_USAGE=16688 + endif + endif +endif + ifeq ($(USE_GCC_HEADLESS),1) ifeq ($(USE_GCC),1) ifneq ($(USE_CLANG),1) @@ -1831,6 +1847,13 @@ ifeq ($(TZEN),1) CFLAGS+=-DTZEN endif +ifeq ($(WOLFBOOT_SECURE_APP),1) + CFLAGS+=-DWOLFBOOT_SECURE_APP + ifneq ($(WOLFBOOT_SECURE_HANDOFF_ADDRESS),) + CFLAGS+=-D"WOLFBOOT_SECURE_HANDOFF_ADDRESS=$(WOLFBOOT_SECURE_HANDOFF_ADDRESS)" + endif +endif + # Auxiliary algorithms: compile extra wolfCrypt code beyond what SIGN/HASH # selects, for features that need it (cert-chain verification, TPM, ...). # Auxiliary algorithms are never used to verify image signatures. diff --git a/src/boot_arm.c b/src/boot_arm.c index f59c76c2d5..5403f36f12 100644 --- a/src/boot_arm.c +++ b/src/boot_arm.c @@ -491,7 +491,7 @@ void isr_crpt(void) __attribute__((weak, alias("isr_empty"))); */ #if defined(__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U) && \ - defined(TZEN) + defined(TZEN) && !defined(WOLFBOOT_SECURE_APP) #include "hal.h" #define VTOR (*(volatile uint32_t *)(0xE002ED08)) /* Non-secure VTOR */ #else @@ -501,7 +501,8 @@ void isr_crpt(void) __attribute__((weak, alias("isr_empty"))); static void *app_entry; static uint32_t app_end_stack; -#if defined(__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U) && defined(TZEN) +#if defined(__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U) && defined(TZEN) && \ + !defined(WOLFBOOT_SECURE_APP) static uint32_t ns_entry; #endif @@ -544,7 +545,15 @@ void RAMFUNCTION do_boot(const uint32_t *app_offset) * and VTOR_NS points there directly. */ VTOR = ((uint32_t)app_offset); asm volatile("msr msplim, %0" ::"r"(0)); -# if defined (__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U) && \ +# if defined(WOLFBOOT_SECURE_APP) + /* A secure application is a signed Secure runtime, not a Non-secure guest: + * stay in Secure state and branch through its own reset vector. wolfBoot's + * MPU stays off across the jump; the runtime installs its own map. */ + mpu_off(); + asm volatile("msr msp, %0" :: "r"(app_end_stack)); + asm volatile("cpsie i"); + asm volatile("mov pc, %0" :: "r"(app_entry)); +# elif defined (__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U) && \ defined(TZEN) asm volatile("msr msp_ns, %0" ::"r"(app_end_stack)); #if defined(TARGET_stm32n6) diff --git a/src/update_flash.c b/src/update_flash.c index 58a23f9e86..e6cdc3bd2a 100644 --- a/src/update_flash.c +++ b/src/update_flash.c @@ -30,6 +30,9 @@ #include "spi_flash.h" #include "target.h" #include "wolfboot/wolfboot.h" +#if defined(WOLFBOOT_SECURE_APP) +#include "wolfboot/secure_handoff.h" +#endif #include "delta.h" #include "printf.h" @@ -1535,6 +1538,27 @@ int wolfBoot_unlock_disk(void) #ifdef __CCRX__ #pragma section FRAM #endif +#if defined(WOLFBOOT_SECURE_APP) +static int wolfBoot_prepare_secure_handoff( + const struct wolfBoot_image* boot) +{ + volatile wolfBoot_secure_handoff_t* handoff = + (volatile wolfBoot_secure_handoff_t*) + WOLFBOOT_SECURE_HANDOFF_ADDRESS; + uint32_t lifecycle = WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_UNKNOWN; + + if ((boot == NULL) || (boot->sha_hash == NULL)) { + return -1; + } + + if (hal_attestation_get_lifecycle(&lifecycle) != 0) { + lifecycle = WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_UNKNOWN; + } + return wolfBoot_secure_handoff_build(handoff, boot->sha_hash, + wolfBoot_get_blob_version(boot->hdr), lifecycle); +} +#endif + void RAMFUNCTION wolfBoot_start(void) { int bootRet; @@ -1750,6 +1774,11 @@ void RAMFUNCTION wolfBoot_start(void) #ifndef WOLFBOOT_SKIP_BOOT_VERIFY PART_SANITY_CHECK(&boot); FW_BASE_SANITY_CHECK(&boot); +#endif +#if defined(WOLFBOOT_SECURE_APP) + if (wolfBoot_prepare_secure_handoff(&boot) != 0) { + wolfBoot_panic(); + } #endif do_boot((void *)boot.fw_base); } diff --git a/tools/config.mk b/tools/config.mk index ef81339f7d..da3a5fc110 100644 --- a/tools/config.mk +++ b/tools/config.mk @@ -66,6 +66,8 @@ ifeq ($(ARCH),) WOLFBOOT_UDS_UID_FALLBACK_FORTEST?=0 WOLFBOOT_UDS_OBKEYS?=0 TZEN?=0 + WOLFBOOT_SECURE_APP?=0 + WOLFBOOT_SECURE_HANDOFF_ADDRESS?= WOLFCRYPT_TZ?=0 WOLFCRYPT_TZ_PKCS11?=0 WOLFCRYPT_TZ_PSA?=0 @@ -107,7 +109,7 @@ CONFIG_VARS:= ARCH TARGET SIGN HASH MCUXSDK MCUXPRESSO MCUXPRESSO_CPU MCUXPRESSO CORTEX_M0 CORTEX_M7 CORTEX_M23 CORTEX_M33 CORTEX_M55 NO_ASM EXT_FLASH SPI_FLASH NO_XIP UART_FLASH ALLOW_DOWNGRADE NVM_FLASH_WRITEONCE \ DISABLE_BACKUP WOLFBOOT_VERSION V NO_MPU ENCRYPT FLAGS_HOME FLAGS_INVERT \ SPMATH SPMATHALL RAM_CODE DUALBANK_SWAP IMAGE_HEADER_SIZE PKA TZEN PSOC6_CRYPTO \ - WOLFTPM WOLFBOOT_TPM_VERIFY MEASURED_BOOT WOLFBOOT_TPM_SEAL WOLFBOOT_TPM_KEYSTORE \ + WOLFTPM WOLFBOOT_TPM_VERIFY MEASURED_BOOT WOLFBOOT_TPM_SEAL WOLFBOOT_TPM_KEYSTORE WOLFBOOT_SECURE_APP WOLFBOOT_SECURE_HANDOFF_ADDRESS \ WOLFBOOT_TPM_MFG_AUTH_DERIVE \ WOLFBOOT_ATTESTATION_IAK \ WOLFBOOT_ATTESTATION_TEST \ From 0ee0a5bc3bd64b03af224e240d28d5088848a575 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 18 Sep 2026 13:36:02 -0700 Subject: [PATCH 2/2] Enable the secure application handoff on STM32H5 --- Makefile | 6 +- config/examples/stm32h5-tz-wolftrust.config | 36 ++++ hal/stm32_tz.c | 36 +++- hal/stm32h5.c | 18 +- hal/stm32h5.h | 5 +- hal/stm32h5_lifecycle.h | 99 ++++++++++ tools/unit-tests/Makefile | 5 + .../unit-tests/test-secure-handoff-config.sh | 69 +++++++ tools/unit-tests/unit-secure-handoff.c | 171 ++++++++++++++++++ 9 files changed, 436 insertions(+), 9 deletions(-) create mode 100644 config/examples/stm32h5-tz-wolftrust.config create mode 100644 hal/stm32h5_lifecycle.h create mode 100755 tools/unit-tests/test-secure-handoff-config.sh create mode 100644 tools/unit-tests/unit-secure-handoff.c diff --git a/Makefile b/Makefile index 6deec40694..9c065451aa 100644 --- a/Makefile +++ b/Makefile @@ -358,7 +358,11 @@ endif ifeq ($(TARGET),stm32h5) # Don't build a contiguous image - MAIN_TARGET:=wolfboot.bin test-app/image_v1_signed.bin + ifeq ($(WOLFBOOT_SECURE_APP),1) + MAIN_TARGET:=wolfboot.bin + else + MAIN_TARGET:=wolfboot.bin test-app/image_v1_signed.bin + endif endif ifeq ($(TARGET),stm32n6) diff --git a/config/examples/stm32h5-tz-wolftrust.config b/config/examples/stm32h5-tz-wolftrust.config new file mode 100644 index 0000000000..1e81de5739 --- /dev/null +++ b/config/examples/stm32h5-tz-wolftrust.config @@ -0,0 +1,36 @@ +ARCH?=ARM +TZEN?=1 +TARGET?=stm32h5 +SIGN?=ECC256 +HASH?=SHA256 +DEBUG?=0 +VTOR?=1 +CORTEX_M0?=0 +CORTEX_M33?=1 +NO_ASM?=0 +NO_MPU=1 +EXT_FLASH?=0 +SPI_FLASH?=0 +ALLOW_DOWNGRADE?=0 +NVM_FLASH_WRITEONCE?=1 +WOLFBOOT_VERSION?=1 +SPMATH?=1 +RAM_CODE?=1 +DUALBANK_SWAP?=0 +WOLFBOOT_SECURE_APP?=1 +WOLFBOOT_SECURE_HANDOFF_ADDRESS?=0x30020000 +WOLFBOOT_PARTITION_SIZE?=0x20000 +WOLFBOOT_SECTOR_SIZE?=0x2000 +WOLFBOOT_KEYVAULT_ADDRESS?=0x0C05C000 +WOLFBOOT_KEYVAULT_SIZE?=0 +WOLFBOOT_NSC_ADDRESS?=0x0C05C000 +WOLFBOOT_NSC_SIZE?=0x4000 +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x0C060000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x0C100000 +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x0C120000 +FLAGS_HOME=0 +DISABLE_BACKUP=0 +WOLFCRYPT_TZ=1 +WOLFCRYPT_TZ_PKCS11=0 +IMAGE_HEADER_SIZE?=1024 +ARMORED=1 diff --git a/hal/stm32_tz.c b/hal/stm32_tz.c index 829596a9f2..ec2b8b8bd8 100644 --- a/hal/stm32_tz.c +++ b/hal/stm32_tz.c @@ -61,6 +61,11 @@ static void RAMFUNCTION hal_flash_nonsecure_lock(void) static int is_range_nonsecure(uint32_t address, int len) { +#if defined(WOLFBOOT_SECURE_APP) + (void)address; + (void)len; + return 0; +#else #ifndef DUALBANK_SWAP /* The non secure area begins at the BOOT partition */ uint32_t min = WOLFBOOT_PARTITION_BOOT_ADDRESS; @@ -89,6 +94,7 @@ static int is_range_nonsecure(uint32_t address, int len) return 1; return 0; #endif +#endif /* WOLFBOOT_SECURE_APP */ } @@ -215,11 +221,16 @@ void hal_gtzc_init(void) * 0: Non-secure access only to block */ - /* Configure SRAM1 as secure (Low 256 KB). - * wolfBoot links its own RAM/RAM_HEAP into the SRAM1 secure alias - * (0x30000000-0x3003FFFF, see hal/stm32h5.ld), so SRAM1 must stay - * secure for wolfBoot's .bss/stack/heap to remain accessible. */ + /* Configure SRAM1 as secure. The secure application handoff enters with + * its MSP at 0x300A0000, so the whole 512-KiB SRAM1 window must stay + * Secure until the secure runtime installs its own memory split. */ +#if defined(WOLFBOOT_SECURE_APP) + for (i = 0; i < 32; i++) { +#else + /* wolfBoot links its own RAM/RAM_HEAP into the lower SRAM1 secure alias + * (0x30000000-0x3003FFFF, see hal/stm32h5.ld). */ for (i = 0; i < 16; i++) { +#endif SET_GTZC1_MPCBBx_SECCFGR_VCTR(1, i, 0xFFFFFFFF); } @@ -230,10 +241,16 @@ void hal_gtzc_init(void) * unprivileged; with the reset default (PRIVCFGR=0xFFFFFFFF) the * DMA's descriptor/buffer reads from SRAM2 raise illegal-access * (TZIC1_SR4 bit 26) and the channel suspends with TPS=6 (TBU). */ +#if defined(WOLFBOOT_SECURE_APP) + for (i = 0; i < 4; i++) { + SET_GTZC1_MPCBBx_SECCFGR_VCTR(2, i, 0xFFFFFFFF); + } +#else for (i = 0; i < 4; i++) { SET_GTZC1_MPCBBx_SECCFGR_VCTR(2, i, 0x0); SET_GTZC1_MPCBBx_PRIVCFGR_VCTR(2, i, 0x0); } +#endif /* Configure SRAM3 as non-secure (320 KB) but PRIVILEGED. The NS CPU * runs privileged (Thread mode) and can use SRAM3 freely; only the @@ -241,9 +258,15 @@ void hal_gtzc_init(void) * descriptors/buffers are pinned to SRAM2 (.eth_buffers). Leaving * SRAM3 privileged lets a future NS OS own the unprivileged * boundary. */ +#if defined(WOLFBOOT_SECURE_APP) + for (i = 0; i < 20; i++) { + SET_GTZC1_MPCBBx_SECCFGR_VCTR(3, i, 0xFFFFFFFF); + } +#else for (i = 0; i < 20; i++) { SET_GTZC1_MPCBBx_SECCFGR_VCTR(3, i, 0x0); } +#endif } #elif defined(TARGET_stm32u5) @@ -327,9 +350,12 @@ void hal_tz_sau_init(void) sau_init_region(0, WOLFBOOT_NSC_ADDRESS, WOLFBOOT_NSC_ADDRESS + WOLFBOOT_NSC_SIZE - 1, 1); - /* Non-secure flash alias (boot partition only) */ + /* Non-secure flash alias (boot partition only). A secure application is + * deliberately kept out of the SAU NS window and is entered Secure. */ +#if !defined(WOLFBOOT_SECURE_APP) sau_init_region(1, WOLFBOOT_PARTITION_BOOT_ADDRESS, WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE - 1, 0); +#endif /* Non-secure RAM region: SRAM2 (64 KB) + SRAM3 (320 KB). * Lower bound widened from 0x20050000 to 0x20040000 to cover SRAM2, diff --git a/hal/stm32h5.c b/hal/stm32h5.c index 279edff436..7665ef53fa 100644 --- a/hal/stm32h5.c +++ b/hal/stm32h5.c @@ -49,12 +49,17 @@ #if TZ_SECURE() static int is_flash_nonsecure(uint32_t address) { +#if defined(WOLFBOOT_SECURE_APP) + (void)address; + return 0; +#else if (address >= WOLFBOOT_PARTITION_BOOT_ADDRESS && address < WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE) { return 1; } return 0; +#endif } #endif @@ -301,11 +306,17 @@ int hal_uds_derive_key(uint8_t *out, size_t out_len) int hal_attestation_get_lifecycle(uint32_t *lifecycle) { + uint32_t debugAuthStatus; + uint32_t productState; + if (lifecycle == NULL) { return -1; } - *lifecycle = 0x3000u; /* PSA_LIFECYCLE_SECURED (default) */ + productState = (FLASH_OPTSR_CUR & FLASH_OPTSR_PRODUCT_STATE_MASK) >> + FLASH_OPTSR_PRODUCT_STATE_SHIFT; + debugAuthStatus = *(volatile uint32_t *)CORTEX_M_DAUTHSTATUS_ADDRESS; + *lifecycle = stm32h5_attestation_lifecycle(productState, debugAuthStatus); return 0; } @@ -776,9 +787,12 @@ void hal_init(void) void hal_prepare_boot(void) { - /* Keep clock settings when staging a NS-application */ + /* Keep clock settings when staging a NS-application. A secure application + * owns the TrustZone peripherals after the handoff. */ #if (TZ_SECURE()) +#if !defined(WOLFBOOT_SECURE_APP) periph_unsecure(); +#endif #else #ifdef WOLFBOOT_RESTORE_CLOCK clock_pll_off(); diff --git a/hal/stm32h5.h b/hal/stm32h5.h index 521a3e4079..3cabdd7f81 100644 --- a/hal/stm32h5.h +++ b/hal/stm32h5.h @@ -23,6 +23,8 @@ #ifndef STM32H5_DEF_INCLUDED #define STM32H5_DEF_INCLUDED +#include "stm32h5_lifecycle.h" + #define PERIPH_CLOCK_FREQ (64000000) /* Assembly helpers */ @@ -280,7 +282,8 @@ #define FLASH_OPTSR_CUR (*(volatile uint32_t *)(FLASH_BASE + 0x50)) #define FLASH_OPTSR_PRG (*(volatile uint32_t *)(FLASH_BASE + 0x54)) #define FLASH_OPTSR_SWAP_BANK (1 << 31) - +#define FLASH_OPTSR_PRODUCT_STATE_SHIFT 8 +#define FLASH_OPTSR_PRODUCT_STATE_MASK (0xFFu << FLASH_OPTSR_PRODUCT_STATE_SHIFT) /* Register values (for both secure and non secure registers) * RM0481 Table 75 */ diff --git a/hal/stm32h5_lifecycle.h b/hal/stm32h5_lifecycle.h new file mode 100644 index 0000000000..00bffd889e --- /dev/null +++ b/hal/stm32h5_lifecycle.h @@ -0,0 +1,99 @@ +/* stm32h5_lifecycle.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#ifndef WOLFBOOT_STM32H5_LIFECYCLE_H +#define WOLFBOOT_STM32H5_LIFECYCLE_H + +#include + +#define FLASH_PRODUCT_STATE_OPEN 0xEDu +#define FLASH_PRODUCT_STATE_PROVISIONING 0x17u +#define FLASH_PRODUCT_STATE_IROT_PROVISIONED 0x2Eu +#define FLASH_PRODUCT_STATE_TZ_CLOSED 0xC6u +#define FLASH_PRODUCT_STATE_CLOSED 0x72u +#define FLASH_PRODUCT_STATE_LOCKED 0x5Cu + +#define CORTEX_M_DAUTHSTATUS_ADDRESS 0xE000EFB8u +#define CORTEX_M_DAUTHSTATUS_NSID_SHIFT 0u +#define CORTEX_M_DAUTHSTATUS_NSNID_SHIFT 2u +#define CORTEX_M_DAUTHSTATUS_SID_SHIFT 4u +#define CORTEX_M_DAUTHSTATUS_SNID_SHIFT 6u +#define CORTEX_M_DAUTHSTATUS_FIELD_MASK 0x3u +#define CORTEX_M_DAUTHSTATUS_ENABLED 0x3u + +static inline uint32_t stm32h5_product_state_to_psa_lifecycle( + uint32_t productState) +{ + uint32_t lifecycle; + + switch (productState) { + case FLASH_PRODUCT_STATE_OPEN: + lifecycle = 0x1000u; /* PSA_LIFECYCLE_ASSEMBLY_AND_TEST */ + break; + case FLASH_PRODUCT_STATE_PROVISIONING: + case FLASH_PRODUCT_STATE_IROT_PROVISIONED: + lifecycle = 0x2000u; /* PSA_LIFECYCLE_PSA_ROT_PROVISIONING */ + break; + case FLASH_PRODUCT_STATE_TZ_CLOSED: + lifecycle = 0x4000u; /* PSA_LIFECYCLE_NON_PSA_ROT_DEBUG */ + break; + case FLASH_PRODUCT_STATE_CLOSED: + case FLASH_PRODUCT_STATE_LOCKED: + lifecycle = 0x3000u; /* PSA_LIFECYCLE_SECURED */ + break; + default: + lifecycle = 0x0000u; /* PSA_LIFECYCLE_UNKNOWN */ + break; + } + return lifecycle; +} + +static inline int stm32h5_debug_status_field_enabled(uint32_t debugAuthStatus, + uint32_t shift) +{ + return (((debugAuthStatus >> shift) & + CORTEX_M_DAUTHSTATUS_FIELD_MASK) == + CORTEX_M_DAUTHSTATUS_ENABLED); +} + +static inline uint32_t stm32h5_attestation_lifecycle(uint32_t productState, + uint32_t debugAuthStatus) +{ + uint32_t lifecycle = stm32h5_product_state_to_psa_lifecycle(productState); + + if ((productState == FLASH_PRODUCT_STATE_CLOSED) || + (productState == FLASH_PRODUCT_STATE_LOCKED)) { + if (stm32h5_debug_status_field_enabled(debugAuthStatus, + CORTEX_M_DAUTHSTATUS_SID_SHIFT) || + stm32h5_debug_status_field_enabled(debugAuthStatus, + CORTEX_M_DAUTHSTATUS_SNID_SHIFT)) { + lifecycle = 0x5000u; /* PSA_LIFECYCLE_RECOVERABLE_PSA_ROT_DEBUG */ + } + else if (stm32h5_debug_status_field_enabled(debugAuthStatus, + CORTEX_M_DAUTHSTATUS_NSID_SHIFT) || + stm32h5_debug_status_field_enabled(debugAuthStatus, + CORTEX_M_DAUTHSTATUS_NSNID_SHIFT)) { + lifecycle = 0x4000u; /* PSA_LIFECYCLE_NON_PSA_ROT_DEBUG */ + } + } + return lifecycle; +} + +#endif /* WOLFBOOT_STM32H5_LIFECYCLE_H */ diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 9306819684..2b5eb649f9 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -189,6 +189,7 @@ TESTS+=unit-flash-write-same51 TESTS+=unit-imx-rt-cache-align TESTS+=unit-xspi-tfd-index TESTS+=unit-pic32-pfswap +TESTS+=unit-secure-handoff # linux_loader.c is x86 32-bit only, so its unit tests need a working 32-bit # (multilib) toolchain. Probe whether "gcc -m32" can link, and only add the @@ -225,6 +226,7 @@ run: $(TESTS) for unit in $(TESTS); do \ WOLFBOOT_SECTOR_SIZE=0x400 ./$$unit || exit 1; \ done + ./test-secure-handoff-config.sh python3 unit-sign-delta-tlv.py || exit 1 python3 unit-sign-delta-cert-inv-off.py || exit 1 python3 unit-sign-delta-basehash-cleanup.py || exit 1 @@ -880,6 +882,9 @@ unit-fit-fpga: ../../include/target.h unit-fit-fpga.c unit-update-flash: ../../include/target.h unit-update-flash.c gcc -o $@ unit-update-flash.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) +unit-secure-handoff: unit-secure-handoff.c + gcc -o $@ unit-secure-handoff.c $(CFLAGS) + unit-update-flash-hook: ../../include/target.h unit-update-flash.c gcc -o $@ unit-update-flash.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/test-secure-handoff-config.sh b/tools/unit-tests/test-secure-handoff-config.sh new file mode 100755 index 0000000000..a9c10a09ea --- /dev/null +++ b/tools/unit-tests/test-secure-handoff-config.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +# test-secure-handoff-config.sh +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfBoot. +# +# wolfBoot is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# wolfBoot is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see . + +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) + +expect_rejected() +{ + expected=$1 + shift + if output=$(make -C "$ROOT" -n "$@" wolfboot.bin 2>&1); then + printf 'configuration unexpectedly accepted: %s\n' "$expected" >&2 + exit 1 + fi + printf '%s\n' "$output" | grep -Fq "$expected" +} + +expect_stack_usage() +{ + expected=$1 + shift + output=$(make -C "$ROOT" -pn "$@" 2>/dev/null || true) + actual=$(printf '%s\n' "$output" | awk \ + '$1 == "STACK_USAGE" && $2 == "=" { value = $3 } END { print value }') + if [ "$actual" != "$expected" ]; then + printf 'STACK_USAGE: expected %s, got %s\n' \ + "$expected" "$actual" >&2 + exit 1 + fi +} + +expect_rejected \ + "SIGN=NONE is incompatible with the authenticated secure-app handoff" \ + TARGET=stm32h5 WOLFBOOT_SECURE_APP=1 SIGN=NONE + +expect_rejected \ + "WOLFBOOT_SKIP_BOOT_VERIFY=1 is incompatible with the authenticated secure-app handoff" \ + TARGET=stm32h5 WOLFBOOT_SECURE_APP=1 WOLFBOOT_SKIP_BOOT_VERIFY=1 \ + WOLFBOOT_SELF_HEADER=1 SELF_UPDATE_MONOLITHIC=1 \ + WOLFBOOT_PARTITION_SELF_HEADER_ADDRESS=0x0C140000 + +expect_stack_usage 25000 TARGET=stm32h5 WOLFBOOT_SECURE_APP=1 \ + WOLFCRYPT_TZ=0 WOLFCRYPT_TZ_PSA=0 WOLFCRYPT_TZ_PKCS11=0 SIGN=ML_DSA + +expect_stack_usage 69232 TARGET=stm32h5 WOLFBOOT_SECURE_APP=1 \ + WOLFCRYPT_TZ=0 WOLFCRYPT_TZ_PSA=0 WOLFCRYPT_TZ_PKCS11=0 \ + SIGN=RSA4096 SPMATH=0 + +expect_stack_usage 16688 TARGET=stm32h5 WOLFBOOT_SECURE_APP=1 \ + WOLFCRYPT_TZ=0 WOLFCRYPT_TZ_PSA=0 WOLFCRYPT_TZ_PKCS11=0 SIGN=ECC256 diff --git a/tools/unit-tests/unit-secure-handoff.c b/tools/unit-tests/unit-secure-handoff.c new file mode 100644 index 0000000000..7148d30ace --- /dev/null +++ b/tools/unit-tests/unit-secure-handoff.c @@ -0,0 +1,171 @@ +/* unit-secure-handoff.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include +#include + +#define WOLFBOOT_HASH_SHA256 +#include "wolfboot/secure_handoff.h" +#include "../../hal/stm32h5_lifecycle.h" + +static int test_secure_handoff_record(void) +{ + wolfBoot_secure_handoff_t handoff; + uint8_t measurement[WOLFBOOT_SECURE_HANDOFF_DIGEST_SIZE]; + uint32_t i; + + for (i = 0u; i < sizeof(measurement); ++i) { + measurement[i] = (uint8_t)(i + 1u); + } + (void)memset(&handoff, 0xA5, sizeof(handoff)); + + if (wolfBoot_secure_handoff_build(&handoff, measurement, 7u, + WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_SECURED) != 0) { + return -1; + } + if ((handoff.magic != WOLFBOOT_SECURE_HANDOFF_MAGIC) || + (handoff.magic_inverse != ~WOLFBOOT_SECURE_HANDOFF_MAGIC) || + (handoff.version != WOLFBOOT_SECURE_HANDOFF_VERSION) || + (handoff.size != sizeof(handoff)) || + (handoff.lifecycle != + WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_SECURED) || + (handoff.image_version != 7u) || + (handoff.hash_algorithm != + WOLFBOOT_SECURE_HANDOFF_HASH_SHA256) || + (handoff.measurement_size != + WOLFBOOT_SECURE_HANDOFF_DIGEST_SIZE) || + (sizeof(handoff) != 56u)) { + return -1; + } + for (i = 0u; i < sizeof(measurement); ++i) { + if (handoff.measurement[i] != measurement[i]) { + return -1; + } + } + return 0; +} + +static int test_secure_handoff_invalid_input(void) +{ + wolfBoot_secure_handoff_t handoff; + uint8_t measurement[WOLFBOOT_SECURE_HANDOFF_DIGEST_SIZE] = { 0u }; + + if (wolfBoot_secure_handoff_build(NULL, measurement, 1u, + WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_UNKNOWN) != -1) { + return -1; + } + if (wolfBoot_secure_handoff_build(&handoff, NULL, 1u, + WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_UNKNOWN) != -1) { + return -1; + } + return 0; +} + +static int test_stm32h5_product_state_lifecycle_map(void) +{ + if (stm32h5_product_state_to_psa_lifecycle( + FLASH_PRODUCT_STATE_OPEN) != 0x1000u) { + return -1; + } + if (stm32h5_product_state_to_psa_lifecycle( + FLASH_PRODUCT_STATE_PROVISIONING) != 0x2000u) { + return -1; + } + if (stm32h5_product_state_to_psa_lifecycle( + FLASH_PRODUCT_STATE_IROT_PROVISIONED) != 0x2000u) { + return -1; + } + if (stm32h5_product_state_to_psa_lifecycle( + FLASH_PRODUCT_STATE_TZ_CLOSED) != 0x4000u) { + return -1; + } + if (stm32h5_product_state_to_psa_lifecycle( + FLASH_PRODUCT_STATE_CLOSED) != 0x3000u) { + return -1; + } + if (stm32h5_product_state_to_psa_lifecycle( + FLASH_PRODUCT_STATE_LOCKED) != 0x3000u) { + return -1; + } + if (stm32h5_product_state_to_psa_lifecycle(0xFFu) != + WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_UNKNOWN) { + return -1; + } + return 0; +} + +static int test_stm32h5_debug_lifecycle_map(void) +{ + uint32_t disabled = 0xAAu; + + if (stm32h5_attestation_lifecycle(FLASH_PRODUCT_STATE_CLOSED, + disabled) != 0x3000u) { + return -1; + } + if (stm32h5_attestation_lifecycle(FLASH_PRODUCT_STATE_CLOSED, + CORTEX_M_DAUTHSTATUS_ENABLED << + CORTEX_M_DAUTHSTATUS_NSID_SHIFT) != 0x4000u) { + return -1; + } + if (stm32h5_attestation_lifecycle(FLASH_PRODUCT_STATE_CLOSED, + CORTEX_M_DAUTHSTATUS_ENABLED << + CORTEX_M_DAUTHSTATUS_NSNID_SHIFT) != 0x4000u) { + return -1; + } + if (stm32h5_attestation_lifecycle(FLASH_PRODUCT_STATE_CLOSED, + CORTEX_M_DAUTHSTATUS_ENABLED << + CORTEX_M_DAUTHSTATUS_SID_SHIFT) != 0x5000u) { + return -1; + } + if (stm32h5_attestation_lifecycle(FLASH_PRODUCT_STATE_CLOSED, + CORTEX_M_DAUTHSTATUS_ENABLED << + CORTEX_M_DAUTHSTATUS_SNID_SHIFT) != 0x5000u) { + return -1; + } + if (stm32h5_attestation_lifecycle(FLASH_PRODUCT_STATE_CLOSED, + (CORTEX_M_DAUTHSTATUS_ENABLED << + CORTEX_M_DAUTHSTATUS_NSID_SHIFT) | + (CORTEX_M_DAUTHSTATUS_ENABLED << + CORTEX_M_DAUTHSTATUS_SID_SHIFT)) != 0x5000u) { + return -1; + } + if (stm32h5_attestation_lifecycle(FLASH_PRODUCT_STATE_OPEN, + 0xFFu) != 0x1000u) { + return -1; + } + return 0; +} + +int main(void) +{ + if (test_secure_handoff_record() != 0) { + return 1; + } + if (test_secure_handoff_invalid_input() != 0) { + return 1; + } + if (test_stm32h5_product_state_lifecycle_map() != 0) { + return 1; + } + if (test_stm32h5_debug_lifecycle_map() != 0) { + return 1; + } + return 0; +}