Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
3 changes: 2 additions & 1 deletion docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
2 changes: 2 additions & 0 deletions docs/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
524 changes: 518 additions & 6 deletions docs/MIMXRT700-Guide.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/Macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
227 changes: 227 additions & 0 deletions docs/Provisioning.md
Original file line number Diff line number Diff line change
@@ -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=<port> tests/target/provisioning/provisioning_ctrl.sh <command> [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 <state>` | 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 <state>` | 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 <state>
```

`advance` puts the part in `<state>` 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 <state>
```

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 <state>
```

It previews again, then asks:

```text
!!! Moving this <PORT> to <state>
!!! This is IRREVERSIBLE: <what this port loses for good>
!!! Are you sure? Type "I ACCEPT <code>" 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 <code>`. 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_<target>.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.
Loading
Loading