Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
709faed
Derive the STM32L5 PSA lifecycle from the RDP and TZEN option bytes
aidangarske Nov 9, 2026
8ab44fa
Keep STM32L5 flash, SRAM and peripherals Secure for a secure applicat…
aidangarske Nov 9, 2026
8c1539f
Fix an unused variable that breaks STM32L5 DEBUG_UART builds
aidangarske Nov 9, 2026
361816b
Add an STM32L5 wolfTrust secure application handoff config
aidangarske Nov 9, 2026
fb0ca8d
Run the STM32L5 secure application handoff on the m33mu emulator
aidangarske Nov 9, 2026
506cf2f
Document the STM32L5 secure application handoff
aidangarske Nov 9, 2026
cffba36
Fix the STM32L5 secure application option byte names and signing head…
aidangarske Nov 10, 2026
f00ac70
Report STM32L5 RDP level 1 as Non-PSA RoT debug and drop the overlapp…
aidangarske Nov 10, 2026
5ed6d5f
Keep the STM32L5 peripheral window Secure for a secure application ha…
aidangarske Nov 10, 2026
86852e0
Disable bootloader self-update in the STM32L5 secure application config
aidangarske Nov 10, 2026
998e8ed
Keep the PLL clock running for an STM32L5 secure application handoff
aidangarske Nov 11, 2026
7c24c9d
Use the 110 MHz handoff clock for the STM32L5 secure app test UART
aidangarske Nov 12, 2026
0695fb1
Export no NSC entries from an STM32L5 secure application without WOLF…
aidangarske Nov 13, 2026
de2da43
Keep the STM32L5 PLL for the secure app with WOLFBOOT_RESTORE_CLOCK=0
aidangarske Nov 13, 2026
c3003c2
Leave the STM32L5 MPCBB reset value in place for a secure application
aidangarske Nov 13, 2026
781343e
Drop the unused heap from the STM32L5 secure app linker script
aidangarske Nov 13, 2026
ee7a700
Translate the STM32L5 Secure flash alias on erase with the write path…
aidangarske Nov 13, 2026
6e60901
Update the STM32L5 secure app docs for the NSC, clock and GTZC changes
aidangarske Nov 13, 2026
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
6 changes: 6 additions & 0 deletions .github/workflows/test-configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,12 @@ jobs:
arch: arm
config-file: ./config/examples/stm32l5-wolfcrypt-tz.config

stm32l5_tz_wolftrust_test:
uses: ./.github/workflows/test-build.yml
with:
arch: arm
config-file: ./config/examples/stm32l5-tz-wolftrust.config

stm32u3_test:
uses: ./.github/workflows/test-build.yml
with:
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/trustzone-emulator-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,35 @@ jobs:
| tee /tmp/m33mu-lpc55s69-pkcs11-second.log
grep -q "pkcs11: restored persistent PKCS11 objects" /tmp/m33mu-lpc55s69-pkcs11-second.log
grep -q "pkcs11: success" /tmp/m33mu-lpc55s69-pkcs11-second.log

# Boot, update and rollback on the m33mu STM32L552 model with the
# TrustZone secure-application (wolfTrust) handoff config.
stm32l5-secure-app-emulator-tests:
runs-on: ubuntu-latest
container:
image: ghcr.io/wolfssl/wolfboot-ci-m33mu:latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Init submodules
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git submodule update --init --single-branch

- name: Configure stm32l5-tz-wolftrust and build wolfboot
run: |
cp config/examples/stm32l5-tz-wolftrust.config .config
make wolfboot.bin

- name: Run emu test (stm32l5 secure app)
working-directory: test-app/emu-test-apps
run: |
BOOT_TIMEOUT=30 UPDATE_TIMEOUT=60 REBOOT_TIMEOUT=30 TARGET=stm32l5 ./test.sh

