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/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/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/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 \
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;
+}