diff --git a/Makefile b/Makefile index 2853fc46..1eefff20 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ include mk/common.mk .DEFAULT_GOAL := all -.PHONY: all secure-image size-report test c99-check test-conformance test-target test-hardware fetch-psa-ff-tests \ +.PHONY: all secure-image size-report test test-provisioning c99-check test-conformance test-target test-hardware fetch-psa-ff-tests \ clean firmware-stm32h563 run-stm32h563 run-stm32h563-tui run-stm32h563-uarts \ test-domain-host test-domain-compilers test-domain-sanitize \ test-domain-valgrind test-manifest-host test-manifest-compilers \ @@ -41,6 +41,14 @@ all: $(ARCH_DEFAULT_GOALS) test: @$(MAKE) --no-print-directory -C tests/host test + @$(MAKE) --no-print-directory test-provisioning + +# Production lock gates of both provisioning backends, against stub tools. +test-provisioning: + @mkdir -p $(BUILD_DIR) + @tests/target/provisioning/test_provisioning_gates.sh > $(BUILD_DIR)/provisioning-gates.log 2>&1 \ + || { cat $(BUILD_DIR)/provisioning-gates.log; exit 1; } + @tail -1 $(BUILD_DIR)/provisioning-gates.log C99_CFLAGS := -std=c99 -pedantic-errors -Werror=vla \ -D_POSIX_C_SOURCE=200809L @@ -83,13 +91,16 @@ else fi endif -# Real STM32H563 hardware equivalence suite: positive lifecycle + restart -# recovery + cross-domain isolation on a Nucleo-H563ZI, the on-silicon -# counterpart of test-target. Needs the ST-Link + board (detect_h5.sh) and a -# container toolchain for the build (WT_H5_DOCKER_IMAGE); skips otherwise so it -# never silently passes. HARDWARE evidence — recorded separately from emulator. +# Real hardware suite, the on-silicon counterpart of test-target: STM32H563 on a +# Nucleo-H563ZI (detect_h5.sh; container toolchain via WT_H5_DOCKER_IMAGE), or +# TARGET=mimxrt700 on the EVK from its probe host (detect_rt700.sh). Skips +# without a board so it never silently passes. test-hardware: +ifeq ($(TARGET),mimxrt700) + @tests/target/run_rt700_suite.sh +else @tests/target/run_h5_suite.sh +endif test-compilers: @$(MAKE) --no-print-directory -C tests/host test-compilers diff --git a/README.md b/README.md index 4800307e..dbef3bf3 100644 --- a/README.md +++ b/README.md @@ -169,8 +169,10 @@ documentation source: - [Porting](docs/Porting.md) - [Building](docs/Building.md) - [Testing](docs/Testing.md) +- [Provisioning](docs/Provisioning.md) - [Project Structure](docs/Project-Structure.md) - [STM32H5 Guide](docs/STM32H5-Guide.md) +- [MIMXRT700 Guide](docs/MIMXRT700-Guide.md) The source tree is authoritative: diff --git a/docs/Architecture.md b/docs/Architecture.md index a006aba1..cfb4c4cf 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -93,7 +93,8 @@ MPU isolates writable state, not code identity; this is an explicit difference from separately linked partition images. Both guest images share one Non-secure flash attribution window, so a privileged -guest can read peer flash. WRP plus `WT_GUEST_FLASH_WRP=1` protects guest-flash +guest can read peer flash. Hardware write protection (STM32 WRP or the +MIMXRT700 guest fence) plus `WT_GUEST_FLASH_WRP=1` protects guest-flash integrity but not confidentiality. Peripheral and Non-secure NVIC attribution are deployment responsibilities; see [Threat Model](Threat-Model.md). diff --git a/docs/Home.md b/docs/Home.md index 6916ccc9..eebff2c3 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -114,5 +114,7 @@ port's five CMSE gateway veneers. | [Porting](Porting.md) | Architecture and target port contracts | | [Building](Building.md) | Build targets, outputs, and cross-build options | | [Testing](Testing.md) | Host, M33MU, and STM32H563 validation | +| [Provisioning](Provisioning.md) | Rehearse, validate, then lock: the production life cycle flow for every port | | [Project Structure](Project-Structure.md) | Repository layout | | [STM32H5 Guide](STM32H5-Guide.md) | STM32H563 provisioning, flashing, WRP, and recovery safety | +| [MIMXRT700 Guide](MIMXRT700-Guide.md) | MIMXRT700 provisioning, flashing, XSPI guest fence, and recovery safety | diff --git a/docs/MIMXRT700-Guide.md b/docs/MIMXRT700-Guide.md index e507e8c3..26ff9c24 100644 --- a/docs/MIMXRT700-Guide.md +++ b/docs/MIMXRT700-Guide.md @@ -28,10 +28,13 @@ Read the current state first and keep a development board recoverable. RAM with a per-dispatch SAU window, because the AHB secure controller's SRAM rules do not gate CPU0 on this silicon (an earlier fabric-filter attempt let a guest with its Non-secure MPU disabled write the other guest's RAM). -- **Not yet ported:** `SERVICE_VNET` and the NOR guest-window write-protect - check. The target has no VNET manifest, so `CONFIG_VNET=y` stops the build - with an error, and a `WT_GUEST_FLASH_WRP=1` build refuses to launch any - guest. `SERVICE_FWU` stages a candidate into the wolfBoot update partition +- **Validated on the EVK and in emulation:** the XSPI guest flash fence + (`wrpfence`, `wrpoff`, `wrpneg`) and the mock lock of every life cycle + state, described under Guest flash write protection and Provisioning and + life cycle below. The real fuse burn is gated and has never been run. +- **Not yet ported:** `SERVICE_VNET`. The target has no VNET manifest, so + `CONFIG_VNET=y` stops the build with an error. `SERVICE_FWU` stages a + candidate into the wolfBoot update partition (`0x38180000`, the `imx-rt700-tz.config` update address) and arms the swap trigger in its trailer, as the STM32H563 port does; staging must be contiguous from offset 0, so a finished candidate has no unwritten gap. `WT_CONFORMANCE=1` @@ -56,6 +59,8 @@ wolfBoot; the Secure runtime changes live in wolfTrust. | `config/examples/imx-rt700-tz.config` | TrustZone enabled with the generic Secure-application handoff (`WOLFBOOT_SECURE_APP`): wolfBoot writes the measured-boot record to Secure SRAM and stays in Secure state across the jump to the Secure runtime. | | `config/examples/imx-rt700-mldsa.config` | ML-DSA-87 image signatures for a CNSA 2.0 boot chain. | | Boot-region protection | Before handoff, wolfBoot programs and locks the XSPI Secure Flash Protection descriptors so the bootloader region is read-only to the application, and refuses to continue if the protection cannot be read back. | +| Guest flash fence | Carried as `tests/target/wolfboot-imxrt700-guest-fence.patch`: with `XSPI_GUEST_FENCE_START`/`END` defined, a further locked descriptor makes both guest windows read-only to every initiator until the next reset. | +| Life cycle | Carried as `tests/target/wolfboot-imxrt700-lifecycle.patch`: `hal_attestation_get_lifecycle()` reads the OTP `LC_STATE` shadow, its redundant copy, the A0/A1 bit-protection copies when present, and `DAUTHSTATUS`, and maps them to the PSA life cycle in the handoff. In Field reports SECURED only when every `DAUTHSTATUS` field reads implemented and disabled (`0xAA`); an enabled field lowers it to `0x5000` or `0x4000`, and any other encoding reports UNKNOWN. | The loader satisfies the [Porting](Porting.md) bootloader contract: it authenticates the Secure image, provides `wt_boot_handoff_t` (SHA-256 @@ -189,13 +194,62 @@ similar bit-28 IDAU part: - MIMXRT700-EVK with its on-board MCU-Link (CMSIS-DAP) and USB serial - `arm-none-eabi-gcc` with newlib headers, and `arm-none-eabi-{nm,objcopy,size}` -- NXP SPSDK (`nxpimage` for FCB and bootable-image assembly) +- NXP SPSDK (`nxpimage` for FCB and bootable-image assembly; `shadowregs` + support for `mimxrt798s`, checked by the life cycle preflight) - pyOCD with MIMXRT798S pack support (flash and SWD inspection) - Python 3 - wolfBoot key tools and a signing key for the Secure payload - a hardware runner host that owns the probe, with a controllable reset line to the EVK, and a serial console (default `/dev/ttyACM0`) +## Read-only preflight + +Before any command that writes to the board, read the life cycle, debug, and +fence state, then run the preflight that gates `advance`: + +```sh +TARGET=mimxrt700 tests/target/provisioning/provisioning_ctrl.sh status +TARGET=mimxrt700 tests/target/provisioning/provisioning_ctrl.sh discover +``` + +Both only read over SWD, with the generic Cortex-M attach that never resets the +chip. A factory EVK running the fenced chain (after `restore`) reads: + +```text +OTP life cycle LC_STATE=0x03 (Develop) LC_STATE_RED=0x03 +LOCK_CFG3 0x00000000 (LIFE_CYCLE_LOCK=0: 0 = shadow override and fuse burn both open) +DAUTHSTATUS 0x000000ff +XSPI SFP MGC=0xa8000400 TG0MDAD=0xa000c000 +guest fence armed FRAD2 acp=0x00000000 word3=0xa0000000 +wolfTrust saw 0x00001000 (ASSEMBLY_AND_TEST) +guest launches verified=0x00000003 refused=0x00000000 +``` + +| Line | Meaning | +| --- | --- | +| `OTP life cycle` | the `LC_STATE` shadow and its redundant copy; they must agree | +| `LOCK_CFG3` | `LIFE_CYCLE_LOCK` bits: bit 0 blocks burning, bit 1 blocks shadow over-ride, bit 2 blocks reads | +| `DAUTHSTATUS` | Cortex-M debug authentication; `0xff` means Secure and Non-secure debug are open | +| `XSPI SFP` | global flash-protection configuration and the initiator domain; `0xa8000400` and `0xa000c000` mean valid and sealed | +| `guest fence` | the descriptor spanning the guest windows; `armed` needs write access `0` and a hard-reset lock | +| `wolfTrust saw` | the life cycle wolfBoot handed wolfTrust at `0x30180000` | +| `guest launches` | wolfTrust's launch-verified and launch-refused guest masks | + +On an unfenced chain the fence line instead reads +`open FRAD1 acp=0x00000007 word3=0xa0000000`: FRAD1 grants write access over the +guest windows. `discover` then checks the preflight: + +```text + [check] PASS SPSDK shadowregs supports mimxrt798s + [check] PASS fused life cycle is Develop and its redundant copy agrees + [check] PASS life cycle shadow over-ride is open (LOCK_CFG3 0x00000000) + [check] PASS the boot handoff life cycle is readable (0x00001000, ASSEMBLY_AND_TEST) +PASS: discovery stamped (/home//.cache/wolftrust/mimxrt700/discovery) +``` + +The stamp lives under `WT_PROVISION_STATE/mimxrt700` (default `~/.cache/wolftrust/mimxrt700`). +Running `discover` again clears both it and any earlier `regress` stamp. + ## Build, flash, and verify The hardware runner (`tests/target/run_rt700_hardware.sh`) drives image @@ -203,7 +257,12 @@ assembly and flashing so the addresses stay paired; its emulator sibling (`tests/target/run_rt700_m33mu.sh`) runs the same chain and scenario names under M33MU. The `romsmoke` scenario proves the BootROM XIP path; the `positive` scenario is the wolfTrust chain; `ahbscneg` adds the guest -isolation negative. The emulator runner then carries the STM32H563 scenario +isolation negative; `wrpfence`, `wrpoff`, and `wrpneg` cover the guest flash +fence. `make test-hardware TARGET=mimxrt700` runs that set through +`tests/target/run_rt700_suite.sh` on the probe host and skips without a board. +Both runners build the wolfBoot first stage from one pinned upstream commit +plus the carried patches (`tests/target/lib/rt700_wolfboot.sh`) unless +`RT700_WOLFBOOT_DIR` names a prebuilt tree. The emulator runner then carries the STM32H563 scenario matrix (restart and launch refusal, SP fault recovery, the Secure-verdict negatives, the PSA guest's lifecycle and negatives, and Arm's conformance suites), listed in [Testing](Testing.md). @@ -259,6 +318,450 @@ runner also reads guest 1's RAM over SWD to confirm the sentinel never landed and that the core is not parked in a fault handler, and logs the AHBSC0 violation latches for reference. +## Guest flash write protection + +The STM32H563 protects guest flash with persistent WRP option bytes. This part +has none; the equivalent is the XSPI Secure Flash Protection fabric, whose +region descriptors (FRADs) are programmed by wolfBoot on every boot and locked +until the next hard reset. With the guest fence armed, wolfBoot's descriptor +layout is: + +| FRAD | Range | Writable | +| --- | --- | --- | +| 0 | boot root, `0x28000000`-`0x2803FFFF` | no | +| 1 | Secure image, `0x28040000`-`0x2807FFFF` | yes | +| 2 | guest windows, `0x28080000`-`0x2813FFFF` | no (the guest fence) | +| 3 | update, swap, and storage, `0x28140000`-end of NOR | yes | +| 4-7 | unused | locked invalid | + +The fence refuses writes from every initiator, the Secure runtime included, so +guest images are installed before wolfBoot arms it and a scenario that +deliberately rewrites guest flash (`remeasureneg`) runs unfenced. The fence +bounds come from the same `WT_GUEST*_FLASH_*` values the wolfTrust build uses +(`tests/target/lib/rt700_fence.sh`), and the build refuses a layout whose guest +windows are not contiguous and 64 KiB aligned. + +Build wolfTrust with `WT_GUEST_FLASH_WRP=1` and every required launch checks, +from the registers alone, that the SFP configuration is valid and sealed, the +initiator domain descriptor is valid and locked, and valid, hard-reset-locked, +write-denying descriptors cover the whole guest window with no write-granting +or unlocked descriptor overlapping it. Anything less refuses the launch. The +runners set this up per scenario: + +```sh +tests/target/run_rt700_hardware.sh wrpfence # fence armed: both guests run +tests/target/run_rt700_hardware.sh wrpoff # no fence: both guests refused +tests/target/run_rt700_hardware.sh wrpneg # the silicon refuses a fenced erase +TARGET=mimxrt700 tests/target/provisioning/provisioning_ctrl.sh verify-wrp +``` + +Because the fence is rebuilt on every boot and cleared by every reset, there is +no `set-wrp` or `clear-wrp` step: the probe always flashes a parked core with +the controller unfenced, and which wolfBoot is flashed decides the posture. + +## Provisioning and life cycle + +This section is the MIMXRT700 part of [Provisioning](Provisioning.md). That +page covers the shared flow (rehearse, validate, then lock), the command +table, the gates, and what a production lock does to the firmware. Here are +the MIMXRT700 states, the mock lock on the EVK, and the real lock, with output. + +The life cycle lives in OTP fuses (`LC_STATE` and its redundant copy +`LC_STATE_RED`). Programming a fuse is permanent, and on this EVK +`LOCK_CFG3.LIFE_CYCLE_LOCK` is open, so nothing in silicon would stop it. Each +state therefore has two commands: + +- **`advance `** is the mock lock. It moves the life cycle only in the + OTP shadow registers, which every hardware reset reloads from the fuses. + `regress` resets the part back. +- **`lock `** is the real lock. It burns the fuses. + +| `LC_STATE` | NXP state | Nearest STM32H5 state | PSA life cycle wolfBoot hands wolfTrust | +| --- | --- | --- | --- | +| `0x03` | Develop (as the EVK ships) | Open | `0x1000` ASSEMBLY_AND_TEST | +| `0x07` | Develop2 | Provisioning | `0x2000` PSA_ROT_PROVISIONING | +| `0x0F` | In Field | Closed | `0x3000` SECURED with `DAUTHSTATUS` `0xAA`; `0x5000`/`0x4000` while debug is open; `0x0000` for any other encoding | +| `0xCF` | In Field Locked | Locked | as In Field | +| `0x1F` | In Field Return | none | `0x6000` DECOMMISSIONED | +| other, or copies disagree | NXP Blank, Fab, FA, Dev, Bricked, or corrupt | none | `0x0000` UNKNOWN | + +The state names are NXP's, as SPSDK's life cycle check uses them. The STM32H5 +column is only an orientation, because the two machines differ: +- The STM32H5 has a TrustZone Closed state and returns Closed parts to Open by + regression. +- The MIMXRT700 has no TrustZone Closed. The life cycle is a thermometer code: + each step only adds fuse bits. +- In Field Return is a one-way failure-analysis state, not a return to Develop. + +All commands below use the single entry point with `TARGET=mimxrt700`: + +```sh +export TARGET=mimxrt700 +``` + +### Stage 1: prepare the production chain + +Flash the production posture (the fenced wolfBoot and a `WT_GUEST_FLASH_WRP=1` +wolfTrust, verified by the `wrpfence` checks), then run the read-only +preflight: + +```sh +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh restore +tests/target/provisioning/provisioning_ctrl.sh verify-wrp +tests/target/provisioning/provisioning_ctrl.sh discover +``` + +Output on the EVK: + +```text + [check] PASS XSPI SFP configuration valid and sealed until reset + [check] PASS a locked, write-denying FRAD spans the guest windows (0x28080000-0x28140000) + [check] PASS launch-verified guest mask 0x00000003 (want 0x00000003) + [check] PASS launch-refused guest mask 0x00000000 (want 0x00000000) + [check] PASS guest0 done: FF-M connect verified, status 0x600D600D (600d600d) + [check] PASS guest1 done: FF-M connect verified, status 0x600D600D (600d600d) +PASS: hardware/wrpfence + [check] PASS guest fence armed FRAD2 acp=0x00000000 word3=0xa0000000 + [check] PASS SPSDK shadowregs supports mimxrt798s + [check] PASS fused life cycle is Develop and its redundant copy agrees + [check] PASS life cycle shadow over-ride is open (LOCK_CFG3 0x00000000) + [check] PASS the boot handoff life cycle is readable (0x00001000, ASSEMBLY_AND_TEST) +PASS: discovery stamped (~/.cache/wolftrust/mimxrt700/discovery) +``` + +`discover` gates `advance`. It requires the fused life cycle copies to agree, +and the shadow override to be open. + +### Stage 2: rehearse a state (mock lock) + +`advance` puts the part in the target state until the next reset, and +`regress`, after the validation in stage 3, brings it back. Together they are +the rehearsal that the real `lock` for that state requires. Nothing here is +permanent. Rehearse one state at a time: `advance 0x07`, stage 3, then the +same for `0x0F` and `0xCF`. + +```sh +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh advance 0x07 +``` + +Output on the EVK for Develop2: + +```text +ADVANCING the life cycle shadow to 0x07 (develop2); regress or any reset undoes it +halted in wolfBoot at 0x28005910; shadow LC_STATE=0x07 LC_STATE_RED=0x07 +wolfTrust saw 0x00002000 (PSA_ROT_PROVISIONING) + [check] PASS wolfTrust booted with the develop2 life cycle + [check] PASS guests launched (verified=0x00000003 refused=0x00000000) + [check] PASS the images on the part match the host build (8841d566395ee97b) +rehearsal of develop2 (0x07) recorded; 'regress' completes it +``` + +How `advance` works: +1. It halts the core inside wolfBoot, after the ROM has loaded the shadows and + before wolfBoot reads them. +2. It writes both copies and resumes. +3. It checks that wolfTrust received the matching PSA life cycle. It then + waits until every guest in `RT700_GUEST_MASK` (default `0x3`, both guests) + has launched verified with none refused. In Field Return (`0x1F`) only + needs the life cycle. +4. It reads the four flashed images back over SWD and compares them with the + host build: the wrapped wolfBoot, the signed wolfTrust image, and both + guests. +5. Only then does it record the rehearsal. The record holds the fused state, + the SHA-256 of those four images, whether the guest fence was armed, and + the time. `lock` accepts only a rehearsal with the fence armed, so rehearse + the fenced chain that `restore` flashes. + +Past Develop2, `advance` also requires a proven `regress`. That is a hardware +reset through the board's reset line, which the debug port cannot block. + +### Stage 3: validate the rehearsed state, then regress + +While the part is still in the mock state, check that it behaves like the +product you intend to ship, then `regress` to complete the rehearsal. Output +on the EVK after `advance 0xCF`, the mock locked state: + +```text +$ tests/target/provisioning/provisioning_ctrl.sh status +OTP life cycle LC_STATE=0xcf (in-field-locked) LC_STATE_RED=0xcf +LOCK_CFG3 0x00000000 (LIFE_CYCLE_LOCK=0: 0 = shadow override and fuse burn both open) +DAUTHSTATUS 0x000000ff +XSPI SFP MGC=0xa8000400 TG0MDAD=0xa000c000 +guest fence armed FRAD2 acp=0x00000000 word3=0xa0000000 +wolfTrust saw 0x00005000 (RECOVERABLE_PSA_ROT_DEBUG) +guest launches verified=0x00000003 refused=0x00000000 + +$ WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh regress + [check] PASS hardware reset reloaded the fused develop life cycle + [check] PASS wolfTrust booted ASSEMBLY_AND_TEST again + [check] PASS rehearsal of in-field-locked (0xCF) complete +``` + +Check four things: +- the life cycle copies agree; +- the guest fence is armed; +- both guests launched verified; +- wolfTrust received the life cycle you expect. + +A shadow-only advance cannot close debug, because debug enablement is decided +from the fuses at boot. That is why this EVK attests `0x5000` rather than +`0x3000`. SECURED proper needs a part whose fuse configuration closes debug. + +A refused command changes nothing and exits with status 2: + +```text +$ WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh advance 0x0F +REFUSED: prove 'regress' from Develop2 before advancing to 0x0F. +$ tests/target/provisioning/provisioning_ctrl.sh burn +REFUSED: 'burn' programs OTP fuses, which is permanent on the MIMXRT700 +``` + +### Stage 4: production lock + +> **Production only. IRREVERSIBLE.** `lock` burns OTP fuses. A burned fuse can +> never be cleared, so a locked part never returns to an earlier life cycle +> state, and the software on it stays bound to the keys you locked it with. +> Never run `lock` on a development EVK: `LOCK_CFG3` is open on it, so nothing +> in silicon would stop the burn. + +**What each state does to the part:** + +- **Develop2 (`0x07`)** is the first production state. The part keeps + development behaviour and can still move on, but never returns to Develop. +- **In Field (`0x0F`)** is the shipping state. From here on the BootROM + applies the policy burned in the fuses: + - debug access follows the fused debug configuration; + - the root key table hash (RKTH) decides which signed first-stage images + the ROM accepts. + + wolfTrust enforces guest rollback floors, never reformats the vault, and + attests the new life cycle. See + [what a production lock does to the firmware](Provisioning.md#what-a-production-lock-does-to-the-firmware). +- **In Field Locked (`0xCF`)** is final. No further life cycle step exists, + including the field-return path. +- **In Field Return (`0x1F`)**, reached only from In Field, is also final. It is + for failure analysis. + +**How it binds the software.** After the lock, the software can only change +through wolfBoot's signed update path. It is held in place by: +- the fused RKTH; +- the wolfBoot signing key; +- the guest measurement records. + +See [how a production lock binds the software](Provisioning.md#how-a-production-lock-binds-the-software). + +> **Prerequisites not yet in the port, and enforced.** The reference chain boots +> wolfBoot as a plain XIP image that the BootROM does not authenticate. Locking +> the life cycle to In Field without ROM authentication would leave the first +> stage replaceable. So `lock` refuses In Field, In Field Locked, and In Field +> Return until the port builds wolfBoot as a ROM-signed image under a fused +> RKTH. Develop2 is the only burnable step today. + +`lock` burns one step at a time, and only the next one: + +| Command | Runs only when the fuses read | Burns | Also needs | +| --- | --- | --- | --- | +| `lock develop2` (`0x07`) | develop `0x03` | Develop2 | a fresh rehearsal of `0x07` with the current images and the guest fence armed; `WT_FIXTURE_BOUND=1` | +| `lock in-field` (`0x0F`) | develop2 `0x07` | In Field | **refused until ROM authentication** | +| `lock in-field-locked` (`0xCF`) | in-field `0x0F` | In Field Locked (final) | **refused until ROM authentication** | +| `lock in-field-return` (`0x1F`) | in-field `0x0F` | In Field Return (final) | **refused until ROM authentication** | + +The burn runs over the ISP USB link, and no chip identity is documented that +both the SWD rehearsal and ISP can read. So every RT700 burn needs +`WT_FIXTURE_BOUND=1`, set only on a fixture that wires the debug probe and ISP +USB to one socket. On top of [the shared gates](Provisioning.md#the-lock-gates), +`lock` checks these on this port: +- It reads the life cycle from the burned fuses over the ISP connection, not + from the shadows that `advance` changes. +- The life cycle words must hold nothing above the state byte, which is the B0 + layout. A0/A1 silicon keeps a bit-protection copy of each byte in bits + 16-23; that burn encoding is not validated, so `discover` and `lock` refuse + those parts. +- It needs a rehearsal of that exact state, with the SHA-256 of the four + images the runner flashes, from a fused state earlier in the ladder. The + rehearsal read those images back from the part; the burn runs over ISP and + does not read them again. Develop2 leaves the flash writable after the burn, + so this binds nothing that a later reflash could not change. A burn into + In Field or later will need a live read-back at burn time. +- The rehearsal must have run through the same debug probe that is attached + now, and that probe must be the only one attached. The EVK's MCU-Link is + soldered to the board, so this binds the record to the board; on a + production fixture with its own probe, it binds the record to the station. +- The rehearsal must be recent: at most `WT_REHEARSAL_MAX_AGE` seconds old, + one hour by default, and not dated in the future. No silicon UID is + documented to bind a record to the part itself. On a fixture, the probe plus + a fresh, single-use rehearsal stands in for that binding: rehearse the part + in the fixture right before its own burn. +- Each burn uses its rehearsal up. + +The steps below mix three kinds of output: +- The rehearsal output above was captured on the EVK. +- The preview and prompt come from the same script run offline against a + stubbed `blhost`. +- The burn has never been run on a wolfTrust board, so its output is marked + as expected. + +1. **Rehearse and validate the step on this part**, as in stages 2 and 3, + right before the burn. + +2. **Preview the burn.** Put the part in ISP mode. Without `WT_LOCK_CONFIRM=1`, + `lock` runs every check, prints the exact blhost script, and writes nothing: + + ```sh + RT700_ISP='-u 0x1fc9,0x014f' tests/target/provisioning/provisioning_ctrl.sh lock develop2 + ``` + + ```text + Lock step: develop (0x03) -> develop2 (0x07) + checked: next state, rehearsal of develop2 (0x07) with images 8841d566395ee97b (240s ago), part identity not readable here (needs WT_FIXTURE_BOUND=1), same debug probe 2GMGHYXZEONQS + will run: blhost -u 0x1fc9,0x014f efuse-program-once 0x25 00000007 --no-verify + will run: blhost -u 0x1fc9,0x014f efuse-program-once 0x8F 00000007 --no-verify + REFUSED: preview only, nothing was written. A production station re-runs this with WT_LOCK_CONFIRM=1. + ``` + + Each line is a permanent fuse write. `lock` burns only the two life cycle + words, the redundant copy first, each exactly the requested state. After + the burn it requires the low byte of both words to be the new state, with + the upper bits unchanged. + + > **Not yet burnable: the root key hash and debug root.** `lock` refuses a + > fuse configuration file. The burn runs over the ISP USB link, and no chip + > identity is documented that both the SWD rehearsal and ISP can read, so + > nothing would stop such a file being burned into a different part than the + > one rehearsed. + + > **Fixture required for the burn.** The same gap applies to the life + > cycle step: the rehearsal is bound to the debug probe, but the burn goes + > over ISP USB. `lock` therefore burns only with `WT_FIXTURE_BOUND=1`, + > which a station sets only on a fixture whose single socket wires both the + > probe and ISP USB to the part. Without it, `lock` stops after the preview. + +3. **Burn it**, on the production station only: + + > **Warning:** this step is permanent. After it, the part can never return + > to Develop. + + ```sh + export WT_PRODUCTION_LOCK=1 WT_FIXTURE_BOUND=1 RT700_ISP='-u 0x1fc9,0x014f' + WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh lock develop2 + ``` + + After the same preview it asks, and only a person at a terminal typing the + acceptance exactly continues: + + ```text + !!! Burning life cycle Develop2 (0x07) into this MIMXRT700's fuses + !!! This is IRREVERSIBLE: fuses cannot be unburned, and the part never returns to Develop. + !!! Are you sure? Type "I ACCEPT 0x07" to continue: I ACCEPT 0x07 + ``` + + Expected output: blhost prints one status per fuse. `lock` then reads both + life cycle fuses back, and fails unless they carry the new state. + + ```text + Response status = 0 (0x0) Success. + Response status = 0 (0x0) Success. + [check] PASS life cycle fuses are Develop2 (0x07); reset the part, then run: tests/target/provisioning/provisioning_ctrl.sh status + ``` + + The two life cycle copies are separate fuse words. A burn interrupted + between them leaves them disagreeing, which wolfBoot reports as UNKNOWN, so + keep the station powered and the ISP link stable. + +4. **Verify.** Reset the part and run `status`. Expected: both life cycle + copies at the burned value, the guest fence armed, both guests verified, + and wolfTrust receiving `0x2000`: + + ```text + OTP life cycle LC_STATE=0x07 (develop2) LC_STATE_RED=0x07 + guest fence armed FRAD2 acp=0x00000000 word3=0xa0000000 + wolfTrust saw 0x00002000 (PSA_ROT_PROVISIONING) + guest launches verified=0x00000003 refused=0x00000000 + ``` + + Then run the production image's hardware scenarios. + +A refused `lock` exits with status 2 and burns nothing. Refusals seen on the +EVK: + +```text +$ tests/target/provisioning/provisioning_ctrl.sh lock 0x0F +REFUSED: In Field (0x0F) needs the BootROM to authenticate wolfBoot (a signed image under the fused root key hash), which this port does not build yet; see the MIMXRT700 Guide. +$ tests/target/provisioning/provisioning_ctrl.sh lock 0x07 +REFUSED: set RT700_ISP to the blhost ISP connection (for example '-u 0x1fc9,0x014f'). +$ RT700_ISP='-u 0x1fc9,0x014f' tests/target/provisioning/provisioning_ctrl.sh lock 0x07 +REFUSED: cannot read the life cycle fuses over RT700_ISP (-u 0x1fc9,0x014f). +``` + +Refusals from the offline gate tests: + +```text +REFUSED: the life cycle fuses disagree (LC 0x00000003, RED 0x00000007). +REFUSED: no rehearsal for develop2 (0x07) on this part with these images and credentials in the last 3600s: run 'advance 0x07' and 'regress' first. +REFUSED: confirmation did not match; nothing was changed. +``` + +Field returns go to In Field Return through NXP's debug credential flow. That +needs the debug credential root fused and a validated credential chain. +wolfTrust attests In Field Return as DECOMMISSIONED. + +### Verified on the EVK + +Every command was run on a MIMXRT700-EVK (fused Develop, `LOCK_CFG3` `0x0`) +on 2026-09-30, from an empty provisioning state directory, 31 steps in one +session. Commands that refuse exit with status 2 and change nothing: + +| Command | Result | +| --- | --- | +| `set-perimeter`, `set-wrp`, `clear-wrp` | explains there is no persistent RT700 form, points at `restore` and `verify-wrp` | +| `provision-da`, `burn` | refused: fuse programming is permanent | +| `restore`, `regress`, `advance` without `WT_LOCK_CONFIRM=1` | refused before touching the board | +| `advance 0x5C`, `advance 0xFF`, `advance junk` | refused: only `0x07`, `0x0F`, `0xCF`, `0x1F` | +| `advance 0x07` before `discover` | refused: run `discover` first | +| `advance 0x0F` before a proven `regress` | refused | +| `restore` | fenced chain built, flashed, read back; both guests complete | +| `verify-wrp` | `armed FRAD2 acp=0x00000000 word3=0xa0000000` | +| `discover` | four checks pass; stamps the preflight | + +`status` in each state (`MGC=0xa8000400`, `TG0MDAD=0xa000c000`, and +`DAUTHSTATUS=0x000000ff` throughout): + +| State | `LC_STATE` / `LC_STATE_RED` | Handoff life cycle | Guest fence | Guests verified / refused | +| --- | --- | --- | --- | --- | +| fused, after `restore` | `0x03` / `0x03` | `0x1000` ASSEMBLY_AND_TEST | armed (FRAD2) | `0x3` / `0x0` | +| `advance 0x07` | `0x07` / `0x07` | `0x2000` PSA_ROT_PROVISIONING | armed | `0x3` / `0x0` | +| `advance 0x0F` | `0x0F` / `0x0F` | `0x5000` RECOVERABLE_PSA_ROT_DEBUG | armed | `0x3` / `0x0` | +| `advance 0xCF` (mock locked) | `0xCF` / `0xCF` | `0x5000` RECOVERABLE_PSA_ROT_DEBUG | armed | `0x3` / `0x0` | +| after each `regress` | `0x03` / `0x03` | `0x1000` ASSEMBLY_AND_TEST | armed | `0x3` / `0x0` | + +Each `advance` halted the core inside wolfBoot (`pc` between `0x28004f6a` and +`0x28005394` across runs), wrote both copies, and read them back before +resuming. In the mock locked state the device runs its production posture, with +the fence armed and both guests launched, while the attestation stays below +SECURED because debug is open. Each `regress` restored the fused life cycle +through the reset line. + +The rehearsal that `lock` requires was run on the same EVK for every state, +each `advance` followed by `regress`, with the fenced production chain: + +| Rehearsal | Handoff life cycle | Guests verified / refused | Record | +| --- | --- | --- | --- | +| `advance 0x07`, `regress` | `0x2000` PSA_ROT_PROVISIONING | `0x3` / `0x0` | `fused=0x03 fence=armed` | +| `advance 0x0F`, `regress` | `0x5000` RECOVERABLE_PSA_ROT_DEBUG | `0x3` / `0x0` | `fused=0x03 fence=armed` | +| `advance 0xCF`, `regress` | `0x5000` RECOVERABLE_PSA_ROT_DEBUG | `0x3` / `0x0` | `fused=0x03 fence=armed` | +| `advance 0x1F`, `regress` | `0x6000` DECOMMISSIONED | not required | `fused=0x03 fence=armed` | + +`lock 0x07` then refused without `RT700_ISP`, and with it refused because the +EVK was not in ISP mode, so the fuses could not be read. Nothing was burned. + +After the security review the rehearsal was run again with the stricter +checks: +- `advance 0x07` required `verified=0x3 refused=0x0`. +- It read the four flashed images back and matched the host build. +- It recorded the full image SHA-256 with a timestamp. + +`lock 0x0F` then refused, because the first stage is not ROM-authenticated. + ## Recovery rules - If the BootROM does not run the image, confirm the FCB is present and the @@ -292,6 +795,15 @@ violation latches for reference. HardFault, which currently stops the whole system rather than the one guest. - Never reuse another NXP part's FCB, clock, or pin table without checking its reference manual and NOR geometry. +- The guest fence and a shadow life cycle both end at the next hard reset, so a + board can never be left stuck protected or advanced: flash from a parked core, + or run `TARGET=mimxrt700 tests/target/provisioning/provisioning_ctrl.sh regress`. +- With an advanced shadow life cycle live, the device-pack reset sequence can + fail with a FAULT ACK; `regress` resets through the board's reset line, which + the debug port cannot block, and restores the fused state. +- Never program a life cycle, debug credential, or root key fuse on a + development board. `LOCK_CFG3` is open on the EVK, so the silicon will not + stop a burn, and none of it can be undone. See [Porting](Porting.md) for the generic port contract, [Testing](Testing.md) for scenario selection, and [Security Model](Security-Model.md) for the policy diff --git a/docs/Macros.md b/docs/Macros.md index 9a8eeb6f..0e1a619a 100644 --- a/docs/Macros.md +++ b/docs/Macros.md @@ -30,7 +30,7 @@ selected values into C preprocessor defines. Defaults below come from | Define | Description | Requirement | | --- | --- | --- | -| `WT_GUEST_FLASH_WRP` | When `1`, verify full STM32 guest-window WRP coverage before launch; default `0`. | Set to `1` for the hardened STM32H563 image and provision WRP after flashing. M33MU does not model WRP. | +| `WT_GUEST_FLASH_WRP` | When `1`, verify full hardware write protection of each guest window before launch (STM32 WRP groups, or MIMXRT700 locked XSPI flash region descriptors); default `0`. | Set to `1` for hardened images: provision STM32H563 WRP after flashing, or boot the MIMXRT700 fenced wolfBoot. M33MU models the MIMXRT700 descriptors but not STM32 WRP. | | `WT_ENGINE_HSM` | Legacy engine selector; unset by default. `0` maps to `WT_ENGINE=native` and `1` maps to `WT_ENGINE=hsm` when the public selector is not supplied. The build also derives this internal value from `WT_ENGINE`. | Prefer `WT_ENGINE` for new builds and do not supply conflicting selectors. The guest and Secure image must select the same engine. | | `WT_ATTEST_COSE` | Must remain `1` in the current STM32H563 reference build; default `1`. The `0` configuration does not compile because the reset path still references attestation-gated handoff variables. | Requires the wolfCOSE submodule and the configured attestation key backend. | | `WT_WOLFCRYPT_SP_ASM` | Enable wolfCrypt SP Cortex-M assembly; default `1`. | Requires compatible Armv8-M assembly sources and toolchain. | diff --git a/docs/Provisioning.md b/docs/Provisioning.md new file mode 100644 index 00000000..02219bd9 --- /dev/null +++ b/docs/Provisioning.md @@ -0,0 +1,227 @@ +# Provisioning + +Provisioning takes a wolfTrust part from development to production. You flash +the production images, set the protections the part ships with, and move its +life cycle forward until debug is closed and the part can no longer be +reflashed from outside. The last steps are permanent. + +One script does this on every port, with the same commands: + +```sh +TARGET= tests/target/provisioning/provisioning_ctrl.sh [state] +``` + +`TARGET` is `stm32h563` (the default) or `mimxrt700`. `help` lists the +commands and the port's states. Only the state codes and a few device commands +differ between ports. This page covers the shared flow; each port guide covers +its states, quirks, and a walkthrough with real output: + +- [STM32H5 Guide: Provisioning and product state](STM32H5-Guide.md#provisioning-and-product-state) +- [MIMXRT700 Guide: Provisioning and life cycle](MIMXRT700-Guide.md#provisioning-and-life-cycle) + +> **Production locks are permanent.** `lock` is for a production station and a +> part you intend to ship, never a development board. Every step before it is +> reversible: a mock that a reset or a regression undoes. + +## The flow + +Every port follows the same four stages, one manual command at a time: + +| Stage | Command | What it does | +| --- | --- | --- | +| 1. Prepare | `restore`, `status`, `discover` | flash the production images, read the part, run the preflight | +| 2. Rehearse | `advance ` | enter the state as a reversible mock and record what the part showed | +| 3. Validate, then return | `status` in the mock state, then `regress` | check the part behaves like the product you will ship, then return it and complete the rehearsal | +| 4. Lock | `lock ` | make that one state permanent, after a preview and a typed acceptance | + +A state is given by its code or its name: `lock 0x72` or `lock closed` on the +STM32H5, `lock 0x07` or `lock develop2` on the MIMXRT700. + +### 1. Prepare + +```sh +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh restore +tests/target/provisioning/provisioning_ctrl.sh status +tests/target/provisioning/provisioning_ctrl.sh discover +``` + +Build the production images first, with production signing keys and the guest +flash protection on (`WT_GUEST_FLASH_WRP=1`). Every command that writes to the +board needs `WT_LOCK_CONFIRM=1`. + +### 2. Rehearse (the mock lock) + +```sh +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh advance +``` + +`advance` puts the part in `` in a way that can be undone. It records a +pending rehearsal only if the part shows the evidence the port asks for: the +firmware booted in that state, the images read back match the build, and the +part's identity. Leave the part in the mock state for stage 3. + +### 3. Validate, then return + +```sh +tests/target/provisioning/provisioning_ctrl.sh status +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh regress +``` + +While the part is in the mock state, check it is the product you intend to +ship: `status`, the guests running, and the attestation token's life cycle. +What the part does here is what it will do once the state is permanent. Then +`regress` takes the part back and completes the rehearsal. A rehearsal is +bound to the part, the images, any credentials it used, and the hour it was +made in; `lock` refuses without one. Repeat stages 2 and 3 for each state you +will lock. + +### 4. Lock + +```sh +tests/target/provisioning/provisioning_ctrl.sh lock +``` + +Without `WT_LOCK_CONFIRM=1`, `lock` is a preview: it runs every check, prints +the exact write, and changes nothing. On a production station: + +```sh +export WT_PRODUCTION_LOCK=1 +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh lock +``` + +It previews again, then asks: + +```text +!!! Moving this to +!!! This is IRREVERSIBLE: +!!! Are you sure? Type "I ACCEPT " to continue: +``` + +Only a person at a terminal typing the acceptance exactly continues. Each +`lock` writes one state; run it again for the next state. + +## The lock gates + +Every port runs the same gates, in this order, from one place in +`provisioning_ctrl.sh`. A failed gate exits with status 2 and writes nothing. + +1. **Next state only.** `lock` reads the part's real state (fuses or option + bytes, never the mock) and refuses anything but the next state. +2. **A rehearsal of this state** on this part, with these images and + credentials, completed in the last hour (`WT_REHEARSAL_MAX_AGE`). +3. **The same part.** If the port can read the part's identity in this state, + it must match the rehearsal. If it cannot, the step runs only with + `WT_FIXTURE_BOUND=1`, which a station sets only on a fixture that holds one + part from the rehearsal to the lock. +4. **Provisioned first.** The port's own checks, such as safe perimeter + values, guest flash protection, and production credentials. +5. **Preview** of the exact write. +6. **`WT_LOCK_CONFIRM=1`** and **`WT_PRODUCTION_LOCK=1`**. +7. **An interactive terminal**, never a pipe or script. +8. **The typed acceptance**, `I ACCEPT `. Right after it, `lock` reads + the state and the part again, repeats every rehearsal check (images, + credentials, part, age), and reruns the port checks, so nothing that changed + at the prompt is written. +9. **Read-back.** After the write, the new state must read back, or `lock` + fails and says what state the part is in. +10. **Single use.** A successful lock deletes the rehearsal of its own state, + and a permanent step deletes whatever rehearsal it used. One exception is + deliberate: the STM32H5 Provisioning step runs on the Closed rehearsal and + keeps it, because the closing step that follows needs it and a part in + Provisioning cannot be rehearsed again (its images are no longer + readable). That closing step then consumes it, within the same age limit + and on a fixture that holds the part (`WT_FIXTURE_BOUND=1`). + +These follow the vendors' own provisioning tools: +- NXP's Secure Provisioning tool offers a "Test life cycle" mode and lists + each irreversible operation before it writes. +- ST's `ROT_Provisioning` scripts provision keys and Debug Authentication + before the product state. +- TF-M advances its PSA life cycle only once provisioning is complete. + +`provisioning_ctrl.sh` adds the one-step rule, the bound rehearsal, and a +typed acceptance instead of a keypress. + +## What a production lock does to the firmware + +wolfBoot passes the life cycle to wolfTrust as a PSA life cycle value. Past +`0x2000` PSA_ROT_PROVISIONING, wolfTrust stops treating the part as a +development board: + +- **Rollback floors are enforced.** A guest image older than its recorded + floor is refused at launch. +- **The vault is never reformatted.** The sealed device key and write-once + storage survive; a damaged store stops boot provisioning instead of being + wiped. +- **Attestation reports the life cycle,** so a relying party can tell a + production part from a development one: `0x3000` SECURED once debug is + closed, `0x4000` or `0x5000` while some debug is open. +- **Guest flash protection stays in force** at every launch: the STM32H5 WRP, + the MIMXRT700 XSPI fence. + +## How a production lock binds the software + +With debug closed, nothing outside the firmware can reflash the part. The +software then only changes through wolfBoot's signed update path +(`SERVICE_FWU`). These keys hold it in place for the life of the part: + +| Key | What it decides | +| --- | --- | +| wolfBoot signing key | which wolfTrust images wolfBoot boots and accepts as updates | +| guest measurement records (signed into the wolfTrust image) | which guests wolfTrust launches | +| MIMXRT700 root key table hash (fused) | which first-stage images the BootROM boots | +| STM32H5 Debug Authentication certificate chain | whether a part short of Locked can be regressed | + +Back these up before the first lock. A lost signing key means the parts locked +with it can never be updated; a leaked one means they trust whoever holds it. + +## PSA life cycle by port + +| PSA life cycle | STM32H5 product state | MIMXRT700 life cycle | +| --- | --- | --- | +| `0x1000` ASSEMBLY_AND_TEST | open `0xED` | develop `0x03` | +| `0x2000` PSA_ROT_PROVISIONING | provisioning `0x17` | develop2 `0x07` | +| `0x4000` NON_PSA_ROT_DEBUG | tz-closed `0xC6` | in-field, only Non-secure debug open | +| `0x5000` RECOVERABLE_PSA_ROT_DEBUG | closed, Secure debug open | in-field, Secure debug open | +| `0x3000` SECURED | closed `0x72`, locked `0x5C` | in-field `0x0F`, in-field-locked `0xCF` | +| `0x6000` DECOMMISSIONED | none | in-field-return `0x1F` | +| `0x0000` UNKNOWN | any other value | copies disagree, or an NXP-internal state | + +## Environment + +| Variable | Meaning | +| --- | --- | +| `TARGET` | the port: `stm32h563` (default) or `mimxrt700` | +| `WT_LOCK_CONFIRM=1` | allow a board write; without it `lock` only previews | +| `WT_PRODUCTION_LOCK=1` | marks a production station; `lock` never writes without it | +| `WT_FIXTURE_BOUND=1` | the fixture holds one part from rehearsal to lock; needed where the part's identity cannot be read | +| `WT_PROVISION_STATE` | where rehearsal records live (default `~/.cache/wolftrust`, one folder per port) | +| `WT_REHEARSAL_MAX_AGE` | seconds a rehearsal stays valid (default `3600`) | +| `WT_DA_OBK`, `WT_DA_KEY`, `WT_DA_CERT`, `WT_DA_PWD` | STM32H5 Debug Authentication inputs; a production lock requires all four, none of them ST's sample | +| `STM32_CLI`, `H5_SERIAL` | STM32H5: STM32CubeProgrammer CLI and the board UART | +| `RT700_ISP`, `RT700_SPSDK_VENV`, `RT700_GUEST_MASK` | MIMXRT700: blhost ISP connection, SPSDK environment, guests a rehearsal must launch | + +## Adding a port + +A port is one file, `tests/target/provisioning/provisioning_ctrl_.sh`, +that answers device questions through a fixed set of functions: + +| Function | Answers | +| --- | --- | +| `port_ladder` | the states: code, name, whether it has a mock, whether it can be locked, permanent or reversible, and the state it is reached from | +| `port_status`, `port_discover`, `port_restore` | read the part, preflight, put the production images back | +| `port_advance_check`, `port_advance`, `port_booted`, `port_regress` | enter and leave a mock state, and the evidence a rehearsal records | +| `port_lock_current`, `port_lock_identity` | the real state, and the part's identity where it can be read | +| `port_image_digest`, `port_cred_fp`, `port_record_ok` | what binds a rehearsal to these images, credentials, and part | +| `port_rehearsal_for`, `port_ready` | which rehearsals count, and the port's provisioned-first checks | +| `port_lock_plan`, `port_lock_write`, `port_lock_verify`, `port_consequence` | the permanent write: preview, do, read back, and what it costs | +| `port_usage_extra`, `port_extra` | device-only commands | + +The gates, records, and prompts stay in `provisioning_ctrl.sh`, so a new port +cannot weaken them. + +## Testing + +`make test` runs `make test-provisioning`: every gate of both ports against +stub vendor tools, from the generic refusals to the typed acceptance. Nothing +touches a board; the typed-acceptance cases need `expect` and skip without it. diff --git a/docs/STM32H5-Guide.md b/docs/STM32H5-Guide.md index 94fbb5f7..235a77d8 100644 --- a/docs/STM32H5-Guide.md +++ b/docs/STM32H5-Guide.md @@ -36,7 +36,7 @@ Overrides: Read the product state and Secure watermarks: ```sh -tests/target/provisioning_ctrl.sh status +tests/target/provisioning/provisioning_ctrl.sh status tests/target/h5_lock_preflight.sh ``` @@ -69,7 +69,7 @@ the build and flash addresses stay paired. ## TrustZone perimeter -`tests/target/provisioning_ctrl.sh set-perimeter` programs the +`tests/target/provisioning/provisioning_ctrl.sh set-perimeter` programs the reference option bytes: | Option | Value | Purpose | @@ -86,7 +86,7 @@ Changing `TZEN` can mass-erase the device. The command requires an explicit write confirmation: ```sh -WT_LOCK_CONFIRM=1 tests/target/provisioning_ctrl.sh set-perimeter +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh set-perimeter ``` Do not copy these values to another STM32H5 part without checking its reference @@ -107,7 +107,7 @@ TZ-Closed, Closed, and Locked, and RM0481 separately defines the them: ```sh -WT_LOCK_CONFIRM=1 tests/target/provisioning_ctrl.sh set-wrp +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh set-wrp ``` Read back the live value: @@ -125,7 +125,7 @@ To reflash with the supported helper workflow, keep the device Open and clear WRP: ```sh -WT_LOCK_CONFIRM=1 tests/target/provisioning_ctrl.sh clear-wrp +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh clear-wrp ``` Then flash and reapply WRP before allowing a hardened image to launch. The @@ -167,45 +167,377 @@ every required host tool. It reports a skip when the CLI or serial VCP is absent and, when `lsusb` is available, when no ST-Link is detected. Without `lsusb`, a missing probe appears later as a flash failure. -## Reversible product-state flow +## Provisioning and product state + +This section is the STM32H5 part of [Provisioning](Provisioning.md). That page +covers the shared flow (rehearse, validate, then lock), the command table, the +gates, and what a production lock does to the firmware. Here are the STM32H5 +product states, the mock lock on the NUCLEO-H563ZI, and the real lock, with +output. Every output block below is from a NUCLEO-H563ZI run on 2026-09-30, +unless it says otherwise. + +| State | Value | Debug | Way back | PSA life cycle | +| --- | ---: | --- | --- | --- | +| Open | `0xED` | open | none needed | `0x1000` | +| Provisioning | `0x17` | open; the wolfTrust chain does not run | DA regression | `0x2000` | +| TrustZone Closed | `0xC6` | Secure side sealed | DA regression (mass erase) | `0x4000` | +| Closed | `0x72` | closed; the SWD link drops | DA full regression (mass erase) | `0x3000` | +| Locked | `0x5C` | closed forever | **none** | `0x3000` | + +Each state has two commands: + +- **`advance `** is the mock lock: it writes the product state, and a + Debug Authentication (DA) regression takes the part back to Open. `advance` + refuses Locked. +- **`lock `** is the real, gated production step. + +The STM32H5 has three quirks, all seen on the board: + +- **The closed states are written only from Provisioning.** Once a part is + TrustZone Closed, the debug link cannot write the next state. So TrustZone + Closed, Closed, and Locked are each reached directly from Provisioning, + which is also how ST's own provisioning script works. `advance` and `lock` + both refuse anything else. +- **A closed part drops the SWD link.** Its option bytes cannot be read, so the + script reads its state through DA discovery (`ST_LIFECYCLE_CLOSED`). The CLI + also reports `Unable to reconnect after setting the Option Bytes` after + every closing write. That is expected; the script judges the write by the + read-back, not by the CLI's exit status. +- **Provisioning does not run the wolfTrust chain**, even after a reset. The + boot proof in a rehearsal therefore comes from the closed states. + +### Stage 1: prepare the production chain + +Build the production images with production signing keys and +`WT_GUEST_FLASH_WRP=1`, then set the perimeter, flash, protect the guests, and +verify. `restore` does all four: -Product-state values used by the control script are: +```sh +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh restore +tests/target/provisioning/provisioning_ctrl.sh status +``` -| State | Value | -| --- | ---: | -| Open | `0xED` | -| Provisioning | `0x17` | -| TrustZone Closed | `0xC6` | -| Closed | `0x72` | -| Locked | `0x5C` | +```text +Setting wolfTrust OEM-iRoT perimeter: TZEN=0xB4 BOOT_UBE=0xB4 SWAP_BANK=0x0 SECWM1_STRT=0x0 SECWM1_END=0x4F SECWM2_STRT=0x0 SECWM2_END=0x7F +Option Bytes successfully programmed +Download verified successfully +Write-protecting guest flash (bank1 sectors 0x50-0x7F): WRPSGn1=0x000FFFFF + WRPSGn1 : 0xFFFFF (0x8000000) + [check] PASS wolfTrust chain boots on silicon +PASS: wolfTrust restored and booting + PRODUCT_STATE: 0xED (Open) + BOOT_UBE : 0xB4 (OEM-iRoT (user flash) selected) + TZEN : 0xB4 (Trust zone enabled) +``` + +### Stage 2: rehearse a state (mock lock) -Locked is permanent and the script refuses it. The reversible development -sequence is deliberately manual: +Enter Provisioning, provision the DA certificate, confirm discovery offers +Full Regression, then close the part: ```sh -tests/target/provisioning_ctrl.sh status -WT_LOCK_CONFIRM=1 tests/target/provisioning_ctrl.sh advance 0x17 -WT_LOCK_CONFIRM=1 tests/target/provisioning_ctrl.sh provision-da -tests/target/provisioning_ctrl.sh discover -WT_LOCK_CONFIRM=1 tests/target/provisioning_ctrl.sh advance 0x72 -WT_LOCK_CONFIRM=1 tests/target/provisioning_ctrl.sh regress -``` - -Provision the certificate-based Debug Authentication data in Provisioning. -`discover` performs read-only device discovery; it supplies neither the key nor -certificate and does not authenticate the certificate chain or validate the -regression action. Do not close the device unless the exact certificate chain -and permitted regression action have been validated in a controlled, -recoverable test. Regression performs a full mass-erase back to Open. - -After regression, rerun `set-perimeter`, rebuild and flash the complete chain -with the current hardware runner, reapply WRP, and rerun the positive checks. -Do not use `provisioning_ctrl.sh flash` or `restore` until its Guest 1 address is -changed from the stale `0x080C0000` value to the current `0x080E0000` layout. - -Every board-writing control command requires `WT_LOCK_CONFIRM=1`. -Review the exact current command in -`tests/target/provisioning_ctrl.sh` before execution. +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh advance 0x17 +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh provision-da +tests/target/provisioning/provisioning_ctrl.sh discover +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh advance 0x72 +``` + +```text + [check] PASS the images on the part match the host build (c1f89defe6238bcc) +ADVANCING product state 0xED -> 0x17 (regress is the only way back) +Option Bytes successfully programmed +Provisioning does not run the wolfTrust chain; a closed-state rehearsal proves the boot +now: 0x17 +Provisioning DA OBK (ST obk_provisioning.sh order): .../ROT_Provisioning/DA/Binary/DA_Config.obk +discovery: PSA lifecycle...................:ST_LIFECYCLE_PROVISIONING +discovery: ST provisioning integrity status:0xeaeaeaea +discovery: permission if authorized...........:(a/14) ==> Full Regression +discovery: permission if authorized...........:(b/12) ==> To TZ Regression +Debug Authentication: Discovery Success +ADVANCING product state 0x17 -> 0x72 (regress is the only way back) +Error: failed to reconnect after reset ! +Error: Unable to reconnect after setting the Option Bytes +now: ST_LIFECYCLE_CLOSED + [check] PASS the part reads back as closed (0x72) + [check] PASS wolfTrust chain boots in closed (0x72) +rehearsal of closed (0x72) recorded; 'regress' completes it +``` + +> **Warning:** only close the part when discovery shows integrity +> `0xeaeaeaea` and Full Regression. Without them, Closed cannot be regressed +> and the part is closed for good. `advance` checks this itself and refuses a +> closed state without them. + +`advance 0x17` reads the four images back over SWD while the part is still +Open: Provisioning closes Secure debug, so this is the last point where the +Secure images can be read. A closing `advance` requires that read-back for the +current images, and `flash` or `regress` discards it. `advance` also captures +the UART across the reset that the write causes. The two `[check]` lines +prove the part reached Closed (read back by DA discovery) and that the images +booted there, with debug closed. Only both together record the Closed +rehearsal. + +### Stage 3: validate, then regress + +In Closed, check the part behaves like the product: +- the boot check above passed; +- discovery reports `ST_LIFECYCLE_CLOSED`; +- the attestation token from the guests reports the life cycle you expect. + +`status` cannot read a closed part, and `lock` refuses one: + +```text +$ tests/target/provisioning/provisioning_ctrl.sh lock 0x5C +REFUSED: cannot read the product state over SWD. +``` + +Then regress, which mass-erases the part back to Open and completes the +rehearsal, and restore the chain: + +```sh +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh regress +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh restore +``` + +```text +DA certificate Full Regression -> Open (mass-erase): +SDMAuthenticate : 1634 : client : Authentication successful +Debug Authentication Success +state after regression: 0xED + [check] PASS wolfTrust chain boots on silicon +PASS: wolfTrust restored and booting +``` + +The rehearsal binds to one physical part: +- `advance 0x17` runs in Open and reads several things back: the four images + over SWD with the core held in reset (the running chain hides guest flash + from the debugger), the 96-bit device UID (`UID_BASE`, `0x08FFF800`, readable only in + Open), and the values of the perimeter and guest WRP option bytes. A + closing `advance` requires that read-back for the current images, from the + last hour. +- `regress` reads the UID again after the mass erase, and records the + regression only if it is the same part. +- The records also hold a fingerprint of every DA input the regression used: + key, certificate chain, OBK, and password. + +The records from the board, with the first 16 hex digits of each digest: + +```text +h5-booted-0x72: image=c1f89defe6238bcc... uid=002100453332511238363236 ob=dd12796198f30eac... time=1790874603 +h5-regressed-0x72: image=c1f89defe6238bcc... da=0b5e16e7754c68c1... uid=002100453332511238363236 ob=dd12796198f30eac... time=1790874619 +``` + +> **Warning:** rehearse with the production DA chain (`WT_DA_OBK`, +> `WT_DA_KEY`, `WT_DA_CERT`), not ST's sample. A production `lock` refuses +> ST's sample, and accepts only a rehearsal whose regression used the same +> certificate chain it is about to rely on. The run above used ST's sample, as +> a development board does. + +One Closed rehearsal covers `lock 0x17`, `lock 0x72`, and `lock 0x5C`. For +`lock 0xC6`, rehearse with `advance 0xC6` in place of `advance 0x72`. + +Refused commands change nothing and exit with status 2: + +```text +$ tests/target/provisioning/provisioning_ctrl.sh lock 0x72 +REFUSED: the part is Open (0xED); lock 0x72 runs only from Provisioning (0x17). +$ WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh advance 0x72 +REFUSED: advance to Closed runs only from Provisioning (0x17); state=0xED. +``` + +### Stage 4: production lock + +> **Production only.** `lock` moves a production part up the product-state +> ladder for good. +> +> - Locked (`0x5C`) is **IRREVERSIBLE**. Debug closes forever, with no DA +> regression, no mass erase, and no reflash. wolfBoot and its signing key +> stay fixed for the life of the part. +> - Provisioning, TrustZone Closed, and Closed only come back through a DA +> chain you have proven can regress the part, and that regression +> mass-erases it. Without such a chain they are permanent too. +> +> Never lock a development board. + +**What each state does to the part.** +- **Provisioning** is where the DA certificate is provisioned. +- **TrustZone Closed** seals the Secure side. +- **Closed** closes debug and drops the SWD link. Your certificate chain can + still regress it. +- **Locked** closes debug for good. + +From TrustZone Closed on, wolfTrust enforces guest rollback floors, never +reformats the vault, and attests the new life cycle. See +[what a production lock does to the firmware](Provisioning.md#what-a-production-lock-does-to-the-firmware). + +**How it binds the software.** With debug closed, nothing outside the firmware +can change the TrustZone perimeter, the guest WRP, or the images. After that, +the software only changes through wolfBoot's signed update path. It is held in +place by: +- the wolfBoot signing key; +- the guest measurement records; +- the DA certificate chain, until the part is Locked. + +See [how a production lock binds the software](Provisioning.md#how-a-production-lock-binds-the-software). + +| Command | Runs from | Writes | Also needs | +| --- | --- | --- | --- | +| `lock provisioning` (`0x17`) | open `0xED` | Provisioning | a rehearsal of Provisioning or a closed state on this part | +| `lock tz-closed` (`0xC6`) | provisioning `0x17` | TrustZone Closed | a rehearsal of `0xC6`; guest WRP; production DA, provisioned; `WT_FIXTURE_BOUND=1` | +| `lock closed` (`0x72`) | provisioning `0x17` | Closed | a rehearsal of `0x72`; guest WRP; production DA, provisioned; `WT_FIXTURE_BOUND=1` | +| `lock locked` (`0x5C`) | provisioning `0x17` | Locked (final) | a rehearsal of `0x72`; guest WRP; production DA, provisioned (the read-back needs DA discovery); `WT_FIXTURE_BOUND=1` | + +On top of [the shared gates](Provisioning.md#the-lock-gates), this port checks: + +- **The same part.** In Open, `lock` reads the 96-bit UID live and requires + the rehearsed one, and reads the four images back under reset. Provisioning + masks the UID, so a closed state's `lock` needs `WT_FIXTURE_BOUND=1`: the + station asserts the fixture holds the part it rehearsed and moved to + Provisioning. +- **Safe option bytes.** The perimeter values must be wolfTrust's (TZEN, + BOOT_UBE, SWAP_BANK, SECWM1 and SECWM2), with the guest WRP + (`WRPSGn1=0x000FFFFF`) for a closed state, and must match the rehearsal. +- **Production DA,** for every closed state. `WT_DA_OBK`, `WT_DA_KEY`, + `WT_DA_CERT`, and `WT_DA_PWD` must be set, must not match ST's sample, and + must be the same four the rehearsal regressed with. The script carries the + SHA-256 of every file in ST's NUCLEO-H563ZI sample DA material, so a renamed + copy is caught. DA discovery must show an intact OBK offering Full + Regression. The sample check only catches ST's published files: it cannot + prove a credential is private. Generate the production DA keys yourself and + keep the private key off the station once the parts are provisioned. +- **The DA this script installed.** A regression wipes the DA, so step 3 below + is required: `lock` needs a record that `provision-da` installed these four + files after the rehearsal. Discovery cannot authenticate the OBK on the + part, so a key installed with other tools in between is not caught; provision + DA on the station only through this script. +- **Read-back.** After a closed state's write, DA discovery must report it and + the wolfTrust boot must show on the UART, or `lock` fails without repeating + the write. The serial port is drained before the write, so only output from + the boot after the write counts. + +The steps, one manual command each. The preview output is from the board. + +1. **Flash the production part** as in stage 1, with the images you rehearsed. + +2. **Preview and lock Provisioning:** + + ```text + $ tests/target/provisioning/provisioning_ctrl.sh lock provisioning + Lock step: open (0xED) -> provisioning (0x17) + checked: next state, rehearsal of closed (0x72) with images c1f89defe6238bcc (26s ago), same part 002100453332511238363236, images read back, perimeter values + will run: STM32_Programmer_CLI -c port=SWD mode=HotPlug -ob PRODUCT_STATE=0x17 + REFUSED: preview only, nothing was written. A production station re-runs this with WT_LOCK_CONFIRM=1. + ``` + + ```sh + export WT_PRODUCTION_LOCK=1 + WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh lock provisioning + ``` + + ```text + !!! Moving this STM32H563 to provisioning (0x17) + !!! Only a DA regression, which mass-erases the part, returns it to Open. + !!! Are you sure? Type "I ACCEPT 0x17" to continue: + ``` + +3. **Provision the production DA chain** and check it: + + ```sh + export WT_DA_OBK=production/DA_Config.obk WT_DA_KEY=production/leaf.pem \ + WT_DA_CERT=production/leaf_chain.b64 WT_DA_PWD=production/password.bin + WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh provision-da + tests/target/provisioning/provisioning_ctrl.sh discover + ``` + + > **Warning:** only continue when discovery shows integrity `0xeaeaeaea` + > and Full Regression; without them a closed part cannot come back. + +4. **Close the part**, on the fixture that held it since the rehearsal. + A Closed part still regresses with your certificate chain, so stop here if + field regression is wanted: + + ```sh + WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh lock closed + ``` + + ```text + !!! Moving this STM32H563 to closed (0x72) + !!! Only a DA regression, which mass-erases the part, returns it to Open. + !!! Are you sure? Type "I ACCEPT 0x72" to continue: I ACCEPT 0x72 + ``` + + Expected output, as in the rehearsal: + + ```text + Error: Unable to reconnect after setting the Option Bytes + [check] PASS wolfTrust chain boots in closed (0x72) + [check] PASS STM32H563 is closed (0x72) + ``` + +5. **Or lock for good** in place of step 4, only when field regression is not + wanted: + + > **Warning:** this is IRREVERSIBLE. Debug never opens again, the part + > cannot be regressed or reflashed, and wolfBoot and its key are fixed for + > the life of the part. + + ```sh + WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh lock locked + ``` + + ```text + !!! Moving this STM32H563 to locked (0x5C) + !!! This is IRREVERSIBLE: debug closes for good, no regression or mass erase, and only a wolfBoot-signed update can change the firmware. + !!! Are you sure? Type "I ACCEPT 0x5C" to continue: + ``` + +6. **Verify** from the firmware: the attestation token must report `0x3000` + SECURED, and the production scenarios must pass. + +Refusals from the board, none of which wrote anything: + +```text +$ tests/target/provisioning/provisioning_ctrl.sh lock closed +REFUSED: the part is open (0xED); lock 0x72 runs only from provisioning (0x17) +$ WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 tests/target/provisioning/provisioning_ctrl.sh lock provisioning valid != 0u && region->locked != 0u && + region->write_acp == 0u) ? 1 : 0; +} + +int wt_guest_flash_frad_covers(const wt_frad_region_t* regions, size_t count, + uintptr_t window_base, size_t window_size) +{ + uintptr_t last; + uintptr_t cursor; + size_t i; + size_t step; + int covered = 0; + int found; + int ret = WT_GUEST_VERIFY_OK; + + if (regions == NULL || count == 0u || window_size == 0u) { + return WT_GUEST_VERIFY_ERROR_ARGUMENT; + } + last = window_base + (uintptr_t)window_size - 1u; + if (last < window_base) { + return WT_GUEST_VERIFY_ERROR_ARGUMENT; + } + + /* Overlapping descriptors resolve in hardware-defined order, so every + * valid descriptor that touches the window must itself deny writes. */ + for (i = 0u; i < count && ret == WT_GUEST_VERIFY_OK; ++i) { + if (regions[i].valid != 0u && + (uintptr_t)regions[i].start <= last && + (uintptr_t)regions[i].end >= window_base && + wt_frad_region_denies_write(®ions[i]) == 0) { + ret = WT_GUEST_VERIFY_ERROR_WRP; + } + } + + cursor = window_base; + for (step = 0u; step < count && ret == WT_GUEST_VERIFY_OK && covered == 0; + ++step) { + found = 0; + for (i = 0u; i < count && found == 0; ++i) { + if (wt_frad_region_denies_write(®ions[i]) != 0 && + (uintptr_t)regions[i].start <= cursor && + (uintptr_t)regions[i].end >= cursor) { + found = 1; + if ((uintptr_t)regions[i].end >= last) { + covered = 1; + } + else { + cursor = (uintptr_t)regions[i].end + 1u; + } + } + } + if (found == 0) { + ret = WT_GUEST_VERIFY_ERROR_WRP; + } + } + + if (ret == WT_GUEST_VERIFY_OK && covered == 0) { + ret = WT_GUEST_VERIFY_ERROR_WRP; + } + + return ret; +} + int wt_runtime_verify_decide(const void* window_base, size_t window_size, const wt_guest_measurement_t* record, uint32_t min_version, int launch_required) diff --git a/tests/host/Makefile b/tests/host/Makefile index 645b4821..e45a6a71 100644 --- a/tests/host/Makefile +++ b/tests/host/Makefile @@ -32,7 +32,7 @@ SUBMAKE_FLAGS := --no-print-directory -s endif UNIT_SUITES := domain manifest lifecycle guest_verify rollback sp_recovery \ - fabric_windows periph irq_claim rt700_trng rt700_xspi \ + fabric_windows periph irq_claim rt700_trng rt700_xspi rt700_lifecycle \ ipc ffm spm \ vnet wolfhsm_loopback wolfhsm_relay native_wire native_wire_client \ keystore_isolation \ diff --git a/tests/host/guest_verify/main.c b/tests/host/guest_verify/main.c index 7e324cc5..02e4d794 100644 --- a/tests/host/guest_verify/main.c +++ b/tests/host/guest_verify/main.c @@ -251,6 +251,140 @@ static void wt_test_flash_wrp(void) WT_GUEST_VERIFY_ERROR_LAYOUT); } +/* WT-SYS-0002 descriptor-fenced predicate over the MIMXRT700 XSPI0 NOR layout: + * guest0 = 0x28080000+0x80000, guest1 = 0x28100000+0x40000, fenced by one + * 64 KiB-granular FRAD [0x28080000, 0x2813FFFF]. */ +#define WT_TEST_NOR_BASE 0x28000000u +#define WT_TEST_NOR_LAST 0x2BFFFFFFu +#define WT_TEST_RT_G0_BASE 0x28080000u +#define WT_TEST_RT_G0_SIZE 0x80000u +#define WT_TEST_RT_G1_BASE 0x28100000u +#define WT_TEST_RT_G1_SIZE 0x40000u +#define WT_TEST_FRADS 8u + +static void wt_frad_set(wt_frad_region_t* region, uint32_t start, uint32_t end, + uint32_t write_acp, uint32_t valid, uint32_t locked) +{ + region->start = start; + region->end = end; + region->write_acp = write_acp; + region->valid = valid; + region->locked = locked; +} + +/* The planned wolfBoot layout: boot root, secure image, guest fence, rest. */ +static void wt_frad_armed(wt_frad_region_t* regions) +{ + uint32_t i; + + (void)memset(regions, 0, sizeof(wt_frad_region_t) * WT_TEST_FRADS); + wt_frad_set(®ions[0], WT_TEST_NOR_BASE, 0x2803FFFFu, 0u, 1u, 1u); + wt_frad_set(®ions[1], 0x28040000u, 0x2807FFFFu, 3u, 1u, 1u); + wt_frad_set(®ions[2], 0x28080000u, 0x2813FFFFu, 0u, 1u, 1u); + wt_frad_set(®ions[3], 0x28140000u, WT_TEST_NOR_LAST, 3u, 1u, 1u); + for (i = 4u; i < WT_TEST_FRADS; i++) { + wt_frad_set(®ions[i], 0u, 0u, 0u, 0u, 1u); + } +} + +static void wt_test_flash_frad(void) +{ + wt_frad_region_t regions[WT_TEST_FRADS]; + wt_frad_region_t reversed[WT_TEST_FRADS]; + uint32_t i; + + wt_frad_armed(regions); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G0_BASE, WT_TEST_RT_G0_SIZE), WT_GUEST_VERIFY_OK); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G1_BASE, WT_TEST_RT_G1_SIZE), WT_GUEST_VERIFY_OK); + + /* A single granule at the fence start is covered. */ + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G0_BASE, 0x10000u), WT_GUEST_VERIFY_OK); + + /* Descriptor order does not matter. */ + for (i = 0u; i < WT_TEST_FRADS; i++) { + reversed[i] = regions[WT_TEST_FRADS - 1u - i]; + } + EXPECT_RESULT(wt_guest_flash_frad_covers(reversed, WT_TEST_FRADS, + WT_TEST_RT_G1_BASE, WT_TEST_RT_G1_SIZE), WT_GUEST_VERIFY_OK); + + /* Today's unfenced wolfBoot layout: everything above the boot root is + * writable, so both guests fail closed. */ + (void)memset(regions, 0, sizeof(regions)); + wt_frad_set(®ions[0], WT_TEST_NOR_BASE, 0x2803FFFFu, 0u, 1u, 1u); + wt_frad_set(®ions[1], 0x28040000u, WT_TEST_NOR_LAST, 3u, 1u, 1u); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G0_BASE, WT_TEST_RT_G0_SIZE), + WT_GUEST_VERIFY_ERROR_WRP); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G1_BASE, WT_TEST_RT_G1_SIZE), + WT_GUEST_VERIFY_ERROR_WRP); + + /* The fence itself unlocked, invalid, or write-granting fails closed. */ + wt_frad_armed(regions); + regions[2].locked = 0u; + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G0_BASE, WT_TEST_RT_G0_SIZE), + WT_GUEST_VERIFY_ERROR_WRP); + wt_frad_armed(regions); + regions[2].valid = 0u; + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G0_BASE, WT_TEST_RT_G0_SIZE), + WT_GUEST_VERIFY_ERROR_WRP); + wt_frad_armed(regions); + regions[2].write_acp = 1u; + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G1_BASE, WT_TEST_RT_G1_SIZE), + WT_GUEST_VERIFY_ERROR_WRP); + + /* The fence split across two chained descriptors still covers; a 64 KiB + * gap between them fails the guest it lands in and only that guest. */ + wt_frad_armed(regions); + wt_frad_set(®ions[2], 0x28080000u, 0x280FFFFFu, 0u, 1u, 1u); + wt_frad_set(®ions[4], 0x28100000u, 0x2813FFFFu, 0u, 1u, 1u); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G0_BASE, WT_TEST_RT_G0_SIZE), WT_GUEST_VERIFY_OK); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G1_BASE, WT_TEST_RT_G1_SIZE), WT_GUEST_VERIFY_OK); + regions[4].start = 0x28110000u; + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G1_BASE, WT_TEST_RT_G1_SIZE), + WT_GUEST_VERIFY_ERROR_WRP); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G0_BASE, WT_TEST_RT_G0_SIZE), WT_GUEST_VERIFY_OK); + + /* A valid write-granting descriptor overlapping one guest fails that guest + * even though the fence also covers it. */ + wt_frad_armed(regions); + wt_frad_set(®ions[5], 0x280C0000u, 0x280CFFFFu, 3u, 1u, 1u); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G0_BASE, WT_TEST_RT_G0_SIZE), + WT_GUEST_VERIFY_ERROR_WRP); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G1_BASE, WT_TEST_RT_G1_SIZE), WT_GUEST_VERIFY_OK); + + /* A window one byte past the fence reaches the writable rest of NOR. */ + wt_frad_armed(regions); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G1_BASE, WT_TEST_RT_G1_SIZE + 1u), + WT_GUEST_VERIFY_ERROR_WRP); + + /* Argument abuse: NULL, no descriptors, empty or wrapping window. */ + EXPECT_RESULT(wt_guest_flash_frad_covers(NULL, WT_TEST_FRADS, + WT_TEST_RT_G0_BASE, WT_TEST_RT_G0_SIZE), + WT_GUEST_VERIFY_ERROR_ARGUMENT); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, 0u, + WT_TEST_RT_G0_BASE, WT_TEST_RT_G0_SIZE), + WT_GUEST_VERIFY_ERROR_ARGUMENT); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + WT_TEST_RT_G0_BASE, 0u), WT_GUEST_VERIFY_ERROR_ARGUMENT); + EXPECT_RESULT(wt_guest_flash_frad_covers(regions, WT_TEST_FRADS, + UINTPTR_MAX - 0x10u, 0x100u), + WT_GUEST_VERIFY_ERROR_ARGUMENT); +} + int main(void) { size_t i; @@ -266,6 +400,7 @@ int main(void) wt_test_arguments(); wt_test_measurement_table(); wt_test_flash_wrp(); + wt_test_flash_frad(); if (g_failures != 0u) { (void)fprintf(stderr, "guest-verify checks failed: %u/%u\n", diff --git a/tests/host/rt700_lifecycle/Makefile b/tests/host/rt700_lifecycle/Makefile new file mode 100644 index 00000000..60a910fa --- /dev/null +++ b/tests/host/rt700_lifecycle/Makefile @@ -0,0 +1,71 @@ +# Makefile +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfTrust. +# +# wolfTrust 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. +# +# wolfTrust 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + +ROOT := ../../.. + +CC ?= cc +BUILD_DIR ?= build + +# The mapping header ships inside the carried wolfBoot patch; test that copy. +PATCH := $(ROOT)/tests/target/wolfboot-imxrt700-lifecycle.patch +HEADER := $(BUILD_DIR)/imx_rt7xx_lifecycle.h + +CFLAGS := \ + -I$(BUILD_DIR) \ + -std=c11 -O0 -g -Wall -Wextra -Werror -pedantic +CFLAGS += $(EXTRA_CFLAGS) + +TEST_BIN := $(BUILD_DIR)/test_rt700_lifecycle + +.PHONY: all run compilers sanitize valgrind clean + +all: $(TEST_BIN) + +$(BUILD_DIR): + mkdir -p $@ + +$(HEADER): $(PATCH) | $(BUILD_DIR) + awk '/^\+\+\+ b\/hal\/imx_rt7xx_lifecycle\.h$$/ { f = 1; next } \ + f && /^diff --git / { exit } \ + f && /^\+/ { print substr($$0, 2) }' $(PATCH) > $@ + test -s $@ + +$(TEST_BIN): main.c $(HEADER) + $(CC) $(CFLAGS) $(EXTRA_LDFLAGS) -o $@ main.c + +run: $(TEST_BIN) + $(TEST_BIN) + +compilers: + $(MAKE) clean run CC=gcc BUILD_DIR=$(BUILD_DIR)/gcc + $(MAKE) clean run CC=clang BUILD_DIR=$(BUILD_DIR)/clang + +sanitize: + $(MAKE) clean run CC=clang \ + BUILD_DIR=$(BUILD_DIR)/sanitize \ + EXTRA_CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \ + EXTRA_LDFLAGS="-fsanitize=address,undefined" + +valgrind: clean $(TEST_BIN) + valgrind --error-exitcode=1 --leak-check=full --show-leak-kinds=all \ + $(TEST_BIN) + +clean: + rm -rf $(BUILD_DIR) diff --git a/tests/host/rt700_lifecycle/main.c b/tests/host/rt700_lifecycle/main.c new file mode 100644 index 00000000..ba677fda --- /dev/null +++ b/tests/host/rt700_lifecycle/main.c @@ -0,0 +1,183 @@ +/* main.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfTrust. + * + * wolfTrust 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. + * + * wolfTrust 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 . + */ + +/* The MIMXRT700 life cycle mapping wolfBoot hands wolfTrust, tested from the + * copy inside the carried wolfBoot patch: only an agreeing, known OTP life + * cycle maps to a PSA state, and no fault or debug state reads as SECURED. */ + +#include "imx_rt7xx_lifecycle.h" + +#include +#include + +static int checks; +static int failures; + +#define EXPECT_LC(actual, expected) \ + do { \ + uint32_t a_ = (actual); \ + uint32_t e_ = (expected); \ + checks++; \ + if (a_ != e_) { \ + (void)fprintf(stderr, "line %d: expected 0x%04x, got 0x%04x\n", \ + __LINE__, (unsigned)e_, (unsigned)a_); \ + failures++; \ + } \ + } while (0) + +/* DAUTHSTATUS: each 2-bit field is 0b11 enabled, 0b10 implemented and disabled. */ +#define DAUTH_CLOSED 0xAAu +#define DAUTH_NS_ONLY 0xABu +#define DAUTH_NSNID 0xAEu +#define DAUTH_SECURE 0xBAu +#define DAUTH_SNID 0xEAu +#define DAUTH_ALL 0xFFu + +static void test_state_table(void) +{ + uint32_t lc; + + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(IMX_RT7XX_LC_DEVELOP, + IMX_RT7XX_LC_DEVELOP), 0x1000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(IMX_RT7XX_LC_DEVELOP2, + IMX_RT7XX_LC_DEVELOP2), 0x2000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD), 0x3000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(IMX_RT7XX_LC_IN_FIELD_LOCKED, + IMX_RT7XX_LC_IN_FIELD_LOCKED), 0x3000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(IMX_RT7XX_LC_IN_FIELD_RETURN, + IMX_RT7XX_LC_IN_FIELD_RETURN), 0x6000u); + + /* NXP-internal, blank, bricked, and every unlisted code are unknown. */ + for (lc = 0u; lc <= 0xFFu; lc++) { + if (lc == IMX_RT7XX_LC_DEVELOP || lc == IMX_RT7XX_LC_DEVELOP2 || + lc == IMX_RT7XX_LC_IN_FIELD || + lc == IMX_RT7XX_LC_IN_FIELD_LOCKED || + lc == IMX_RT7XX_LC_IN_FIELD_RETURN) { + continue; + } + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(lc, lc), 0x0000u); + } +} + +static void test_redundancy(void) +{ + /* The redundant copy must agree; a single flipped byte never promotes. */ + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_DEVELOP), 0x0000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(IMX_RT7XX_LC_DEVELOP, + IMX_RT7XX_LC_IN_FIELD), 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + 0x00u, DAUTH_CLOSED), 0x0000u); + + /* A0/A1 bit-protection copies in bits 16-23 must agree with each byte. */ + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(0x000F000Fu, 0x000F000Fu), + 0x3000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(0x000F000Fu, 0x000F000Fu, + DAUTH_CLOSED), 0x3000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(0x0003000Fu, 0x000F000Fu), + 0x0000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(0x000F000Fu, 0x0007000Fu), + 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(0x0003000Fu, 0x0003000Fu, + DAUTH_CLOSED), 0x0000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(0x000F000Fu, 0x0000000Fu), + 0x0000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(0x0000000Fu, 0x000F000Fu), + 0x0000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(0x00030007u, 0x00030007u), + 0x0000u); + + /* Bits 8-15 and 24-31 are reserved on every revision. */ + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(0x0000010Fu, 0x0000000Fu), + 0x0000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(0x0000000Fu, 0x0000800Fu), + 0x0000u); + EXPECT_LC(imx_rt7xx_lc_to_psa_lifecycle(0x0100000Fu, 0x0100000Fu), + 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(0x800F000Fu, 0x000F000Fu, + DAUTH_CLOSED), 0x0000u); +} + +static void test_debug_refinement(void) +{ + /* A secured part with debug closed attests SECURED. */ + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, DAUTH_CLOSED), 0x3000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD_LOCKED, + IMX_RT7XX_LC_IN_FIELD_LOCKED, DAUTH_CLOSED), 0x3000u); + + /* Any Secure debug open downgrades to recoverable PSA RoT debug. */ + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, DAUTH_SECURE), 0x5000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, DAUTH_SNID), 0x5000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD_LOCKED, + IMX_RT7XX_LC_IN_FIELD_LOCKED, DAUTH_ALL), 0x5000u); + + /* Only Non-secure debug open is non-PSA-RoT debug. */ + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, DAUTH_NS_ONLY), 0x4000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, DAUTH_NSNID), 0x4000u); + + /* Not implemented, reserved, or mixed encodings never prove debug closed. */ + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, 0x00u), 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, 0x55u), 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, 0x9Au), 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, 0xA0u), 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, 0x20u), 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, 0x03u), 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, 0xA3u), 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD, + IMX_RT7XX_LC_IN_FIELD, 0xF8u), 0x0000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD_LOCKED, + IMX_RT7XX_LC_IN_FIELD_LOCKED, 0x3Bu), 0x0000u); + + /* Debug state never refines an open, provisioning, or returned part. */ + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_DEVELOP, + IMX_RT7XX_LC_DEVELOP, DAUTH_ALL), 0x1000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_DEVELOP2, + IMX_RT7XX_LC_DEVELOP2, DAUTH_ALL), 0x2000u); + EXPECT_LC(imx_rt7xx_attestation_lifecycle(IMX_RT7XX_LC_IN_FIELD_RETURN, + IMX_RT7XX_LC_IN_FIELD_RETURN, DAUTH_ALL), 0x6000u); +} + +int main(void) +{ + test_state_table(); + test_redundancy(); + test_debug_refinement(); + + if (failures != 0) { + (void)fprintf(stderr, "rt700 lifecycle checks failed: %d/%d\n", + failures, checks); + return 1; + } + (void)printf("rt700 lifecycle checks passed: %d\n", checks); + return 0; +} diff --git a/tests/target/lib/rt700_fence.sh b/tests/target/lib/rt700_fence.sh new file mode 100644 index 00000000..b3913186 --- /dev/null +++ b/tests/target/lib/rt700_fence.sh @@ -0,0 +1,45 @@ +# shellcheck shell=sh +# MIMXRT700 XSPI guest fence: wolfBoot arms one FRAD over both guest windows +# each boot and a WT_GUEST_FLASH_WRP=1 wolfTrust verifies it. The bounds use +# the wolfTrust build's own layout values, so they cannot drift. Needs $repo. + +rt700_layout_value() { + eval "rt700_v=\${$1:-}" + if [ -z "$rt700_v" ]; then + # shellcheck disable=SC2154 # $repo is set by the sourcing runner + rt700_v=$(awk -v n="$1" '$1 == n && $2 == "?=" { print $3; exit }' \ + "$repo/mk/target-mimxrt700.mk") + fi + printf '%s' "$rt700_v" +} + +# Sets RT700_GUEST_FENCE_START/END (END exclusive); fails when the guest +# windows are not contiguous or not on the 64 KB FRAD granule. +rt700_fence_bounds() { + g0=$(rt700_layout_value WT_GUEST0_FLASH_BASE) + g0s=$(rt700_layout_value WT_GUEST0_FLASH_SIZE) + g1=$(rt700_layout_value WT_GUEST1_FLASH_BASE) + g1s=$(rt700_layout_value WT_GUEST1_FLASH_SIZE) + if [ -z "$g0" ] || [ -z "$g0s" ] || [ -z "$g1" ] || [ -z "$g1s" ]; then + echo "rt700 fence: guest layout not found in mk/target-mimxrt700.mk" >&2 + return 1 + fi + if [ $((g0 + g0s)) -ne $((g1)) ]; then + echo "rt700 fence: guest windows are not contiguous" >&2 + return 1 + fi + if [ $((g0 & 0xFFFF)) -ne 0 ] || [ $(((g1 + g1s) & 0xFFFF)) -ne 0 ]; then + echo "rt700 fence: guest windows are not 64 KB aligned" >&2 + return 1 + fi + RT700_GUEST_FENCE_START=$(printf '0x%08X' $((g0))) + RT700_GUEST_FENCE_END=$(printf '0x%08X' $((g1 + g1s))) +} + +# The wolfBoot CFLAGS_EXTRA that arms the fence (tests/target/ +# wolfboot-imxrt700-guest-fence.patch reads these two defines). +rt700_fence_cflags() { + rt700_fence_bounds || return 1 + printf '%s' "-DXSPI_GUEST_FENCE_START=$RT700_GUEST_FENCE_START" \ + " -DXSPI_GUEST_FENCE_END=$RT700_GUEST_FENCE_END" +} diff --git a/tests/target/lib/rt700_wolfboot.sh b/tests/target/lib/rt700_wolfboot.sh new file mode 100644 index 00000000..2d3abc7a --- /dev/null +++ b/tests/target/lib/rt700_wolfboot.sh @@ -0,0 +1,51 @@ +# shellcheck shell=sh +# The pinned MIMXRT700 wolfBoot first stage both RT700 runners boot: upstream +# at RT700_WOLFBOOT_REF plus the two carried patches, built from the +# imx-rt700-tz config. Sourced; needs $here (tests/target). +RT700_WOLFBOOT_REF=e6d169c7218d82e33bd04e2c086146ed37ec0cca + +# What a cached tree must match: the ref, both patches, and the make overrides. +rt700_wolfboot_stamp() { + # shellcheck disable=SC2154 # $here is set by the sourcing runner + printf '%s %s %s [%s]' "$RT700_WOLFBOOT_REF" \ + "$(cksum "$here/wolfboot-imxrt700-lifecycle.patch" | cut -d' ' -f1)" \ + "$(cksum "$here/wolfboot-imxrt700-guest-fence.patch" | cut -d' ' -f1)" \ + "$*" +} + +rt700_wolfboot_current() { + [ -s "$1/wolfboot.bin" ] && [ -x "$1/tools/keytools/sign" ] && + [ -s "$1/wolfboot_signing_private_key.der" ] && + [ "$(cat "$1/.wt_first_stage" 2>/dev/null)" = "$2" ] +} + +# rt700_wolfboot_build [make VAR=value...]: builds /wolfboot.bin at +# the pin unless the tree there already matches; nonzero on any failure. +rt700_wolfboot_build() { + rt700_dir="$1" + shift + rt700_stamp=$(rt700_wolfboot_stamp "$@") + if rt700_wolfboot_current "$rt700_dir" "$rt700_stamp"; then + return 0 + fi + rm -rf "$rt700_dir" && + git clone --no-checkout https://github.com/wolfSSL/wolfBoot.git \ + "$rt700_dir" && + git -C "$rt700_dir" fetch --depth 1 origin "$RT700_WOLFBOOT_REF" && + git -C "$rt700_dir" checkout --detach "$RT700_WOLFBOOT_REF" && + git -C "$rt700_dir" apply "$here/wolfboot-imxrt700-lifecycle.patch" && + git -C "$rt700_dir" apply "$here/wolfboot-imxrt700-guest-fence.patch" && + git -C "$rt700_dir" submodule update --init --single-branch --depth 1 && + cp "$rt700_dir/config/examples/imx-rt700-tz.config" "$rt700_dir/.config" && + # keygen writes src/keystore.c, which the loader links, so the key and the + # tools that locate it from the working directory come first. wolfBoot's + # TARGET comes from its .config, never from a calling make. + ( + unset TARGET MAKEFLAGS MFLAGS + cd "$rt700_dir" && + make keytools && + make -j1 wolfboot_signing_private_key.der && + make -j1 "$@" wolfboot.bin + ) && + printf '%s\n' "$rt700_stamp" > "$rt700_dir/.wt_first_stage" +} diff --git a/tests/target/lib/scenario.sh b/tests/target/lib/scenario.sh index 2befedcb..36e22689 100644 --- a/tests/target/lib/scenario.sh +++ b/tests/target/lib/scenario.sh @@ -39,6 +39,7 @@ scenario_secure_flags() { remeasureneg) echo "WT_REMEASURE_PROBE=1" ;; bootupdate) echo "WT_BOOTUPDATE_PROBE=1" ;; spbudgetneg) echo "WT_SP_FAULT_ALWAYS_PROBE=1" ;; + wrpfence|wrpoff|wrpneg) echo "WT_GUEST_FLASH_WRP=1" ;; vnet) echo "CONFIG_VNET=y" ;; vnetneg) echo "CONFIG_VNET=y WT_VNET_NEG_PROBE=1" ;; manifestneg) echo "WT_MANIFEST_NEG_PROBE=1" ;; diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index 0ed2aa2c..fdaab214 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -112,6 +112,8 @@ ("spfaultneg panicneg", "RT700 SP fault and panic recovery"), ("fpneg", "RT700 FP isolation (partition FP faults, contained)"), ("sealbootneg", "RT700 damaged main-stack seal refuses to boot"), + ("wrpfence wrpoff wrpneg", + "RT700 XSPI guest fence: launches, refuses unfenced, blocks erase"), ("bothpsa bothiso", "RT700 both-guest PSA lifecycle and isolation"), ("attestneg fwustage", "RT700 attestation negatives and FWU staging"), diff --git a/tests/target/m33mu-imxrt700.patch b/tests/target/m33mu-imxrt700.patch new file mode 100644 index 00000000..8fac234b --- /dev/null +++ b/tests/target/m33mu-imxrt700.patch @@ -0,0 +1,13 @@ +diff --git a/cpu/imxrt700/imxrt700_secure.c b/cpu/imxrt700/imxrt700_secure.c +--- a/cpu/imxrt700/imxrt700_secure.c ++++ b/cpu/imxrt700/imxrt700_secure.c +@@ -407,6 +407,9 @@ static void fuses_seed(void) + /* Unique ID words, stable across runs. */ + fuses[0x10] = 0x52543730u; /* "RT70" */ + fuses[0x11] = 0x00000798u; ++ /* LC_STATE_RED and LC_STATE: Develop (0x03), the state an EVK ships in. */ ++ fuses[0x25] = 0x00000003u; ++ fuses[0x8F] = 0x00000003u; + fuses_init = MM_TRUE; + } + diff --git a/tests/target/provisioning/README.md b/tests/target/provisioning/README.md new file mode 100644 index 00000000..cebdc057 --- /dev/null +++ b/tests/target/provisioning/README.md @@ -0,0 +1,26 @@ +# Provisioning + +`provisioning_ctrl.sh` takes a wolfTrust part from development to a production +lock with the same commands on every port. `TARGET` selects the port: + +```sh +TARGET=stm32h563 tests/target/provisioning/provisioning_ctrl.sh help +TARGET=mimxrt700 tests/target/provisioning/provisioning_ctrl.sh help +``` + +The flow is the same everywhere: `advance` a state as a reversible mock, +check the part in it, `regress`, then `lock` that state for real. Each `lock` +is one manual, previewed step that ends in an "are you sure" prompt and a typed +`I ACCEPT `. + +How to provision a device, generically and per port, is in +[docs/Provisioning.md](../../../docs/Provisioning.md). + +| File | Role | +| --- | --- | +| `provisioning_ctrl.sh` | the only script you run: commands, rehearsal records, and every lock gate | +| `provisioning_ctrl_.sh` | a port: the device's states and how to read, mock, and write them | +| `test_provisioning_gates.sh` | offline tests of every gate against stub tools (`make test-provisioning`) | + +To add a port, copy a port file and implement its `port_*` functions; the +main script and its gates stay unchanged. diff --git a/tests/target/provisioning/provisioning_ctrl.sh b/tests/target/provisioning/provisioning_ctrl.sh new file mode 100755 index 00000000..98d42d01 --- /dev/null +++ b/tests/target/provisioning/provisioning_ctrl.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +# provisioning_ctrl.sh +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfTrust. +# +# wolfTrust 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. +# +# wolfTrust 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 . + +# wolfTrust provisioning control: one command set for every port. TARGET picks +# the port file (provisioning_ctrl_.sh), which answers the device +# questions; everything that guards a permanent write lives here, once. +# See docs/Provisioning.md. +set -euo pipefail + +here="$(cd "$(dirname "$0")" && pwd)" +repo="$(cd "$here/../../.." && pwd)" +target="${TARGET:-stm32h563}" +port_file="$here/provisioning_ctrl_$target.sh" +state_dir="${WT_PROVISION_STATE:-$HOME/.cache/wolftrust}/$target" +rehearsal_max_age="${WT_REHEARSAL_MAX_AGE:-3600}" +cmd="${1:-help}" +[ "$#" -eq 0 ] || shift + +pass() { printf ' [check] PASS %s\n' "$1"; } +fail() { printf ' [check] FAIL %s (%s)\n' "$1" "$2"; exit 1; } +refuse() { echo "REFUSED: $1" >&2; exit 2; } +confirm() { + [ "${WT_LOCK_CONFIRM:-0}" = "1" ] || + refuse "'$cmd' writes to the board. Re-run with WT_LOCK_CONFIRM=1." +} +sha256() { + if command -v sha256sum >/dev/null 2>&1; then sha256sum; else shasum -a 256; fi +} +hexstate() { printf '0x%02X' "$(( $1 ))"; } +put() { mkdir -p "$state_dir"; echo "$2" > "$state_dir/$1"; } +field() { sed -n "s/.* $1=\([^ ]*\).*/\1/p" <<<" $2"; } +age_of() { echo $(( $(date +%s) - ${1:-0} )); } +fresh() { + local age + [ -n "${1:-}" ] || return 1 + age="$(age_of "$1")" + [ "$age" -ge 0 ] && [ "$age" -le "$rehearsal_max_age" ] +} + +# The port's flashed_images, each address and length ahead of its bytes, so +# moving bytes between images changes the digest. +framed_digest() { + local a f + while read -r a f; do [ -s "$f" ] || return 1; done < <(flashed_images) + flashed_images | while read -r a f; do + printf '%s %s\n' "$a" "$(wc -c < "$f" | tr -d ' ')" + cat "$f" + done | sha256 | cut -c1-64 +} + +# A rehearsal record binds these images, credentials, and part, recently. +rehearsal_ok() { + [ -n "$1" ] && [ "$(field image "$1")" = "$2" ] && [ "$(field cred "$1")" = "$3" ] && + fresh "$(field completed "$1")" && port_record_ok "$1" +} + +ports() { + local f + for f in "$here"/provisioning_ctrl_*.sh; do + f="${f##*/provisioning_ctrl_}" + printf '%s ' "${f%.sh}" + done +} +# Evidence (UART captures, image read-backs) stays in a private directory. +wt_tmp="$(mktemp -d "${TMPDIR:-/tmp}/wolftrust.XXXXXX")" +trap 'rm -rf "$wt_tmp"' EXIT +[ -f "$port_file" ] || refuse "no provisioning port for TARGET=$target (have: $(ports))" +# shellcheck source=provisioning_ctrl_stm32h563.sh +. "$port_file" + +# The port's ladder: "code name mock lock permanence from-codes" per line. +ladder_line() { + local code="" + if [[ "$1" =~ ^0[xX][0-9A-Fa-f]{1,2}$ ]]; then code="$(hexstate "$1")"; fi + port_ladder | awk -v c="$code" -v n="$1" '$1 == c || $2 == n { print; exit }' +} +state_name() { port_ladder | awk -v c="$1" '$1 == c { print $2; exit }'; } +label() { echo "$(state_name "$1") ($1)"; } +# resolve : the code, if the ladder allows that use. +resolve() { + local line + line="$(ladder_line "${1:-}")" + [ -n "$line" ] || refuse "unknown state '${1:-}'. $PORT_NAME states: $(port_ladder | awk '{ printf "%s %s, ", $1, $2 }')" + case "$2" in + mock) [ "$(awk '{print $3}' <<<"$line")" = "yes" ] || + refuse "$(awk '{print $2 " (" $1 ")"}' <<<"$line") has no mock: it is a permanent state, set only by 'lock'." ;; + lock) [ "$(awk '{print $4}' <<<"$line")" = "yes" ] || + refuse "$(awk '{print $2 " (" $1 ")"}' <<<"$line") is not a lock target." ;; + esac + awk '{print $1}' <<<"$line" +} + +usage() { + cat < [state] + + status read the part's life cycle and protections + discover read-only preflight for this port + restore flash the production images and verify them (writes) + advance mock: enter reversibly and record a rehearsal (writes) + regress return from the mock state and complete the rehearsal (writes) + lock real: make the next step permanent; previews unless + WT_LOCK_CONFIRM=1, writes only with WT_PRODUCTION_LOCK=1 + and a typed "I ACCEPT " +$(port_usage_extra) +$PORT_NAME states (code name; mock; lock; permanence; from): +$(port_ladder | awk '{ printf " %-6s %-18s mock=%-3s lock=%-3s %-10s from %s\n", $1, $2, $3, $4, $5, $6 }') +EOF +} + +# The last gate: a production station opts in, and a person at a terminal +# types the acceptance back. Nothing piped or scripted can pass it. +lock_confirm() { + local answer + [ "${WT_PRODUCTION_LOCK:-0}" = "1" ] || + refuse "$2 is a production lock step. Only a production station sets WT_PRODUCTION_LOCK=1." + [ -t 0 ] || refuse "a production lock needs an interactive terminal, not a pipe or script." + printf '\n!!! %s\n!!! %s\n!!! Are you sure? Type "%s" to continue: ' "$2" "$3" "$1" >&2 + read -r answer || answer="" + [ "$answer" = "$1" ] || refuse "confirmation did not match; nothing was changed." +} + +case "$cmd" in + help|-h|--help) usage ;; + status) port_status ;; + discover) port_discover ;; + restore) confirm; port_restore ;; + + advance) + confirm + state="$(resolve "${1:-}" mock)" + port_advance_check "$state" + run="$(od -An -N8 -tx1 /dev/urandom | tr -d ' \n')" + rm -f "$state_dir/pending" + port_advance "$state" + EVIDENCE="" + if port_booted "$state"; then + put pending "state=$state run=$run $EVIDENCE time=$(date +%s)" + echo "rehearsal of $(label "$state") recorded; 'regress' completes it" + else + fail "advance" "no rehearsal recorded: $(label "$state") did not show the evidence above" + fi + ;; + + regress) + confirm + PENDING="$(cat "$state_dir/pending" 2>/dev/null || true)" + rm -f "$state_dir/pending" + REGRESS_EXTRA="" + # With nothing pending this is plain recovery: it returns the part, records nothing. + port_regress || fail "regress" "regression did not complete; nothing was recorded" + if [ -n "$PENDING" ]; then + s="$(field state "$PENDING")" + put "rehearsal-$s" "$PENDING cred=$(port_cred_fp) $REGRESS_EXTRA completed=$(date +%s)" + pass "rehearsal of $(label "$s") complete" + fi + ;; + + lock) + # One permanent step: the next state only, rehearsed on this part with + # these images, previewed, then written only past every gate below. + [ "$#" -le 1 ] || refuse "lock takes one state." + state="$(resolve "${1:-}" lock)" + line="$(ladder_line "$state")" + cur="$(port_lock_current)" + from="$(awk '{print $6}' <<<"$line" | tr ',' ' ')" + case " $from " in + *" $cur "*) ;; + *) refuse "the part is $(label "$cur"); lock $state runs only from $(for f in $from; do printf '%s ' "$(label "$f")"; done)" ;; + esac + image="$(port_image_digest)" || refuse "missing a flashed image: build the production images first." + cred="$(port_cred_fp)" + rec="" + for s in $(port_rehearsal_for "$state"); do + r="$(cat "$state_dir/rehearsal-$s" 2>/dev/null || true)" + if rehearsal_ok "$r" "$image" "$cred"; then + rec="$r" + break + fi + done + [ -n "$rec" ] || + refuse "no rehearsal for $(label "$state") on this part with these images and credentials in the last ${rehearsal_max_age}s: run 'advance $(port_rehearsal_for "$state" | awk '{print $NF}')' and 'regress' first." + checked="next state, rehearsal of $(label "$(field state "$rec")") with images ${image:0:16} ($(age_of "$(field completed "$rec")")s ago)" + id="$(port_lock_identity)" + if [ -n "$id" ]; then + [ "$id" = "$(field id "$rec")" ] || + refuse "this part ($id) is not the one rehearsed ($(field id "$rec")): rehearse this part." + checked="$checked, same part $id" + else + checked="$checked, part identity not readable here (needs WT_FIXTURE_BOUND=1)" + fi + # port_ready refuses or appends ", " items to CHECKED. + CHECKED="$checked" + port_ready "$state" + checked="$CHECKED" + echo "Lock step: $(label "$cur") -> $(label "$state")" + echo " checked: $checked" + port_lock_plan "$state" | sed 's/^/ will run: /' + [ "${WT_LOCK_CONFIRM:-0}" = "1" ] || + refuse "preview only, nothing was written. A production station re-runs this with WT_LOCK_CONFIRM=1." + [ -n "$id" ] || [ "${WT_FIXTURE_BOUND:-0}" = "1" ] || + refuse "the part's identity cannot be read in this state, so nothing proves it is the rehearsed one: run this only on a fixture that holds one part from rehearsal to lock, and set WT_FIXTURE_BOUND=1 there." + if [ "$(awk '{print $5}' <<<"$line")" = "permanent" ]; then + why="This is IRREVERSIBLE: $(port_consequence "$state")" + else + why="$(port_consequence "$state")" + fi + lock_confirm "I ACCEPT $state" "Moving this $PORT_NAME to $(label "$state")" "$why" + # The prompt can wait a long time: check the part again right before the write. + [ "$(port_lock_current)" = "$cur" ] || refuse "the part changed while waiting for the acceptance; nothing was changed." + if [ "$(cat "$state_dir/rehearsal-$(field state "$rec")" 2>/dev/null || true)" != "$rec" ] || + ! rehearsal_ok "$rec" "$(port_image_digest || true)" "$(port_cred_fp)"; then + refuse "the rehearsal no longer matches these images, credentials, and part, or it expired while waiting; nothing was changed." + fi + [ -z "$id" ] || [ "$(port_lock_identity)" = "$id" ] || + refuse "a different part is attached than the one checked; nothing was changed." + port_ready "$state" + port_lock_write "$state" + port_lock_verify "$state" + # Single use: a permanent step consumes even a rehearsal of another state. + if [ "$(field state "$rec")" = "$state" ] || [ "$(awk '{print $5}' <<<"$line")" = "permanent" ]; then + rm -f "$state_dir/rehearsal-$(field state "$rec")" + fi + pass "$PORT_NAME is $(label "$state")" + ;; + + *) + port_extra "$cmd" "$@" || { usage >&2; exit 2; } + ;; +esac diff --git a/tests/target/provisioning/provisioning_ctrl_mimxrt700.sh b/tests/target/provisioning/provisioning_ctrl_mimxrt700.sh new file mode 100644 index 00000000..4a2e1818 --- /dev/null +++ b/tests/target/provisioning/provisioning_ctrl_mimxrt700.sh @@ -0,0 +1,457 @@ +# shellcheck shell=bash +# provisioning_ctrl_mimxrt700.sh +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfTrust. +# +# wolfTrust 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. +# +# wolfTrust 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 . + +# MIMXRT700 (MIMXRT700-EVK) port for provisioning_ctrl.sh: the OTP life cycle, +# rehearsed in the OTP shadow registers that every reset reloads, burned over +# the BootROM's ISP. Sourced, never run. See docs/MIMXRT700-Guide.md. +# shellcheck disable=SC2034,SC2154 # PORT_* are read, and $cur/$PENDING set, by the main script + +# A port file only works inside provisioning_ctrl.sh, which holds the gates. +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + echo "REFUSED: run TARGET=mimxrt700 tests/target/provisioning/provisioning_ctrl.sh, not this port file." >&2 + exit 2 +fi + +PORT_NAME="MIMXRT700" +target_dir="$here/.." +pyocd_target="${RT700_TARGET:-mimxrt798sgfob}" +spsdk_venv="${RT700_SPSDK_VENV:-$HOME/spsdk-venv}" +# Set here, not in a helper: the controller runs many port calls in subshells. +[ ! -d "$spsdk_venv/bin" ] || PATH="$spsdk_venv/bin:$PATH" +# The Python that runs SPSDK: the venv's, else the one behind pyocd on PATH. +if [ -x "$spsdk_venv/bin/python" ]; then + spsdk_py="$spsdk_venv/bin/python" +else + spsdk_py="$(sed -n '1s/^#! *\([^ ]*python[^ ]*\).*/\1/p' "$(command -v pyocd 2>/dev/null || echo /dev/null)" 2>/dev/null)" + spsdk_py="${spsdk_py:-python3}" +fi +guest_mask="${RT700_GUEST_MASK:-0x3}" +case "$guest_mask" in + 0x1|0x2|0x3|1|2|3) ;; + *) refuse "RT700_GUEST_MASK must be 0x1, 0x2, or 0x3 (both guests), not '$guest_mask'." ;; +esac +elf="$repo/build/wolftrust.elf" + +# OTP shadow words (fuse index * 4 from 0x50018000, both silicon revisions). +LC_STATE=0x5001823C +LC_STATE_RED=0x50018094 +LOCK_CFG3=0x5001800C +DAUTHSTATUS=0xE000EFB8 +XSPI_MGC=0x50184920 +XSPI_TG0MDAD=0x50184900 +LC_DEVELOP=0x03 +# OTP fuse word indexes blhost addresses (not shadow addresses). +FUSE_LC_RED=0x25 +FUSE_LC=0x8F + +# shellcheck source=../lib/rt700_fence.sh disable=SC1091 +. "$target_dir/lib/rt700_fence.sh" + +port_ladder() { + cat <<'EOF' +0x03 develop no no permanent - +0x07 develop2 yes yes permanent 0x03 +0x0F in-field yes yes permanent 0x07 +0xCF in-field-locked yes yes permanent 0x0F +0x1F in-field-return yes yes permanent 0x0F +EOF +} +port_usage_extra() { + cat <<'EOF' + verify-wrp check the running chain's XSPI guest fence (read-only) +EOF +} + +lc_hex() { printf '0x%02X' $(( $1 & 0xFF )); } +ensure_spsdk() { + if ! command -v pyocd >/dev/null 2>&1; then + [ -x "$spsdk_venv/bin/pyocd" ] || fail "tools" "pyocd not found (set RT700_SPSDK_VENV)" + PATH="$spsdk_venv/bin:$PATH" + export PATH + fi +} +# Words over SWD with the generic attach, which never resets the chip. +read_words() { + local -a cmds=() + local a + for a in "$@"; do cmds+=(-c "read32 $a"); done + timeout 60 pyocd cmd -t cortex_m "${cmds[@]}" 2>&1 | awk '/^[0-9a-f]+:/ { print $2 }' +} +psa_name() { + case "$1" in + 00001000) echo "ASSEMBLY_AND_TEST" ;; 00002000) echo "PSA_ROT_PROVISIONING" ;; + 00003000) echo "SECURED" ;; 00004000) echo "NON_PSA_ROT_DEBUG" ;; + 00005000) echo "RECOVERABLE_PSA_ROT_DEBUG" ;; 00006000) echo "DECOMMISSIONED" ;; + *) echo "UNKNOWN" ;; + esac +} +# The PSA life cycles wolfTrust may report while the life cycle is $1. +expect_psa() { + case "$1" in + 0x03) echo "00001000" ;; 0x07) echo "00002000" ;; + 0x0F|0xCF) echo "00003000 00004000 00005000" ;; 0x1F) echo "00006000" ;; + esac +} +# The life cycle fused when discover ran (the shadow can differ until a reset). +fused_lc() { + local w + w="$(sed -n 's/^LC=\(0x[0-9A-Fa-f]*\) .*/\1/p' "$state_dir/discovery" 2>/dev/null)" + [ -n "$w" ] && lc_hex "$w" +} +elf_sym() { arm-none-eabi-nm "$elf" | awk -v s="$1" '$3 == s && !f { print "0x" $1; f = 1 }'; } +# The life cycle wolfBoot handed wolfTrust: the raw handoff record while it is +# intact, else wolfTrust's consumed copy. +handoff_lifecycle() { + local magic inv lc sym + read -r magic inv lc < <(read_words 0x30180000 0x30180004 0x3018000C | tr '\n' ' '; echo) + if [ "${magic:-}" = "5742484f" ] && [ $((0x$magic ^ 0x${inv:-0})) -eq $((0xFFFFFFFF)) ]; then + echo "$lc" + return 0 + fi + [ -s "$elf" ] || return 1 + sym="$(elf_sym g_boot_lifecycle)" + [ -n "$sym" ] || return 1 + read_words "$sym" +} +# The same predicate as wt_platform_guest_flash_wrp_ok: SFP sealed, every valid +# descriptor touching the guest windows locked and write-denying, no gap. +fence_line() { + local -a cmds=(-c "read32 0x50184900 4" -c "read32 0x50184920 4") st=() en=() deny=() desc=() + local addr w0 w1 w2 w3 n mgc="" mdad="" lock last cur found out="" + rt700_fence_bounds || return 1 + for n in 0 1 2 3 4 5 6 7; do + cmds+=(-c "read32 $(printf '0x%x' $((0x50184800 + n * 0x20))) 16") + done + while read -r addr w0 w1 w2 w3; do + case "$addr" in + 50184900:) mdad="$w0" ;; + 50184920:) mgc="$w0" ;; + *) + [ $((0x${w3:-0} & 0x80000000)) -ne 0 ] || continue + lock=$((0x$w3 & 0x60000000)) + st+=("$((0x$w0 & 0xFFFF0000))") + en+=("$(((0x$w1 & 0xFFFF0000) | 0xFFFF))") + desc+=("FRAD$(( (0x${addr%:} - 0x50184800) / 0x20 )) acp=0x$w2 word3=0x$w3") + if [ $((0x$w2 & 0x3F)) -eq 0 ] && [ $((0x$w3 & 0x03000000)) -eq 0 ] && + { [ "$lock" -eq $((0x20000000)) ] || [ "$lock" -eq $((0x60000000)) ]; }; then + deny+=(1) + else + deny+=(0) + fi ;; + esac + done < <(timeout 60 pyocd cmd -t cortex_m "${cmds[@]}" 2>&1 | awk '/^50184/ { print $1, $2, $3, $4, $5 }') + if [ -z "$mgc" ] || [ -z "$mdad" ] || + [ $((0x$mgc & 0xA8000000)) -ne $((0xA8000000)) ] || [ $((0x$mgc & 0xC00)) -eq 0 ] || + [ $((0x$mdad & 0xA0000000)) -ne $((0xA0000000)) ]; then + echo "open SFP not sealed (MGC=0x${mgc:-?} TG0MDAD=0x${mdad:-?})" + return 1 + fi + last=$((RT700_GUEST_FENCE_END - 1)) + for n in "${!st[@]}"; do + if [ "${st[$n]}" -le "$last" ] && [ "${en[$n]}" -ge $((RT700_GUEST_FENCE_START)) ] && [ "${deny[$n]}" = 0 ]; then + echo "open ${desc[$n]} touches the guest windows without a locked write deny" + return 1 + fi + done + cur=$((RT700_GUEST_FENCE_START)) + while [ "$cur" -le "$last" ]; do + found="" + for n in "${!st[@]}"; do + if [ "${deny[$n]}" = 1 ] && [ "${st[$n]}" -le "$cur" ] && [ "${en[$n]}" -ge "$cur" ]; then + found="$n" + break + fi + done + if [ -z "$found" ]; then + echo "open no locked write deny covers $(printf '0x%08X' "$cur")" + return 1 + fi + out="${out:+$out, }${desc[$found]}" + cur=$((en[found] + 1)) + done + echo "armed $out" +} +# The four images run_rt700_hardware.sh flashes, as "address file" lines. +flashed_images() { + printf '%s %s\n' \ + 0x28000000 "${RT700_WORK:-$repo/build/rt700}/flash_wolfboot.bin" \ + 0x28040000 "$repo/build/wolftrust_v1_signed.bin" \ + 0x28080000 "$repo/tests/firmware/mimxrt700-baremetal/build/guest0.bin" \ + 0x28100000 "$repo/tests/firmware/mimxrt700-baremetal/build/guest1.bin" +} +port_image_digest() { framed_digest; } +images_on_device() { + local a f + while read -r a f; do + timeout 120 pyocd cmd -t cortex_m \ + -c "savemem $a $(wc -c < "$f" | tr -d ' ') $state_dir/readback.bin" \ + >/dev/null 2>&1 && cmp -s "$state_dir/readback.bin" "$f" || return 1 + done < <(flashed_images) +} +# The one attached debug probe; the EVK's MCU-Link is soldered to the board. +probe_uid() { + local ids + ids="$(timeout 30 pyocd list 2>/dev/null | awk '$1 ~ /^[0-9]+$/ { print $(NF-1) }')" + [ "$(printf '%s\n' "$ids" | grep -c .)" = "1" ] && echo "$ids" +} +# fuse_word : the burned OTP word over the ISP connection, not the shadow. +fuse_word() { + # shellcheck disable=SC2086 # RT700_ISP is a blhost option list + blhost $RT700_ISP -j efuse-read-once "$1" 2>/dev/null | + "$spsdk_py" -c ' +import json, sys +r = json.load(sys.stdin) +if r.get("status", {}).get("value") != 0 or len(r.get("response", [])) != 2: + sys.exit(1) +print("0x%08X" % r["response"][1])' 2>/dev/null +} + +port_status() { + local lc lcr lock dauth mgc mdad hl vm rf + ensure_spsdk + read -r lc lcr lock dauth mgc mdad < <(read_words "$LC_STATE" "$LC_STATE_RED" \ + "$LOCK_CFG3" "$DAUTHSTATUS" "$XSPI_MGC" "$XSPI_TG0MDAD" | tr '\n' ' '; echo) + echo "OTP life cycle LC_STATE=0x${lc: -2} ($(state_name "$(lc_hex "0x$lc")" || true)) LC_STATE_RED=0x${lcr: -2}" + echo "LOCK_CFG3 0x$lock (LIFE_CYCLE_LOCK=$((0x$lock & 7)): 0 = shadow override and fuse burn both open)" + echo "DAUTHSTATUS 0x$dauth" + echo "XSPI SFP MGC=0x$mgc TG0MDAD=0x$mdad" + echo "guest fence $(fence_line || true)" + if hl="$(handoff_lifecycle)"; then echo "wolfTrust saw 0x$hl ($(psa_name "$hl"))"; fi + if [ -s "$elf" ]; then + read -r vm rf < <(read_words "$(elf_sym g_wt_launch_verified_mask)" \ + "$(elf_sym g_wt_launch_refused_mask)" | tr '\n' ' '; echo) + echo "guest launches verified=0x${vm:-?} refused=0x${rf:-?}" + fi +} +port_discover() { + local lc lcr lock dauth hl + ensure_spsdk + mkdir -p "$state_dir" + rm -f "$state_dir/discovery" "$state_dir/regress-ok" + command -v shadowregs >/dev/null 2>&1 || PATH="$spsdk_venv/bin:$PATH" + shadowregs get-families 2>/dev/null | grep -qi mimxrt798s || + fail "discover" "SPSDK shadowregs has no mimxrt798s support" + pass "SPSDK shadowregs supports mimxrt798s" + read -r lc lcr lock dauth < <(read_words "$LC_STATE" "$LC_STATE_RED" "$LOCK_CFG3" "$DAUTHSTATUS" | tr '\n' ' '; echo) + [ "$(lc_hex "0x$lc")" = "$(lc_hex "0x$lcr")" ] || fail "discover" "life cycle copies disagree (LC 0x$lc, RED 0x$lcr)" + [ $((0x$lc & 0xFFFFFF00)) -eq 0 ] && [ $((0x$lcr & 0xFFFFFF00)) -eq 0 ] || + fail "discover" "life cycle words 0x$lc/0x$lcr carry bits above the state byte (A0/A1 bit-protection copies); only the B0 encoding is validated" + case "$(lc_hex "0x$lc")" in + 0x03|0x07|0x0F) ;; + *) fail "discover" "fused life cycle $(lc_hex "0x$lc") has no further rehearsal step" ;; + esac + pass "fused life cycle is $(label "$(lc_hex "0x$lc")") and its redundant copy agrees" + [ $((0x$lock & 2)) -eq 0 ] || fail "discover" "LIFE_CYCLE_LOCK over-ride protect is set (0x$lock)" + pass "life cycle shadow over-ride is open (LOCK_CFG3 0x$lock)" + hl="$(handoff_lifecycle)" || fail "discover" "no readable boot handoff: run tests/target/run_rt700_hardware.sh first" + pass "the boot handoff life cycle is readable (0x$hl, $(psa_name "$hl"))" + printf 'LC=0x%s RED=0x%s LOCK_CFG3=0x%s DAUTH=0x%s\n' "$lc" "$lcr" "$lock" "$dauth" > "$state_dir/discovery" + echo "PASS: discovery stamped ($state_dir/discovery)" +} +port_restore() { "$target_dir/run_rt700_hardware.sh" wrpfence; } +port_extra() { + local line + case "$1" in + verify-wrp) + ensure_spsdk + line="$(fence_line)" || fail "verify-wrp" "$line" + pass "guest fence $line" ;; + provision-da|burn) + refuse "'$1' programs OTP fuses: 'lock' burns only the life cycle, one rehearsed step at a time. Burning the root key hash and debug root waits for a chip identity that binds the rehearsed part to the ISP target; see the MIMXRT700 Guide." ;; + set-perimeter|set-wrp|clear-wrp) + refuse "'$1' has no MIMXRT700 form: wolfBoot and wolfTrust program the TrustZone perimeter and the XSPI guest fence on every boot, and every reset clears them. Use 'restore' and 'verify-wrp'." ;; + *) return 1 ;; + esac +} + +port_advance_check() { + [ -s "$state_dir/discovery" ] || refuse "run 'discover' first." + [ "$1" = "0x07" ] || [ -s "$state_dir/regress-ok" ] || + refuse "prove 'regress' from Develop2 before advancing to $1." +} +# Halt inside wolfBoot, after the ROM loaded the shadows and before wolfBoot +# reads them, write both life cycle copies, and resume. +port_advance() { + local entry vm rf + ensure_spsdk + vm="$(elf_sym g_wt_launch_verified_mask)"; rf="$(elf_sym g_wt_launch_refused_mask)" + [ -n "$vm" ] && [ -n "$rf" ] || fail "advance" "launch masks not found in $elf" + entry="$(read_words 0x28004004)" + [ -n "$entry" ] && [ "$entry" != "00000000" ] && [ "$entry" != "ffffffff" ] || + fail "advance" "no wolfBoot reset vector at 0x28004004 (flash the chain first)" + echo "ADVANCING the life cycle shadow to $1 ($(state_name "$1")); regress or any reset undoes it" + "$spsdk_py" - "0x$entry" "$1" "$LC_STATE" "$LC_STATE_RED" "$pyocd_target" \ + "$vm" "$rf" 0x30180000 <<'PYEOF' +import sys +import time +from pyocd.core.helpers import ConnectHelper +from pyocd.core.target import Target + +WOLFBOOT_TEXT = (0x28004000, 0x28040000) +entry, value, lc, lc_red = (int(a, 0) for a in sys.argv[1:5]) +with ConnectHelper.session_with_chosen_probe( + options={"target_override": sys.argv[5], "resume_on_disconnect": True, + "reset_type": "hw"}) as s: + t = s.target + t.reset_and_halt() + pc = t.read_core_register("pc") + if pc < WOLFBOOT_TEXT[0]: + t.set_breakpoint(entry & ~1) + t.resume() + deadline = time.time() + 10 + while t.get_state() != Target.State.HALTED or \ + t.read_core_register("pc") != (entry & ~1): + if time.time() > deadline: + sys.exit("wolfBoot entry breakpoint not reached") + time.sleep(0.02) + t.remove_breakpoint(entry & ~1) + pc = t.read_core_register("pc") + if not WOLFBOOT_TEXT[0] <= pc < WOLFBOOT_TEXT[1]: + sys.exit("halted at 0x%08x, outside wolfBoot: too late to move the life cycle" % pc) + t.write32(lc, value) + t.write32(lc_red, value) + got = (t.read32(lc) & 0xFF, t.read32(lc_red) & 0xFF) + # SRAM survives the reset: clear the launch masks and the handoff magic so + # only this boot's wolfBoot and wolfTrust can set them. + for a in sys.argv[6:9]: + t.write32(int(a, 0), 0) + if t.read32(int(a, 0)) != 0: + sys.exit("could not clear the boot evidence at %s" % a) + t.resume() +print("halted in wolfBoot at 0x%08x; shadow LC_STATE=0x%02x LC_STATE_RED=0x%02x" + % (pc, got[0], got[1])) +sys.exit(0 if got == (value, value) else 3) +PYEOF + sleep 3 +} +# The rehearsal evidence: the life cycle wolfTrust saw, every guest launched +# verified, the fence, the images read back, and the one attached probe. +port_booted() { + local hl want vm rf fence digest probe + # advance cleared the handoff and the launch masks, so both are this boot's. + hl="$(handoff_lifecycle)" || { echo "no boot handoff written after the advance"; return 1; } + echo "wolfTrust saw 0x$hl ($(psa_name "$hl"))" + case " $(expect_psa "$1") " in + *" $hl "*) pass "wolfTrust booted with the $(state_name "$1") life cycle" ;; + *) echo "wolfTrust saw 0x$hl, not the life cycle of $1"; return 1 ;; + esac + # In Field Return is decommissioned: wolfTrust need not launch guests there. + if [ "$1" != "0x1F" ]; then + want=$((guest_mask)) + for _ in 1 2 3 4 5 6 7 8 9 10; do + read -r vm rf < <(read_words "$(elf_sym g_wt_launch_verified_mask)" \ + "$(elf_sym g_wt_launch_refused_mask)" | tr '\n' ' '; echo) + [ -n "${vm:-}" ] && [ $(((0x$vm | 0x${rf:-0}) & want)) -eq "$want" ] && break + sleep 1 + done + [ -n "${vm:-}" ] && [ $((0x$vm)) -eq "$want" ] && [ $((0x${rf:-1})) -eq 0 ] || { + echo "guests did not all launch (verified=0x${vm:-?} refused=0x${rf:-?}, want $guest_mask)" + return 1 + } + pass "guests launched (verified=0x$vm refused=0x$rf)" + fi + fence="open" + if fence_line >/dev/null; then fence="armed"; fi + digest="$(port_image_digest)" || { echo "missing a flashed image"; return 1; } + images_on_device || { echo "the images on the part differ from the host build: run 'restore'"; return 1; } + pass "the images on the part match the host build (${digest:0:16})" + probe="$(probe_uid)" || { echo "attach exactly one debug probe"; return 1; } + EVIDENCE="id=$probe image=$digest fused=$(fused_lc) fence=$fence" +} +port_regress() { + local fused lc lcr hl + ensure_spsdk + timeout 60 pyocd reset -t "$pyocd_target" -m hw >/dev/null 2>&1 || true + "$target_dir/lib/rt700_reset.sh" reset + sleep 3 + fused="$(fused_lc || echo "$LC_DEVELOP")" + read -r lc lcr < <(read_words "$LC_STATE" "$LC_STATE_RED" | tr '\n' ' '; echo) + [ "$(lc_hex "0x$lc")" = "$fused" ] && [ "$(lc_hex "0x$lcr")" = "$fused" ] || + fail "regress" "life cycle after reset is 0x$lc/0x$lcr, not the fused $fused (run discover)" + pass "hardware reset reloaded the fused $(state_name "$fused") life cycle" + hl="$(handoff_lifecycle)" || fail "regress" "cannot read the life cycle wolfTrust saw after the reset" + case " $(expect_psa "$fused") " in + *" $hl "*) pass "wolfTrust booted $(psa_name "$hl") again" ;; + *) fail "regress" "wolfTrust saw 0x$hl after regress" ;; + esac + # Only a regress out of a recorded mock proves the way back for later steps. + if [ -n "$PENDING" ]; then + mkdir -p "$state_dir" + date -u +%FT%TZ > "$state_dir/regress-ok" + fi +} + +port_cred_fp() { echo none; } +port_lock_current() { + local lc lcr + [ -n "${RT700_ISP:-}" ] || refuse "set RT700_ISP to the blhost ISP connection (for example '-u 0x1fc9,0x014f')." + ensure_spsdk + command -v blhost >/dev/null 2>&1 || PATH="$spsdk_venv/bin:$PATH" + if ! lc="$(fuse_word "$FUSE_LC")" || ! lcr="$(fuse_word "$FUSE_LC_RED")"; then + refuse "cannot read the life cycle fuses over RT700_ISP ($RT700_ISP)." + fi + [ "$(lc_hex "$lc")" = "$(lc_hex "$lcr")" ] || refuse "the life cycle fuses disagree (LC $lc, RED $lcr)." + [ $((lc & 0xFFFFFF00)) -eq 0 ] && [ $((lcr & 0xFFFFFF00)) -eq 0 ] || + refuse "the life cycle fuses $lc/$lcr carry bits above the state byte (A0/A1 bit-protection copies); only the B0 burn encoding is validated." + lc_hex "$lc" +} +# The burn runs over ISP USB, and no chip identity is documented that both the +# SWD rehearsal and ISP can read, so the main script requires a bound fixture. +port_lock_identity() { :; } +port_rehearsal_for() { echo "$1"; } +port_record_ok() { + local rfused + rfused="$(field fused "$1")" + [ -n "$rfused" ] && [ $((rfused & ~cur & 0xFF)) -eq 0 ] && + [ "$(field fence "$1")" = "armed" ] && + [ "$(probe_uid || true)" = "$(field id "$1")" ] +} +port_ready() { + case "$1" in + 0x0F|0xCF|0x1F) + refuse "$(label "$1") needs the BootROM to authenticate wolfBoot (a signed image under the fused root key hash), which this port does not build yet; see the MIMXRT700 Guide." ;; + esac + CHECKED="$CHECKED, same debug probe $(probe_uid)" +} +burn_script() { + printf 'efuse-program-once %s %08X --no-verify\nefuse-program-once %s %08X --no-verify\n' \ + "$FUSE_LC_RED" "$(($1))" "$FUSE_LC" "$(($1))" +} +port_lock_plan() { burn_script "$1" | sed "s|^|blhost $RT700_ISP |"; } +port_consequence() { echo "fuses cannot be unburned, and the part never returns to $(label "$cur")."; } +port_lock_write() { + LC0="$(fuse_word "$FUSE_LC")" + LCR0="$(fuse_word "$FUSE_LC_RED")" + mkdir -p "$state_dir" + burn_script "$1" > "$state_dir/burn-$1.bls" + # shellcheck disable=SC2086 # RT700_ISP is a blhost option list + blhost $RT700_ISP batch "$state_dir/burn-$1.bls" || + echo "blhost batch failed part way; reading both life cycle words back" +} +port_lock_verify() { + local lc lcr + if ! lc="$(fuse_word "$FUSE_LC")" || ! lcr="$(fuse_word "$FUSE_LC_RED")"; then + fail "lock" "cannot read the life cycle fuses back" + fi + [ "$(lc_hex "$lc")" = "$1" ] && [ "$(lc_hex "$lcr")" = "$1" ] && + [ $((lc >> 8)) -eq $((LC0 >> 8)) ] && [ $((lcr >> 8)) -eq $((LCR0 >> 8)) ] || + fail "lock" "fuses read LC $lc, RED $lcr after the burn (before: $LC0, $LCR0), expected $1" + echo "reset the part, then run: TARGET=mimxrt700 $0 status" +} diff --git a/tests/target/provisioning/provisioning_ctrl_stm32h563.sh b/tests/target/provisioning/provisioning_ctrl_stm32h563.sh new file mode 100644 index 00000000..d6053288 --- /dev/null +++ b/tests/target/provisioning/provisioning_ctrl_stm32h563.sh @@ -0,0 +1,455 @@ +# shellcheck shell=bash +# provisioning_ctrl_stm32h563.sh +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfTrust. +# +# wolfTrust 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. +# +# wolfTrust 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 . + +# STM32H563 (NUCLEO-H563ZI) port for provisioning_ctrl.sh: product states set +# by option bytes through STM32CubeProgrammer, rehearsed by a Debug +# Authentication (DA) regression. Sourced, never run. See docs/STM32H5-Guide.md. +# shellcheck disable=SC2034,SC2154 # PORT_* are read, and $cmd/$PENDING set, by the main script + +# A port file only works inside provisioning_ctrl.sh, which holds the gates. +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + echo "REFUSED: run TARGET=stm32h563 tests/target/provisioning/provisioning_ctrl.sh, not this port file." >&2 + exit 2 +fi + +PORT_NAME="STM32H563" +CP="${STM32_CP:-$HOME/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin}" +CLI="${STM32_CLI:-$CP/STM32_Programmer_CLI}" +SERIAL="${H5_SERIAL:-/dev/ttyACM0}" + +# ST's NUCLEO-H563ZI ROT_Provisioning/DA sample. wolfTrust runs TZEN enabled, +# so DA is certificate based (AN6008); a password OBK cannot regress the part. +DA_SAMPLE_DIR="$HOME/st-rot-h5/Projects/NUCLEO-H563ZI/ROT_Provisioning/DA" +# SHA-256 of every file in that public sample (Binary, Keys, Certificates), so +# a renamed copy is caught even where the sample tree is not installed. +DA_SAMPLE_SHA256=" +2cafcf533300aebe1ebaaa2b6bd6e99c3502ef88acd668b2d41f910515527862 +f4d40b1b669a635e15719eaf0f6fd9f7b5494e3616a6337d2c350eca7f2d4547 +774e73f4c0ec7f9617da41405b0eb02d560ea498af8717de91b411203d1af499 +32227fc98010224ab39dd8fb5bb3b917820047fb52a1be91551e9c7e6e424590 +ef86c2ea01df5fdf526fb7a68fb4876436131f575093b3bc90f7428e677e72b2 +b00d6ad78f9d8a5b1a9299bc618fd651b7637ed0873fb9104a674edd5a19569b +d21811a12f5533f474901f2bec27ae4c85c76a4968394e221edc55f4c291ac0a +1c343faca2e1c81c913afe520aa0de26b8b01a71c0ab2d7796a356cd4d127a59 +67ba2294171501a8df9864da5d18ecee386b69f1f197138cb3452ed4aed8d854 +d4ac966902c0129bd311a23794d5430d4e4ecb75071195135adfd64373c56d19 +10ef8afae7bad8608cb01d803347dfc396210c65eaa2f808ce1b52b757a3ea3d +9c6c7589abc052671f107a5b3cc7e9437865b342ed5909d8fef73c3f5771b560 +d8617a54b88c6061f310d75b66d6319e9b87212a8f53401f039067f9aa520894 +9a77fd6ab8533715976bc83dc72182c3c1cb568f30fc3630e911eb1c64a33e59 +" +DA_DIR="${WT_DA_DIR:-$DA_SAMPLE_DIR}" +DA_OBK="${WT_DA_OBK:-$DA_DIR/Binary/DA_Config.obk}" +DA_PWD="${WT_DA_PWD:-$DA_DIR/Binary/password.bin}" +DA_KEY="${WT_DA_KEY:-$DA_DIR/Keys/key_3_leaf.pem}" +DA_CERT="${WT_DA_CERT:-$DA_DIR/Certificates/cert_leaf_chain.b64}" +DA_CONN="-c port=SWD speed=fast ap=1 mode=Hotplug" +DA_CONN_RST="-c port=SWD speed=fast ap=1 mode=Hotplug -hardRst" + +# The OEM-iRoT perimeter read from a known-good wolfTrust board. SECWM1_END +# must span the whole boot partition, or writes past 0x08080000 are dropped. +WT_OB=(TZEN=0xB4 BOOT_UBE=0xB4 SWAP_BANK=0x0 + SECWM1_STRT=0x0 SECWM1_END=0x4F SECWM2_STRT=0x0 SECWM2_END=0x7F) +# WRPSGn1: 0 protects four sectors per bit; 0x000FFFFF covers the guests. +WRP_GUEST=0x000FFFFF; WRP_OPEN=0xFFFFFFFF +PS_OPEN=0xED; PS_PROVISIONING=0x17 + +WOLFBOOT=0x0C000000; WOLFTRUST=0x0C060000; GUEST0=0x080A0000; GUEST1=0x080E0000 +wb="$repo/wolfBoot/wolfboot.bin" +wt="$repo/build/wolftrust_v1_signed.bin" +g0="$repo/tests/firmware/zephyr-stm32h5/build/guest0_psa/zephyr/zephyr.bin" +g1="$repo/tests/firmware/zephyr-stm32h5/build/freertos_guest1/freertos_guest1.bin" + +port_ladder() { + cat <<'EOF' +0xED open no no reversible - +0x17 provisioning yes yes reversible 0xED +0xC6 tz-closed yes yes reversible 0x17 +0x72 closed yes yes reversible 0x17 +0x5C locked no yes permanent 0x17 +EOF +} + +port_usage_extra() { + cat <<'EOF' + set-perimeter set the wolfTrust TrustZone option bytes (writes) + set-wrp, clear-wrp protect or release the guest flash (writes, Open only) + flash, verify flash the images; reset and check the boot + provision-da provision the DA OBK (writes, Provisioning only) +EOF +} + +strip() { sed -e 's/\x1b\[[0-9;]*[A-Za-z]//g'; } + +# A read right after a reset can return garbage, so only a known code counts. +product_state() { + local v + for _ in 1 2 3; do + v="$("$CLI" -c port=SWD mode=HotPlug -ob displ 2>&1 | strip \ + | grep -iE "PRODUCT_STATE" | grep -oE "0x[0-9A-Fa-f]+" | head -1 || true)" + case "$(printf '0x%02X' "$(( ${v:-0x100} ))")" in + 0xED|0x17|0x2E|0xC6|0x72|0x5C) hexstate "$v"; return 0 ;; + esac + sleep 2 + done + return 1 +} +st_lifecycle() { + case "$1" in + 0x17) echo "ST_LIFECYCLE_PROVISIONING" ;; 0xC6) echo "ST_LIFECYCLE_TZ_CLOSED" ;; + 0x72) echo "ST_LIFECYCLE_CLOSED" ;; 0x5C) echo "ST_LIFECYCLE_LOCKED" ;; + esac +} +# shellcheck disable=SC2086 # DA_CONN is an option list +da_discovery() { "$CLI" $DA_CONN pwd="$DA_PWD" debugauth=2 2>&1 | strip; } +da_ready() { + local disc + disc="$(da_discovery)" + grep -q "0xeaeaeaea" <<<"$disc" && grep -q "Full Regression" <<<"$disc" +} +# A closed part drops the debug link, so its state is read by DA discovery. +da_lifecycle() { da_discovery | grep -oE "ST_LIFECYCLE_[A-Z_]+" | head -1; } +# Only output that arrives after the capture starts counts as a boot: a tty is +# drained first, and a file (a replay or test) is read from its current end. +uart_capture() { + rm -f "$2.from" + if [ -c "$SERIAL" ]; then + stty -F "$SERIAL" 115200 raw -echo 2>/dev/null || true + timeout 1 cat "$SERIAL" >/dev/null 2>&1 || true + ( timeout "$1" cat "$SERIAL" > "$2" 2>/dev/null & ) + else + : > "$2" + wc -c < "$SERIAL" | tr -d ' ' > "$2.from" + fi +} +booted() { + if [ -s "$1.from" ]; then tail -c +$(( $(cat "$1.from") + 1 )) "$SERIAL"; else cat "$1"; fi | + strip | grep -aqE "guest0_psa|heartbeat|TEE client" +} + +flashed_images() { + printf '%s %s\n' "$WOLFBOOT" "$wb" "$WOLFTRUST" "$wt" "$GUEST0" "$g0" "$GUEST1" "$g1" +} +port_image_digest() { framed_digest; } +# The running chain hides guest flash from the debugger, so read under reset. +images_on_device() { + local a f n=0 rc=0 + local -a reads=() + while read -r a f; do + reads+=(-u "$a" "$(wc -c < "$f" | tr -d ' ')" "$wt_tmp/readback.$n.bin") + n=$((n + 1)) + done < <(flashed_images) + "$CLI" -c port=SWD mode=UR "${reads[@]}" >/dev/null 2>&1 || rc=1 + "$CLI" -c port=SWD mode=UR -rst >/dev/null 2>&1 || true + n=0 + while read -r a f; do + [ "$rc" = 0 ] && cmp -s "$wt_tmp/readback.$n.bin" "$f" || rc=1 + n=$((n + 1)) + done < <(flashed_images) + return "$rc" +} +# The 96-bit device UID (RM0481 UID_BASE): readable under reset in Open only. +device_uid() { + local u + u="$("$CLI" -c port=SWD mode=UR -r32 0x08FFF800 12 2>&1 | strip \ + | awk '$1 == "0x08FFF800" { print $3 $4 $5 }')" + "$CLI" -c port=SWD mode=UR -rst >/dev/null 2>&1 || true + case "$u" in + ""|000000000000000000000000|ffffffffffffffffffffffff|FFFFFFFFFFFFFFFFFFFFFFFF) return 1 ;; + esac + echo "$u" +} +# The perimeter and guest WRP option bytes this port sets, as NAME=value. +ob_values() { + "$CLI" -c port=SWD mode=HotPlug -ob displ 2>&1 | strip \ + | awk '$1 ~ /^(TZEN|BOOT_UBE|SWAP_BANK|SECWM[12]_(STRT|END)|WRPSGn1)$/ && $2 == ":" { print $1 "=" $3 }' \ + | sort || true +} +# A read near a reset can be transient, so two reads in a row must agree. +ob_snapshot() { + local a b + a="$(ob_values)" + for _ in 1 2 3; do + sleep 1 + b="$(ob_values)" + if [ "$(grep -c . <<<"$b")" -eq 8 ] && [ "$a" = "$b" ]; then + sha256 <<<"$b" | cut -c1-64 + return 0 + fi + a="$b" + done + return 1 +} +# ob_safe [wrp]: the live option bytes are wolfTrust's perimeter (and guest WRP). +ob_safe() { + local v kv name want + v="$(ob_values)" + for kv in "${WT_OB[@]}" ${1:+WRPSGn1=$WRP_GUEST}; do + name="${kv%%=*}"; want="${kv#*=}" + [ "$(( $(sed -n "s/^$name=//p" <<<"$v" | head -1) ))" = "$(( want ))" ] 2>/dev/null || return 1 + done +} + +# Every DA input a regression used: key, certificate chain, OBK, and password. +port_cred_fp() { + local f + for f in "$DA_KEY" "$DA_CERT" "$DA_OBK" "$DA_PWD"; do + [ -s "$f" ] || { echo none; return 0; } + done + for f in "$DA_KEY" "$DA_CERT" "$DA_OBK" "$DA_PWD"; do sha256 < "$f"; done | sha256 | cut -c1-64 +} +da_is_sample() { + local h s + h="$(sha256 < "$1" | cut -c1-64)" + case "$DA_SAMPLE_SHA256" in *"$h"*) return 0 ;; esac + if [ -d "$DA_SAMPLE_DIR" ]; then + while IFS= read -r s; do + [ "$(sha256 < "$s" | cut -c1-64)" != "$h" ] || return 0 + done < <(find "$DA_SAMPLE_DIR" -type f) + fi + return 1 +} +# A production part must carry its own DA credential, never ST's sample. +da_production_ready() { + local f + [ -n "${WT_DA_OBK:-}" ] && [ -n "${WT_DA_KEY:-}" ] && [ -n "${WT_DA_CERT:-}" ] && + [ -n "${WT_DA_PWD:-}" ] || return 1 + for f in "$DA_OBK" "$DA_KEY" "$DA_CERT" "$DA_PWD"; do + [ -s "$f" ] || return 1 + ! da_is_sample "$f" || return 1 + done +} +da_refuse_sample() { + refuse "a production part needs its own DA credential: set WT_DA_OBK, WT_DA_KEY, WT_DA_CERT, and WT_DA_PWD, not ST's sample." +} + +set_perimeter() { + echo "Setting wolfTrust OEM-iRoT perimeter: ${WT_OB[*]}" + # TZEN first: an off-to-on flip mass-erases. + "$CLI" -c port=SWD mode=UR -ob TZEN=0xB4 2>&1 | strip | tail -3 + "$CLI" -c port=SWD mode=UR -ob "${WT_OB[@]}" 2>&1 | strip | tail -4 +} +set_wrp() { + [ "$(product_state || true)" = "$PS_OPEN" ] || fail "set-wrp" "WRP is settable only in Open" + echo "Write-protecting guest flash (bank1 sectors 0x50-0x7F): WRPSGn1=$WRP_GUEST" + "$CLI" -c port=SWD mode=UR -ob WRPSGn1="$WRP_GUEST" 2>&1 | strip | tail -4 +} +clear_wrp() { + echo "Clearing guest-flash write protection: WRPSGn1=$WRP_OPEN" + "$CLI" -c port=SWD mode=UR -ob WRPSGn1="$WRP_OPEN" 2>&1 | strip | tail -4 +} +flash_images() { + local f + rm -f "$state_dir/readback" + for f in "$wb" "$wt" "$g0" "$g1"; do [ -s "$f" ] || fail "flash" "missing image: $f"; done + "$CLI" -c port=SWD mode=UR -d "$wb" "$WOLFBOOT" -d "$wt" "$WOLFTRUST" \ + -d "$g0" "$GUEST0" -d "$g1" "$GUEST1" --verify -hardRst 2>&1 | strip \ + | grep -iE "verified successfully|error|download" | tail -4 +} +verify_boot() { + uart_capture 8 "$wt_tmp/verify.log" + "$CLI" -c port=SWD mode=UR -rst >/dev/null 2>&1 || true + sleep 7 + booted "$wt_tmp/verify.log" || fail "verify" "no wolfTrust boot markers on $SERIAL" + pass "wolfTrust chain boots on silicon" +} +provision_da() { + [ -s "$DA_OBK" ] || fail "provision-da" "DA OBK not found: $DA_OBK" + [ "${WT_PRODUCTION_LOCK:-0}" != "1" ] || da_production_ready || da_refuse_sample + [ "$(product_state || true)" = "$PS_PROVISIONING" ] || fail "provision-da" "only in Provisioning" + echo "Provisioning DA OBK: $DA_OBK" + # shellcheck disable=SC2086 + "$CLI" $DA_CONN_RST >/dev/null 2>&1 || true + # shellcheck disable=SC2086 + "$CLI" $DA_CONN -sdp "$DA_OBK" 2>&1 | strip | tail -2 + # shellcheck disable=SC2086 + "$CLI" $DA_CONN_RST >/dev/null 2>&1 || true + da_ready || fail "provision-da" "discovery does not show an intact OBK offering Full Regression" + put da-provisioned "cred=$(port_cred_fp) time=$(date +%s)" + pass "DA provisioned and recorded for this credential set" +} + +port_status() { + "$CLI" -c port=SWD mode=HotPlug -ob displ 2>&1 | strip \ + | grep -iE "PRODUCT_STATE|TZEN|BOOT_UBE|SECWM|SECBOOT|WRPSGn1" | head -20 +} +port_discover() { + echo "DA discovery (non-destructive):" + da_discovery | grep -iE "PSA lifecycle|integrity|permission|Discovery Success|not supported|error" || true +} +port_restore() { set_perimeter; clear_wrp; flash_images; set_wrp; verify_boot; } +port_extra() { + case "$1" in + set-perimeter) confirm; set_perimeter ;; + set-wrp) confirm; set_wrp ;; + clear-wrp) confirm; clear_wrp ;; + flash) confirm; flash_images ;; + verify) verify_boot ;; + provision-da) confirm; provision_da ;; + *) return 1 ;; + esac +} + +# Closed states are written from Provisioning: from TrustZone Closed the debug +# link cannot write the next state (seen 2026-08-19). +port_advance_check() { + local cur rb + cur="$(product_state || true)" + if [ "$1" = "$PS_PROVISIONING" ]; then + [ "$cur" = "$PS_OPEN" ] || refuse "advance to Provisioning runs from Open; state=${cur:-unreadable}." + return 0 + fi + [ "$cur" = "$PS_PROVISIONING" ] || + refuse "advance to $(label "$1") runs only from Provisioning (0x17); state=${cur:-unreadable}." + da_ready || refuse "Debug Authentication is not provisioned (no intact OBK offering Full Regression): without it $(label "$1") cannot be regressed. Run 'provision-da' and 'discover'." + rb="$(cat "$state_dir/readback" 2>/dev/null || true)" + if [ "$(field image "$rb")" != "$(port_image_digest)" ] || ! fresh "$(field time "$rb")"; then + refuse "no recent read-back of these images: 'restore', then 'advance 0x17' from Open reads them back." + fi +} +# Provisioning closes Secure debug, so the part is read back while still Open. +port_advance() { + local uid ob + if [ "$1" = "$PS_PROVISIONING" ]; then + rm -f "$state_dir/readback" + ob="$(ob_snapshot || true)" + uid="$(device_uid || true)" + if [ -n "$uid" ] && [ -n "$ob" ] && images_on_device; then + put readback "image=$(port_image_digest) id=$uid ob=$ob time=$(date +%s)" + pass "the images on device $uid match the host build ($(port_image_digest | cut -c1-16))" + else + refuse "could not read back the images, device UID, and option bytes; nothing was written. Run 'restore' first." + fi + fi + echo "ADVANCING product state $(product_state || echo '?') -> $1 (regress is the only way back)" + uart_capture 12 "$wt_tmp/advance.log" + # The CLI fails its post-write reconnect once debug closes; the read-back decides. + "$CLI" -c port=SWD mode=HotPlug -ob PRODUCT_STATE="$1" 2>&1 | strip | tail -4 || true + sleep 10 + echo "now: $(product_state || da_lifecycle || true)" +} +# The read-back resets the part, so only a closed state's capture proves the boot. +port_booted() { + local rb + rb="$(cat "$state_dir/readback" 2>/dev/null || true)" + [ -n "$rb" ] || return 1 + if [ "$(product_state || true)" != "$1" ] && + [ "$(da_lifecycle || true)" != "$(st_lifecycle "$1")" ]; then + echo "the part does not read back as $(label "$1") after the write" + return 1 + fi + pass "the part reads back as $(label "$1")" + if [ "$1" != "$PS_PROVISIONING" ]; then + booted "$wt_tmp/advance.log" || { echo "no wolfTrust boot markers on $SERIAL after the write"; return 1; } + pass "wolfTrust chain boots in $(label "$1")" + fi + EVIDENCE="id=$(field id "$rb") image=$(field image "$rb") ob=$(field ob "$rb")" +} +port_regress() { + local after uid + rm -f "$state_dir/readback" "$state_dir/da-provisioned" + echo "DA certificate Full Regression -> Open (mass-erase):" + "$CLI" -c port=SWD mode=HotPlug -rst 2>&1 | strip | tail -1 || true + "$CLI" -c port=SWD per=a key="$DA_KEY" cert="$DA_CERT" pwd="$DA_PWD" \ + debugauth=1 &1 | strip | tail -4 + after="$(product_state || true)" + echo "state after regression: ${after:-unreadable}" + [ "$after" = "$PS_OPEN" ] || return 1 + [ -n "$PENDING" ] || return 0 + uid="$(device_uid || true)" + [ -n "$uid" ] && [ "$uid" = "$(field id "$PENDING")" ] || { + echo "device UID ${uid:-unreadable} is not the rehearsed one; not recorded" + return 1 + } +} + +port_lock_current() { + product_state || refuse "cannot read the product state over SWD (a closed part drops the link)." +} +port_lock_identity() { + [ "$(product_state || true)" = "$PS_OPEN" ] || return 0 + device_uid || refuse "cannot read the device UID over SWD." +} +# Provisioning runs no wolfTrust chain, so its step is covered by any closing +# rehearsal; Locked cannot be rehearsed, so Closed stands in for it. +port_rehearsal_for() { + case "$1" in + 0x17) echo "0x17 0xC6 0x72" ;; 0x5C) echo "0x72" ;; *) echo "$1" ;; + esac +} +port_record_ok() { [ "$(ob_snapshot || true)" = "$(field ob "$1")" ]; } +port_ready() { + local dp dt rt + if [ "$cur" = "$PS_OPEN" ]; then + images_on_device || refuse "the images on this part differ from the rehearsed build: 'restore' it first." + CHECKED="$CHECKED, images read back" + else + CHECKED="$CHECKED, images not readable in Provisioning" + fi + if [ "$1" = "$PS_PROVISIONING" ]; then + ob_safe || refuse "the TrustZone perimeter option bytes are not wolfTrust's: run 'set-perimeter' in Open." + CHECKED="$CHECKED, perimeter values" + return 0 + fi + ob_safe wrp || refuse "the perimeter or guest WRP option bytes are not wolfTrust's: run 'set-perimeter' and 'set-wrp' in Open." + CHECKED="$CHECKED, perimeter and guest WRP values" + # Locked too: the read-back after every closing write needs DA discovery. + if da_production_ready; then + CHECKED="$CHECKED, production DA credential" + else + [ "${WT_PRODUCTION_LOCK:-0}" != "1" ] || da_refuse_sample + CHECKED="$CHECKED, DA credential is ST's sample or unset (a production lock refuses it)" + fi + da_ready || refuse "Debug Authentication is not provisioned (no intact OBK offering Full Regression): run 'provision-da' and 'discover'." + # Regression wipes the DA, so the one on the part must be this tool's install + # of the rehearsed credentials since that rehearsal; discovery cannot tell. + dp="$(cat "$state_dir/da-provisioned" 2>/dev/null || true)" + dt="$(field time "$dp")"; rt="$(field completed "$rec")" + if [ "$(field cred "$dp")" != "$(port_cred_fp)" ] || [ "${dt:-0}" -lt "${rt:-0}" ]; then + refuse "the DA on this part was not installed by 'provision-da' with these credentials since the rehearsal: run 'provision-da'." + fi + CHECKED="$CHECKED, DA provisioned with the rehearsed credentials" +} +port_lock_plan() { echo "$CLI -c port=SWD mode=HotPlug -ob PRODUCT_STATE=$1"; } +port_consequence() { + case "$1" in + 0x5C) echo "debug closes for good, no regression or mass erase, and only a wolfBoot-signed update can change the firmware." ;; + *) echo "Only a DA regression, which mass-erases the part, returns it to Open." ;; + esac +} +port_lock_write() { + uart_capture 12 "$wt_tmp/lock.log" + "$CLI" -c port=SWD mode=HotPlug -ob PRODUCT_STATE="$1" 2>&1 | strip | tail -4 || true + sleep 10 +} +port_lock_verify() { + local now + if [ "$1" = "$PS_PROVISIONING" ]; then + now="$(product_state || true)" + [ "$now" = "$1" ] || fail "lock" "product state reads ${now:-unreadable} after the write, expected $1" + return 0 + fi + now="$(da_lifecycle || true)" + [ "$now" = "$(st_lifecycle "$1")" ] || + fail "lock" "DA discovery reports ${now:-nothing} after the write, expected $(st_lifecycle "$1"); the write may still have landed, so do not repeat it" + booted "$wt_tmp/lock.log" || + fail "lock" "the part is $(label "$1") but wolfTrust did not boot on $SERIAL; do not ship it" + pass "wolfTrust chain boots in $(label "$1")" +} diff --git a/tests/target/provisioning/test_provisioning_gates.sh b/tests/target/provisioning/test_provisioning_gates.sh new file mode 100755 index 00000000..0792f926 --- /dev/null +++ b/tests/target/provisioning/test_provisioning_gates.sh @@ -0,0 +1,364 @@ +#!/usr/bin/env bash +# test_provisioning_gates.sh +# +# Copyright (C) 2026 wolfSSL Inc. +# +# This file is part of wolfTrust. +# +# wolfTrust 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. +# +# wolfTrust 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 . + +# Offline tests of the production lock gates in provisioning_ctrl.sh (STM32H5) +# and provisioning_ctrl_rt700.sh (MIMXRT700). Every vendor tool is a stub that +# records writes to a file; nothing here touches a board. +# shellcheck disable=SC2015 # result lines are "cond && pass || fail" on purpose +set -u +SRC="$(cd "$(dirname "$0")/../../.." && pwd)" +T="$(mktemp -d)" && [ -n "$T" ] && [ -d "$T" ] || { echo "cannot create a temporary directory" >&2; exit 1; } +trap 'rm -rf "$T"' EXIT +R="$T/repo" +mkdir -p "$R/build" "$R/wolfBoot" "$R/tests/firmware/zephyr-stm32h5/build/guest0_psa/zephyr" \ + "$R/tests/firmware/zephyr-stm32h5/build/freertos_guest1" "$T/st" "$T/bin" "$T/venv/bin" +cp -R "$SRC/tests/target" "$R/tests/" +mkdir -p "$R/mk"; cp "$SRC/mk/target-mimxrt700.mk" "$R/mk/" +echo wb > "$R/wolfBoot/wolfboot.bin"; echo wt > "$R/build/wolftrust_v1_signed.bin" +echo g0 > "$R/tests/firmware/zephyr-stm32h5/build/guest0_psa/zephyr/zephyr.bin" +echo g1 > "$R/tests/firmware/zephyr-stm32h5/build/freertos_guest1/freertos_guest1.bin" +echo elf > "$R/build/wolftrust.elf" +mkdir -p "$R/build/rt700" "$R/tests/firmware/mimxrt700-baremetal/build" +echo rwb > "$R/build/rt700/flash_wolfboot.bin" +echo rg0 > "$R/tests/firmware/mimxrt700-baremetal/build/guest0.bin" +echo rg1 > "$R/tests/firmware/mimxrt700-baremetal/build/guest1.bin" +SD="$T/home/st-rot-h5/Projects/NUCLEO-H563ZI/ROT_Provisioning/DA" +mkdir -p "$SD/Binary" "$SD/Keys" "$SD/Certificates" "$T/prodda" +echo sobk > "$SD/Binary/DA_Config.obk"; echo spwd > "$SD/Binary/password.bin" +echo skey > "$SD/Keys/key_3_leaf.pem"; echo scert > "$SD/Certificates/cert_leaf_chain.b64" +echo pobk > "$T/prodda/obk"; echo pkey > "$T/prodda/key"; echo pcert > "$T/prodda/cert"; echo ppwd > "$T/prodda/pwd" +echo pkey2 > "$T/prodda/key2" +mkdir -p "$T/flash" + +# STM32 CLI stub: state in $T/ps, $T/wrp; logs writes to $T/writes. +cat > "$T/bin/stcli" < Full Regression"; true ;; + debugauth=1) [ -e $T/noregress ] || echo 0xED > $T/ps; echo "Debug Authentication Success" ;; + -sdp) echo 0xeaeaeaea > $T/da; echo "OBKey Provisioned successfully" ;; + PRODUCT_STATE=*) echo "\$a" >> $T/writes; [ -e $T/stuck ] || echo "\${a#PRODUCT_STATE=}" > $T/ps; [ ! -e $T/boots ] || echo "guest0_psa heartbeat" >> $T/uart; echo "Error: failed to reconnect after reset !"; exit 1 ;; + esac +done +EOF +printf '#!/bin/sh\nshift\nexec "$@"\n' > "$T/bin/timeout"; chmod +x "$T/bin/timeout" +echo "guest0_psa heartbeat" > "$T/uart"; touch "$T/boots"; echo 0xf5f5f5f5 > "$T/da" +# blhost stub: fuses in $T/fuse.; batch logs to $T/writes. +cat > "$T/venv/bin/blhost" </dev/null || echo 0) + printf '{"command":"efuse-read-once","response":[4,%d],"status":{"value":0}}\n' \$((v)) ;; + batch) cat "\$2" >> $T/writes + while read -r c a d _; do i=\$((a)); o=\$(cat $T/fuse.\$i 2>/dev/null || echo 0) + echo \$(( o | 0x\$d )) > $T/fuse.\$i; [ ! -e $T/batchfail ] || exit 1; done < "\$2" ;; +esac +EOF +cat > "$T/venv/bin/pyocd" </dev/null; exit 0; fi +[ "\$1" = list ] || exit 0 +echo " # Probe/Board Unique ID Target" +echo "-----------------------------------" +n=0; for u in \$(cat $T/probe 2>/dev/null); do echo " \$n NXP MCU-LINK on-board \$u n/a"; n=\$((n+1)); done +EOF +echo PROBEA > "$T/probe" +cp "$T/bin/timeout" "$T/venv/bin/timeout" +printf '#!/bin/sh\nexec %s "$@"\n' "$(command -v python3)" > "$T/venv/bin/python"; chmod +x "$T/venv/bin/python" +sha() { if command -v sha256sum >/dev/null 2>&1; then sha256sum; else shasum -a 256; fi; } +subst() { sed "s/$1/$2/" "$3" > "$3.tmp" && mv "$3.tmp" "$3"; } +have_expect=0; command -v expect >/dev/null 2>&1 && have_expect=1 +chmod +x "$T/bin/stcli" "$T/venv/bin/blhost" "$T/venv/bin/pyocd" + +pass=0; failn=0 +check() { # check -- cmd... + local name="$1" want="$2" pat="$3"; shift 4 + out="$("$@" 2>&1 "$T/writes"; fi; } + +P="$R/tests/target/provisioning/provisioning_ctrl.sh" +H5=(env TARGET=stm32h563 HOME="$T/home" PATH="$T/bin:$PATH" STM32_CLI="$T/bin/stcli" H5_SERIAL="$T/uart" WT_PROVISION_STATE="$T/st" "$P") +HS="$T/st/stm32h563" +dafp() { for f in "$@"; do sha < "$f"; done | sha | cut -c1-64; } +SFP="$(dafp "$SD/Keys/key_3_leaf.pem" "$SD/Certificates/cert_leaf_chain.b64" "$SD/Binary/DA_Config.obk" "$SD/Binary/password.bin")" +PFP="$(dafp "$T/prodda/key" "$T/prodda/cert" "$T/prodda/obk" "$T/prodda/pwd")" +MFP="$(dafp "$T/prodda/key" "$T/prodda/cert" "$T/prodda/obk" "$SD/Binary/password.bin")" +flashsync() { cp "$R/wolfBoot/wolfboot.bin" "$T/flash/0x0C000000"; cp "$R/build/wolftrust_v1_signed.bin" "$T/flash/0x0C060000" + cp "$R/tests/firmware/zephyr-stm32h5/build/guest0_psa/zephyr/zephyr.bin" "$T/flash/0x080A0000" + cp "$R/tests/firmware/zephyr-stm32h5/build/freertos_guest1/freertos_guest1.bin" "$T/flash/0x080E0000"; } +flashsync +PROD=(WT_DA_OBK="$T/prodda/obk" WT_DA_KEY="$T/prodda/key" WT_DA_CERT="$T/prodda/cert" WT_DA_PWD="$T/prodda/pwd") +: > "$T/writes"; echo 0xED > "$T/ps"; echo 0x000FFFFF > "$T/wrp"; echo 0xB4 > "$T/tzen"; echo "00210045 33325112 38363236" > "$T/uid" +UID1=002100453332511238363236 + +echo "== shared" +check "help lists the port's states" 0 "0x72 closed" -- "${H5[@]}" help +check "a port file refuses to run alone" 2 "not this port file" -- bash "$R/tests/target/provisioning/provisioning_ctrl_stm32h563.sh" lock 0x72 +check "the other port file too" 2 "not this port file" -- bash "$R/tests/target/provisioning/provisioning_ctrl_mimxrt700.sh" lock 0x07 +check "unknown port" 2 "no provisioning port for TARGET=nope" -- env TARGET=nope "$P" help +check "unknown state" 2 "unknown state '0x99'" -- "${H5[@]}" lock 0x99 +check "no state" 2 "unknown state ''" -- "${H5[@]}" lock +check "start state is not a lock target" 2 "open (0xED) is not a lock target" -- "${H5[@]}" lock open +check "permanent state has no mock" 2 "locked (0x5C) has no mock" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" advance locked +check "writes need WT_LOCK_CONFIRM" 2 "writes to the board" -- "${H5[@]}" advance 0x17 +nowrite + +echo "== STM32H563" +check "lock skipping a state" 2 "runs only from provisioning (0x17)" -- "${H5[@]}" lock closed +check "lock unrehearsed" 2 "no rehearsal for provisioning (0x17)" -- "${H5[@]}" lock provisioning +check "advance Closed from Open" 2 "runs only from Provisioning" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" advance 0x72 +nowrite +echo x >> "$R/build/wolftrust_v1_signed.bin" +check "advance with host images not on the part" 2 "could not read back" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" advance 0x17 +[ "$(cat "$T/ps")" = 0xED ] && { pass=$((pass+1)); echo "ok a failed read-back leaves the part in Open"; } || { failn=$((failn+1)); echo "FAIL part left in $(cat "$T/ps")"; } +nowrite +echo 0xeaeaeaea > "$T/da"; echo 0x17 > "$T/ps" +check "closing advance without a read-back" 2 "no recent read-back" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" advance 0x72 +echo 0xf5f5f5f5 > "$T/da" +echo wt > "$R/build/wolftrust_v1_signed.bin"; echo 0xED > "$T/ps"; : > "$T/writes" +check "advance by name reads the part back in Open" 0 "the images on device $UID1 match" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" advance provisioning +check "closed advance without DA" 2 "cannot be regressed" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" advance 0x72 +echo 0xeaeaeaea > "$T/da" +rm -f "$T/boots" +check "advance without a boot records nothing" 1 "no rehearsal recorded" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" advance 0x72 +echo 0x17 > "$T/ps"; touch "$T/boots" +touch "$T/stuck" +check "a write that did not land records nothing" 1 "does not read back as closed (0x72)" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" advance closed +[ ! -e "$HS/pending" ] && { pass=$((pass+1)); echo "ok no pending rehearsal for an unconfirmed state"; } || { failn=$((failn+1)); echo "FAIL pending after a stuck write"; } +rm -f "$T/stuck" +check "advance Closed, boot captured" 0 "rehearsal of closed (0x72) recorded" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" advance closed +: > "$T/writes" +check "lock while Closed (link down)" 2 "cannot read the product state" -- "${H5[@]}" lock 0x5C +touch "$T/noregress"; cp "$HS/pending" "$T/pending.keep" +check "a failed regression exits nonzero" 1 "nothing was recorded" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" regress +[ ! -e "$HS/rehearsal-0x72" ] && { pass=$((pass+1)); echo "ok a failed regression records no rehearsal"; } || { failn=$((failn+1)); echo "FAIL rehearsal after failed regress"; } +rm -f "$T/noregress"; cp "$T/pending.keep" "$HS/pending" +check "regress completes the rehearsal" 0 "rehearsal of closed (0x72) complete" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" regress +grep -q " id=$UID1 .* cred=$SFP " "$HS/rehearsal-0x72" && { pass=$((pass+1)); echo "ok rehearsal binds UID and every DA input"; } || { failn=$((failn+1)); echo "FAIL record: $(cat "$HS/rehearsal-0x72")"; } +check "a second regress records nothing stale" 0 "state after regression" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" regress +[ "$(grep -c . "$HS/rehearsal-0x72")" = 1 ] && [ ! -e "$HS/pending" ] && { pass=$((pass+1)); echo "ok no pending rehearsal survives a regress"; } || { failn=$((failn+1)); echo "FAIL stale pending"; } +check "lock 0x17 preview on the rehearsed part" 2 "same part $UID1, images read back, perimeter values" -- "${H5[@]}" lock 0x17 +echo "11111111 22222222 33333333" > "$T/uid" +check "lock on another part" 2 "is not the one rehearsed" -- "${H5[@]}" lock 0x17 +echo "00210045 33325112 38363236" > "$T/uid" +echo bad > "$T/flash/0x080A0000" +check "lock with different flash" 2 "differ from the rehearsed build" -- "${H5[@]}" lock 0x17 +flashsync +echo 0xC3 > "$T/tzen" +check "option bytes changed since the rehearsal" 2 "no rehearsal for" -- "${H5[@]}" lock 0x17 +cp "$HS/rehearsal-0x72" "$T/keep" +obnow="$(env PATH="$T/bin:$PATH" "$T/bin/stcli" -ob displ | awk '$1 ~ /^(TZEN|BOOT_UBE|SWAP_BANK|SECWM[12]_(STRT|END)|WRPSGn1)$/ && $2 == ":" { print $1 "=" $3 }' | sort | sha | cut -c1-64)" +subst "ob=[0-9a-f]*" "ob=$obnow" "$HS/rehearsal-0x72" +check "consistent but unsafe perimeter" 2 "perimeter option bytes are not wolfTrust's" -- "${H5[@]}" lock 0x17 +cp "$T/keep" "$HS/rehearsal-0x72"; echo 0xB4 > "$T/tzen" +subst "completed=[0-9]*" "completed=$(( $(date +%s) - 7200 ))" "$HS/rehearsal-0x72" +check "expired rehearsal" 2 "in the last 3600s" -- "${H5[@]}" lock 0x17 +cp "$T/keep" "$HS/rehearsal-0x72" +nowrite +echo 0x17 > "$T/ps" +check "closing preview before provision-da" 2 "not installed by 'provision-da'" -- "${H5[@]}" lock 0x72 +check "provision-da records the install" 0 "DA provisioned and recorded" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" provision-da +check "closing preview: identity unreadable, sample DA noted" 2 "identity not readable here (needs WT_FIXTURE_BOUND=1)" -- "${H5[@]}" lock 0x72 +check "closing preview lists DA checks" 2 "DA credential is ST's sample or unset" -- "${H5[@]}" lock closed +check "Locked preview" 2 "provisioning (0x17) -> locked (0x5C)" -- "${H5[@]}" lock locked +echo 0xf5f5f5f5 > "$T/da" +check "Locked without DA" 2 "Debug Authentication is not provisioned" -- "${H5[@]}" lock locked +echo 0xeaeaeaea > "$T/da" +check "closing lock without a bound fixture" 2 "set WT_FIXTURE_BOUND=1 there" -- env WT_LOCK_CONFIRM=1 "${H5[@]}" lock 0x72 +check "no production opt-in" 2 "Only a production station" -- env WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 "${H5[@]}" lock 0x72 +check "production lock with ST sample DA" 2 "needs its own DA credential" -- env WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 "${H5[@]}" lock 0x72 +check "production DA set to sample copies" 2 "needs its own DA credential" -- env WT_DA_OBK="$SD/Binary/DA_Config.obk" WT_DA_KEY="$SD/Keys/key_3_leaf.pem" WT_DA_CERT="$SD/Certificates/cert_leaf_chain.b64" WT_DA_PWD="$SD/Binary/password.bin" WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 "${H5[@]}" lock 0x72 +check "production provision-da with sample" 2 "needs its own DA credential" -- env WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 "${H5[@]}" provision-da +check "rehearsed with sample, locking with production DA" 2 "no rehearsal for" -- env "${PROD[@]}" "${H5[@]}" lock 0x72 +subst "cred=$SFP" "cred=$MFP" "$HS/rehearsal-0x72" +check "production DA without WT_DA_PWD" 2 "needs its own DA credential" -- env WT_DA_OBK="$T/prodda/obk" WT_DA_KEY="$T/prodda/key" WT_DA_CERT="$T/prodda/cert" WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 "${H5[@]}" lock 0x72 +subst "cred=$MFP" "cred=$PFP" "$HS/rehearsal-0x72" +check "DA key swapped after the rehearsal" 2 "no rehearsal for" -- env "${PROD[@]}" WT_DA_KEY="$T/prodda/key2" "${H5[@]}" lock 0x72 +check "DA installed with other credentials" 2 "not installed by 'provision-da'" -- env "${PROD[@]}" "${H5[@]}" lock 0x72 +env WT_LOCK_CONFIRM=1 "${PROD[@]}" "${H5[@]}" provision-da >/dev/null +subst "time=[0-9]*" "time=$(( $(field_of completed "$HS/rehearsal-0x72") - 10 ))" "$HS/da-provisioned" +check "DA provisioned before the rehearsal" 2 "not installed by 'provision-da'" -- env "${PROD[@]}" "${H5[@]}" lock 0x72 +env WT_LOCK_CONFIRM=1 "${PROD[@]}" "${H5[@]}" provision-da >/dev/null +check "production DA preview" 2 "production DA credential" -- env "${PROD[@]}" "${H5[@]}" lock 0x72 +check "piped confirmation" 2 "interactive terminal" -- env "${PROD[@]}" WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 "${H5[@]}" lock 0x72 +check "TZ-Closed unrehearsed" 2 "no rehearsal for tz-closed" -- env "${PROD[@]}" "${H5[@]}" lock 0xC6 +echo x >> "$R/build/wolftrust_v1_signed.bin" +check "rebuilt images" 2 "no rehearsal for" -- env "${PROD[@]}" "${H5[@]}" lock 0x72 +echo wt > "$R/build/wolftrust_v1_signed.bin" +nowrite +if [ "$have_expect" = 1 ]; then +H5X="env TARGET=stm32h563 HOME=$T/home WT_DA_OBK=$T/prodda/obk WT_DA_KEY=$T/prodda/key WT_DA_CERT=$T/prodda/cert WT_DA_PWD=$T/prodda/pwd PATH=$T/bin:$PATH STM32_CLI=$T/bin/stcli H5_SERIAL=$T/uart WT_PROVISION_STATE=$T/st WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 $P" +for phrase in "yes" "LOCK 0x72" "I ACCEPT 0x5C" "i accept 0x72"; do + expect -c "set timeout 300; spawn $H5X lock 0x72; expect \"to continue: \"; send \"$phrase\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]" >/dev/null + rc=$?; [ $rc = 2 ] && [ ! -s "$T/writes" ] && { pass=$((pass+1)); echo "ok wrong phrase '$phrase' refused"; } || { failn=$((failn+1)); echo "FAIL phrase '$phrase' rc=$rc"; } +done +rm -f "$T/boots" +out="$(expect -c "set timeout 300; spawn $H5X lock 0x72; expect \"to continue: \"; send \"I ACCEPT 0x72\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]")" +rc=$?; [ $rc = 1 ] && grep -q "wolfTrust did not boot" <<<"$out" && [ -e "$HS/rehearsal-0x72" ] && { pass=$((pass+1)); echo "ok stale UART markers without a post-write boot fail the lock and keep the rehearsal"; } || { failn=$((failn+1)); echo "FAIL no-boot lock rc=$rc"; echo "$out" | tail -4; } +touch "$T/boots"; : > "$T/writes"; echo 0x17 > "$T/ps"; cp "$HS/rehearsal-0x72" "$T/keep72" +echo 0xED > "$T/ps" +out="$(expect -c "set timeout 300; spawn $H5X lock 0x17; expect \"to continue: \"; exec sh -c {echo 11111111 22222222 33333333 > $T/uid}; send \"I ACCEPT 0x17\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]")" +rc=$?; [ $rc = 2 ] && [ ! -s "$T/writes" ] && grep -q "different part is attached" <<<"$out" && { pass=$((pass+1)); echo "ok a part swapped during the prompt is not written"; } || { failn=$((failn+1)); echo "FAIL swap during prompt rc=$rc writes=$(cat "$T/writes")"; echo "$out" | tail -4; } +echo "00210045 33325112 38363236" > "$T/uid"; : > "$T/writes" +out="$(expect -c "set timeout 300; spawn $H5X lock 0x17; expect \"to continue: \"; exec sh -c {echo x >> $R/build/wolftrust_v1_signed.bin}; send \"I ACCEPT 0x17\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]")" +rc=$?; [ $rc = 2 ] && [ ! -s "$T/writes" ] && grep -q "no longer matches" <<<"$out" && { pass=$((pass+1)); echo "ok images changed during the prompt are not written"; } || { failn=$((failn+1)); echo "FAIL image change rc=$rc"; echo "$out" | tail -3; } +echo wt > "$R/build/wolftrust_v1_signed.bin" +out="$(expect -c "set timeout 300; spawn $H5X lock 0x17; expect \"to continue: \"; exec sh -c {sed -i.bak s/completed=.*/completed=1/ $HS/rehearsal-0x72}; send \"I ACCEPT 0x17\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]")" +rc=$?; [ $rc = 2 ] && [ ! -s "$T/writes" ] && grep -q "no longer matches" <<<"$out" && { pass=$((pass+1)); echo "ok a rehearsal that expires during the prompt is not used"; } || { failn=$((failn+1)); echo "FAIL expiry rc=$rc"; echo "$out" | tail -3; } +cp "$T/keep72" "$HS/rehearsal-0x72"; rm -f "$HS/rehearsal-0x72.bak"; echo 0x17 > "$T/ps"; : > "$T/writes" +out="$(expect -c "set timeout 300; spawn $H5X lock 0x72; expect \"to continue: \"; send \"I ACCEPT 0x72\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]")" +rc=$?; [ $rc = 0 ] && [ "$(cat "$T/writes")" = "PRODUCT_STATE=0x72" ] && grep -q "STM32H563 is closed (0x72)" <<<"$out" && [ ! -e "$HS/rehearsal-0x72" ] && { pass=$((pass+1)); echo "ok exact phrase writes Closed (stub) and consumes the rehearsal"; } || { failn=$((failn+1)); echo "FAIL exact phrase rc=$rc writes=$(cat "$T/writes")"; echo "$out" | tail -5; } +: > "$T/writes"; echo 0x17 > "$T/ps"; cp "$T/keep72" "$HS/rehearsal-0x72" +out="$(expect -c "set timeout 300; spawn $H5X lock 0x5C; expect \"to continue: \"; send \"I ACCEPT 0x5C\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]")" +rc=$?; [ $rc = 0 ] && [ "$(cat "$T/writes")" = "PRODUCT_STATE=0x5C" ] && grep -q "STM32H563 is locked (0x5C)" <<<"$out" && [ ! -e "$HS/rehearsal-0x72" ] && { pass=$((pass+1)); echo "ok Locked (stub) consumes the Closed rehearsal it used"; } || { failn=$((failn+1)); echo "FAIL Locked consume rc=$rc writes=$(cat "$T/writes")"; echo "$out" | tail -5; } +: > "$T/writes"; echo 0x17 > "$T/ps" +else + echo "skip typed confirmation tests (no expect)" + rm -f "$HS"/rehearsal-* +fi +check "a consumed rehearsal cannot lock again" 2 "no rehearsal for" -- env "${PROD[@]}" "${H5[@]}" lock 0x5C +nowrite + +echo "== MIMXRT700" +RT=(env TARGET=mimxrt700 RT700_SPSDK_VENV="$T/venv" WT_PROVISION_STATE="$T/st" PATH="$T/venv/bin:$PATH" "$P") +RS="$T/st/mimxrt700" +ISP=(RT700_ISP="-u 0x1fc9,0x014f") +fuse() { echo "$2" > "$T/fuse.$(($1))"; } +edig() { + local a f + for a in 0x28000000:build/rt700/flash_wolfboot.bin 0x28040000:build/wolftrust_v1_signed.bin \ + 0x28080000:tests/firmware/mimxrt700-baremetal/build/guest0.bin \ + 0x28100000:tests/firmware/mimxrt700-baremetal/build/guest1.bin; do + f="$R/${a#*:}"; printf '%s %s\n' "${a%%:*}" "$(wc -c < "$f" | tr -d ' ')"; cat "$f" + done | sha | cut -c1-64 +} +# rec [probe] +rec() { mkdir -p "$RS"; echo "state=$1 run=r id=${6:-PROBEA} image=$3 fused=$2 fence=$4 time=$5 cred=none completed=$5" > "$RS/rehearsal-$1"; } +now() { date +%s; } +rm -f "$T"/fuse.*; fuse 0x8F 0x03; fuse 0x25 0x03 +check "help lists the port's states" 0 "in-field-locked" -- "${RT[@]}" help +for m in 0 0x0 0x4 0x7 junk; do + check "RT700_GUEST_MASK=$m refused" 2 "RT700_GUEST_MASK must be" -- env RT700_GUEST_MASK="$m" WT_LOCK_CONFIRM=1 "${RT[@]}" advance 0x07 +done +# sfp [extra descriptor line] +sfp() { + printf '50184900: a000c000\n50184920: %s\n' "$1" + printf '50184800: 28000000 2803ffff 00000000 a0000000\n50184820: 28040000 2807ffff 00000007 a0000000\n' + printf '50184840: 28080000 %s 00000000 %s\n50184860: 28140000 2bffffff 00000007 a0000000\n' "$2" "$3" + printf '%s\n' "${4:-50184880: 00000000 00000000 00000000 20000000}" +} +sfp a8000400 2813ffff a0000000 > "$T/sfp" +check "verify-wrp: wolfBoot's fence" 0 "armed FRAD2" -- "${RT[@]}" verify-wrp +sfp 28000400 2813ffff a0000000 > "$T/sfp" +check "verify-wrp: SFP not valid" 1 "SFP not sealed" -- "${RT[@]}" verify-wrp +sfp a8000000 2813ffff a0000000 > "$T/sfp" +check "verify-wrp: SFP not locked" 1 "SFP not sealed" -- "${RT[@]}" verify-wrp +sfp a8000400 2813ffff 80000000 > "$T/sfp" +check "verify-wrp: fence descriptor unlocked" 1 "FRAD2 .* touches the guest windows" -- "${RT[@]}" verify-wrp +sfp a8000400 2813ffff a0000000 "50184880: 28100000 2810ffff 00000007 a0000000" > "$T/sfp" +check "verify-wrp: writable overlap" 1 "FRAD4 .* touches the guest windows" -- "${RT[@]}" verify-wrp +sfp a8000400 280fffff a0000000 > "$T/sfp" +check "verify-wrp: gap in the fence" 1 "covers 0x28100000" -- "${RT[@]}" verify-wrp +rm -f "$T/sfp" +check "no ISP" 2 "set RT700_ISP" -- "${RT[@]}" lock 0x07 +check "skip ahead" 2 "runs only from develop2 (0x07)" -- env "${ISP[@]}" "${RT[@]}" lock in-field +fuse 0x25 0x07 +check "copies disagree" 2 "disagree" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +fuse 0x8F 0x00030003; fuse 0x25 0x00030003 +check "A0/A1 protection copies" 2 "only the B0 burn encoding" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +fuse 0x8F 0x03; fuse 0x25 0x03 +check "unrehearsed" 2 "no rehearsal for develop2" -- env "${ISP[@]}" "${RT[@]}" lock develop2 +rec 0x07 0x03 0000 armed "$(now)" +check "rehearsed other images" 2 "no rehearsal for" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +G="$R/tests/firmware/mimxrt700-baremetal/build" +rec 0x07 0x03 "$(edig)" armed "$(now)" +printf 'rg0\nr' > "$G/guest0.bin"; printf 'g1\n' > "$G/guest1.bin" +check "bytes moved between images" 2 "no rehearsal for" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +echo rg0 > "$G/guest0.bin"; echo rg1 > "$G/guest1.bin" +rec 0x07 0x03 "$(edig)" armed $(( $(now) - 7200 )) +check "stale rehearsal" 2 "in the last 3600s" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +rec 0x07 0x03 "$(edig)" armed $(( $(now) + 600 )) +check "future-dated rehearsal" 2 "in the last 3600s" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +rec 0x07 0x03 "$(edig)" open "$(now)" +check "rehearsed without the guest fence" 2 "no rehearsal for" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +rec 0x07 0x03 "$(edig)" armed "$(now)" PROBEB +check "rehearsed through another probe" 2 "no rehearsal for" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +rec 0x07 0x03 "$(edig)" armed "$(now)" +: > "$T/probe" +check "no probe attached" 2 "no rehearsal for" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +echo PROBEA > "$T/probe" +check "Develop2 preview" 2 "efuse-program-once 0x8F 00000007" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +check "SPSDK venv off PATH" 2 "efuse-program-once 0x8F 00000007" -- env "${ISP[@]}" TARGET=mimxrt700 RT700_SPSDK_VENV="$T/venv" WT_PROVISION_STATE="$T/st" PATH="$T/bin:/usr/bin:/bin" "$P" lock 0x07 +check "SPSDK installed system-wide" 2 "efuse-program-once 0x8F 00000007" -- env "${ISP[@]}" TARGET=mimxrt700 RT700_SPSDK_VENV="$T/no-venv" WT_PROVISION_STATE="$T/st" PATH="$T/venv/bin:$T/bin:/usr/bin:/bin" "$P" lock 0x07 +check "preview names the probe" 2 "same debug probe PROBEA" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 +check "burn without a bound fixture" 2 "set WT_FIXTURE_BOUND=1 there" -- env WT_LOCK_CONFIRM=1 "${ISP[@]}" "${RT[@]}" lock 0x07 +check "no production opt-in" 2 "Only a production station" -- env WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 "${ISP[@]}" "${RT[@]}" lock 0x07 +check "piped confirmation" 2 "interactive terminal" -- env WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 "${ISP[@]}" "${RT[@]}" lock 0x07 +check "lock takes one state" 2 "lock takes one state" -- env "${ISP[@]}" "${RT[@]}" lock 0x07 fuses.yaml +check "fuse burn commands refused" 2 "programs OTP fuses" -- "${RT[@]}" burn +nowrite +if [ "$have_expect" = 1 ]; then +RTX="env WT_FIXTURE_BOUND=1 TARGET=mimxrt700 RT700_SPSDK_VENV=$T/venv WT_PROVISION_STATE=$T/st PATH=$T/venv/bin:$PATH RT700_ISP=-u0x1fc9,0x014f WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 $P" +expect -c "set timeout 300; spawn $RTX lock 0x07; expect \"to continue: \"; send \"BURN 0x07\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]" >/dev/null +rc=$?; [ $rc = 2 ] && [ ! -s "$T/writes" ] && { pass=$((pass+1)); echo "ok old phrase refused"; } || { failn=$((failn+1)); echo "FAIL old phrase rc=$rc"; } +out="$(expect -c "set timeout 300; spawn $RTX lock 0x07; expect \"to continue: \"; exec sh -c {echo PROBEB > $T/probe}; send \"I ACCEPT 0x07\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]")" +rc=$?; [ $rc = 2 ] && [ ! -s "$T/writes" ] && grep -q "no longer matches" <<<"$out" && { pass=$((pass+1)); echo "ok a probe swapped during the prompt is not burned"; } || { failn=$((failn+1)); echo "FAIL probe swap rc=$rc"; echo "$out" | tail -3; } +echo PROBEA > "$T/probe" +expect -c "set timeout 300; spawn $RTX lock 0x07; expect \"to continue: \"; send \"I ACCEPT 0x07\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]" >/dev/null +rc=$?; [ $rc = 0 ] && [ "$(cat "$T/fuse.143")" = 7 ] && [ "$(cat "$T/fuse.37")" = 7 ] && [ ! -e "$RS/rehearsal-0x07" ] && { pass=$((pass+1)); echo "ok exact phrase burns Develop2 (stub) and consumes the rehearsal"; } || { failn=$((failn+1)); echo "FAIL exact phrase rc=$rc"; } +[ "$(head -1 "$T/writes" | cut -d' ' -f2)" = 0x25 ] && { pass=$((pass+1)); echo "ok life cycle words burn RED then LC"; } || { failn=$((failn+1)); echo "FAIL order"; } +: > "$T/writes"; fuse 0x8F 0x03; fuse 0x25 0x03; touch "$T/batchfail" +rec 0x07 0x03 "$(edig)" armed "$(now)" +out="$(expect -c "set timeout 300; spawn $RTX lock 0x07; expect \"to continue: \"; send \"I ACCEPT 0x07\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]")" +rc=$?; [ $rc = 1 ] && grep -q "batch failed part way" <<<"$out" && grep -q "fuses read LC 0x00000003, RED 0x00000007" <<<"$out" && { pass=$((pass+1)); echo "ok a burn that fails part way still reads both words back"; } || { failn=$((failn+1)); echo "FAIL partial burn rc=$rc"; echo "$out" | tail -4; } +rm -f "$T/batchfail" "$RS/rehearsal-0x07"; fuse 0x8F 0x07; fuse 0x25 0x07; : > "$T/writes" +else + echo "skip typed burn tests (no expect)"; fuse 0x8F 0x07; fuse 0x25 0x07 +fi +check "the next step after the burn" 2 "runs only from in-field (0x0F)" -- env "${ISP[@]}" "${RT[@]}" lock in-field-locked +rec 0x0F 0x07 "$(edig)" armed "$(now)" +check "In Field refused until ROM authentication" 2 "needs the BootROM to authenticate wolfBoot" -- env "${ISP[@]}" "${RT[@]}" lock in-field +nowrite +echo "provisioning gates: $pass passed, $failn failed" +[ "$failn" = 0 ] diff --git a/tests/target/provisioning_ctrl.sh b/tests/target/provisioning_ctrl.sh deleted file mode 100755 index 61b46b9b..00000000 --- a/tests/target/provisioning_ctrl.sh +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env bash -# wolfTrust STM32H563 provisioning + lock control. One flag-driven entry point -# for the whole silicon lifecycle: set the OEM-iRoT option-byte perimeter, flash -# the wolfTrust chain, advance/regress product state for the reversible lock, and -# restore. It supersedes wolfBoot's set-stm32-tz-option-bytes.sh (which computes -# the wrong SECWM for wolfTrust's secure-alias layout and never sets BOOT_UBE) -# with the exact values captured from a known-good wolfTrust board, and folds in -# ST's tested DA provisioning/regression command order (ROT_Provisioning/DA). -# -# BRICK SAFETY (non-negotiable): -# * board-writing commands refuse to run without WT_LOCK_CONFIRM=1 -# * the permanent Locked product state (0x5C) is refused outright -# * regression returns to Open (fully debuggable, reflashable) — never a brick -# * DA uses the certificate OBK that matches TZEN-enabled (ST AN6008 pairing) -# * restore reproduces the captured-verified perimeter, so recovery is proven -# on the Open board BEFORE any state advance is ever attempted -# -# Commands: -# status read product state + option bytes (read-only) -# set-perimeter set the wolfTrust OEM-iRoT option bytes (restore pt 1) -# flash flash wolfBoot + wolfTrust + guests (restore pt 2) -# verify reset + capture UART, assert the wolfTrust chain boots -# restore set-perimeter + flash + verify (full recovery) -# provision-da -sdp the DA OBK (only valid in Provisioning state) -# discover prove the DA credential authenticates (non-destructive) -# advance set PRODUCT_STATE (GATED; Locked refused) -# regress DA-authenticate + full regression back to Open (GATED) -set -euo pipefail - -CP="${STM32_CP:-$HOME/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin}" -CLI="${STM32_CLI:-$CP/STM32_Programmer_CLI}" -SERIAL="${H5_SERIAL:-/dev/ttyACM0}" -repo="$(cd "$(dirname "$0")/../.." && pwd)" - -# ST DA credential from the pinned NUCLEO-H563ZI ROT_Provisioning/DA folder. -# wolfTrust runs with TrustZone ENABLED, so DA is CERTIFICATE-based: AN6008 -# requires the certificate method when TZEN=0xB4, and a password OBK provisioned -# here cannot authenticate and blocks regression (verified on board -# 2026-08-19). Use DA_Config.obk with the leaf key and certificate chain, not -# DA_ConfigWithPassword.obk. Override once a wolfTrust-owned certificate chain -# replaces ST's sample. -DA_DIR="${WT_DA_DIR:-$HOME/st-rot-h5/Projects/NUCLEO-H563ZI/ROT_Provisioning/DA}" -DA_OBK="${WT_DA_OBK:-$DA_DIR/Binary/DA_Config.obk}" -DA_PWD="${WT_DA_PWD:-$DA_DIR/Binary/password.bin}" -DA_KEY="${WT_DA_KEY:-$DA_DIR/Keys/key_3_leaf.pem}" -DA_CERT="${WT_DA_CERT:-$DA_DIR/Certificates/cert_leaf_chain.b64}" -# ST ROT_Provisioning/DA connect strings (regression is sensitive to these). -DA_CONN="-c port=SWD speed=fast ap=1 mode=Hotplug" -DA_CONN_RST="-c port=SWD speed=fast ap=1 mode=Hotplug -hardRst" - -# wolfTrust OEM-iRoT perimeter — the EXACT option bytes read from a known-good -# wolfTrust STM32H563 board. BOOT_UBE -# selects the OEM-iRoT boot path (so SECBOOTADD is unused); SECWM1 covers the -# secure wolfBoot+wolfTrust region, SECWM2 the secure bank-2 window. -# SECWM1_END must span the WHOLE boot partition (through 0x0809FFFF): with the -# earlier 0x3F, flash writes past 0x08080000 were silently dropped and wolfBoot -# integrity-rejected any secure image over 128K (found by MP5 confboot). -WT_OB=(TZEN=0xB4 BOOT_UBE=0xB4 SWAP_BANK=0x0 - SECWM1_STRT=0x0 SECWM1_END=0x4F SECWM2_STRT=0x0 SECWM2_END=0x7F) - -# Guest-flash write protection (WRPSGn1, 0 = protected, 4 sectors per bit). -# 0x000FFFFF clears bits 20-31 -> bank-1 sectors 0x50-0x7F protected, the guest -# region; 0xFFFFFFFF leaves all sectors writable (factory default). -WRP_GUEST=0x000FFFFF; WRP_OPEN=0xFFFFFFFF - -# Product-state codes (RM0481). -PS_OPEN=0xED; PS_PROVISIONING=0x17; PS_TZCLOSED=0xC6; PS_CLOSED=0x72; PS_LOCKED=0x5C - -# Flash layout (matches run_h5_hardware.sh). -WOLFBOOT=0x0C000000; WOLFTRUST=0x0C060000; GUEST0=0x080A0000; GUEST1=0x080C0000 -wb="$repo/wolfBoot/wolfboot.bin" -wt="$repo/build/wolftrust_v1_signed.bin" -g0="$repo/tests/firmware/zephyr-stm32h5/build/guest0_psa/zephyr/zephyr.bin" -g1="$repo/tests/firmware/zephyr-stm32h5/build/freertos_guest1/freertos_guest1.bin" - -strip() { sed -e 's/\x1b\[[0-9;]*[A-Za-z]//g'; } -pass() { printf ' [check] PASS %s\n' "$1"; } -fail() { printf ' [check] FAIL %s (%s)\n' "$1" "$2"; exit 1; } -confirm() { [ "${WT_LOCK_CONFIRM:-0}" = "1" ] || { - echo "REFUSED: '$cmd' writes to the board. Re-run with WT_LOCK_CONFIRM=1." >&2; exit 2; }; } -product_state() { - "$CLI" -c port=SWD mode=HotPlug -ob displ 2>&1 | strip \ - | grep -iE "PRODUCT_STATE" | grep -oE "0x[0-9A-Fa-f]+" | head -1 -} - -cmd="${1:-status}" -case "$cmd" in - status) - "$CLI" -c port=SWD mode=HotPlug -ob displ 2>&1 | strip \ - | grep -iE "PRODUCT_STATE|TZEN|BOOT_UBE|SECWM|SECBOOT" | head -20 - ;; - - set-perimeter) - confirm - echo "Setting wolfTrust OEM-iRoT perimeter: ${WT_OB[*]}" - # TZEN first (an off->on flip mass-erases); then the rest. Setting the same - # values on an already-provisioned board is a safe no-op. - "$CLI" -c port=SWD mode=UR -ob TZEN=0xB4 2>&1 | strip | tail -3 - "$CLI" -c port=SWD mode=UR -ob "${WT_OB[@]}" 2>&1 | strip | tail -4 - ;; - - set-wrp) - # Write-protect the guest flash region so a privileged Non-secure guest - # cannot reprogram a peer guest's image (WT-SYS-0002 hardware root fix). - # WRPSGn1 groups four 8 KiB sectors per bit and 0 means protected, so - # 0x000FFFFF protects bank-1 sectors 0x50-0x7F (0x080A0000-0x080FFFFF), - # the whole guest region, and leaves the secure/FWU region writable. WRP is - # mutable only in Open; run this AFTER flashing the guests (a protected - # sector rejects the image write) and before advancing product state. - confirm - [ "$(product_state)" = "$PS_OPEN" ] || \ - fail "set-wrp" "WRP is settable only in Open ($PS_OPEN); state=$(product_state)" - echo "Write-protecting guest flash (bank1 sectors 0x50-0x7F): WRPSGn1=$WRP_GUEST" - "$CLI" -c port=SWD mode=UR -ob WRPSGn1="$WRP_GUEST" 2>&1 | strip | tail -4 - "$CLI" -c port=SWD mode=HotPlug -ob displ 2>&1 | strip | grep -iE "WRPSGn1" - ;; - - clear-wrp) - # Remove guest-flash write protection so the images can be reflashed. - confirm - echo "Clearing guest-flash write protection: WRPSGn1=$WRP_OPEN" - "$CLI" -c port=SWD mode=UR -ob WRPSGn1="$WRP_OPEN" 2>&1 | strip | tail -4 - ;; - - flash) - confirm - for f in "$wb" "$wt" "$g0" "$g1"; do - [ -s "$f" ] || fail "flash" "missing image: $f (build first)"; done - "$CLI" -c port=SWD mode=UR \ - -d "$wb" "$WOLFBOOT" -d "$wt" "$WOLFTRUST" \ - -d "$g0" "$GUEST0" -d "$g1" "$GUEST1" --verify -hardRst 2>&1 | strip \ - | grep -iE "verified successfully|error|download" | tail -4 - ;; - - verify) - stty -F "$SERIAL" 115200 raw -echo 2>/dev/null || true - ( timeout 8 cat "$SERIAL" > /tmp/wt-verify.log 2>/dev/null & ) - "$CLI" -c port=SWD mode=UR -rst >/dev/null 2>&1 || true - sleep 7 - if strip < /tmp/wt-verify.log | grep -aqE "guest0_psa|heartbeat|TEE client"; then - pass "wolfTrust chain boots on silicon" - else - fail "verify" "no wolfTrust boot markers on $SERIAL" - fi - ;; - - restore) - confirm - WT_LOCK_CONFIRM=1 "$0" set-perimeter - WT_LOCK_CONFIRM=1 "$0" clear-wrp - WT_LOCK_CONFIRM=1 "$0" flash - WT_LOCK_CONFIRM=1 "$0" set-wrp - "$0" verify - echo "PASS: wolfTrust restored and booting" - ;; - - provision-da) - confirm - [ -s "$DA_OBK" ] || fail "provision-da" "DA OBK not found: $DA_OBK" - [ "$(product_state)" = "$PS_PROVISIONING" ] || \ - fail "provision-da" "must be in Provisioning ($PS_PROVISIONING); state=$(product_state)" - echo "Provisioning DA OBK (ST obk_provisioning.sh order): $DA_OBK" - "$CLI" $DA_CONN_RST >/dev/null 2>&1 || true - "$CLI" $DA_CONN -sdp "$DA_OBK" 2>&1 | strip | tail -5 - "$CLI" $DA_CONN_RST >/dev/null 2>&1 || true - ;; - - discover) - echo "DA discovery (non-destructive) with $DA_PWD:" - "$CLI" $DA_CONN pwd="$DA_PWD" debugauth=2 2>&1 | strip \ - | grep -iE "permission|regression|discovery|not supported|error|auth" | head - ;; - - advance) - confirm - state="${2:-}" - case "$state" in - "$PS_LOCKED"|0x5c) echo "REFUSED: Locked (0x5C) is permanent — never on a dev board." >&2; exit 2 ;; - "$PS_PROVISIONING"|"$PS_TZCLOSED"|"$PS_CLOSED"|0x17|0xc6|0x72) ;; - *) echo "REFUSED: advance needs a reversible state (0x17/0xC6/0x72), got '${state:-none}'." >&2; exit 2 ;; - esac - echo "ADVANCING product state $(product_state) -> $state (regress is the only way back)" - "$CLI" -c port=SWD mode=HotPlug -ob PRODUCT_STATE="$state" 2>&1 | strip | tail -4 - echo "now: $(product_state)" - ;; - - regress) - confirm - # Certificate DA Full Regression -> Open. VERIFIED on board 2026-08-19. - # wolfTrust runs TZEN enabled, so the credential is the certificate (per=a). - # Do NOT send debugauth=3 first: it locks the debug session and then blocks - # AP access for the handshake. Reset to clear any stale lock, then - # authenticate on a bare "-c port=SWD" (default NORMAL/under-reset so the RSS - # answers) with the key+cert; CubeProgrammer selects the certificate because - # TZEN is enabled and the RSS mass-erases the device back to Open. - echo "DA certificate Full Regression -> Open (mass-erase):" - "$CLI" -c port=SWD mode=HotPlug -rst 2>&1 | strip | tail -1 || true - "$CLI" -c port=SWD per=a key="$DA_KEY" cert="$DA_CERT" pwd="$DA_PWD" \ - debugauth=1 &1 | strip | tail -14 - echo "state after regression: $(product_state)" - ;; - - *) echo "usage: $0 status|set-perimeter|flash|verify|restore|provision-da|discover|advance |regress" >&2; exit 2 ;; -esac diff --git a/tests/target/run_rt700_hardware.sh b/tests/target/run_rt700_hardware.sh index 0a1c3c0b..d386fe12 100755 --- a/tests/target/run_rt700_hardware.sh +++ b/tests/target/run_rt700_hardware.sh @@ -20,6 +20,17 @@ # guest1's RAM, which the per-dispatch SAU window keeps Secure; the # store must be blocked, guest1's RAM must not hold the sentinel, # and guest1 must keep running. +# wrpfence a WT_GUEST_FLASH_WRP=1 wolfTrust behind a wolfBoot that arms the +# XSPI guest fence: the fence reads back sealed over SWD and both +# guests launch. +# wrpoff the same wolfTrust behind an unfenced wolfBoot: wolfTrust must +# refuse both guests (launch refused mask 0x3, no mailbox written). +# wrpneg wrpfence with wolfBoot's flash-protect selftest: the silicon must +# refuse an erase inside the guest fence and leave the block as is. +# +# The wolfBoot first stage is RT700_WOLFBOOT_REF plus the carried patches +# (lib/rt700_wolfboot.sh), cached under ~/.cache/wolftrust, unless +# RT700_WOLFBOOT_DIR names a prebuilt tree. set -euo pipefail scenario="${1:-}" @@ -28,7 +39,7 @@ repo="$(cd "$here/../.." && pwd)" work="${RT700_WORK:-$repo/build/rt700}" target="${RT700_TARGET:-mimxrt798sgfob}" fcb="${RT700_FCB:-$HOME/rt700-boot/fcb.bin}" -wolfboot_dir="${RT700_WOLFBOOT_DIR:-$HOME/wolfBoot-rt700}" +wolfboot_dir="${RT700_WOLFBOOT_DIR:-}" spsdk_venv="${RT700_SPSDK_VENV:-$HOME/spsdk-venv}" xspi0_base=0x28000000 mbi_offset=0x4000 @@ -39,6 +50,16 @@ hsm_nvm_addr=0x281E0000 hsm_nvm_size=0x2000 guest_build="$repo/tests/firmware/mimxrt700-baremetal/build" +# shellcheck source=lib/rt700_fence.sh disable=SC1091 +. "$here/lib/rt700_fence.sh" +# shellcheck source=lib/rt700_wolfboot.sh disable=SC1091 +. "$here/lib/rt700_wolfboot.sh" +case "$scenario" in + wrpfence|wrpneg) guest_fence=1; export WT_GUEST_FLASH_WRP=1 ;; + wrpoff) guest_fence=0; export WT_GUEST_FLASH_WRP=1 ;; + *) guest_fence="${WT_XSPI_GUEST_FENCE:-${WT_GUEST_FLASH_WRP:-0}}" ;; +esac + log() { printf '%s\n' "$*"; } stage() { printf ' ... %s\n' "$*"; } fail() { log "FAIL: $*"; exit 1; } @@ -134,14 +155,20 @@ erase_range() { -s "$range" >/dev/null 2>&1 || fail "erase of $range failed" } -# SRAM survives a warm reset, so the last run's mailboxes and sentinel would -# otherwise read back as this run's result if the chain never reached a guest. +# SRAM survives a warm reset, so the last run's mailboxes, sentinel, and launch +# masks would otherwise read back as this run's result. clear_mailboxes() { + local verified refused + verified="$(elf_sym g_wt_launch_verified_mask)" + refused="$(elf_sym g_wt_launch_refused_mask)" + [ -n "$verified" ] && [ -n "$refused" ] || fail "launch masks not found in wolftrust.elf" park_core timeout 60 pyocd cmd -t "$target" -O resume_on_disconnect=false \ -c "write32 0x20100000 0 0 0 0 0 0 0 0 0 0" \ -c "write32 0x20140000 0 0 0 0 0 0 0 0 0 0" \ - -c "write32 0x20170000 0" >/dev/null 2>&1 || \ + -c "write32 0x20170000 0" \ + -c "write32 0x20180080 0 0 0 0 0 0 0 0 0 0 0 0" \ + -c "write32 $verified 0" -c "write32 $refused 0" >/dev/null 2>&1 || \ fail "could not clear the guest mailboxes" } @@ -154,14 +181,47 @@ reset_board() { } +# The first stage to flash: a prebuilt RT700_WOLFBOOT_DIR as-is (a fence run +# must still match the pin), otherwise the pinned build for this fence setting. +ensure_wolfboot() { + local fence_cflags="" + + if [ "$guest_fence" = "1" ]; then + fence_cflags="$(rt700_fence_cflags)" || fail "guest fence bounds" + fi + if [ "$scenario" = "wrpneg" ]; then + fence_cflags="$fence_cflags -DXSPI_FLASH_PROTECT_SELFTEST" + fi + if [ -n "$wolfboot_dir" ]; then + if [ -n "${WT_GUEST_FLASH_WRP:-}" ]; then + rt700_wolfboot_current "$wolfboot_dir" \ + "$(rt700_wolfboot_stamp "CFLAGS_EXTRA=$fence_cflags")" || \ + fail "RT700_WOLFBOOT_DIR=$wolfboot_dir was not built by lib/rt700_wolfboot.sh with this guest fence" + fi + else + wolfboot_dir="$HOME/.cache/wolftrust/wolfboot-rt700" + [ "$guest_fence" = "1" ] && wolfboot_dir="$wolfboot_dir-fence" + [ "$scenario" = "wrpneg" ] && wolfboot_dir="$wolfboot_dir-selftest" + mkdir -p "$(dirname "$wolfboot_dir")" + stage "wolfBoot $RT700_WOLFBOOT_REF (imx-rt700-tz${fence_cflags:+, guest fence})" + rt700_wolfboot_build "$wolfboot_dir" "CFLAGS_EXTRA=$fence_cflags" \ + > "$work/wolfboot-build.log" 2>&1 || { + tail -20 "$work/wolfboot-build.log" + fail "wolfBoot build failed" + } + fi + [ -s "$wolfboot_dir/wolfboot.bin" ] || \ + fail "wolfBoot TZ image missing at $wolfboot_dir/wolfboot.bin (RT700_WOLFBOOT_DIR)" +} + # Build wolfTrust and both guests, pin the guest measurements, sign, flash the # whole chain, boot it from a fresh vault, and verify every image by readback. run_chain() { local guest_flags="$1" ensure_spsdk - [ -s "$wolfboot_dir/wolfboot.bin" ] || \ - fail "wolfBoot TZ image missing at $wolfboot_dir/wolfboot.bin (RT700_WOLFBOOT_DIR)" + mkdir -p "$work" + ensure_wolfboot # RT700 wolfBoot uses a 1024-byte image header, so wolfTrust links at the # boot base + 0x400 and is signed with a matching header. Exported so the @@ -227,6 +287,59 @@ check_guest() { "guest$id reaches its Non-secure console (LPUART0 VERID 0x$uart)" } +# A wolfTrust global's address, from the image this run built and flashed. +elf_sym() { + arm-none-eabi-nm "$repo/build/wolftrust.elf" | + awk -v s="$1" '$3 == s && !f { print "0x" $1; f = 1 }' +} + +# All eight XSPI0 FRADs as "start end acp word3" lines, in one debugger +# session; only words 0-3 of each 0x20 stride are readable. +frad_dump() { + local -a cmds=() + local n + for n in 0 1 2 3 4 5 6 7; do + cmds+=(-c "read32 $(printf '0x%x' $((0x50184800 + n * 0x20))) 16") + done + dap "${cmds[@]}" | awk '/^50184[89]/ { print $2, $3, $4, $5 }' +} + +# wolfBoot sealed the SFP configuration until the next reset. +sfp_sealed() { + local mgc mdad + mgc=$((0x$(mailbox_word 0x50184920 0))) + mdad=$((0x$(mailbox_word 0x50184900 0))) + [ $((mgc & 0xA8000000)) -eq $((0xA8000000)) ] && [ $((mgc & 0xC00)) -ne 0 ] && + [ $((mdad & 0xA0000000)) -eq $((0xA0000000)) ] +} + +# One valid, hard-reset-locked (EAL clear), write-denying FRAD spans the guest +# windows; the Secure-side predicate is the authority, this is the evidence. +fence_armed() { + local w0 w1 w2 w3 lock + rt700_fence_bounds || fail "guest fence bounds" + while read -r w0 w1 w2 w3; do + lock=$((0x$w3 & 0x63000000)) + if [ $((0x$w0 & 0xFFFF0000)) -le $((RT700_GUEST_FENCE_START)) ] && + [ $(((0x$w1 & 0xFFFF0000) | 0xFFFF)) -ge $((RT700_GUEST_FENCE_END - 1)) ] && + [ $((0x$w3 & 0x80000000)) -ne 0 ] && [ $((0x$w2 & 0x3F)) -eq 0 ] && + { [ "$lock" -eq $((0x20000000)) ] || [ "$lock" -eq $((0x60000000)) ]; }; then + return 0 + fi + done < <(frad_dump) + return 1 +} + +check_launch_masks() { + local want_verified="$1" want_refused="$2" verified refused + verified="$(mailbox_word "$(elf_sym g_wt_launch_verified_mask)" 0)" + refused="$(mailbox_word "$(elf_sym g_wt_launch_refused_mask)" 0)" + check "$([ "$verified" = "$want_verified" ]; echo $?)" \ + "launch-verified guest mask 0x$verified (want 0x$want_verified)" + check "$([ "$refused" = "$want_refused" ]; echo $?)" \ + "launch-refused guest mask 0x$refused (want 0x$want_refused)" +} + case "$scenario" in romsmoke) mkdir -p "$work" @@ -293,8 +406,55 @@ positive|ahbscneg) fi log "PASS: hardware/$scenario" ;; +wrpfence) + run_chain "" + rt700_fence_bounds || fail "guest fence bounds" + check "$(sfp_sealed; echo $?)" "XSPI SFP configuration valid and sealed until reset" + check "$(fence_armed; echo $?)" \ + "a locked, write-denying FRAD spans the guest windows ($RT700_GUEST_FENCE_START-$RT700_GUEST_FENCE_END)" + check_launch_masks 00000003 00000000 + for g in 0:0x20100000 1:0x20140000; do + check_guest "${g%%:*}" "${g##*:}" + done + log "PASS: hardware/$scenario" + ;; +wrpoff) + run_chain "" + rt700_fence_bounds || fail "guest fence bounds" + check "$(sfp_sealed; echo $?)" "XSPI SFP configuration valid and sealed until reset" + check "$(fence_armed && echo 1 || echo 0)" \ + "no FRAD fences the guest windows (unfenced wolfBoot)" + check_launch_masks 00000000 00000003 + for g in 0:0x20100000 1:0x20140000; do + sig="$(mailbox_word "${g##*:}" 0)" + check "$([ "$sig" = "00000000" ]; echo $?)" \ + "guest${g%%:*} never entered its domain (mailbox 0x$sig)" + done + log "PASS: hardware/$scenario" + ;; +wrpneg) + run_chain "" + rt700_fence_bounds || fail "guest fence bounds" + check "$(fence_armed; echo $?)" \ + "a locked, write-denying FRAD spans the guest windows ($RT700_GUEST_FENCE_START-$RT700_GUEST_FENCE_END)" + # wolfBoot's selftest verdicts (hal/imx_rt7xx.c PST mailbox). + pst() { mailbox_word 0x20180080 $((4 * $1)); } + check "$([ "$(pst 0)" = "50510002" ]; echo $?)" "wolfBoot's flash-protect selftest ran to completion" + # Only the refusal: the block's contents depend on what the board held. + check "$([ "$(pst 3)" = "$(pst 4)" ]; echo $?)" \ + "the boot-root erase at 0x$(pst 1) returned the FRAD check error ($(pst 3))" + check "$([ "$(pst 9)" = "$(pst 4)" ]; echo $?)" \ + "the erase at 0x$(pst 7) in the guest fence returned the FRAD check error ($(pst 9))" + check "$([ "$(pst 11)" = "505150aa" ]; echo $?)" \ + "the guest-fence block is unchanged (0x$(pst 8) -> 0x$(pst 10))" + check_launch_masks 00000003 00000000 + for g in 0:0x20100000 1:0x20140000; do + check_guest "${g%%:*}" "${g##*:}" + done + log "PASS: hardware/$scenario" + ;; *) - log "usage: $0 romsmoke|positive|ahbscneg" + log "usage: $0 romsmoke|positive|ahbscneg|wrpfence|wrpoff|wrpneg" exit 2 ;; esac diff --git a/tests/target/run_rt700_m33mu.sh b/tests/target/run_rt700_m33mu.sh index 423fa44a..b8927170 100755 --- a/tests/target/run_rt700_m33mu.sh +++ b/tests/target/run_rt700_m33mu.sh @@ -28,6 +28,11 @@ # with no operating system. hsmattackneg drives the raw wolfHSM client wire, # so it exists only under WT_ENGINE=hsm. # +# wrpfence and wrpoff build wolfTrust to refuse any guest the XSPI guest fence +# does not cover: behind a fenced wolfBoot both guests launch; behind an +# unfenced one neither may enter its domain. wrpneg adds wolfBoot's +# flash-protect selftest, which must see an erase in the fence refused. +# # confboot, devstorage, devcrypto, devattest, devattestqcbor, vaultrecover, and # vaultrecoversec host Arm's unmodified psa-arch-tests val NSPE in the PSA guest # (guest0) against the conformance Secure image, the same drop-in proof the @@ -38,14 +43,19 @@ # M33MU prebuilt emulator at M33MU_REF or later; otherwise # M33MU_REF is built under /tmp # RT700_WOLFBOOT_DIR wolfBoot tree holding wolfboot.bin, tools/keytools/sign -# and wolfboot_signing_private_key.der, built with -# tests/target/wolfboot-imxrt700-lifecycle.patch applied; -# otherwise WOLFBOOT_REF is built from +# and wolfboot_signing_private_key.der, built by +# lib/rt700_wolfboot.sh with the same overrides; +# otherwise RT700_WOLFBOOT_REF plus both carried +# wolfboot-imxrt700-*.patch files is built from # config/examples/imx-rt700-tz.config (the MCUXpresso # SDK/DFP must be reachable exactly as for any RT700 # wolfBoot build) # RT700_M33MU_TIMEOUT emulator wall-clock budget in seconds (default 60, # 180 for the PSA guest scenarios) +# WT_GUEST_FLASH_WRP 1 builds wolfTrust to refuse a guest the XSPI guest +# fence does not cover, and wolfBoot to arm that fence +# WT_XSPI_GUEST_FENCE overrides the wolfBoot fence alone (default follows +# WT_GUEST_FLASH_WRP); 0 with WRP=1 proves the refusal set -eu # make test-target hands TARGET and MAKEFLAGS to every child make; wolfBoot's # own TARGET must come from its config, so drop both before any build. @@ -82,10 +92,29 @@ case "$scenario" in esac guest_build="$repo/$guest_dir/build" guest1_build="$repo/$guest1_dir/build" -wolfboot_dir="${RT700_WOLFBOOT_DIR:-/tmp/wolfboot_rt700}" + +# shellcheck source=lib/rt700_fence.sh disable=SC1091 +. "$here/lib/rt700_fence.sh" +# shellcheck source=lib/rt700_wolfboot.sh disable=SC1091 +. "$here/lib/rt700_wolfboot.sh" +case "$scenario" in + wrpfence|wrpneg) WT_XSPI_GUEST_FENCE=1 ;; + wrpoff) WT_XSPI_GUEST_FENCE=0 ;; +esac +guest_fence="${WT_XSPI_GUEST_FENCE:-${WT_GUEST_FLASH_WRP:-0}}" +fence_cflags="" +if [ "$guest_fence" = "1" ]; then + fence_cflags=$(rt700_fence_cflags) || exit 2 + wolfboot_dir="${RT700_WOLFBOOT_DIR:-/tmp/wolfboot_rt700_fence}" + if [ "$scenario" = "wrpneg" ]; then + fence_cflags="$fence_cflags -DXSPI_FLASH_PROTECT_SELFTEST" + wolfboot_dir="${RT700_WOLFBOOT_DIR:-/tmp/wolfboot_rt700_fence_selftest}" + fi +else + wolfboot_dir="${RT700_WOLFBOOT_DIR:-/tmp/wolfboot_rt700}" +fi log="$repo/build/rt700_m33mu_$scenario.log" -WOLFBOOT_REF=e6d169c7218d82e33bd04e2c086146ed37ec0cca M33MU_REF=f3c03675260264cdec815adebe4b020bb6fe57b8 # The guests' manifest restart budget (port/mimxrt700/partitions.c): ahbscneg @@ -106,10 +135,13 @@ GUEST_STARTED_RE='wolfTrust RT700 guest[01]: start' # shellcheck disable=SC2034 GUEST_DONE_RE='wolfTrust RT700 guest[01]: FF-M connect ok, done' -# --- The pinned upstream M33MU. --- +# --- The pinned upstream M33MU, plus the carried RT700 fuse seed (the +# Develop life cycle) until M33MU seeds it itself. --- +m33mu_stamp="$M33MU_REF $(cksum "$here/m33mu-imxrt700.patch" | cut -d' ' -f1)" if [ -n "${M33MU:-}" ] && [ -x "$M33MU" ]; then log "Using prebuilt M33MU: $M33MU" -elif [ -x /tmp/m33mu_rt700_src/build/m33mu ]; then +elif [ -x /tmp/m33mu_rt700_src/build/m33mu ] && + [ "$(cat /tmp/m33mu_rt700_src/.wt_m33mu 2>/dev/null)" = "$m33mu_stamp" ]; then M33MU=/tmp/m33mu_rt700_src/build/m33mu log "Reusing M33MU from a prior scenario: $M33MU" else @@ -118,10 +150,12 @@ else git clone --no-checkout https://github.com/danielinux/m33mu.git /tmp/m33mu_rt700_src git -C /tmp/m33mu_rt700_src fetch --depth 1 origin "$M33MU_REF" git -C /tmp/m33mu_rt700_src checkout --detach "$M33MU_REF" + git -C /tmp/m33mu_rt700_src apply "$here/m33mu-imxrt700.patch" cmake -S /tmp/m33mu_rt700_src -B /tmp/m33mu_rt700_src/build \ -DM33MU_ENABLE_WOLFSSL=OFF -DM33MU_BUILD_TESTS=OFF \ -DM33MU_ENABLE_RUST_PLUGINS=OFF cmake --build /tmp/m33mu_rt700_src/build --target m33mu -j"$(nproc)" + printf '%s\n' "$m33mu_stamp" > /tmp/m33mu_rt700_src/.wt_m33mu M33MU=/tmp/m33mu_rt700_src/build/m33mu fi @@ -129,39 +163,15 @@ fi # M33MU has no RT700 boot ROM or FCB and takes the reset vector from the # start of the NOR, so wolfBoot links at the NOR base instead of # 0x28004000, the same override the wolfBoot emulator tests apply. --- -# The cache is keyed on the wolfBoot ref and the lifecycle patch, so a first -# stage left by an earlier checkout is rebuilt rather than reused. -wolfboot_stamp="$WOLFBOOT_REF $(cksum "$here/wolfboot-imxrt700-lifecycle.patch" | cut -d' ' -f1)" -if [ ! -s "$wolfboot_dir/wolfboot.bin" ] || \ - [ "$(cat "$wolfboot_dir/.wt_first_stage" 2>/dev/null)" != "$wolfboot_stamp" ]; then - if [ -n "${RT700_WOLFBOOT_DIR:-}" ]; then - fail "wolfboot.bin in RT700_WOLFBOOT_DIR=$wolfboot_dir is missing or was built from another wolfBoot ref or lifecycle patch; rebuild it there or unset it" - fi - stage "build wolfBoot $WOLFBOOT_REF (imx-rt700-tz, emulator link offset)" - rm -rf "$wolfboot_dir" - git clone --no-checkout https://github.com/wolfSSL/wolfBoot.git "$wolfboot_dir" - git -C "$wolfboot_dir" fetch --depth 1 origin "$WOLFBOOT_REF" - git -C "$wolfboot_dir" checkout --detach "$WOLFBOOT_REF" - # The RT700 HAL reports no PSA lifecycle at WOLFBOOT_REF, which leaves the - # attestation service degraded; drop the patch once wolfBoot carries it. - git -C "$wolfboot_dir" apply "$here/wolfboot-imxrt700-lifecycle.patch" - git -C "$wolfboot_dir" submodule update --init --single-branch --depth 1 - cp "$wolfboot_dir/config/examples/imx-rt700-tz.config" "$wolfboot_dir/.config" - # wolfBoot locates its key tools from the shell's working directory, so - # build from inside the tree. keygen writes src/keystore.c, which the loader - # links, so the key comes first: wolfboot.elf lists its objects before it. - ( - cd "$wolfboot_dir" - make keytools - make -j1 wolfboot_signing_private_key.der - make -j1 ARCH_FLASH_OFFSET=0x28000000 BOOTLOADER_PARTITION_SIZE=0x40000 \ - wolfboot.bin - ) - printf '%s\n' "$wolfboot_stamp" > "$wolfboot_dir/.wt_first_stage" +set -- ARCH_FLASH_OFFSET=0x28000000 BOOTLOADER_PARTITION_SIZE=0x40000 \ + "CFLAGS_EXTRA=$fence_cflags" +if [ -n "${RT700_WOLFBOOT_DIR:-}" ]; then + rt700_wolfboot_current "$wolfboot_dir" "$(rt700_wolfboot_stamp "$@")" || \ + fail "wolfboot.bin in RT700_WOLFBOOT_DIR=$wolfboot_dir was not built from $RT700_WOLFBOOT_REF with this link offset, carried patches, and guest fence; rebuild it there or unset it" +else + stage "wolfBoot $RT700_WOLFBOOT_REF (imx-rt700-tz, emulator link offset)" + rt700_wolfboot_build "$wolfboot_dir" "$@" || fail "wolfBoot build failed" fi -[ -x "$wolfboot_dir/tools/keytools/sign" ] || fail "keytools missing in $wolfboot_dir" -[ -s "$wolfboot_dir/wolfboot_signing_private_key.der" ] || \ - fail "signing key missing in $wolfboot_dir" # --- wolfTrust and both guests, measurements pinned, signed for the boot # partition: the same recipe run_rt700_hardware.sh flashes. --- @@ -385,6 +395,24 @@ case "$scenario" in check "$([ "$sau_refusals" -eq 1 ]; echo $?)" \ "the refusal came from the SAU" ;; + wrpfence|wrpneg) + if [ "$scenario" = "wrpneg" ]; then + expect "the boot-root erase was refused" "xspi protect selftest PASS" + expect "an erase inside the guest fence was refused, block unchanged" \ + "xspi guest fence selftest PASS" + fi + for guest in guest0 guest1; do + expect_n "$guest launched behind the armed guest fence" 1 \ + "wolfTrust RT700 $guest: start" + expect_n "$guest reached the SPM and finished" 1 \ + "wolfTrust RT700 $guest: FF-M connect ok, done" + done + expect_n "both guests reached the storage service" 2 ": storage connect ok" + ;; + wrpoff) + refute_re "no guest entered its domain without the guest fence" \ + "wolfTrust RT700 guest[01]: start" + ;; authneg) refute_re "the tampered guest0 never entered its domain" \ "wolfTrust RT700 guest0: start" diff --git a/tests/target/run_rt700_suite.sh b/tests/target/run_rt700_suite.sh new file mode 100755 index 00000000..8efead33 --- /dev/null +++ b/tests/target/run_rt700_suite.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# wolfTrust MIMXRT700 hardware suite, the RT700 sibling of run_h5_suite.sh: the +# positive chain, the guest isolation negative, and the XSPI guest-fence set, +# each built and flashed to the EVK on the host that owns the probe. Without a +# board detect_rt700.sh reports why and the suite SKIPs; it never silently passes. +set -euo pipefail + +repo="$(cd "$(dirname "$0")/../.." && pwd)" +runner="$repo/tests/target/run_rt700_hardware.sh" +scenarios="${WT_RT700_SCENARIOS:-positive ahbscneg wrpfence wrpoff wrpneg}" +spsdk_venv="${RT700_SPSDK_VENV:-$HOME/spsdk-venv}" + +# The runner puts the SPSDK venv on PATH itself; detection needs it first. +if [ -d "$spsdk_venv/bin" ]; then + PATH="$spsdk_venv/bin:$PATH" + export PATH +fi +if ! "$repo/tests/target/detect_rt700.sh" >/dev/null 2>&1; then + echo "SKIP: RT700 hardware suite ($("$repo/tests/target/detect_rt700.sh" 2>&1))" + exit 0 +fi + +logs="$repo/build/rt700-suite" +mkdir -p "$logs" +rc=0 +for s in $scenarios; do + echo "RUN: hardware/$s" + if "$runner" "$s" >"$logs/$s.log" 2>&1; then + grep -F ' [check] ' "$logs/$s.log" || true + echo "PASS: hardware/$s" + else + grep -F ' [check] ' "$logs/$s.log" || true + echo "FAIL: hardware/$s (tail of $logs/$s.log):" + tail -15 "$logs/$s.log" || true + rc=1 + fi +done + +if [ "$rc" -eq 0 ]; then echo "PASS: hardware/all"; else echo "FAIL: hardware/all"; exit 1; fi diff --git a/tests/target/wolfboot-imxrt700-guest-fence.patch b/tests/target/wolfboot-imxrt700-guest-fence.patch new file mode 100644 index 00000000..0d82c12e --- /dev/null +++ b/tests/target/wolfboot-imxrt700-guest-fence.patch @@ -0,0 +1,117 @@ +diff --git a/hal/imx_rt7xx.c b/hal/imx_rt7xx.c +index bf9227a6..c9a7fd18 100644 +--- a/hal/imx_rt7xx.c ++++ b/hal/imx_rt7xx.c +@@ -861,10 +861,39 @@ static void RAMFUNCTION xspi_flash_protect_selftest(uint32_t end) + (PST(5) == 0xFFFFFFFFu)) ? 0x505150AAu : 0x505150EEu; + wolfBoot_printf("xspi protect selftest %s\n", + (PST(6) == 0x505150AAu) ? "PASS" : "FAIL"); ++#if defined(XSPI_GUEST_FENCE_START) && defined(XSPI_GUEST_FENCE_END) ++ /* The guest fence must refuse the same erase in its last 64 KB block and ++ * leave the block as it was, whatever that block holds. */ ++ PST(7) = (uint32_t)XSPI_GUEST_FENCE_END - FRAD_GRANULE; ++ PST(8) = *(volatile uint32_t *)PST(7); ++ PST(9) = (uint32_t)xspi_ip_command(PST(7), LUT_SEQ_IDX_ERASE_SECTOR); ++ (void)xspi_ahb_flush(); ++ PST(10) = *(volatile uint32_t *)PST(7); ++ PST(11) = ((PST(9) == (uint32_t)kStatus_XSPI_FradCheckError) && ++ (PST(10) == PST(8))) ? 0x505150AAu : 0x505150EEu; ++ wolfBoot_printf("xspi guest fence selftest %s\n", ++ (PST(11) == 0x505150AAu) ? "PASS" : "FAIL"); ++#endif + PST(0) = 0x50510002u; + } + #endif + ++#if defined(XSPI_GUEST_FENCE_START) && defined(XSPI_GUEST_FENCE_END) ++#if ((XSPI_GUEST_FENCE_START) & 0xFFFF) != 0 || ((XSPI_GUEST_FENCE_END) & 0xFFFF) != 0 ++#error "XSPI guest fence bounds must be 64 KB aligned" ++#endif ++#define FRAD_GUEST_FENCE 1 ++static int RAMFUNCTION frad_readback_ok(uint32_t n, uint32_t start, ++ uint32_t last, uint32_t acp) ++{ ++ return (((FRAD_WORD(n, 0) & 0xFFFF0000u) == (start & 0xFFFF0000u)) && ++ ((FRAD_WORD(n, 1) & 0xFFFF0000u) == (last & 0xFFFF0000u)) && ++ ((FRAD_WORD(n, 2) & FRAD_WORD2_ACP_MASK) == acp) && ++ ((FRAD_WORD(n, 3) & FRAD_WORD3_LOCK_VLD) == ++ (FRAD_WORD3_LOCKED | XSPI_FRAD0_WORD3_VLD_MASK))) ? 1 : 0; ++} ++#endif ++ + /* Bootloader write protection via the XSPI SFP flash region descriptors. + * FRAD0 covers the boot root from the start of the NOR (so the FCB the + * BootROM reads is fenced too) through the end of the requested region and +@@ -890,6 +919,13 @@ int RAMFUNCTION hal_flash_protect(uint32_t address, int len) + if ((g_xspi_ready == 0) && (xspi_nor_init_guarded() != kStatus_Success)) { + return -1; + } ++#ifdef FRAD_GUEST_FENCE ++ if ((end >= (uint32_t)XSPI_GUEST_FENCE_START) || ++ ((uint32_t)XSPI_GUEST_FENCE_START >= (uint32_t)XSPI_GUEST_FENCE_END) || ++ ((uint32_t)XSPI_GUEST_FENCE_END >= NOR_BASE + NOR_SIZE)) { ++ return -1; ++ } ++#endif + + for (i = 0u; i < XSPI_SFP_FRAD_COUNT; i++) { + frad.fradConfig[i].startAddress = 0u; +@@ -909,6 +945,21 @@ int RAMFUNCTION hal_flash_protect(uint32_t address, int len) + frad.fradConfig[1].tg1MasterAccess = FRAD_ACCESS_NONE; + frad.fradConfig[1].assignIsValid = true; + frad.fradConfig[1].descriptorLock = kXSPI_DescriptorLockDisabled; ++#ifdef FRAD_GUEST_FENCE ++ /* FRAD2 fences the Secure application's Non-secure guest images against ++ * every initiator, the Secure application included; FRAD3 keeps the ++ * update, swap and storage partitions above it writable. */ ++ frad.fradConfig[1].endAddress = (uint32_t)XSPI_GUEST_FENCE_START - 1u; ++ frad.fradConfig[2].startAddress = (uint32_t)XSPI_GUEST_FENCE_START; ++ frad.fradConfig[2].endAddress = (uint32_t)XSPI_GUEST_FENCE_END - 1u; ++ frad.fradConfig[2].assignIsValid = true; ++ frad.fradConfig[3].startAddress = (uint32_t)XSPI_GUEST_FENCE_END; ++ frad.fradConfig[3].endAddress = NOR_BASE + NOR_SIZE - 1u; ++ frad.fradConfig[3].tg0MasterAccess = FRAD_ACCESS_ALL; ++ frad.fradConfig[3].tg1MasterAccess = FRAD_ACCESS_NONE; ++ frad.fradConfig[3].assignIsValid = true; ++ frad.fradConfig[3].descriptorLock = kXSPI_DescriptorLockDisabled; ++#endif + + /* Region policy is held per initiator domain, so the fence needs a + * domain to bind to: one locked domain that every initiator matches +@@ -928,6 +979,10 @@ int RAMFUNCTION hal_flash_protect(uint32_t address, int len) + XSPI_UpdateSFPConfig(XSPI_NOR_S, &mdad, &frad); + FRAD_WORD(1, 2) = FRAD_WORD2_ACP_ALL; + FRAD_WORD(1, 3) |= FRAD_WORD3_LOCKED; ++#ifdef FRAD_GUEST_FENCE ++ FRAD_WORD(3, 2) = FRAD_WORD2_ACP_ALL; ++ FRAD_WORD(3, 3) |= FRAD_WORD3_LOCKED; ++#endif + XSPI_NOR_S->MGC |= XSPI_MGC_GCLCK(1u); + + /* The driver cannot report a locked or ignored write, so trust only +@@ -951,6 +1006,17 @@ int RAMFUNCTION hal_flash_protect(uint32_t address, int len) + (FRAD_WORD3_LOCKED | XSPI_FRAD0_WORD3_VLD_MASK))) { + return -1; + } ++#ifdef FRAD_GUEST_FENCE ++ if ((frad_readback_ok(1u, end, (uint32_t)XSPI_GUEST_FENCE_START - 1u, ++ FRAD_WORD2_ACP_ALL) == 0) || ++ (frad_readback_ok(2u, (uint32_t)XSPI_GUEST_FENCE_START, ++ (uint32_t)XSPI_GUEST_FENCE_END - 1u, 0u) == 0) || ++ (frad_readback_ok(3u, (uint32_t)XSPI_GUEST_FENCE_END, ++ NOR_BASE + NOR_SIZE - 1u, FRAD_WORD2_ACP_ALL) == 0)) { ++ return -1; ++ } ++ for (i = 4u; i < XSPI_SFP_FRAD_COUNT; i++) { ++#else + if (((FRAD_WORD(1, 0) & 0xFFFF0000u) != (end & 0xFFFF0000u)) || + ((FRAD_WORD(1, 1) & 0xFFFF0000u) != + ((NOR_BASE + NOR_SIZE - 1u) & 0xFFFF0000u)) || +@@ -960,6 +1026,7 @@ int RAMFUNCTION hal_flash_protect(uint32_t address, int len) + return -1; + } + for (i = 2u; i < XSPI_SFP_FRAD_COUNT; i++) { ++#endif + if ((FRAD_WORD(i, 3) & FRAD_WORD3_LOCK_VLD) != FRAD_WORD3_LOCKED) { + return -1; + } diff --git a/tests/target/wolfboot-imxrt700-lifecycle.patch b/tests/target/wolfboot-imxrt700-lifecycle.patch index 3ed7a1df..f1bf0ad7 100644 --- a/tests/target/wolfboot-imxrt700-lifecycle.patch +++ b/tests/target/wolfboot-imxrt700-lifecycle.patch @@ -1,28 +1,188 @@ diff --git a/hal/imx_rt7xx.c b/hal/imx_rt7xx.c -index a5b40299..559c8822 100644 +index a5b40299..bf9227a6 100644 --- a/hal/imx_rt7xx.c +++ b/hal/imx_rt7xx.c -@@ -21,6 +21,7 @@ +@@ -21,6 +21,8 @@ #include #include "image.h" #include "hal.h" +#include "wolfboot/secure_handoff.h" ++#include "imx_rt7xx_lifecycle.h" #include "loader.h" #include "printf.h" #include "imx_rt7xx.h" -@@ -1051,3 +1052,15 @@ void hal_prepare_boot(void) +@@ -1051,3 +1053,24 @@ void hal_prepare_boot(void) } #endif } + -+/* The PSA security lifecycle the Secure application attests. The OTP -+ * lifecycle state is not read yet, so this reports the development state -+ * rather than claim a secured device. */ ++/* The PSA security lifecycle the Secure application attests, from the OTP ++ * life cycle shadow and the debug authentication state. */ +int hal_attestation_get_lifecycle(uint32_t *lifecycle) +{ ++ uint32_t lcState; ++ uint32_t lcStateRed; ++ uint32_t debugAuthStatus; ++ + if (lifecycle == NULL) { + return -1; + } -+ *lifecycle = WOLFBOOT_SECURE_HANDOFF_LIFECYCLE_ASSEMBLY_AND_TEST; ++ lcState = *(volatile uint32_t *)(IMX_RT7XX_OTP_SHADOW_BASE_S + ++ IMX_RT7XX_OTP_LC_STATE_OFFSET); ++ lcStateRed = *(volatile uint32_t *)(IMX_RT7XX_OTP_SHADOW_BASE_S + ++ IMX_RT7XX_OTP_LC_STATE_RED_OFFSET); ++ debugAuthStatus = *(volatile uint32_t *)IMX_RT7XX_DAUTHSTATUS_ADDRESS; ++ *lifecycle = imx_rt7xx_attestation_lifecycle(lcState, lcStateRed, ++ debugAuthStatus); + return 0; +} +diff --git a/hal/imx_rt7xx_lifecycle.h b/hal/imx_rt7xx_lifecycle.h +new file mode 100644 +index 00000000..c6a99736 +--- /dev/null ++++ b/hal/imx_rt7xx_lifecycle.h +@@ -0,0 +1,144 @@ ++/* imx_rt7xx_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_IMX_RT7XX_LIFECYCLE_H ++#define WOLFBOOT_IMX_RT7XX_LIFECYCLE_H ++ ++#include ++ ++/* OTP shadow registers, the same on both silicon revisions: the life cycle ++ * byte and its redundant copy, which must agree before either is trusted. */ ++#define IMX_RT7XX_OTP_SHADOW_BASE_S 0x50018000u ++#define IMX_RT7XX_OTP_LC_STATE_OFFSET 0x23Cu ++#define IMX_RT7XX_OTP_LC_STATE_RED_OFFSET 0x094u ++#define IMX_RT7XX_OTP_LC_STATE_MASK 0xFFu ++/* A0/A1 only: a bit-protection copy of each byte in bits 16-23; B0 reads 0. */ ++#define IMX_RT7XX_OTP_BP_LC_STATE_SHIFT 16u ++/* No revision defines anything else in these words. */ ++#define IMX_RT7XX_OTP_LC_RESERVED_MASK 0xFF00FF00u ++ ++#define IMX_RT7XX_LC_DEVELOP 0x03u ++#define IMX_RT7XX_LC_DEVELOP2 0x07u ++#define IMX_RT7XX_LC_IN_FIELD 0x0Fu ++#define IMX_RT7XX_LC_IN_FIELD_RETURN 0x1Fu ++#define IMX_RT7XX_LC_IN_FIELD_LOCKED 0xCFu ++ ++#define IMX_RT7XX_DAUTHSTATUS_ADDRESS 0xE000EFB8u ++#define IMX_RT7XX_DAUTHSTATUS_NSID_SHIFT 0u ++#define IMX_RT7XX_DAUTHSTATUS_NSNID_SHIFT 2u ++#define IMX_RT7XX_DAUTHSTATUS_SID_SHIFT 4u ++#define IMX_RT7XX_DAUTHSTATUS_SNID_SHIFT 6u ++#define IMX_RT7XX_DAUTHSTATUS_FIELD_MASK 0x3u ++#define IMX_RT7XX_DAUTHSTATUS_ENABLED 0x3u ++#define IMX_RT7XX_DAUTHSTATUS_DISABLED 0x2u ++ ++static inline uint32_t imx_rt7xx_lc_to_psa_lifecycle(uint32_t lcState, ++ uint32_t lcStateRed) ++{ ++ uint32_t lifecycle; ++ uint32_t lc = lcState & IMX_RT7XX_OTP_LC_STATE_MASK; ++ uint32_t lcRed = lcStateRed & IMX_RT7XX_OTP_LC_STATE_MASK; ++ uint32_t bp = (lcState >> IMX_RT7XX_OTP_BP_LC_STATE_SHIFT) & ++ IMX_RT7XX_OTP_LC_STATE_MASK; ++ uint32_t bpRed = (lcStateRed >> IMX_RT7XX_OTP_BP_LC_STATE_SHIFT) & ++ IMX_RT7XX_OTP_LC_STATE_MASK; ++ ++ if ((lcState & IMX_RT7XX_OTP_LC_RESERVED_MASK) != 0u || ++ (lcStateRed & IMX_RT7XX_OTP_LC_RESERVED_MASK) != 0u || ++ lc != lcRed || (bp == 0u) != (bpRed == 0u) || ++ (bp != 0u && (bp != lc || bpRed != lcRed))) { ++ return 0x0000u; /* PSA_LIFECYCLE_UNKNOWN */ ++ } ++ switch (lc) { ++ case IMX_RT7XX_LC_DEVELOP: ++ lifecycle = 0x1000u; /* PSA_LIFECYCLE_ASSEMBLY_AND_TEST */ ++ break; ++ case IMX_RT7XX_LC_DEVELOP2: ++ lifecycle = 0x2000u; /* PSA_LIFECYCLE_PSA_ROT_PROVISIONING */ ++ break; ++ case IMX_RT7XX_LC_IN_FIELD: ++ case IMX_RT7XX_LC_IN_FIELD_LOCKED: ++ lifecycle = 0x3000u; /* PSA_LIFECYCLE_SECURED */ ++ break; ++ case IMX_RT7XX_LC_IN_FIELD_RETURN: ++ lifecycle = 0x6000u; /* PSA_LIFECYCLE_DECOMMISSIONED */ ++ break; ++ default: ++ lifecycle = 0x0000u; /* PSA_LIFECYCLE_UNKNOWN */ ++ break; ++ } ++ return lifecycle; ++} ++ ++static inline int imx_rt7xx_debug_status_field_is(uint32_t debugAuthStatus, ++ uint32_t shift, uint32_t value) ++{ ++ return (((debugAuthStatus >> shift) & ++ IMX_RT7XX_DAUTHSTATUS_FIELD_MASK) == value); ++} ++ ++/* Arm encodes each field 0b11 enabled, 0b10 disabled; anything else is ++ * malformed and the whole register is untrusted. */ ++static inline int imx_rt7xx_debug_status_field_valid(uint32_t debugAuthStatus, ++ uint32_t shift) ++{ ++ return imx_rt7xx_debug_status_field_is(debugAuthStatus, shift, ++ IMX_RT7XX_DAUTHSTATUS_ENABLED) || ++ imx_rt7xx_debug_status_field_is(debugAuthStatus, shift, ++ IMX_RT7XX_DAUTHSTATUS_DISABLED); ++} ++ ++static inline uint32_t imx_rt7xx_attestation_lifecycle(uint32_t lcState, ++ uint32_t lcStateRed, uint32_t debugAuthStatus) ++{ ++ uint32_t lifecycle = imx_rt7xx_lc_to_psa_lifecycle(lcState, lcStateRed); ++ ++ if (lifecycle == 0x3000u) { ++ if (!imx_rt7xx_debug_status_field_valid(debugAuthStatus, ++ IMX_RT7XX_DAUTHSTATUS_NSID_SHIFT) || ++ !imx_rt7xx_debug_status_field_valid(debugAuthStatus, ++ IMX_RT7XX_DAUTHSTATUS_NSNID_SHIFT) || ++ !imx_rt7xx_debug_status_field_valid(debugAuthStatus, ++ IMX_RT7XX_DAUTHSTATUS_SID_SHIFT) || ++ !imx_rt7xx_debug_status_field_valid(debugAuthStatus, ++ IMX_RT7XX_DAUTHSTATUS_SNID_SHIFT)) { ++ lifecycle = 0x0000u; /* PSA_LIFECYCLE_UNKNOWN */ ++ } ++ else if (imx_rt7xx_debug_status_field_is(debugAuthStatus, ++ IMX_RT7XX_DAUTHSTATUS_SID_SHIFT, ++ IMX_RT7XX_DAUTHSTATUS_ENABLED) || ++ imx_rt7xx_debug_status_field_is(debugAuthStatus, ++ IMX_RT7XX_DAUTHSTATUS_SNID_SHIFT, ++ IMX_RT7XX_DAUTHSTATUS_ENABLED)) { ++ lifecycle = 0x5000u; /* PSA_LIFECYCLE_RECOVERABLE_PSA_ROT_DEBUG */ ++ } ++ else if (imx_rt7xx_debug_status_field_is(debugAuthStatus, ++ IMX_RT7XX_DAUTHSTATUS_NSID_SHIFT, ++ IMX_RT7XX_DAUTHSTATUS_ENABLED) || ++ imx_rt7xx_debug_status_field_is(debugAuthStatus, ++ IMX_RT7XX_DAUTHSTATUS_NSNID_SHIFT, ++ IMX_RT7XX_DAUTHSTATUS_ENABLED)) { ++ lifecycle = 0x4000u; /* PSA_LIFECYCLE_NON_PSA_ROT_DEBUG */ ++ } ++ } ++ return lifecycle; ++} ++ ++#endif /* WOLFBOOT_IMX_RT7XX_LIFECYCLE_H */