- name: Upload emulator logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: stm32l5-secure-app-emu-logs
path: test-app/emu-test-apps/stm32l552/*.log
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,11 @@ TARGET_H_TEMPLATE:=include/target.h.in
ifeq ($(TZEN),1)
ifeq ($(TARGET),stm32l5)
# 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),stm32u5)
Expand Down
16 changes: 14 additions & 2 deletions arch.mk
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,12 @@ ifeq ($(ARCH),ARM)
ARCH_FLASH_OFFSET=0x08000000
ifeq ($(TZEN),1)
WOLFBOOT_ORIGIN=0x0C000000
ifeq ($(WOLFBOOT_SECURE_APP),1)
ifeq ($(WOLFCRYPT_TZ),1)
$(error stm32l5 WOLFBOOT_SECURE_APP=1 links no NSC; set WOLFCRYPT_TZ=0)
endif
$(call LSCRIPT_IN_DEFAULT,hal/$(TARGET)-secure-app.ld)
endif
else
WOLFBOOT_ORIGIN=0x08000000
$(call LSCRIPT_IN_DEFAULT,hal/$(TARGET)-ns.ld)
Expand Down Expand Up @@ -1105,7 +1111,10 @@ else
# machinery as CORTEX_M33 applies. Kept separate from the M33 block so
# that adding this arm cannot change any existing target.
CFLAGS+=-mcmse
SECURE_LDFLAGS+=-Wl,--cmse-implib -Wl,--out-implib=./src/wolfboot_tz_nsc.o
# A secure app without WOLFCRYPT_TZ has no NSC entries to export
ifneq ($(WOLFBOOT_SECURE_APP)/$(filter 1,$(WOLFCRYPT_TZ)),1/)
SECURE_LDFLAGS+=-Wl,--cmse-implib -Wl,--out-implib=./src/wolfboot_tz_nsc.o
endif
ifeq ($(WOLFCRYPT_TZ),1)
SECURE_OBJS+=./src/wc_callable.o
WOLFCRYPT_OBJS+=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/random.o
Expand Down Expand Up @@ -1136,7 +1145,10 @@ else
endif
endif
CFLAGS+=-mcmse
SECURE_LDFLAGS+=-Wl,--cmse-implib -Wl,--out-implib=./src/wolfboot_tz_nsc.o
# A secure app without WOLFCRYPT_TZ has no NSC entries to export
ifneq ($(WOLFBOOT_SECURE_APP)/$(filter 1,$(WOLFCRYPT_TZ)),1/)
SECURE_LDFLAGS+=-Wl,--cmse-implib -Wl,--out-implib=./src/wolfboot_tz_nsc.o
endif
ifeq ($(WOLFCRYPT_TZ),1)
CORTEXM_ARM_EXTRA_OBJS=
CORTEXM_ARM_EXTRA_CFLAGS=
Expand Down
44 changes: 44 additions & 0 deletions config/examples/stm32l5-tz-wolftrust.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
ARCH?=ARM
TZEN?=1
TARGET?=stm32l5
SIGN?=ECC256
HASH?=SHA256
DEBUG?=0
DEBUG_UART?=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
V?=0
SPMATH?=1
# No bootloader self-update: its erase range assumes the non-secure alias
RAM_CODE?=0
DUALBANK_SWAP?=0
IMAGE_HEADER_SIZE?=1024
WOLFCRYPT_TZ?=0
ARMORED=1

WOLFBOOT_SECURE_APP?=1
WOLFBOOT_SECURE_HANDOFF_ADDRESS?=0x30000000
# The secure app runs on the 110 MHz PLL clock wolfBoot leaves running
WOLFBOOT_RESTORE_CLOCK?=0

# 2KB pages, dual bank (DBANK=1)
WOLFBOOT_SECTOR_SIZE?=0x800

# 48KB wolfBoot region with no NSC, partitions at the Secure flash alias:
# 128KB boot and the swap page in bank 1, 128KB update in bank 2.
# 0x0C038000-0x0C03DFFF is the secure app's NVM and is never touched here.
BOOTLOADER_PARTITION_SIZE?=0xC000
WOLFBOOT_KEYVAULT_ADDRESS?=0x0C00B000
WOLFBOOT_KEYVAULT_SIZE?=0
WOLFBOOT_PARTITION_SIZE?=0x20000
WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x0C00C000
WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x0C040000
WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x0C03E000
61 changes: 61 additions & 0 deletions docs/Targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,67 @@ Updates can be flashed at 0x0804a000:
The two partition are logically remapped by using BANK_SWAP capabilities. This partition
swap is immediate and does not require a SWAP partition.

### Scenario 4: TrustZone Enabled, Secure application handoff (wolfTrust)

wolfBoot verifies and measures a signed **Secure** runtime (for example
wolfTrust) and jumps to it without leaving the Secure state, publishing the
measured-boot record at `0x30000000` (see
[Secure Application Handoff](secure_app_handoff.md)). The secure application
then programs the SAU and GTZC itself and launches its Non-secure guests.

The example configuration is available in
[/config/examples/stm32l5-tz-wolftrust.config](/config/examples/stm32l5-tz-wolftrust.config).
All partitions are at the Secure flash alias:

| Region | Address | Size |
| --- | --- | --- |
| wolfBoot | `0x0C000000` | 48 KB |
| Boot partition (secure application) | `0x0C00C000` | 128 KB |
| Secure application NVM (not touched by wolfBoot) | `0x0C038000` | 24 KB |
| Swap page | `0x0C03E000` | 2 KB |
| Update partition (start of bank 2) | `0x0C040000` | 128 KB |
| Non-secure guests | `0x08060000` | 128 KB |

wolfBoot keeps SRAM1 and SRAM2 Secure across the jump (the GTZC reset value),
enables no SAU region and builds no Non-secure callable entries, keeps the
110 MHz PLL clock running (`WOLFBOOT_RESTORE_CLOCK=0`), and
links its own RAM at `0x30030000` so that `0x30000000-0x3002FFFF` (the record
and the secure application's RAM) is left untouched. The PSA lifecycle in the
record comes from the RDP level in `FLASH_OPTR` (`hal/stm32l5_lifecycle.h`).
The STM32L5 has no product-state register, so with `TZEN=1` RDP level 0 is
reported as PSA RoT provisioning (`0x2000`), levels 0.5 and 1 as Non-PSA RoT
debug (`0x4000`, Secure debug closed), and level 2 as secured (`0x3000`).
Without `TZEN` the lifecycle is unknown.

The partition values are `?=` defaults. A different layout set on the `make`
command line must keep the boot, NVM, swap, update and guest regions disjoint
(the Secure and Non-secure aliases are the same flash) and move the `SECWM`
watermarks below to match.

Option bytes (bank 1 fully secure, bank 2 pages 0-63 secure for the update
slot, pages 64-127 Non-secure for the guests):

```
TZEN = 1 System with TrustZone-M enabled
DBANK = 1 Dual bank mode
SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F All 128 pages of Bank1 secure
SECWM2_PSTRT=0x0 SECWM2_PEND=0x3F Bank2 pages 0-63 (0x08040000-0x0805FFFF) secure
```

1. `cp ./config/examples/stm32l5-tz-wolftrust.config .config`
2. `make` (builds `wolfboot.bin` only)
3. Sign the secure application, e.g.
`IMAGE_HEADER_SIZE=1024 ./tools/keytools/sign --ecc256 --sha256 wolftrust.bin wolfboot_signing_private_key.der 1`
(the header size must match the config's `IMAGE_HEADER_SIZE`)
4. Set the option bytes:
- `STM32_Programmer_CLI -c port=swd mode=hotplug -ob TZEN=1 DBANK=1`
- `STM32_Programmer_CLI -c port=swd mode=hotplug -ob SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F SECWM2_PSTRT=0x0 SECWM2_PEND=0x3F`
5. `STM32_Programmer_CLI -c port=swd -d ./wolfboot.bin 0x0C000000`
6. `STM32_Programmer_CLI -c port=swd -d ./wolftrust_v1_signed.bin 0x0C00C000`

The `trustzone-emulator-tests` workflow runs boot, update and rollback for this
configuration on the m33mu STM32L552 model.


### Debugging

Expand Down
19 changes: 18 additions & 1 deletion docs/secure_app_handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,27 @@ the partitions at the Secure flash alias. `hal/mcxn.c` uses the shared
reports the lifecycle through the weak default. `mcxn-emulator-tests.yml` runs
boot, update and rollback on the m33mu MCXN947 model.

## STM32L5

`config/examples/stm32l5-tz-wolftrust.config` puts the record at `0x30000000`
and the partitions at the Secure flash alias (`0x0C00C000` boot, `0x0C040000`
update, both 128 KB). wolfBoot links its RAM at `0x30030000`
(`hal/stm32l5-secure-app.ld`) to stay clear of the record and the secure app's
RAM. SRAM1 and SRAM2 stay Secure at their GTZC reset value, the SAU has no
region enabled, and without `WOLFCRYPT_TZ` wolfBoot links no `wolfBoot_nsc_*`
entries, so Non-secure code has no path into the update partition.
`hal/stm32l5.c` skips the GTZC peripheral unsecuring, the config sets
`WOLFBOOT_RESTORE_CLOCK=0` so the 110 MHz PLL keeps running, and
`hal_attestation_get_lifecycle()` maps the RDP level to a PSA lifecycle
(`hal/stm32l5_lifecycle.h`; it reports unknown when TZEN is off). The
`trustzone-emulator-tests` workflow runs boot, update and rollback on the m33mu
STM32L552 model and checks on every boot the record and that no SAU region
is enabled.

## Tests

- `tools/unit-tests/unit-secure-handoff.c` — record layout, the builder, and the
STM32H5 lifecycle mapping.
STM32H5 and STM32L5 lifecycle mappings.
- `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.
7 changes: 7 additions & 0 deletions hal/stm32_tz.c
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,16 @@ void hal_tz_sau_init(void)
#else
void hal_tz_sau_init(void)
{
/* A secure application without wolfCrypt TZ exports no NSC entries */
#if !defined(WOLFBOOT_SECURE_APP) || defined(WOLFCRYPT_SECURE_MODE)
/* Non-secure callable: NSC functions area */
sau_init_region(0, WOLFBOOT_NSC_ADDRESS,
WOLFBOOT_NSC_ADDRESS + WOLFBOOT_NSC_SIZE - 1, 1);
#endif

/* A secure application is entered Secure and programs its own
* Non-secure flash, RAM and peripheral windows. */
#if !defined(WOLFBOOT_SECURE_APP)
/* Non-secure: application flash area (boot partition only) */
sau_init_region(1, WOLFBOOT_PARTITION_BOOT_ADDRESS,
WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE - 1,
Expand All @@ -393,6 +399,7 @@ void hal_tz_sau_init(void)

/* Non-secure: internal peripherals */
sau_init_region(3, 0x40000000, 0x4FFFFFFF, 0);
#endif

/* Enable SAU */
SAU_CTRL = SAU_INIT_CTRL_ENABLE;
Expand Down
58 changes: 58 additions & 0 deletions hal/stm32l5-secure-app.ld
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* SRAM below 0x30030000 holds the handoff record and the secure app's RAM. */
MEMORY
{
FLASH (rx) : ORIGIN = @WOLFBOOT_ORIGIN@, LENGTH = @WOLFBOOT_KEYVAULT_ADDRESS@ - @WOLFBOOT_ORIGIN@
RAM (rwx) : ORIGIN = 0x30030000, LENGTH = 0x00010000
FLASH_KEYVAULT(rw): ORIGIN = @WOLFBOOT_KEYVAULT_ADDRESS@, LENGTH = @WOLFBOOT_KEYVAULT_SIZE@
}

SECTIONS
{
.text :
{
_start_text = .;
KEEP(*(.isr_vector))
*(.text*)
*(.rodata*)
. = ALIGN(8);
_end_text = .;
} > FLASH

.edidx :
{
. = ALIGN(4);
*(.ARM.exidx*)
} > FLASH

.data :
{
_start_data = .;
KEEP(*(.data*))
. = ALIGN(8);
KEEP(*(.ramcode))
. = ALIGN(8);
_end_data = .;
} > RAM AT > FLASH
_stored_data = LOADADDR(.data);

.bss (NOLOAD) :
{
_start_bss = .;
__bss_start__ = .;
*(.bss*)
*(COMMON)
. = ALIGN(8);
_end_bss = .;
__bss_end__ = .;
_end = .;
} > RAM
. = ALIGN(8);
}

END_STACK = ORIGIN(RAM) + LENGTH(RAM);

_min_stack = 0x2000;
ASSERT(_end + _min_stack <= END_STACK, "stm32l5 secure-app RAM: .bss leaves less than _min_stack for the stack")

_flash_keyvault = ORIGIN(FLASH_KEYVAULT);
_flash_keyvault_size = LENGTH(FLASH_KEYVAULT);
22 changes: 14 additions & 8 deletions hal/stm32l5.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

#include "hal.h"
#include "hal/stm32l5.h"
#include "hal/stm32l5_lifecycle.h"


static void RAMFUNCTION flash_set_waitstates(unsigned int waitstates)
Expand Down Expand Up @@ -220,12 +221,7 @@ int hal_uds_derive_key(uint8_t *out, size_t out_len)

int hal_attestation_get_lifecycle(uint32_t *lifecycle)
{
if (lifecycle == NULL) {
return -1;
}

*lifecycle = 0x3000u; /* PSA_LIFECYCLE_SECURED (default) */
return 0;
return stm32l5_optr_to_psa_lifecycle(FLASH_OPTR, lifecycle);
}

void RAMFUNCTION hal_flash_unlock(void)
Expand Down Expand Up @@ -280,6 +276,12 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len)
if (len == 0)
return -1;

#if TZ_SECURE() && defined(WOLFBOOT_SECURE_APP)
/* Secure-app partitions are linked at the Secure flash alias */
if (address >= FLASH_SECURE_MMAP_BASE)
address = (address & ~FLASH_SECURE_MMAP_BASE) | FLASHMEM_ADDRESS_SPACE;
#endif

if (address < ARCH_FLASH_OFFSET)
return -1;

Expand Down Expand Up @@ -425,7 +427,8 @@ static void clock_pll_on(int powersave)
DMB();
}

#if TZ_SECURE()
/* A secure application programs its own GTZC peripheral attribution */
#if TZ_SECURE() && !defined(WOLFBOOT_SECURE_APP)
static void periph_unsecure()
{
volatile uint32_t reg;
Expand Down Expand Up @@ -533,7 +536,10 @@ void hal_init(void)

#if TZ_SECURE()
hal_tz_sau_init();
/* A secure application keeps the MPCBB reset value (all SRAM Secure) */
#if !defined(WOLFBOOT_SECURE_APP)
hal_gtzc_init();
#endif
#endif
clock_pll_on(0);

Expand All @@ -544,7 +550,7 @@ void hal_prepare_boot(void)
#ifdef WOLFBOOT_RESTORE_CLOCK
clock_pll_off();
#endif
#if TZ_SECURE()
#if TZ_SECURE() && !defined(WOLFBOOT_SECURE_APP)
periph_unsecure();
#endif
}
Loading
Loading