diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 368cdf159..b1ccd4281 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -6,7 +6,7 @@ matrix on labels, main pushes, and nightly. `codeql.yml` runs C security queries. `coverity.yml` runs on Sundays at 00:00 UTC or by manual dispatch. Both build the host suites and secure images -for STM32H563 and MIMXRT700 with both crypto engines. +for STM32H563, MIMXRT700, and MCXN947 with both crypto engines. Coverity scans wolfTrust runtime, ports, tests, and generated policy code. `tools/ci-coverity-filter.py` removes dependency source units before upload @@ -21,8 +21,8 @@ An accepted upload queues analysis; results appear after Coverity processes it. | Tier | Trigger | Purpose | |------|---------|---------| | **Fast (per-PR)** | every PR; push to main | host unit suites, ISO C99, house style, bare-scope scan, Arm PSA-FF conformance, cross-compile, compiler matrix, sanitizers, valgrind, integrations, core/port split guard | -| **M33MU smoke** | every PR | per port, on both crypto engines: STM32H563 `positive gtzcneg crossdomain bothpsa confboot devcrypto`; MIMXRT700 `positive ahbscneg crossdomain bothpsa confboot devcrypto` | -| **M33MU full** | PR labels `ci:all`, `ci:h5`, `ci:rt700`; push to main; `cron: 0 8 * * *`; `workflow_dispatch` (port input) | every scenario of that port on both engines (see below) | +| **M33MU smoke** | every PR | per port, on both crypto engines: STM32H563 `positive gtzcneg crossdomain bothpsa confboot devcrypto`; MIMXRT700 `positive ahbscneg crossdomain bothpsa confboot devcrypto`; MCXN947 `positive sauneg crossdomain bothpsa confboot devcrypto` | +| **M33MU full** | PR labels `ci:all`, `ci:h5`, `ci:rt700`, `ci:mcxn947`; push to main; `cron: 0 8 * * *`; `workflow_dispatch` (port input) | every scenario of that port on both engines (see below) | The M33MU workflow (`m33mu.yml`) is label-selected the way wolfProvider's `pr-osp-select.yml` is: a `select` job checks the routing table @@ -35,6 +35,7 @@ groups it picked, so a job that is not selected never appears as skipped. | (no label) | each port's smoke tier on both engines | | `ci:h5` | the STM32H563 full matrix (the change touched only that port) | | `ci:rt700` | the MIMXRT700 full matrix (the change touched only that port) | +| `ci:mcxn947` | the MCXN947 full matrix (the change touched only that port) | | `ci:all` / `ci:m33mu` | every scenario of every port (a core change) | A label keeps applying on later pushes to the PR. Pushes to @@ -74,10 +75,14 @@ test-target` also reads. The table below is a representative slice: | `mimxrt700_devstorage_devattest_devattestqcbor_` | `devstorage devattest devattestqcbor` | PSA ITS/PS and Initial Attestation conformance (the token parses under wolfCOSE's shim and reference QCBOR) | | `mimxrt700_devcrypto_vaultrecover_vaultrecoversec_` | `devcrypto vaultrecover vaultrecoversec` | PSA Crypto conformance on wolfPSA; a foreign vault pool self-heals in development and fails closed when SECURED | | `mimxrt700_rollbackneg_remeasureneg_manifestneg_spbudgetneg_` | `rollbackneg remeasureneg manifestneg spbudgetneg` | shared Secure-verdict table: rollback refusal, re-measure tamper, corrupted manifest, restart budget | +| `mcxn947_positive_` | `positive` | MCXN947 chain under the MCXN947 model (internal flash, ROM flash API, EdgeLock entropy), both guests finish | +| `mcxn947_sauneg_` | `sauneg` | guest0's store into guest1's RAM refused by the SAU, contained | +| `mcxn947__` | as the MIMXRT700 groups above | the same scenario groups on the MCXN947 model; `tests/target/lib/scenario_matrix.py` is the list | An unlabeled PR runs only each port's smoke tier; the full matrix needs a `ci:` label. To run a single scenario locally, use `tests/target/run_m33mu_scenario.sh ` (the -RT700 checks use `tests/target/run_rt700_m33mu.sh `); to run a port's +RT700 checks use `tests/target/run_rt700_m33mu.sh `, the MCXN947 checks +`tests/target/run_mcxn947_m33mu.sh `); to run a port's full matrix on a PR, add its `ci:` label; off-PR against a branch, `workflow_dispatch` on `m33mu.yml` with the `port` input. diff --git a/.github/workflows/cross-compile.yml b/.github/workflows/cross-compile.yml index d6bf6f184..676e88947 100644 --- a/.github/workflows/cross-compile.yml +++ b/.github/workflows/cross-compile.yml @@ -49,7 +49,7 @@ jobs: echo "LTO flash: $no_lto_flash -> $lto_flash bytes" - name: Verify an unimplemented isolation level stops the build run: | - for target in stm32h563 mimxrt700; do + for target in stm32h563 mimxrt700 mcxn947; do for level in 1 2; do if make TARGET=$target WT_ISOLATION_LEVEL=$level \ BUILD_DIR=build-level-$level secure-image \ @@ -119,3 +119,22 @@ jobs: WT_CONFORMANCE=1 secure-image TOOLPREFIX=arm-none-eabi- \ BUILD_DIR=build_rt700_conf_$engine done + - name: Build the MCXN947 secure image (both engines, default layout) + run: | + for engine in native hsm; do + make TARGET=mcxn947 WT_ENGINE=$engine WT_ATTEST_COSE=0 \ + secure-image TOOLPREFIX=arm-none-eabi- \ + BUILD_DIR=build_mcxn947_$engine + # The default must leave wolfBoot's 0x400 header before the vectors. + vectors="$(arm-none-eabi-objdump -h build_mcxn947_$engine/wolftrust.elf \ + | awk '$2 == ".vectors" { print $4 }')" + test "$vectors" = "10040400" || \ + { echo "MCXN947 .vectors at 0x$vectors, want 0x10040400"; exit 1; } + done + - name: Build the MCXN947 conformance secure image (both engines) + run: | + for engine in native hsm; do + make TARGET=mcxn947 WT_ENGINE=$engine WT_ATTEST_COSE=0 \ + WT_CONFORMANCE=1 secure-image TOOLPREFIX=arm-none-eabi- \ + BUILD_DIR=build_mcxn947_conf_$engine + done diff --git a/.github/workflows/m33mu.yml b/.github/workflows/m33mu.yml index 68d84ea14..b206d3bf9 100644 --- a/.github/workflows/m33mu.yml +++ b/.github/workflows/m33mu.yml @@ -1,9 +1,9 @@ name: M33MU on: - # Pull requests run each port's smoke tier; the ci:all, ci:h5, and ci:rt700 - # labels, pushes to main, the nightly schedule (workflow_call from - # nightly.yml), and manual dispatch run a port's full matrix. + # Pull requests run each port's smoke tier; the ci:all, ci:h5, ci:rt700, and + # ci:mcxn947 labels, pushes to main, the nightly schedule (workflow_call + # from nightly.yml), and manual dispatch run a port's full matrix. pull_request: types: [opened, synchronize, reopened, labeled] push: @@ -13,7 +13,7 @@ on: port: description: Port whose full matrix to run type: choice - options: [all, stm32h563, mimxrt700] + options: [all, stm32h563, mimxrt700, mcxn947] default: all workflow_call: @@ -278,7 +278,7 @@ jobs: fi # Label-selected, the wolfProvider way: one select job reads the ci:* labels - # (ci:h5 or ci:rt700 for that port's full matrix when only that port + # (ci:h5, ci:rt700 or ci:mcxn947 for that port's full matrix when only that port # changed, ci:all or ci:m33mu for everything after a core change, no label # for each port's smoke tier), the event, and the dispatch input, then one # matrix job runs exactly those scenario groups, so nothing shows up skipped. @@ -324,27 +324,30 @@ jobs: git config --global --add safe.directory "$GITHUB_WORKSPACE" git config --global url."https://github.com/".insteadOf git@github.com: - # The RT700 wolfBoot first stage builds against the MCUXpresso SDK, staged - # the way wolfBoot's own imxrt700 emulator workflow stages it. + # The RT700 and MCXN947 wolfBoot first stages build against the + # MCUXpresso SDK, staged the way wolfBoot's own imxrt700 emulator + # workflow stages it, one board per port. - uses: actions/checkout@v4 - if: matrix.port == 'mimxrt700' + if: matrix.port == 'mimxrt700' || matrix.port == 'mcxn947' with: repository: nxp-mcuxpresso/CMSIS_5 ref: f9cf5df6d51a95f0146930d75af3d71977cf795a path: CMSIS_5 - name: Cache MCUXpresso SDK - if: matrix.port == 'mimxrt700' + if: matrix.port == 'mimxrt700' || matrix.port == 'mcxn947' id: cache-mcuxpresso uses: actions/cache@v4 with: path: | mcuxpresso-sdk venv - key: mcuxpresso-sdk-mimxrt700evk-${{ hashFiles('.github/workflows/m33mu.yml') }} + key: mcuxpresso-sdk-${{ matrix.port == 'mimxrt700' && 'mimxrt700evk' || 'frdmmcxn947' }}-${{ hashFiles('.github/workflows/m33mu.yml') }} - name: Setup MCUXpresso SDK - if: matrix.port == 'mimxrt700' && steps.cache-mcuxpresso.outputs.cache-hit != 'true' + if: (matrix.port == 'mimxrt700' || matrix.port == 'mcxn947') && steps.cache-mcuxpresso.outputs.cache-hit != 'true' + env: + SDK_BOARD: ${{ matrix.port == 'mimxrt700' && 'mimxrt700evk' || 'frdmmcxn947' }} run: | python3 -m venv venv . venv/bin/activate @@ -352,12 +355,12 @@ jobs: west init -m https://github.com/nxp-mcuxpresso/mcuxsdk-manifests.git \ --mr v26.06.00-LTS mcuxpresso-sdk cd mcuxpresso-sdk - sed -i '/se_hostlib\|emwin/d' manifests/boards/mimxrt700evk.yml - west update_board --set board mimxrt700evk + sed -i '/se_hostlib\|emwin/d' "manifests/boards/$SDK_BOARD.yml" + west update_board --set board "$SDK_BOARD" - name: Run target scenarios env: - MCUXSDK: ${{ matrix.port == 'mimxrt700' && '1' || '' }} + MCUXSDK: ${{ (matrix.port == 'mimxrt700' || matrix.port == 'mcxn947') && '1' || '' }} MCUXPRESSO: ${{ github.workspace }}/mcuxpresso-sdk/mcuxsdk MCUXPRESSO_CMSIS: ${{ github.workspace }}/CMSIS_5/CMSIS run: tests/target/run_suite.sh ${{ matrix.arm }} ${{ matrix.key }} diff --git a/.gitignore b/.gitignore index fe2d02fa7..731b7823d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ tests/firmware/stm32h563/build/ tests/firmware/stm32h563-vnet/build/ tests/firmware/mimxrt700-baremetal/build/ +tests/firmware/mcxn947-baremetal/build/ tests/firmware/stm32h563-vnet/stm32h563_OTP.bin *.o /wolftrust diff --git a/Makefile b/Makefile index 256b88300..bd00d8376 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ test: @python3 tests/scripts/test_m33mu_console.py @$(MAKE) --no-print-directory -C tests/host test @$(MAKE) --no-print-directory test-provisioning + @tests/target/test_mcxn947_suite_evidence.sh # Production lock gates of both provisioning backends, against stub tools. test-provisioning: @@ -72,8 +73,8 @@ c99-check: # FF-M target scenarios for one port under the M33MU emulator, separate from # `make test` (host-only). TARGET picks the port (stm32h563 by default, -# mimxrt700 for the RT700 chain, whose runner builds its own pinned emulator -# and wolfBoot first stage); WT_TIER=smoke (default) runs the per-PR set, +# mimxrt700 or mcxn947 for the NXP chains, whose runners build their own +# pinned emulator and wolfBoot first stage); WT_TIER=smoke (default) runs the per-PR set, # WT_TIER=full every scenario, both from tests/target/lib/scenario_matrix.py. # The STM32H563 path auto-detects an M33MU (or WT_TARGET_SCENARIOS=1) and skips # explicitly otherwise so it never silently passes. Runs inside the @@ -84,6 +85,8 @@ WT_SCENARIOS = $(shell python3 tests/target/lib/scenario_matrix.py \ test-target: ifeq ($(TARGET),mimxrt700) @tests/target/run_suite.sh rt700-m33mu $(WT_SCENARIOS) +else ifeq ($(TARGET),mcxn947) + @tests/target/run_suite.sh mcxn947-m33mu $(WT_SCENARIOS) else @if ! tests/target/detect_m33mu.sh >/dev/null 2>&1; then \ echo "SKIP: FF-M target scenarios ($$(tests/target/detect_m33mu.sh 2>&1))"; \ @@ -99,6 +102,8 @@ endif test-hardware: ifeq ($(TARGET),mimxrt700) @tests/target/run_rt700_suite.sh +else ifeq ($(TARGET),mcxn947) + @tests/target/run_mcxn947_suite.sh else @tests/target/run_h5_suite.sh endif diff --git a/README.md b/README.md index dbef3bf30..a7109177c 100644 --- a/README.md +++ b/README.md @@ -88,13 +88,14 @@ current reference port, not a requirement imposed on every intended port. ## Ports -wolfTrust currently supports two ports; each guide covers the board, the +wolfTrust currently supports three ports; each guide covers the board, the first-stage loader contract, and the runners: - STM32H563 (NUCLEO-H563ZI): [STM32H5 Guide](docs/STM32H5-Guide.md) - NXP MIMXRT700 (MIMXRT700-EVK): [MIMXRT700 Guide](docs/MIMXRT700-Guide.md) +- NXP MCXN947 (FRDM-MCXN947): [MCXN947 Guide](docs/MCXN947-Guide.md) -Both build with `TARGET=` (`stm32h563` is the default) and run the +All build with `TARGET=` (`stm32h563` is the default) and run the same scenario set under the M33MU emulator with `make test-target TARGET=`. @@ -137,14 +138,16 @@ Common validation entry points: make test make test-target make test-target TARGET=mimxrt700 +make test-target TARGET=mcxn947 make test-conformance WT_H5_DOCKER_IMAGE=ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.15 make test-hardware +make test-hardware TARGET=mcxn947 ``` `make test-target` runs the port's smoke tier under M33MU (`WT_TIER=full` for every scenario): the STM32H563 default skips explicitly when M33MU is -unavailable, and `TARGET=mimxrt700` builds its pinned emulator and wolfBoot -first stage itself. +unavailable, and `TARGET=mimxrt700` or `TARGET=mcxn947` builds its pinned +emulator and wolfBoot first stage itself. `make test-conformance` instead runs its 20-test host subset and warns that it is not full emulator or hardware evidence. `make test-hardware` skips when board detection fails; on hosts without `lsusb`, a missing ST-Link can instead diff --git a/docs/Home.md b/docs/Home.md index eebff2c37..9a1565b34 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -118,3 +118,4 @@ port's five CMSE gateway veneers. | [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 | +| [MCXN947 Guide](MCXN947-Guide.md) | MCXN947 internal flash, ELS entropy, emulator runner, life cycle rehearsal, and open items | diff --git a/docs/MCXN947-Guide.md b/docs/MCXN947-Guide.md new file mode 100644 index 000000000..e3dc5247d --- /dev/null +++ b/docs/MCXN947-Guide.md @@ -0,0 +1,445 @@ +# MCXN947 Guide + +This guide covers the NXP FRDM-MCXN947 (MCXN947, Cortex-M33 CPU0 with +TrustZone-M) as the third Armv8-M port. Unlike the MIMXRT700 it executes from +2 MiB of internal ECC flash, programmed through the boot ROM's flash API, and +its entropy comes from the EdgeLock (ELS) block. CPU1 is Non-secure only and +out of scope. Changing the OTP life cycle or the CMPA can permanently lock +the part; this port rehearses life cycles and writes none of them. + +## Status + +- **Validated on silicon:** the wolfBoot first stage for this part + (wolfSSL/wolfBoot#948: signed boot with ECC256/SHA-256, update swap and + rollback over the write-once trailer, and the measured Secure-application + handoff to `0x30030000`), and the wolfTrust chain behind it on the + FRDM-MCXN947 (`tests/target/run_mcxn947_hardware.sh`, see + [Build and verify on the board](#build-and-verify-on-the-frdm-mcxn947)): + wolfBoot verifies the Secure image, the SPM consumes the handoff, creates + the wolfHSM vault in the ROM-programmed NVM, and launches both guests on + both crypto engines (`positive`); the SAU refuses a guest's store into its + peer's RAM (`sauneg`) and its read of the SPM's ELS (`periphneg`), and a + guest faulting at launch spends its restart budget and is quarantined + (`restart`), the peer running on throughout; the portable PSA client runs + its crypto, ITS, sealed PS, key-ops, FF-M negative and SHA-256 KAT stages + from both windows with the ELS behind `psa_generate_random`, and verifies + the attestation token it is issued against the IAK the vault provisioned at + first boot (`psa`). +- **Validated in cross-build:** the wolfTrust Secure image + (`make TARGET=mcxn947 secure-image`) on both crypto engines and both + manifests, with the port split, veneer, owner map, and layout checks green. +- **Validated in emulation:** the chain under M33MU's MCXN947 model + (`tests/target/run_mcxn947_m33mu.sh`, see Testing): wolfBoot verifies the + Secure image, both bare-metal guests launch and complete their PSA calls + through the veneers (`positive`, both engines), the isolation negative + (`sauneg`) shows a guest's store into the other guest's RAM refused by the + SAU, contained by the monitor, and the offender quarantined after its + restart budget while the peer keeps running, and every other scenario the + matrix lists for this port (`tests/target/lib/scenario_matrix.py`: the + whole MIMXRT700 set, every isolation level 3 scenario included) passes on + the model on both crypto engines. That includes `confboot`, Arm's + unmodified FF-M IPC suite (85 passed, 4 skipped, as on the MIMXRT700): its + val client, built with GCC 12, uses the post-indexed `LDRSH` (immediate) + form, which the carried M33MU patch decodes (the pinned emulator commit + took it as undefined). +- **Not validated:** any guest flash fence and any life cycle write. The + TRDC and the AHB secure controller are left as the first loader leaves + them. See [Open items](#open-items). + +Record emulator, cross-build, and physical-board evidence separately: M33MU's +MCXN947 model gives emulator evidence, the FRDM board gives silicon evidence, +and neither substitutes for the other. + +## What an MCXN947 port comprises + +### First-stage loader (wolfBoot) + +| Addition | Purpose | +| --- | --- | +| `hal/mcxn.{c,ld}` | LPUART4 debug console and the internal flash through the ROM flash API (`FLASH_Init`, `FLASH_Program`, `FLASH_Erase`, `FLASH_VerifyErase`); the flash path runs from RAM. | +| `config/examples/mcxn-tz-wolftrust.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; 8 KiB sectors, a 1024-byte image header, `NVM_FLASH_WRITEONCE`. | +| `tests/target/wolfboot-mcxn947-lifecycle.patch` (carried here) | reads the OTP life cycle the ROM reflects into `SYSCON.ELS_OTP_LC_STATE` and its duplicate, with `DAUTHSTATUS`, and hands wolfTrust the matching PSA life cycle; the HAL at the pin reports a fixed SECURED value. | + +The loader satisfies the [Porting](Porting.md) bootloader contract: it +authenticates the Secure image, provides `wt_boot_handoff_t` (SHA-256 +measurement, life cycle, image version) at `0x30030000`, reserves the +1024-byte image header, and provides an update partition compatible with the +firmware-update backend. + +### Secure runtime (wolfTrust) + +The port reuses `src/arch/armv8m/`, `src/arch/common/`, and the shared +Armv8-M isolation level 3 layer in `port/common/armv8m/` unchanged, and adds +`port/mcxn947/` and one build fragment: + +| File | Responsibility | +| --- | --- | +| `memory_map.h` | The bit-28 Secure-alias map: flash windows, Secure and guest RAM, the boot-handoff address, and the port-sized level 3 bands (`WT_L3_SPM_RAM_SIZE`, `WT_SP_HSM_DATA_SIZE`) from `port/common/armv8m/l3_layout.h`. | +| `l3_port.h` | The level 3 layer's one board input: the ELS as the Secure peripheral only the SPM drives. | +| `mcxn947_regs.h` | Register bases for SYSCON, the FMU, the ELS, LPUART4, the TRDC and the AHBSC, the boot ROM and its API tree, and the ELS command interface. | +| `platform_mcxn947.c` | Every `wt_platform_*` operation: the SAU table, the Secure MPU whitelist (eight regions, one of them the boot ROM for the flash API), the boot-handoff region, fault logging, panic, and reset. | +| `partitions.c` | The guest and capability tables, the profile capability bitmap (the fabric filter is claimed on the per-dispatch SAU window, as on the MIMXRT700), and the pinned guest-measurement slot. | +| `rom_flash.c/.h` | Internal flash program and erase through the boot ROM's API tree, called directly with a local definition of the driver interface layout (no vendor SDK): one 16-byte phrase is programmed at most once per erase, each program is issued from its 128-byte page base behind all-ones padding as the ROM requires, every erase is verified by the ROM, and the code-bus cache is cleared after each change. | +| `hsm_flash.c/.h`, `hsm_flash_ctx.c` | The `port_nvm.h` backend for the wolfHSM store and the FWU staging backend: reads through the Secure alias, program and erase through `rom_flash.c`. | +| `rng_entropy.c` | The `CUSTOM_RAND_GENERATE_BLOCK` entropy source over the ELS `RND_REQ` command, preserving the unprivileged-to-privileged trap. | +| `secure.ld` | The port's Secure linker script, which INCLUDEs the shared level 3 regions, band sections, and layout ASSERTs; the SG veneers sit in the image's second 1 KiB, as on the STM32H5. | +| `secure_owners.txt` | The port's objects in the post-link owner map, appended to `tools/secure_owners.txt` by the link. | +| `manifest.json`, `manifest-conformance.json`, `conformance/` | The service partitions and their resources; the conformance manifest and PAL bindings host Arm's test partitions. | +| `mk/target-mcxn947.mk` | `WT_CPU`, the flash and RAM defaults, the linker `--defsym` set, and the source lists. | + +## Reference flash and RAM layout + +Flash is `0x00000000` (Non-secure) / `0x10000000` (Secure); RAMA-H SRAM is +`0x20000000` / `0x30000000`; peripherals `0x40000000` / `0x50000000`. Bit 28 +selects the Secure alias. Flash is two 1 MiB banks (read-while-write across +banks) of 8 KiB sectors and 16-byte ECC phrases. + +| Image or region | Non-secure | Secure alias | +| --- | ---: | ---: | +| wolfBoot (bank 0) | `0x00000000` | `0x10000000` | +| wolfTrust Secure image, BOOT partition (`0x40000`) | `0x00040000` | `0x10040000` | +| Guest 0 (`0x80000`) | `0x00080000` | `0x10080000` | +| Guest 1 (`0x40000`, bank 1) | `0x00100000` | `0x10100000` | +| wolfBoot update partition (`0x40000`) | `0x00180000` | `0x10180000` | +| wolfBoot swap sector (`0x2000`) | `0x001C0000` | `0x101C0000` | +| wolfHSM NVM store (two mirrored sectors) | `0x001E0000` | `0x101E0000` | +| Conformance NVM store (one sector) | `0x001E4000` | `0x101E4000` | + +| RAM region | Address | +| --- | ---: | +| Guest 0 RAM (`0x18000`) | `0x20000000` | +| Guest 1 RAM (`0x18000`) | `0x20018000` | +| Boot-handoff record (4 KiB page, the record is 56 bytes) | `0x30030000` | +| Secure runtime RAM (`0x37000`: 86 KiB SPM, 42 KiB keystore bands, 102 KiB stacks and conformance data) | `0x30031000` | + +The guest windows alias wolfBoot's own RAM (`0x30000000` to `0x3001E000`), +dead after the jump; `wt_platform_init` zeroes both windows before any guest +runs. Every store below the Secure image is in bank 1, so the image keeps +executing from bank 0 while the ROM API programs the NVM and update +partitions: no RAM code band is needed, and the port has none. + +These constants come from `port/mcxn947/memory_map.h` and +`mk/target-mcxn947.mk`; the emulator runner assembles the flash image from +them so the build and flash offsets stay paired. + +## TrustZone perimeter + +The MCXN947 has no option-byte Secure watermark. The Secure boundary is set at +run time by what the port programs before any guest launches: + +- **IDAU/SAU:** the bit-28 alias makes each address inherently Secure or + Non-secure; the SAU table in `platform_mcxn947.c` opens the static + Non-secure windows (the guest flash, the LPUART4 console) and the + Non-secure-callable veneer window in the Secure image, and leaves + everything else Secure. The guest RAM windows are not static: each dispatch + programs a dynamic SAU region over the arriving guest's window only, so the + peer's window stays Secure while it runs, exactly as on the MIMXRT700. +- **Secure MPU:** a per-partition whitelist confines each Secure Partition to + its own RAM band. The SPM's eight regions are the Secure image, the Secure + alias of everything above it (guest images, update and swap partitions, the + stores: read-only, execute-never), Secure RAM from the handoff record up, + the guest RAM alias, the flash Non-secure alias, the peripheral aperture, + the private peripheral bus, and the boot ROM (privileged read-execute, for + the flash API). +- **TRDC and AHBSC:** not programmed. At reset the flash MBC leaves every + block Non-secure-accessible and the AHB secure controller rules are open, + as the first loader leaves them. The port claims the fabric filter on the + per-dispatch SAU window alone (`partitions.c`): it isolates CPU0's guests + from each other and says nothing about CPU1, DMA, or any other bus master. + No capability is claimed for fencing those. + +As on the other ports, the manifest declares the guests unprivileged but the +runtime launches them with `CONTROL_NS.nPRIV` clear; a guest's Non-secure MPU +is scheduling policy, not a boundary. The SAU window is the boundary, and +`sauneg` shows it under M33MU. + +## Silicon constraints for this port + +- **A phrase is written once.** The flash has ECC per 16-byte phrase; a + programmed phrase cannot be reprogrammed until its sector is erased. The + wolfHSM store runs with `WOLFHSM_CFG_FLASH_UNIT_SIZE` 16 (the wolfTrust + runner setting all ports share), so every journal unit is one phrase and + no unit shares a phrase with another written at a different time; + `rom_flash.c` refuses to program a non-blank phrase and `hsm_flash.c` + refuses any program that is not phrase-aligned. `tests/host/mcxn947_flash` + runs the real driver and the wolfHSM store over a model of this flash and + its ROM and proves it: 643 page-based ROM programs across three partition + compactions and a reopen, zero double programs with 16-byte units (even + through a backend with no phrase guard), 281 double programs with + wolfHSM's 8-byte default, which the port backend refuses outright. +- **The ROM programs from a page base.** On silicon (ROM `K` 3.2.0) the + flash driver's `FLASH_Program` returns `kStatus_FLASH_AlignmentError` (101) + for any start that is not 128-byte page aligned, whatever the length, and + leaves an all-ones phrase exactly as it was (programming only clears bits, + and the erased phrase is a valid ECC word). `rom_flash.c` therefore + programs each phrase from its page base behind all-ones padding, one page + per ROM call; the padding never counts as a program of the phrases it + covers, which the host model checks. The ROM also stores each phrase + through the flash's Non-secure alias for the FMU to capture, so the SPM's + MPU keeps that alias writable (a store outside a ROM program sequence does + not alter flash). +- **Flash is programmed through the ROM.** There is no SDK in the build; the + port calls the ROM API tree at `0x1303FC00` through a local definition of + its layout, checks the geometry the ROM reports (8 KiB sectors, 2 MiB) + before the first program, masks interrupts around each call, and clears + the LPCAC afterwards. Reads, verifies, and blank checks go through the + Secure alias, as wolfBoot's own flash path does on silicon; an erased + phrase reads as `0xFF` without a fault. +- **Entropy is the ELS.** `RND_REQ` (command 24) fills a buffer by DMA; the + port requests whole pages, serves only what a request uses, wipes the rest, + and fails closed on `ELS_ERR` or a locked ELS, counting every refusal in + `g_wt_els_errors` for the hardware runner. The ROM leaves the ELS clocked + (`AHBCLKCTRL2`/`PRESETCTRL2` bit 18); the port enables it and never resets + it. +- **The first loader's image header sets the Secure link address.** wolfBoot + on this part uses a 1024-byte image header, so wolfTrust links at the boot + base plus `0x400` (`WT_SECURE_IMAGE_HEADER_SIZE=0x400`) and is signed with + `IMAGE_HEADER_SIZE=1024` and 8 KiB sectors. +- **NSC in flash works.** The flash is in the Code region, where the IDAU + honours an SAU Non-secure-callable window, so the veneers stay in the + image, as on the STM32H5 (the MIMXRT700's RAM code band is not needed). + +## Required tools + +- `arm-none-eabi-gcc` with newlib headers, and `arm-none-eabi-{nm,objcopy,size}` +- Python 3, CMake (for the emulator build) +- wolfBoot's MCXN HAL builds against the MCUXpresso SDK (`MCUXPRESSO`, + `MCUXPRESSO_CMSIS`, or `~/NXP` as wolfBoot's config expects) +- for a board: the FRDM-MCXN947 with its MCU-Link (CMSIS-DAP and VCOM), + pyOCD with the MCXN947 pack (`pyocd pack install MCXN947`), and SPSDK's + `blhost` for the life cycle rehearsal (`tests/target/detect_mcxn947.sh` + reports what is missing) + +## Build and verify under M33MU + +```sh +make TARGET=mcxn947 secure-image +make test-target TARGET=mcxn947 +tests/target/run_mcxn947_m33mu.sh positive +tests/target/run_mcxn947_m33mu.sh sauneg +``` + +The runner builds its pinned emulator (upstream M33MU plus +`tests/target/m33mu-mcxn947.patch`, which adds the ELS `RND_REQ` register +model behind its `AHBCLKCTRL2` gate, programs flash from a page base by +clearing bits as the ROM does, makes the ROM executable so an API call leaves +no stale `IACCVIOL` in `CFSR`, and reflects the OEM_OPEN life cycle into +SYSCON) and its pinned +wolfBoot (`tests/target/lib/mcxn947_wolfboot.sh`: the wolfSSL/wolfBoot#948 +branch at its fork ref until it merges, built from +`config/examples/mcxn-tz-wolftrust.config` with the carried life cycle +patch), then: + +1. builds the wolfTrust Secure image and its CMSE import library with the + port's default `WT_SECURE_IMAGE_HEADER_SIZE=0x400`; +2. builds the bare-metal guest for both guest windows, linked against the + Secure image's CMSE import library; +3. patches both guest measurement records into the unsigned `wolftrust.bin` + (`tools/measure/patch_guest_digests.py`); +4. signs `wolftrust.bin` with the wolfBoot key tools (`IMAGE_HEADER_SIZE=1024`, + `WOLFBOOT_SECTOR_SIZE=0x2000`); +5. boots `m33mu --cpu mcxn947` with wolfBoot at flash offset 0, the signed + Secure image at `0x40000`, and the guests at `0x80000` and `0x100000`. + +`MCXN947_WOLFBOOT_DIR` reuses a wolfBoot tree the library built; `M33MU` a +prebuilt emulator carrying the patch. + +### Bring-up markers + +Each guest records progress in a mailbox at the base of its own Non-secure RAM +window (`0x20000000` for guest 0, `0x20018000` for guest 1) and echoes it on +LPUART4 (`0x400B4000`): + +| Mailbox word | Offset | Pass value | +| --- | ---: | ---: | +| signature | `+0x00` | `0x47543030` | +| step | `+0x04` | `0x00000005` | +| `psa_framework_version()` | `+0x08` | `0x00000100` | +| `psa_connect(SERVICE_HSM)` handle | `+0x10` | > 0, distinct per guest | +| status | `+0x14` | `0x600D600D` | +| LPUART4 `VERID` as the guest reads it | `+0x18` | non-zero | +| isolation probe latch (`sauneg`) | `+0x1C` | `1` faulted or `2` blocked; `3` leaked | +| isolation probe read-back (`sauneg`) | `+0x20` | not the stored sentinel | + +A `status` of `0x600D600D` in both mailboxes proves the wolfBoot to wolfTrust +to Non-secure-guest chain booted and that the Secure runtime serviced both +Non-secure PSA clients through the veneers. In `sauneg`, guest 0 disables its +own Non-secure MPU and stores a sentinel into guest 1's RAM; the runner +asserts from M33MU's protection-unit trace that the store faulted, refused by +the SAU with the peer window Secure at dispatch. + +## Build and verify on the FRDM-MCXN947 + +```sh +make test-hardware TARGET=mcxn947 # positive sauneg periphneg restart psa wrpoff +tests/target/run_mcxn947_hardware.sh positive +WT_ENGINE=hsm tests/target/run_mcxn947_hardware.sh positive +``` + +`tests/target/run_mcxn947_hardware.sh` runs on the host that owns the +board's MCU-Link (`MCXN947_PROBE_UID` picks one of several probes; the +console is the MCU-Link VCOM, `MCXN947_CONSOLE`). For each scenario it +takes the board lock (`logs/mcxn947-hw.lock`, shared with the suite), then +builds the Secure image and the guests, pins the guest measurements, signs +the image with the pinned wolfBoot's key, flashes wolfBoot, the signed image +and both guests at their physical offsets with pyOCD, erases the update, +swap and NVM sectors so every run starts from a blank vault, clears the +mailboxes and launch masks in SRAM, boots, reads every image back and +compares it, and asserts the result over SWD: + +- the bare-metal mailboxes (signature, framework version, `0x600D600D`, + the LPUART4 `VERID` the guest read) and, for the negatives, the monitor's + `g_wt_restart_events` and `g_wt_quarantine_events` counters, the launch + masks, the peer's advancing idle beat, and that the sentinel never landed; +- the Secure side of every run: `g_wt_rom_flash_last_status` 0 (the ROM + flash API answered from the SVC gate), the wolfHSM backend's last error, + the vault's first state phrase programmed, an erased phrase reading as + `0xFF`, the handoff record consumed, IPSR outside the fault handlers, and + wolfBoot's `Verifying signature...done` on the console; +- for `psa`, each PSA guest's mailbox stage bits (TEE, crypto, ITS, PS, + key-ops, SHA-256 KAT, COSE attestation, done); +- for `wrpoff`, a Secure image built with `WT_GUEST_FLASH_WRP=1` refusing + both guests (launch refused mask `0x3`, both mailboxes untouched), since + the port reports no guest flash fence. + +Each run's console capture, image readbacks and an SWD dump of every word +the checks read are kept under `logs/mcxn947-hw/-/` +(`console.log`, `readback-*.bin`, `swd.txt`), the suite's per-scenario logs +under `logs/mcxn947-suite/`: outside `build/`, which every run cleans before +it builds, and never overwritten by another scenario or engine. + +Three board facts the runner encodes: the device pack resets the chip on +every pyOCD connect, so the park and the flash, erase and SRAM writes that +depend on it share one session (a second session finds the chain running and +pyOCD's flash routine then races wolfTrust's tick); the pack's `reset halt` +leaves a DWT comparator and the debug fault status armed, so the core is +released only after both are cleared; and the MCU-Link forwards early-boot +UART only while the debug link stays active, so the console capture runs +alongside spaced reads. The two guests share LPUART4, so their console lines interleave on +silicon; the assertions are the mailboxes and counters, the console is +evidence. + +## Provisioning and life cycle + +This section is the MCXN947 part of [Provisioning](Provisioning.md). The +life cycle lives in OTP; the ROM reflects it into `SYSCON.ELS_OTP_LC_STATE` +(`0x50000980`) and its duplicate (`0x50000984`), eight bits each, which the +carried wolfBoot patch reads and maps to the PSA life cycle it hands +wolfTrust (`tests/host/mcxn947_lifecycle` tests that mapping): + +| `LC_STATE` | NXP state | PSA life cycle wolfBoot hands wolfTrust | +| --- | --- | --- | +| `0x03` | OEM Open (as the FRDM board ships) | `0x1000` ASSEMBLY_AND_TEST | +| `0x07` | OEM Secure World Closed | `0x2000` PSA_ROT_PROVISIONING | +| `0x0F` | OEM Closed | `0x3000` SECURED with every `DAUTHSTATUS` field disabled; `0x5000`/`0x4000` while debug is open; `0x0000` for a malformed `DAUTHSTATUS` | +| `0xCF` | OEM Locked | as OEM Closed | +| `0x1F` | OEM Field Return | `0x6000` DECOMMISSIONED | +| other, or the copies disagree | NXP Blank, Fab, Dev, Provisioned, Field Return, Shredded, or corrupt | `0x0000` UNKNOWN | + +All commands use the single entry point with `TARGET=mcxn947`: + +```sh +export TARGET=mcxn947 +tests/target/provisioning/provisioning_ctrl.sh status +tests/target/provisioning/provisioning_ctrl.sh discover +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh advance 0x07 +WT_LOCK_CONFIRM=1 tests/target/provisioning/provisioning_ctrl.sh regress +``` + +What differs from the MIMXRT700: + +- **Every device command is bound to one probe.** The host that owns the + board may carry several CMSIS-DAP probes; `MCXN947_PROBE_UID` names the + board's MCU-Link (the one `pyocd list` shows as `MCU-LINK FRDM-MCXN947`), + and with more than one probe and no id every command refuses instead of + reading nothing. `status` prints the probe, `discover` stamps it, the + rehearsal record carries it, and `lock` accepts only a rehearsal made + through the same probe. +- **`restore` is the hardware runner.** It runs + `tests/target/run_mcxn947_hardware.sh positive` on that probe, so the + images a rehearsal binds are the ones the runner built, flashed and read + back. +- **The mock lock is in the handoff record.** The MCXN947 has no life cycle + shadow register (`ELS_OTP_LC_STATE` is read-only), so `advance` halts the + core at wolfTrust's reset vector, after wolfBoot wrote the handoff record + and before wolfTrust consumed it, and replaces the record's PSA life cycle + with the target state's production value (`0x2000`, `0x3000`, or + `0x6000`). Fuses and SYSCON are untouched; `regress` is a hardware reset. + The rehearsal records what wolfTrust saw, the guests launched verified, + the images read back, the device UUID (`0x01100000`), and the probe. +- **`lock` previews the vendor write and refuses it.** The life cycle is an + OTP fuse (SPSDK's fuse map for the part: `LIFE_CYCLE`, index 1, + `LIFECYCLE[7:0]` and its duplicate at bit 24, the same codes the ROM + reflects into SYSCON). `lock oem-secure-world` reads the live CMPA + (`0x01004000`) and the three CFPA pages (`0x01000000` scratch, ping and + pong) over SWD, parses them with `pfr parse`, generates the exact fuse + script with `nxpfuses fuses-script` (`efuse-program-once 1 0x7000007`), + re-exports the CMPA from its parsed configuration with `pfr export` and + diffs it against the live page (the life cycle is not in the page, so the + page is kept), prints all of it as the `will run:` preview, and then + refuses the write on `MCXN947_LOCK_WRITE_ENABLED=0`, a port constant + hard-wired off in this phase. That refusal holds with `WT_LOCK_CONFIRM=1`, + `WT_PRODUCTION_LOCK=1` and the typed acceptance all supplied, which + `make test-provisioning` proves against stubbed vendor tools; + `verify-pages` re-reads the four pages and compares them with the preview's + digests. The closed states (`oem-closed`, `oem-locked`, + `oem-field-return`) are refused earlier: they need the BootROM to + authenticate wolfBoot under a fused root key table hash, which this port + does not build. `burn` and `provision-da` are refused outright. +- **What the vendor flow validated on the board, and what it did not.** + Read-only or reversible SPSDK 3.11 steps ran first and were extracted into + the port: the life cycle and `DAUTHSTATUS` over SWD, the CMPA and CFPA + pages over SWD (the IFR is memory mapped; `pfr read` itself is an ISP + path and was not used), `pfr parse`, `pfr get-template`, `pfr export` of a + closed-state page and its diff, `nxpfuses get-template` and + `nxpfuses fuses-script` for the next and the closed state, + `nxpdebugmbox tool list-probes`, `mem-tool test-connection` and + `tool get-uuid`. `nxpdebugmbox cmd get-dac` is not offered for this part + by SPSDK 3.11. Nothing that writes ran: no `pfr write`, no `blhost`, no + `nxpdebugmbox ispmode`, `program-lifecycle` or `erase`, no fuse, CMPA, + CFPA, IFR or debug-authentication change. + +`make test-provisioning` runs the MCXN947 gates against stub `pyocd`, +`pfr`, `nxpfuses`, `nxpdebugmbox` and `blhost` with the other ports' gates; +`tests/host/mcxn947_lifecycle` checks the life cycle codes against SPSDK's +fuse map and that every ladder step only sets fuse bits. + +## Open items + +- **Life cycle in the handoff.** wolfBoot's HAL at the pin reports no life + cycle (`hal_attestation_get_lifecycle` is the weak default), so the + carried patch supplies it; the hardware runner checks the handed value + against `ELS_OTP_LC_STATE` on every run because an `UNKNOWN` life cycle + silently fails attestation closed (the vault may not provision an IAK + under it). +- **Guest flash fence.** No hardware write protection of the guest windows is + claimed: `wt_platform_guest_flash_wrp_ok` reports every window unprotected, + so a `WT_GUEST_FLASH_WRP=1` image refuses every guest (the `wrpoff` + scenario proves it). The part offers no reset-locked equivalent of the + MIMXRT700's XSPI fence: the FMU has no region or lock registers and takes + program and erase as commands; the TRDC MBC's access policies lock until + reset, but which policy a flash block uses has no lock, so Secure code can + move a block back to a writable one; and the ROM's flash firewall + (`kStatus_FLASH_ModifyProtectedAreaDisallowed`) follows the OTP + `ACTIVE_IMG_PROT` setting, which is permanent and belongs to the lock flow. + A peer guest reaches the flash only through the FMU or the Secure ROM, so + the open question is whether to claim that attribution as the fence. +- **Other bus masters.** CPU1, DMA, and the ELS DMA are not fenced from + Secure RAM; programming the TRDC and the AHB secure controller for them is + silicon work that M33MU does not model. +- **wolfBoot's own flash writes.** The MCXN HAL at the pin programs 4-byte + words at any word-aligned address, which this ROM refuses off a page base + (`kStatus_FLASH_AlignmentError`, checked on the board with a direct ROM + call); wolfTrust's own stores never go through it, the hardware runner + never asks wolfBoot to write, and an update swap on silicon is not claimed + until the HAL programs from page bases. +- **Life cycle write.** A validated life cycle advance on the part does not + exist; the hardware runner rehearses only. +- **Upstream pins.** The wolfBoot pin moves to wolfSSL/wolfBoot once + wolfSSL/wolfBoot#948 merges; the emulator patch goes once M33MU models the + ELS command interface, the ROM's page-based bit-clearing program, an + executable ROM, and seeds the life cycle itself. + +See [Porting](Porting.md) for the generic port contract, [Testing](Testing.md) +for scenario selection, and [Security Model](Security-Model.md) for the policy +enforced after boot. diff --git a/docs/Porting.md b/docs/Porting.md index 0374af003..67f00f3a2 100644 --- a/docs/Porting.md +++ b/docs/Porting.md @@ -4,8 +4,10 @@ wolfTrust separates reusable policy and services from architecture, device, and board-specific execution. The fully silicon-validated build tuple is `armv8m-stm32h563`. A second Armv8-M tuple, `armv8m-mimxrt700` (external octal-NOR execute-in-place), is in hardware bring-up and reuses the -architecture adapter unchanged; see the [STM32H5 Guide](STM32H5-Guide.md) and -[MIMXRT700 Guide](MIMXRT700-Guide.md) for the two worked examples. Support for +architecture adapter unchanged, as does a third, `armv8m-mcxn947` (internal +ECC flash through the boot ROM's flash API, emulator-validated); see the +[STM32H5 Guide](STM32H5-Guide.md), [MIMXRT700 Guide](MIMXRT700-Guide.md), and +[MCXN947 Guide](MCXN947-Guide.md) for the worked examples. Support for additional Cortex-M ports is an intended extension point. Such ports may reuse common policy and service code and an existing architecture adapter when their execution and protection models match. @@ -118,7 +120,9 @@ A port supplies: the conformance sections, .data and .bss); - attribution read-back for every Secure peripheral the SPM uses, panicking at boot on a mismatch; and -- owner lines in `tools/secure_owners.txt` for every object it adds. +- owner lines for every object it adds, in `tools/secure_owners.txt` or in + the port's own `port//secure_owners.txt`, which the secure link + appends to the shared map. The build reads `WT_RAM_S_BASE` and the band placement for the linker and derives the post-link band check from `memory_map.h`, so no band address is @@ -237,7 +241,8 @@ worked examples above give a concrete map for each board. Makefile selects them from `ARCH` and `TARGET`, and `mk/common.mk` needs no change. 4. Supply startup/vector and linker handling appropriate to the target, and - give every object the port links an owner in `tools/secure_owners.txt`. + give every object the port links an owner in `tools/secure_owners.txt` + or in `port//secure_owners.txt`. 5. Generate the manifest at build time and include its digest in the signed Secure image. 6. Integrate application domains with the architecture's client boundary and diff --git a/docs/Provisioning.md b/docs/Provisioning.md index 02219bd92..6ea931da8 100644 --- a/docs/Provisioning.md +++ b/docs/Provisioning.md @@ -11,13 +11,15 @@ One script does this on every port, with the same commands: 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: +`TARGET` is `stm32h563` (the default), `mimxrt700`, or `mcxn947`. `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) +- [MCXN947 Guide: Provisioning and life cycle](MCXN947-Guide.md#provisioning-and-life-cycle) + (rehearsal and lock preview only: this port writes no 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 @@ -177,21 +179,21 @@ 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 | +| PSA life cycle | STM32H5 product state | MIMXRT700 life cycle | MCXN947 life cycle | +| --- | --- | --- | --- | +| `0x1000` ASSEMBLY_AND_TEST | open `0xED` | develop `0x03` | oem-open `0x03` | +| `0x2000` PSA_ROT_PROVISIONING | provisioning `0x17` | develop2 `0x07` | oem-secure-world `0x07` | +| `0x4000` NON_PSA_ROT_DEBUG | tz-closed `0xC6` | in-field, only Non-secure debug open | oem-closed, only Non-secure debug open | +| `0x5000` RECOVERABLE_PSA_ROT_DEBUG | closed, Secure debug open | in-field, Secure debug open | oem-closed, Secure debug open | +| `0x3000` SECURED | closed `0x72`, locked `0x5C` | in-field `0x0F`, in-field-locked `0xCF` | oem-closed `0x0F`, oem-locked `0xCF` | +| `0x6000` DECOMMISSIONED | none | in-field-return `0x1F` | oem-field-return `0x1F` | +| `0x0000` UNKNOWN | any other value | copies disagree, or an NXP-internal state | copies disagree, or an NXP-internal state | ## Environment | Variable | Meaning | | --- | --- | -| `TARGET` | the port: `stm32h563` (default) or `mimxrt700` | +| `TARGET` | the port: `stm32h563` (default), `mimxrt700`, or `mcxn947` | | `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 | @@ -200,6 +202,7 @@ with it can never be updated; a leaked one means they trust whoever holds it. | `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 | +| `MCXN947_PROBE_UID`, `MCXN947_SPSDK_VENV`, `MCXN947_GUEST_MASK`, `MCXN947_TARGET`, `MCXN947_WOLFBOOT_DIR`, `MCXN947_ISP` | MCXN947: the board's MCU-Link unique id (required when the host has more than one probe), the SPSDK environment, guests a rehearsal must launch, the pyOCD target name, the wolfBoot tree the runner flashed, and the blhost ISP connection the lock preview names | ## Adding a port @@ -222,6 +225,7 @@ 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. +`make test` runs `make test-provisioning`: every gate of the three 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/Testing.md b/docs/Testing.md index 258bb17e9..ef3b918bc 100644 --- a/docs/Testing.md +++ b/docs/Testing.md @@ -392,6 +392,64 @@ These are emulator results. They prove the SAU attribution and the monitor's containment on a faithful core model; the silicon `ahbscneg` run on the EVK is recorded separately. +## MCXN947 emulator scenarios + +M33MU also models the MCXN947 (`--cpu mcxn947`: internal flash, the boot ROM +flash API tree, and, with `tests/target/m33mu-mcxn947.patch`, the ELS +`RND_REQ` register model behind its real clock gate, the ROM's page-based +bit-clearing flash program, an executable ROM, the OEM_OPEN life cycle in +SYSCON, and the post-indexed `LDRSH` (immediate) decode the conformance +client needs), so the MCXN947 port has the same emulator gate as the MIMXRT700, +with the same scenario names: + +```sh +make test-target TARGET=mcxn947 +tests/target/run_mcxn947_m33mu.sh positive +tests/target/run_mcxn947_m33mu.sh sauneg +``` + +`sauneg` is the MIMXRT700's `ahbscneg` on this part: guest 0 stores into +guest 1's RAM window, which the per-dispatch SAU window keeps Secure, the +SAU refuses the store, and the monitor contains it. Every other scenario is +the MIMXRT700 one run on the MCXN947 model, with the port's own band, stack, +and SPM peripheral addresses read from `port/mcxn947/memory_map.h`; the +scenarios the port runs, and the level 3 scenarios it still exempts, are in +`tests/target/lib/scenario_matrix.py`. The runner builds its own pinned +emulator and wolfBoot first stage (`tests/target/lib/mcxn947_wolfboot.sh`: +the wolfSSL/wolfBoot#948 branch at its fork ref until it merges, from +`config/examples/mcxn-tz-wolftrust.config` plus +`tests/target/wolfboot-mcxn947-lifecycle.patch`, the life cycle read the +attestation service needs); building it needs the MCUXpresso SDK as any MCXN +wolfBoot build does. `MCXN947_WOLFBOOT_DIR` reuses a tree the library built. +The flash image offsets are physical: wolfBoot at 0, the signed Secure image +at `0x40000`, the guests at `0x80000` and `0x100000`. + +These are emulator results; the board's are in the next section, recorded +separately. + +## MCXN947 hardware + +`make test-hardware TARGET=mcxn947` runs `tests/target/run_mcxn947_suite.sh` +on the host that owns the FRDM-MCXN947's MCU-Link: `positive`, `sauneg`, +`periphneg`, `restart`, `psa`, and `wrpoff` by default (`WT_MCXN947_SCENARIOS` +overrides the list; `WT_ENGINE` selects the crypto engine), each built, +flashed at its physical offset, erased to a fresh vault, read back, and +asserted over the debug port (`tests/target/run_mcxn947_hardware.sh`; the +MCXN947 Guide lists what each scenario asserts). The two guests share one +LPUART, so their console lines interleave on silicon; the console is captured +as evidence and the assertions are the mailboxes and the monitor's counters. +The suite's logs (`logs/mcxn947-suite/`) and the runner's retained evidence +(`logs/mcxn947-hw/-/`: console, readbacks, SWD dump) +live outside `build/`, which every run cleans before it builds, and one batch +at a time owns the board through `logs/mcxn947-hw.lock` (`flock`, shared by +the runner); `tests/target/test_mcxn947_suite_evidence.sh` checks both under +`make test`. +Without the probe, console, pyOCD pack, or SPSDK, `tests/target/detect_mcxn947.sh` +reports why and the suite skips. The device life cycle flow is driven +separately through `TARGET=mcxn947 tests/target/provisioning/provisioning_ctrl.sh` +and writes no fuse; see the MCXN947 Guide. Hardware test commands write flash +and reset the board. + ## MIMXRT700 hardware `make test-hardware TARGET=mimxrt700` runs `tests/target/run_rt700_suite.sh` @@ -502,7 +560,10 @@ The workflows under `.github/workflows/` separately run: home-directory references in the published docs); - fuzz targets; - selected and nightly M33MU scenarios; and -- the MIMXRT700 chain under M33MU's RT700 model (the `RT700` scenario matrix). +- the MIMXRT700 chain under M33MU's RT700 model (the `RT700` scenario matrix); + and +- the MCXN947 chain under M33MU's MCXN947 model (the `MCXN947` scenario + matrix). ### Trigger routing @@ -516,11 +577,13 @@ that picks each port's tier and one matrix job that runs exactly those scenario groups) is tiered. Every pull request runs each port's smoke tier on both crypto engines (STM32H563: `positive`, `gtzcneg`, `crossdomain`, `bothpsa`, `confboot`, `devcrypto`; MIMXRT700: -`positive`, `ahbscneg`, `crossdomain`, `bothpsa`, `confboot`, `devcrypto`). +`positive`, `ahbscneg`, `crossdomain`, `bothpsa`, `confboot`, `devcrypto`; +MCXN947: `positive`, `sauneg`, `crossdomain`, `bothpsa`, `confboot`, +`devcrypto`). The full matrix runs on a push to `main`, on the nightly schedule, on manual dispatch (with a `port` input), and on a pull request that carries the -`ci:h5`, `ci:rt700`, or `ci:all` label. The scenario groups per port and tier +`ci:h5`, `ci:rt700`, `ci:mcxn947`, or `ci:all` label. The scenario groups per port and tier are in `tests/target/lib/scenario_matrix.py`; `make test-target TARGET=` runs the same smoke tier locally and `WT_TIER=full` every scenario. diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 5a180ccd7..fa3132005 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -18,3 +18,4 @@ - [[Project Structure]] - [[STM32H5 Guide]] - [[MIMXRT700 Guide]] +- [[MCXN947 Guide]] diff --git a/mk/arch-armv8m.mk b/mk/arch-armv8m.mk index 31345681e..bf8c62b95 100644 --- a/mk/arch-armv8m.mk +++ b/mk/arch-armv8m.mk @@ -101,10 +101,19 @@ ARCH_LINK_OUTPUTS := $(SECURE_CMSE_IMPLIB) ARCH_LDFLAGS = -Wl,--cmse-implib -Wl,--out-implib=$(SECURE_CMSE_IMPLIB) \ $(WT_L3_LINK_ARGS) +# The owner map the post-link layout check reads: the shared map plus the +# port's own objects (port//secure_owners.txt), so a port adds its +# owners without editing the shared file. +WT_SECURE_OWNERS := $(BUILD_DIR)/secure_owners-$(TARGET).txt +$(WT_SECURE_OWNERS): $(ROOT)/tools/secure_owners.txt \ + $(wildcard $(PORT_DIR)/secure_owners.txt) | $(BUILD_DIR) + cat $^ > $@ + # A changed post-link checker must relink so the image is checked again. $(BUILD_DIR)/wolftrust.elf $(ARCH_LINK_OUTPUTS): \ $(ROOT)/tools/check_no_fp_insn.py $(ROOT)/tools/check_stack_seal.py \ - $(ROOT)/tools/l3_layout_args.py $(wildcard $(PORT_COMMON_DIR)/*.ld) + $(ROOT)/tools/l3_layout_args.py $(wildcard $(PORT_COMMON_DIR)/*.ld) \ + $(WT_SECURE_OWNERS) # Whitelist of non-secure-callable veneers the linked secure image may # export: exactly the five mediated FF-M gateway entries, pinned by full @@ -129,7 +138,7 @@ define arch_image_checks fi @python3 $(ROOT)/tools/check_secure_layout.py \ --nm $(TOOLPREFIX)nm $(WT_SECURE_LAYOUT_ARGS) $(SECURE_ELF) \ - --map $(SECURE_MAP) --owners $(ROOT)/tools/secure_owners.txt \ + --map $(SECURE_MAP) --owners $(WT_SECURE_OWNERS) \ $(if $(filter 1,$(WT_CONFORMANCE)),,--production) \ --objects-nm $(TOOLPREFIX)gcc-nm --objects $(ALL_SECURE_OBJS) @if grep -E ' (malloc|free|calloc|realloc|_sbrk|_malloc_r|_free_r)$$' \ diff --git a/mk/target-mcxn947.mk b/mk/target-mcxn947.mk new file mode 100644 index 000000000..03053990e --- /dev/null +++ b/mk/target-mcxn947.mk @@ -0,0 +1,68 @@ +# NXP MCXN947 (FRDM-MCXN947, Cortex-M33 CPU0) target inputs for the secure +# image build. Included first, before mk/arch-.mk and mk/common.mk; +# repository paths come from the Makefile. +WT_CPU ?= cortex-m33 +PORT_DIR := $(ROOT)/port/mcxn947 +PORT_HEADERS := $(wildcard $(PORT_DIR)/*.h) +TARGET_CONF_DIR := $(PORT_DIR)/conformance + +# WT_CONFORMANCE=1 swaps in the manifest that also hosts Arm's test partitions. +ifeq ($(CONFIG_VNET),y) +$(error TARGET=mcxn947 has no SERVICE_VNET manifest; build with CONFIG_VNET=n) +endif +ifeq ($(WT_CONFORMANCE),1) +MANIFEST_INPUT := $(PORT_DIR)/manifest-conformance.json +else +MANIFEST_INPUT := $(PORT_DIR)/manifest.json +endif + +# Guests share LPUART4 (LP_FLEXCOMM4), the FRDM board's MCU-Link VCOM console. +WT_SHARED_UART ?= 0 +# Clocks left as the first loader configured them (PLL150M core, FRO12M on +# FLEXCOMM4); verify against the RM before a guest derives a baud divider. +WT_GUEST_CORE_CLOCK_HZ ?= 150000000 +WT_GUEST_UART_CLOCK_HZ ?= 12000000 +# The conformance PAL interrupt source: LP_FLEXCOMM1's NVIC line, raised by +# software so no peripheral needs bringing up for it. +WT_CONF_IRQ ?= 36 +WT_CONF_IRQ_HANDLER ?= LP_FLEXCOMM1_IRQHandler + +# Internal flash through its Secure alias. The default is the wolfBoot +# mcxn-tz-wolftrust layout (wolfBoot at flash+0, BOOT at 0x40000). +WT_SECURE_FLASH_BASE ?= 0x10040000 +WT_SECURE_FLASH_SIZE ?= 0x00040000 +# wolfBoot's MCXN image header (IMAGE_HEADER_SIZE=1024) precedes the vectors. +WT_SECURE_IMAGE_HEADER_SIZE ?= 0x400 +WT_GUEST0_FLASH_BASE ?= 0x00080000 +WT_GUEST1_FLASH_BASE ?= 0x00100000 +WT_GUEST0_FLASH_SIZE ?= 0x00080000 +WT_GUEST1_FLASH_SIZE ?= 0x00040000 + +TARGET_CFLAGS := \ + -DWT_SHARED_UART=$(WT_SHARED_UART) \ + -DWT_GUEST_CORE_CLOCK_HZ=$(WT_GUEST_CORE_CLOCK_HZ) \ + -DWT_GUEST_UART_CLOCK_HZ=$(WT_GUEST_UART_CLOCK_HZ) \ + -DWT_CONF_IRQ=$(WT_CONF_IRQ)u \ + -DWT_CONF_IRQ_HANDLER=$(WT_CONF_IRQ_HANDLER) \ + -DWT_SECURE_FLASH_BASE=$(WT_SECURE_FLASH_BASE) \ + -DWT_SECURE_FLASH_SIZE=$(WT_SECURE_FLASH_SIZE) \ + -DWT_SECURE_IMAGE_HEADER_SIZE=$(WT_SECURE_IMAGE_HEADER_SIZE) \ + -DWT_GUEST0_FLASH_BASE=$(WT_GUEST0_FLASH_BASE) \ + -DWT_GUEST1_FLASH_BASE=$(WT_GUEST1_FLASH_BASE) \ + -DWT_GUEST0_FLASH_SIZE=$(WT_GUEST0_FLASH_SIZE) \ + -DWT_GUEST1_FLASH_SIZE=$(WT_GUEST1_FLASH_SIZE) +TARGET_LDFLAGS := \ + -Wl,--defsym=WT_SECURE_FLASH_ORIGIN=$(WT_SECURE_FLASH_BASE) \ + -Wl,--defsym=WT_SECURE_FLASH_SIZE=$(WT_SECURE_FLASH_SIZE) \ + -Wl,--defsym=WT_SECURE_IMAGE_HEADER_SIZE=$(WT_SECURE_IMAGE_HEADER_SIZE) +SECURE_LD := $(PORT_DIR)/secure.ld +# This port links no wolfHAL, so the post-link check expects no wolfHAL state. +WT_SECURE_LAYOUT_EXTRA_ARGS := --no-wolfhal + +TARGET_PLATFORM_SRC := $(PORT_DIR)/platform_mcxn947.c +TARGET_PARTITIONS_SRC := $(PORT_DIR)/partitions.c +TARGET_EXTRA_SRCS := \ + $(wildcard $(PORT_DIR)/rng_entropy.c) \ + $(wildcard $(PORT_DIR)/rom_flash.c) \ + $(wildcard $(PORT_DIR)/hsm_flash.c) \ + $(wildcard $(PORT_DIR)/hsm_flash_ctx.c) diff --git a/port/mcxn947/conformance/conf_nvm.h b/port/mcxn947/conformance/conf_nvm.h new file mode 100644 index 000000000..c126d4770 --- /dev/null +++ b/port/mcxn947/conformance/conf_nvm.h @@ -0,0 +1,39 @@ +/* conf_nvm.h + * + * 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 . + */ + +#ifndef WOLFTRUST_MCXN947_CONF_NVM_H +#define WOLFTRUST_MCXN947_CONF_NVM_H + +#include + +/* Survive-reset NVM seam for the conformance DRIVER partition (P5 K2). The + * unprivileged PAL calls this to persist its shadow: store==0 loads the flash + * sector into buf, store!=0 writes buf back. Returns 0 on success. The target + * build issues an SVC to the privileged flash driver; a host test links its + * own flash-simulator implementation of this symbol instead. */ +int wt_conf_nvm_sync(uint8_t *buf, uint32_t len, int store); + +/* PAL interrupt source control (P4.2c): on!=0 drives the platform UART so its + * NVIC line fires into the DRIVER partition's manifest interrupt signal; on==0 + * quiesces the source. The unprivileged PAL traps to the privileged device + * poke via SVC; the interrupt itself is delivered through the real vector. */ +int wt_conf_irq_set(int on); + +#endif /* WOLFTRUST_MCXN947_CONF_NVM_H */ diff --git a/port/mcxn947/conformance/conf_nvm_sync.c b/port/mcxn947/conformance/conf_nvm_sync.c new file mode 100644 index 000000000..22b438198 --- /dev/null +++ b/port/mcxn947/conformance/conf_nvm_sync.c @@ -0,0 +1,84 @@ +/* conf_nvm_sync.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 . + */ + +/* Target side of the survive-reset NVM seam (P5 K2): trap the DRIVER + * partition's shadow buffer through the same SVC #1 the psa_* veneers use, so + * the privileged SPM can drive the flash controller. The host test provides a + * flash-simulator implementation of wt_conf_nvm_sync instead of this file. */ + +#include "conf_nvm.h" + +#include "wolftrust/ffm.h" +#include "wolftrust/spm_transport.h" +#include "wolftrust/spm_gate.h" + +#include "memory_map.h" +#include "pal_config.h" + +#include + +/* The scheduler carves these holes out of other partitions' domains using the + * memory_map.h constants; the Arm tests probe them via the pal_config.h ones. + * A drift between the two silently breaks the L3 MMIO-isolation panic tests. */ +#if SERVER_PARTITION_MMIO_0_START != WT_CONF_SERVER_MMIO_BASE || \ + SERVER_PARTITION_MMIO_0_END != (WT_CONF_SERVER_MMIO_BASE + \ + WT_CONF_SERVER_MMIO_SIZE) +#error "pal_config SERVER MMIO does not match memory_map WT_CONF_SERVER_MMIO" +#endif +#if DRIVER_PARTITION_MMIO_0_START != WT_CONF_DRV_MMIO_BASE || \ + DRIVER_PARTITION_MMIO_0_END != (WT_CONF_DRV_MMIO_BASE + \ + WT_CONF_DRV_MMIO_SIZE) +#error "pal_config DRIVER MMIO does not match memory_map WT_CONF_DRV_MMIO" +#endif +/* i052/i053 need a valid NSPE base so only the range end is invalid. */ +#if NSPE_MMIO_0_START < WT_GUEST0_RAM_BASE || \ + NSPE_MMIO_0_END >= (WT_GUEST0_RAM_BASE + WT_GUEST_RAM_SIZE) +#error "pal_config NSPE MMIO must lie inside guest0's RAM window" +#endif + +int wt_conf_nvm_sync(uint8_t *buf, uint32_t len, int store) +{ + wt_spm_call_t call; + + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_CONF_NVM_SYNC; + call.buffer = buf; + call.num_bytes = (size_t)len; + call.call_type = store; + + if (wt_spm_sp_call(&call) != WT_FFM_SUCCESS) { + return -1; + } + return call.ret_int; +} + +int wt_conf_irq_set(int on) +{ + wt_spm_call_t call; + + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_CONF_IRQ_SET; + call.call_type = on; + + if (wt_spm_sp_call(&call) != WT_FFM_SUCCESS) { + return -1; + } + return call.ret_int; +} diff --git a/port/mcxn947/conformance/pal_attestation_config.h b/port/mcxn947/conformance/pal_attestation_config.h new file mode 100644 index 000000000..7c147f751 --- /dev/null +++ b/port/mcxn947/conformance/pal_attestation_config.h @@ -0,0 +1,58 @@ +/* pal_attestation_config.h + * + * 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 . + */ + +/* Attestation PAL configuration for the unmodified Arm psa-arch-tests + * dev_apis/initial_attestation suite. PLATFORM_OVERRIDE_ATTEST_PK stays + * undefined: the wolfTrust IAK is generated inside the wolfHSM vault per + * device, so the verify key must be fetched at runtime through + * tfm_initial_attest_get_public_key (conformance_pal.c) rather than + * baked in as upstream's well-known TF-M test key. */ + +#ifndef _PAL_ATTESTATION_CONFIG_H_ +#define _PAL_ATTESTATION_CONFIG_H_ + +#include +#include + +#define CRYPTO_VERSION_BETA3 + +#define COSE_ALGORITHM_ES256 -7 +#define COSE_ALG_SHA256_PROPRIETARY -72000 + +#define USEFUL_BUF_MAKE_STACK_UB UsefulBuf_MAKE_STACK_UB + +#define COSE_SIG_CONTEXT_STRING_SIGNATURE1 "Signature1" + +#define T_COSE_SIGN1_MAX_PROT_HEADER (1 + 1 + 5 + 9) + +#define T_COSE_SIZE_OF_TBS \ + (1 + sizeof(COSE_SIG_CONTEXT_STRING_SIGNATURE1) + 2 + \ + T_COSE_SIGN1_MAX_PROT_HEADER + 3) + +#define NULL_USEFUL_BUF_C NULLUsefulBufC + +#define ECC_CURVE_SECP256R1_PULBIC_KEY_LENGTH (1 + 2 * 32) + +int32_t tfm_initial_attest_get_public_key(uint8_t *public_key_buff, + size_t public_key_buf_size, + size_t *public_key_len, + psa_ecc_family_t *elliptic_family_type); + +#endif /* _PAL_ATTESTATION_CONFIG_H_ */ diff --git a/port/mcxn947/conformance/pal_config.h b/port/mcxn947/conformance/pal_config.h new file mode 100644 index 000000000..b1ee13515 --- /dev/null +++ b/port/mcxn947/conformance/pal_config.h @@ -0,0 +1,115 @@ +/* pal_config.h + * + * 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 . + */ + +/* Arm psa-arch-tests PAL target configuration for wolfTrust on MCXN947. + * P3a supplies the compile surface the partition sources include; the driver + * plane (P3b) binds the UART/WD/NVMEM values to real secure devices and the + * isolation MMIO windows (P4) to enforced regions. */ + +#ifndef _PAL_CONFIG_H_ +#define _PAL_CONFIG_H_ + +#define PLATFORM_PSA_ISOLATION_LEVEL 3 + +#define UART_NUM 1 +#define UART_0_BASE 0x500B4000 +#define UART_0_SIZE 0x3FF +#define UART_0_INTR_ID 0xFF +#define UART_0_PERMISSION TYPE_READ_WRITE + +#define WATCHDOG_NUM 1 +#define WATCHDOG_0_BASE 0x50016000 +#define WATCHDOG_0_SIZE 0x3FF +#define WATCHDOG_0_INTR_ID 0xFF +#define WATCHDOG_0_PERMISSION TYPE_READ_WRITE +#define WATCHDOG_0_NUM_OF_TICK_PER_MICRO_SEC 0x3 +#define WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_LOW 0xF4240 +#define WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_MEDIUM 0x1E8480 +#define WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_HIGH 0x4C4B40 +#define WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_CRYPTO 0x1312D00 + +#define NVMEM_NUM 1 +#define NVMEM_0_START 0x101E4000 +#define NVMEM_0_END 0x101E43FF +#define NVMEM_0_PERMISSION TYPE_READ_WRITE + +#define NSPE_MMIO_NUM 1 +#define NSPE_MMIO_0_START 0x20010F00 +#define NSPE_MMIO_0_END 0x20010F1F +#define NSPE_MMIO_0_PERMISSION TYPE_READ_WRITE + +/* Per-partition pseudo-MMIO holes carved from the top of the shared CONFDATA + * window, above the conformance .data/.bss fill. MUST match the + * WT_CONF_*_MMIO_* constants in port/mcxn947/memory_map.h (an #error + * cross-check in conf_nvm_sync.c enforces it). */ +#define SERVER_PARTITION_MMIO_NUM 1 +#define SERVER_PARTITION_MMIO_0_START 0x3005DC00 +#define SERVER_PARTITION_MMIO_0_END 0x3005DD00 +#define SERVER_PARTITION_MMIO_0_PERMISSION TYPE_READ_WRITE + +#define DRIVER_PARTITION_MMIO_NUM 1 +#define DRIVER_PARTITION_MMIO_0_START 0x3005DE00 +#define DRIVER_PARTITION_MMIO_0_END 0x3005DF00 +#define DRIVER_PARTITION_MMIO_0_PERMISSION TYPE_READ_WRITE + +#define PLATFORM_WD_BASE WATCHDOG_0_BASE +#define PLATFORM_WD_NUM_OF_TICK_PER_MICRO_SEC WATCHDOG_0_NUM_OF_TICK_PER_MICRO_SEC +#define PLATFORM_WD_TIMEOUT_IN_MICRO_SEC_LOW WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_LOW +#define PLATFORM_WD_TIMEOUT_IN_MICRO_SEC_MEDIUM WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_MEDIUM +#define PLATFORM_WD_TIMEOUT_IN_MICRO_SEC_HIGH WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_HIGH +#define PLATFORM_WD_TIMEOUT_IN_MICRO_SEC_CRYPTO WATCHDOG_0_TIMEOUT_IN_MICRO_SEC_CRYPTO + +#define PLATFORM_NVM_BASE NVMEM_0_START + +#define PLATFORM_NSPE_MMIO_START NSPE_MMIO_0_START +#define PLATFORM_SERVER_PARTITION_MMIO_START SERVER_PARTITION_MMIO_0_START +#define PLATFORM_DRIVER_PARTITION_MMIO_START DRIVER_PARTITION_MMIO_0_START +#define PLATFORM_DRIVER_PARTITION_MMIO_END DRIVER_PARTITION_MMIO_0_END + +#ifdef IPC +#include "psa/client.h" +#include "psa_manifest/sid.h" +#include "psa_manifest/pid.h" +#endif + +/* dev_apis Storage (P4-S6): the val NSPE and test TUs reach the PSA storage + * types and API version macros through this per-target config, mirroring the + * upstream tgt_dev_apis targets. */ +#if defined(STORAGE) || defined(INTERNAL_TRUSTED_STORAGE) || \ + defined(PROTECTED_STORAGE) +#include "psa/internal_trusted_storage.h" +#include "psa/protected_storage.h" +#define ARCH_TEST_STORAGE_UID_MAX_SIZE 512 +#endif + +/* dev_apis Crypto (P4-S6): wolfPSA is the guest's psa_* provider; the + * algorithm surface the suite may exercise lives in pal_crypto_config.h. */ +#if defined(CRYPTO) +#include "psa/crypto.h" +#include "pal_crypto_config.h" +#endif + +#if defined(INITIAL_ATTESTATION) +#include "psa/crypto.h" +#include "psa/initial_attestation.h" +#include "pal_attestation_config.h" +#endif + +#endif /* _PAL_CONFIG_H_ */ diff --git a/port/mcxn947/conformance/pal_crypto_config.h b/port/mcxn947/conformance/pal_crypto_config.h new file mode 100644 index 000000000..c75d2ca09 --- /dev/null +++ b/port/mcxn947/conformance/pal_crypto_config.h @@ -0,0 +1,70 @@ +/** @file + * Copyright (c) 2019-2025, Arm Limited or its affiliates. All rights reserved. + * SPDX-License-Identifier : Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +/* Crypto test configuration for the wolfTrust MCXN947 guest, derived from + * the Arm pal_crypto_config.h template. The enabled set mirrors exactly what + * the guest wolfPSA/wolfCrypt build compiles in (module/wolfhsm-client + * user_settings.h): SHA-256, HMAC, HKDF, PBKDF2, TLS-1.2 PRF, AES + * CBC/CTR/GCM/CCM, ECC P-256 ECDSA/ECDH incl. RFC 6979. Everything absent + * from that build (RSA, DES, ChaCha20, CMAC, SHA-1/224/384/512, PAKE) stays + * off so the suite skips those vectors instead of failing on honest + * NOT_SUPPORTED. */ + +#ifndef _PAL_CRYPTO_CONFIG_H_ +#define _PAL_CRYPTO_CONFIG_H_ + +#define ARCH_TEST_RAW + +#define ARCH_TEST_AES +#define ARCH_TEST_AES_128 +#define ARCH_TEST_AES_192 +#define ARCH_TEST_AES_256 +#define ARCH_TEST_AES_512 + +#define ARCH_TEST_CIPHER +#define ARCH_TEST_CIPHER_MODE_CTR +#define ARCH_TEST_CIPHER_MODE_CBC +#define ARCH_TEST_CTR_AES +#define ARCH_TEST_CBC_AES +#define ARCH_TEST_CBC_AES_NO_PADDING +#define ARCH_TEST_CBC_NO_PADDING +#define ARCH_TEST_CBC_PKCS7 + +#define ARCH_TEST_HASH +#define ARCH_TEST_SHA256 + +#define ARCH_TEST_CCM +#define ARCH_TEST_GCM + +#define ARCH_TEST_HMAC +#define ARCH_TEST_HKDF +#define ARCH_TEST_HKDF_EXTRACT +#define ARCH_TEST_HKDF_EXPAND +#define ARCH_TEST_PBKDF2 +#define ARCH_TEST_TLS12_PRF +#define ARCH_TEST_TLS12_PSK_TO_MS +#define ARCH_TEST_TRUNCATED_MAC + +#define ARCH_TEST_ECC +#define ARCH_TEST_ECC_CURVE_SECP256R1 +#define ARCH_TEST_ECDH +#define ARCH_TEST_ECDSA +#define ARCH_TEST_DETERMINISTIC_ECDSA + +#include "pal_crypto_config_check.h" + +#endif /* _PAL_CRYPTO_CONFIG_H_ */ diff --git a/port/mcxn947/conformance/pal_driver_intf.c b/port/mcxn947/conformance/pal_driver_intf.c new file mode 100644 index 000000000..7fe2a5f0e --- /dev/null +++ b/port/mcxn947/conformance/pal_driver_intf.c @@ -0,0 +1,154 @@ +/* pal_driver_intf.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 . + */ + +/* SPE PAL for the Arm psa-arch-tests DRIVER partition on wolfTrust (P3a). + * NVMEM is a flash-backed store surviving an AIRCR reset (P5 K2): a RAM shadow + * loaded from the reserved flash sector at first use and written through on + * every write via wt_conf_nvm_sync (SVC to the privileged flash driver). The + * watchdog and interrupt hooks are no-ops until P4/P6 provide the real + * devices; prints are swallowed until the secure UART routing lands in P3b. + * All state lives in the driver partition's own CONFDATA/.bss window. */ + +#include "conf_nvm.h" + +#include +#include +#include + +typedef uintptr_t addr_t; + +#define WT_CONF_DRV_NVM_SIZE 0x100u +static uint8_t g_drv_nvm[WT_CONF_DRV_NVM_SIZE]; +static uint8_t g_drv_nvm_ready; +static uint8_t g_drv_wd_enabled; + +static int wt_conf_drv_nvm_init(void) +{ + if (g_drv_nvm_ready == 0u) { + /* Reload the persisted contents; a blank sector reads back 0xFF, the + * same power-on state the RAM store used to fabricate. */ + if (wt_conf_nvm_sync(g_drv_nvm, sizeof(g_drv_nvm), 0) != 0) { + (void)memset(g_drv_nvm, 0xFF, sizeof(g_drv_nvm)); + } + g_drv_nvm_ready = 1u; + } + return 0; +} + +#if defined(WT_CONF_NVM_HOST_TEST) +/* Host-test seam: force the next access to reload from the flash simulator, + * modelling what a post-reset boot does. Never compiled into the target. */ +void wt_conf_drv_nvm_test_reset(void) +{ + g_drv_nvm_ready = 0u; +} +#endif + +void pal_uart_init(uint32_t uart_base_addr) +{ + (void)uart_base_addr; +} + +void pal_print_s(const char* str, int32_t data) +{ + (void)str; + (void)data; +} + +int pal_print(uint8_t c) +{ + (void)c; + return 0; +} + +int pal_nvmem_write(addr_t base, uint32_t offset, void* buffer, int size) +{ + (void)base; + (void)wt_conf_drv_nvm_init(); + /* Wrap-safe: offset + size overflows size_t on a 32-bit target, so + * compare each side against the array bound without adding them. */ + if (buffer == NULL || size < 0 || + (size_t)offset > sizeof(g_drv_nvm) || + (size_t)size > sizeof(g_drv_nvm) - (size_t)offset) { + return 0; + } + (void)memcpy(&g_drv_nvm[offset], buffer, (size_t)size); + /* Write through to flash so the value survives an AIRCR reset; a failed + * sync drops the shadow so the next access reloads what was persisted. */ + if (wt_conf_nvm_sync(g_drv_nvm, sizeof(g_drv_nvm), 1) != 0) { + g_drv_nvm_ready = 0u; + return 0; + } + return 1; +} + +int pal_nvmem_read(addr_t base, uint32_t offset, void* buffer, int size) +{ + (void)base; + (void)wt_conf_drv_nvm_init(); + /* Wrap-safe: offset + size overflows size_t on a 32-bit target, so + * compare each side against the array bound without adding them. */ + if (buffer == NULL || size < 0 || + (size_t)offset > sizeof(g_drv_nvm) || + (size_t)size > sizeof(g_drv_nvm) - (size_t)offset) { + return 0; + } + (void)memcpy(buffer, &g_drv_nvm[offset], (size_t)size); + return 1; +} + +int pal_wd_timer_init(addr_t base_addr, uint32_t time_us, + uint32_t timer_tick_us) +{ + (void)base_addr; + (void)time_us; + (void)timer_tick_us; + return 0; +} + +int pal_wd_timer_enable(addr_t base_addr) +{ + (void)base_addr; + g_drv_wd_enabled = 1u; + return 0; +} + +int pal_wd_timer_disable(addr_t base_addr) +{ + (void)base_addr; + g_drv_wd_enabled = 0u; + return 0; +} + +int pal_wd_timer_is_enabled(addr_t base_addr) +{ + (void)base_addr; + return (int)g_drv_wd_enabled; +} + +void pal_generate_interrupt(void) +{ + (void)wt_conf_irq_set(1); +} + +void pal_disable_interrupt(void) +{ + (void)wt_conf_irq_set(0); +} diff --git a/port/mcxn947/hsm_flash.c b/port/mcxn947/hsm_flash.c new file mode 100644 index 000000000..e5627f6dc --- /dev/null +++ b/port/mcxn947/hsm_flash.c @@ -0,0 +1,686 @@ +/* hsm_flash.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 . + */ + +/* wolfHSM NVM backend on the MCXN947 internal flash. Reads go through the + * Secure alias; program and erase go through the boot ROM flash API + * (rom_flash.c), one 16-byte phrase at most once per erase. */ + +#include +#include +#include +#include + +#include "hsm_flash.h" +#include "memory_map.h" +#include "rom_flash.h" +#include "mcxn947_regs.h" +#include "wolftrust/arch.h" +#include "wolftrust/spm_transport.h" +#include "wolftrust/spm_gate.h" +#include "wolftrust/services/fwu_service.h" +#include "wolfhsm/wh_error.h" + +static const wt_hsm_flash_config_t g_hsm_flash_cfg = { + .base = WT_HSM_NVM_FLASH_BASE_S, + .size = WT_HSM_NVM_FLASH_SIZE, + .sector_size = WT_FLASH_SECTOR_SIZE, + .program_unit = WT_FLASH_PHRASE_SIZE, +}; + +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) +/* The conformance DRIVER partition's NVMEM boot flag lives in its own flash + * sector so it survives the suite's panic-test resets. */ +static const wt_hsm_flash_config_t g_conf_nvm_cfg = { + .base = WT_CONF_NVM_FLASH_BASE_S, + .size = WT_CONF_NVM_FLASH_SIZE, + .sector_size = WT_FLASH_SECTOR_SIZE, + .program_unit = WT_FLASH_PHRASE_SIZE, +}; +static wt_hsm_flash_context_t g_conf_nvm_ctx; +#endif +static const wt_hsm_flash_config_t g_fwu_flash_cfg = { + .base = WT_FWU_UPDATE_FLASH_BASE_S, + .size = WT_FWU_UPDATE_FLASH_SIZE, + .sector_size = WT_FLASH_SECTOR_SIZE, + .program_unit = WT_FLASH_PHRASE_SIZE, +}; +static wt_hsm_flash_context_t g_fwu_flash_ctx; +#if defined(WT_REMEASURE_PROBE) +static const wt_hsm_flash_config_t g_remeasure_probe_cfg = { + .base = WT_FLASH_TO_S_ALIAS(WT_GUEST0_FLASH_BASE), + .size = WT_FLASH_SECTOR_SIZE, + .sector_size = WT_FLASH_SECTOR_SIZE, + .program_unit = WT_FLASH_PHRASE_SIZE, +}; +static wt_hsm_flash_context_t g_remeasure_probe_ctx; +#endif +/* Last flash driver failure, read over the debug port by the harness. */ +volatile int32_t g_wt_flash_last_error __attribute__((used)); + +/* The context sits in the keystore band the confined partitions can write, so + * the privileged path takes its geometry from the const config it belongs to. */ +static const wt_hsm_flash_config_t *wt_hsm_flash_cfg_of(const void *context) +{ + if (context == (const void *)&g_hsm_flash_ctx) { + return &g_hsm_flash_cfg; + } +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) + if (context == (const void *)&g_conf_nvm_ctx) { + return &g_conf_nvm_cfg; + } +#endif + if (context == (const void *)&g_fwu_flash_ctx) { + return &g_fwu_flash_cfg; + } +#if defined(WT_REMEASURE_PROBE) + if (context == (const void *)&g_remeasure_probe_ctx) { + return &g_remeasure_probe_cfg; + } +#endif + return NULL; +} + +/* The store is read through the Secure alias; the ROM API takes the offset. */ +static uint32_t wt_hsm_flash_offset(const wt_hsm_flash_config_t *cfg, + uint32_t offset) +{ + return (uint32_t)(cfg->base - WT_FLASH_S_ALIAS_BASE) + offset; +} + +static int wt_hsm_flash_rom_result(int rc) +{ + if (rc == WT_ROM_FLASH_OK) { + return WH_ERROR_OK; + } + g_wt_flash_last_error = (int32_t)rc; + if (rc == WT_ROM_FLASH_ERR_NOTBLANK) { + return WH_ERROR_NOTBLANK; + } + if (rc == WT_ROM_FLASH_ERR_ARGS) { + return WH_ERROR_BADARGS; + } + return WH_ERROR_ABORTED; +} + +static int wt_flash_range_ok(const wt_hsm_flash_config_t *cfg, + uint32_t offset, uint32_t size) +{ + if (cfg == NULL || cfg->sector_size == 0u || cfg->program_unit == 0u) { + return 0; + } + if (offset > cfg->size) { + return 0; + } + if (size > cfg->size - offset) { + return 0; + } + return 1; +} + +/* A confined keystore partition hops to the privileged SVC dispatcher, which + * re-enters the same callback with privilege. */ +static int wt_hsm_flash_gate(void *context, int sub_op, uint32_t offset, + uint32_t size, void *data) +{ + wt_spm_call_t call; + int rc; + + if (context != (void *)&g_hsm_flash_ctx) { + return WH_ERROR_BADARGS; + } + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_KEYSTORE_FLASH; + call.call_type = sub_op; + call.vec_idx = offset; + call.num_bytes = size; + call.buffer = data; + rc = wt_spm_sp_call(&call); + if (rc != WT_FFM_SUCCESS) { + g_wt_flash_gate_aborts++; + if (g_wt_flash_gate_abort_info == 0u) { + g_wt_flash_gate_abort_info = ((uint32_t)sub_op << 24) | + ((uint32_t)rc & 0x00FFFFFFu); + } + return WH_ERROR_ABORTED; + } + return call.ret_int; +} + +static int wt_hsm_flash_init(void *context, const void *config) +{ + wt_hsm_flash_context_t *ctx = (wt_hsm_flash_context_t *)context; + const wt_hsm_flash_config_t *cfg = + (const wt_hsm_flash_config_t *)config; + + if (ctx == NULL || cfg == NULL || cfg != wt_hsm_flash_cfg_of(ctx) || + cfg->base == 0u || cfg->size == 0u || + cfg->sector_size == 0u || cfg->program_unit == 0u || + (cfg->size % cfg->sector_size) != 0u || + (cfg->sector_size % cfg->program_unit) != 0u) { + return WH_ERROR_BADARGS; + } + ctx->write_locked = false; + return WH_ERROR_OK; +} + +static int wt_hsm_flash_cleanup(void *context) +{ + if (context == NULL) { + return WH_ERROR_BADARGS; + } + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_CLEANUP, 0u, 0u, + NULL); + } + return WH_ERROR_OK; +} + +static uint32_t wt_hsm_flash_partition_size(void *context) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + + if (cfg == NULL) { + return 0u; + } + return cfg->sector_size; +} + +static int wt_hsm_flash_write_lock(void *context, uint32_t offset, + uint32_t size) +{ + wt_hsm_flash_context_t *ctx = (wt_hsm_flash_context_t *)context; + (void)offset; + (void)size; + + if (ctx == NULL) { + return WH_ERROR_BADARGS; + } + ctx->write_locked = true; + return WH_ERROR_OK; +} + +static int wt_hsm_flash_write_unlock(void *context, uint32_t offset, + uint32_t size) +{ + wt_hsm_flash_context_t *ctx = (wt_hsm_flash_context_t *)context; + (void)offset; + (void)size; + + if (ctx == NULL) { + return WH_ERROR_BADARGS; + } + ctx->write_locked = false; + return WH_ERROR_OK; +} + +static int wt_hsm_flash_read(void *context, uint32_t offset, uint32_t size, + uint8_t *data) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + + if (data == NULL && size != 0u) { + return WH_ERROR_BADARGS; + } + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_READ, offset, size, + data); + } + if (!wt_flash_range_ok(cfg, offset, size)) { + return WH_ERROR_BADARGS; + } + if (size != 0u) { + (void)memcpy(data, (const uint8_t *)(cfg->base + offset), size); + } + return WH_ERROR_OK; +} + +static int wt_hsm_flash_program(void *context, uint32_t offset, uint32_t size, + const uint8_t *data) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + wt_hsm_flash_context_t *ctx = (wt_hsm_flash_context_t *)context; + + if (data == NULL && size != 0u) { + return WH_ERROR_BADARGS; + } + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_PROGRAM, offset, + size, (void *)(uintptr_t)data); + } + if (!wt_flash_range_ok(cfg, offset, size) || + (offset % cfg->program_unit) != 0u || + (size % cfg->program_unit) != 0u) { + return WH_ERROR_BADARGS; + } + if (size != 0u && ctx->write_locked) { + return WH_ERROR_LOCKED; + } + if (size == 0u) { + return WH_ERROR_OK; + } + return wt_hsm_flash_rom_result( + wt_rom_flash_program(wt_hsm_flash_offset(cfg, offset), data, size)); +} + +static int wt_hsm_flash_erase(void *context, uint32_t offset, uint32_t size) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + wt_hsm_flash_context_t *ctx = (wt_hsm_flash_context_t *)context; + + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_ERASE, offset, size, + NULL); + } + if (!wt_flash_range_ok(cfg, offset, size) || + (offset % cfg->sector_size) != 0u || + (size % cfg->sector_size) != 0u) { + return WH_ERROR_BADARGS; + } + if (size != 0u && ctx->write_locked) { + return WH_ERROR_LOCKED; + } + if (size == 0u) { + return WH_ERROR_OK; + } + return wt_hsm_flash_rom_result( + wt_rom_flash_erase(wt_hsm_flash_offset(cfg, offset), size)); +} + +static int wt_hsm_flash_verify(void *context, uint32_t offset, uint32_t size, + const uint8_t *data) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + + if (data == NULL && size != 0u) { + return WH_ERROR_BADARGS; + } + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_VERIFY, offset, + size, (void *)(uintptr_t)data); + } + if (!wt_flash_range_ok(cfg, offset, size)) { + return WH_ERROR_BADARGS; + } + if (size != 0u && + memcmp((const uint8_t *)(cfg->base + offset), data, size) != 0) { + return WH_ERROR_NOTVERIFIED; + } + return WH_ERROR_OK; +} + +static int wt_hsm_flash_blank_check(void *context, uint32_t offset, + uint32_t size) +{ + const wt_hsm_flash_config_t *cfg = wt_hsm_flash_cfg_of(context); + int rc; + + if (wt_arch_thread_unprivileged()) { + return wt_hsm_flash_gate(context, WT_SPM_KS_FLASH_BLANKCHECK, offset, + size, NULL); + } + if (!wt_flash_range_ok(cfg, offset, size)) { + return WH_ERROR_BADARGS; + } + rc = wt_rom_flash_blank(wt_hsm_flash_offset(cfg, offset), size); + if (rc == WT_ROM_FLASH_ERR_NOTBLANK) { + return WH_ERROR_NOTBLANK; + } + return (rc == WT_ROM_FLASH_OK) ? WH_ERROR_OK : WH_ERROR_BADARGS; +} + +const whFlashCb g_wt_hsm_flash_cb = { + .Init = wt_hsm_flash_init, + .Cleanup = wt_hsm_flash_cleanup, + .PartitionSize = wt_hsm_flash_partition_size, + .WriteLock = wt_hsm_flash_write_lock, + .WriteUnlock = wt_hsm_flash_write_unlock, + .Read = wt_hsm_flash_read, + .Program = wt_hsm_flash_program, + .Erase = wt_hsm_flash_erase, + .Verify = wt_hsm_flash_verify, + .BlankCheck = wt_hsm_flash_blank_check, +}; + +void *wt_hsm_flash_context(void) +{ + return &g_hsm_flash_ctx; +} + +const void *wt_hsm_flash_config(void) +{ + return &g_hsm_flash_cfg; +} + +int wt_hsm_flash_format(void) +{ + return wt_hsm_flash_erase(&g_hsm_flash_ctx, 0u, g_hsm_flash_cfg.size); +} + +/* SERVICE_FWU staging into the wolfBoot update partition, contiguous from + * offset 0 with each sector erased lazily; arm() programs wolfBoot's update + * trigger in the trailer, and the swapped image still faces launch verify. */ + +/* Erased high-water mark: every sector below it is blank or holds staged + * data and is never erased again, whatever the partition's sector count. */ +static uint32_t g_fwu_erased_end; +static uint32_t g_fwu_written_end; + +/* Write-once wolfBoot keeps a trailer copy in each of the last two sectors. */ +#define WT_FWU_TRAILER_SIZE (2u * WT_FLASH_SECTOR_SIZE) + +static int wt_fwu_ensure_erased(uint32_t offset, uint32_t size) +{ + uint32_t sector_size = g_fwu_flash_cfg.sector_size; + uint32_t end = offset + size; + uint32_t s; + + end = ((end + sector_size - 1u) / sector_size) * sector_size; + for (s = g_fwu_erased_end; s < end; s += sector_size) { + if (wt_hsm_flash_erase(&g_fwu_flash_ctx, s, sector_size) != + WH_ERROR_OK) { + return -1; + } + g_fwu_erased_end = s + sector_size; + } + return 0; +} + +static int wt_fwu_backend_begin(void *ctx) +{ + (void)ctx; + if (wt_hsm_flash_init(&g_fwu_flash_ctx, &g_fwu_flash_cfg) != WH_ERROR_OK) { + return -1; + } + g_fwu_erased_end = 0u; + g_fwu_written_end = 0u; + return 0; +} + +static int wt_fwu_backend_write(void *ctx, uint32_t offset, + const uint8_t *data, uint32_t size) +{ + const uint8_t *mapped = (const uint8_t *)(g_fwu_flash_cfg.base + offset); + uint32_t reserved = g_fwu_flash_cfg.size - WT_FWU_TRAILER_SIZE; + + (void)ctx; + /* The trailer sectors hold wolfBoot's swap trigger and are owned by + * arm/disarm alone; staging data must never pre-program them. */ + if (offset >= reserved || size > reserved - offset || + offset != g_fwu_written_end) { + return -1; + } + if (wt_fwu_ensure_erased(offset, size) != 0) { + return -1; + } + if (wt_hsm_flash_program(&g_fwu_flash_ctx, offset, size, data) != + WH_ERROR_OK) { + return -1; + } + if (memcmp(mapped, data, size) != 0) { + return -1; + } + g_fwu_written_end = offset + size; + return 0; +} + +static int wt_fwu_backend_disarm(void *ctx); + +static int wt_fwu_backend_arm(void *ctx, uint32_t image_size, + uint32_t version) +{ + uint32_t block_off = g_fwu_flash_cfg.size - g_fwu_flash_cfg.program_unit; + const uint8_t *mapped = (const uint8_t *)(g_fwu_flash_cfg.base + block_off); + uint8_t block[WT_FLASH_PHRASE_SIZE]; + + (void)ctx; + (void)image_size; + (void)version; + /* The wolfBoot trigger lives in the topmost program unit of the partition + * (state + magic); the block below it stays erased. The trailer sectors + * never hold staged data, so they are erased outright. */ + if (wt_fwu_wolfboot_arm_trailer(block, (uint32_t)sizeof(block)) != 0) { + return -1; + } + if (wt_fwu_backend_disarm(NULL) != 0) { + return -1; + } + if (wt_hsm_flash_program(&g_fwu_flash_ctx, block_off, + (uint32_t)sizeof(block), block) != WH_ERROR_OK || + memcmp(mapped, block, sizeof(block)) != 0) { + /* A partially programmed trigger must not survive a failed arm. */ + (void)wt_fwu_backend_disarm(NULL); + return -1; + } + return 0; +} + +static int wt_fwu_backend_disarm(void *ctx) +{ + uint32_t trailer = g_fwu_flash_cfg.size - WT_FWU_TRAILER_SIZE; + + (void)ctx; + if (wt_hsm_flash_erase(&g_fwu_flash_ctx, trailer, + WT_FWU_TRAILER_SIZE) != WH_ERROR_OK) { + return -1; + } + return 0; +} + +/* wolfBoot image header size for this port (staged images carry it). */ +#define WT_FWU_IMAGE_HEADER_SIZE 0x400u +#define WT_FWU_IMAGE_MAGIC 0x464C4F57u +#define WT_FWU_HDR_TAG_VERSION 0x0001u + +static int wt_fwu_backend_verify(void *ctx, uint32_t staged_size, + uint32_t *header_version) +{ + const uint8_t *hdr = (const uint8_t *)g_fwu_flash_cfg.base; + uint32_t magic; + uint32_t fw_size; + uint32_t version = 0u; + uint32_t offset = 8u; + uint32_t tag; + uint32_t len; + int found = 0; + + (void)ctx; + /* FINISH passes the logical size; the programmed extent is it rounded up + * to a phrase. */ + if (header_version == NULL || + staged_size < WT_FWU_IMAGE_HEADER_SIZE || + staged_size > g_fwu_written_end || + g_fwu_written_end - staged_size >= WT_FLASH_PHRASE_SIZE) { + return -1; + } + memcpy(&magic, hdr, sizeof(magic)); + memcpy(&fw_size, hdr + 4u, sizeof(fw_size)); + if (magic != WT_FWU_IMAGE_MAGIC) { + return -1; + } + /* The staged extent must cover the header plus the declared payload. */ + if (fw_size > staged_size - WT_FWU_IMAGE_HEADER_SIZE) { + return -1; + } + while (offset + 4u <= WT_FWU_IMAGE_HEADER_SIZE) { + if (hdr[offset] == 0xFFu) { + offset++; + continue; + } + tag = (uint32_t)hdr[offset] | ((uint32_t)hdr[offset + 1u] << 8); + len = (uint32_t)hdr[offset + 2u] | ((uint32_t)hdr[offset + 3u] << 8); + if (tag == 0u) { + break; + } + if (offset + 4u + len > WT_FWU_IMAGE_HEADER_SIZE) { + return -1; + } + if (tag == WT_FWU_HDR_TAG_VERSION && len == sizeof(version)) { + memcpy(&version, hdr + offset + 4u, sizeof(version)); + found = 1; + } + offset += 4u + len; + } + if (found == 0) { + return -1; + } + *header_version = version; + return 0; +} + +const wt_fwu_backend_t wt_fwu_flash_backend = { + .begin = wt_fwu_backend_begin, + .write = wt_fwu_backend_write, + .arm = wt_fwu_backend_arm, + .disarm = wt_fwu_backend_disarm, + /* The trailer sectors are arm/disarm-owned, never staging capacity. */ + .capacity = WT_FWU_UPDATE_FLASH_SIZE - WT_FWU_TRAILER_SIZE, + .align = WT_FLASH_PHRASE_SIZE, + .verify = wt_fwu_backend_verify, +}; + +#if defined(WT_REMEASURE_PROBE) +/* Probe-only tamper of guest0's first sector: erase it and program a zero + * phrase where the vector table was, through the same privileged primitives. */ +int wt_hsm_flash_remeasure_tamper(uintptr_t secure_base) +{ + uint8_t block[WT_FLASH_PHRASE_SIZE]; + + if (secure_base != g_remeasure_probe_cfg.base) { + return -1; + } + g_remeasure_probe_ctx.write_locked = false; + if (wt_hsm_flash_erase(&g_remeasure_probe_ctx, 0u, WT_FLASH_SECTOR_SIZE) != + WH_ERROR_OK) { + return -1; + } + (void)memset(block, 0x00, sizeof(block)); + return (wt_hsm_flash_program(&g_remeasure_probe_ctx, 0u, sizeof(block), + block) == WH_ERROR_OK) ? 0 : -1; +} +#endif + +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) +static bool g_conf_nvm_ready; + +int wt_conf_nvm_flash_sync(uint8_t *buf, uint32_t len, int store) +{ + int ret; + + if (buf == NULL || len == 0u || len > g_conf_nvm_cfg.size || + (len % g_conf_nvm_cfg.program_unit) != 0u) { + return -1; + } + if (!g_conf_nvm_ready) { + if (wt_hsm_flash_init(&g_conf_nvm_ctx, &g_conf_nvm_cfg) != WH_ERROR_OK) { + return -1; + } + g_conf_nvm_ready = true; + } + if (store == 0) { + ret = wt_hsm_flash_read(&g_conf_nvm_ctx, 0u, len, buf); + } + else { + ret = wt_hsm_flash_erase(&g_conf_nvm_ctx, 0u, + g_conf_nvm_cfg.sector_size); + if (ret == WH_ERROR_OK) { + ret = wt_hsm_flash_program(&g_conf_nvm_ctx, 0u, len, buf); + } + } + return (ret == WH_ERROR_OK) ? 0 : -1; +} +#endif + +#if defined(WT_DEPUTY_NEG_PROBE) && (WT_DEPUTY_NEG_PROBE == 1) +/* A primitive fed a forged offset must reject it with exactly + * WH_ERROR_BADARGS through a delivered gate call. */ +static int wt_deputy_expect_badargs(int32_t sub_op, uint32_t offset, + uint32_t size, void *buf) +{ + wt_spm_call_t call; + + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_KEYSTORE_FLASH; + call.call_type = sub_op; + call.vec_idx = offset; + call.num_bytes = size; + call.buffer = buf; + if (wt_arch_sp_trap(&call) != WT_FFM_SUCCESS) { + return 0; + } + return (call.ret_int == WH_ERROR_BADARGS) ? 1 : 0; +} + +static int wt_deputy_read_ok(int32_t sub_op, uint8_t *buf, uint32_t size) +{ + wt_spm_call_t call; + + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_KEYSTORE_FLASH; + call.call_type = sub_op; + call.vec_idx = 0u; + call.num_bytes = size; + call.buffer = buf; + return (wt_arch_sp_trap(&call) == WT_FFM_SUCCESS && + call.ret_int == WH_ERROR_OK) ? 1 : 0; +} + +/* The vault's writable context holds no geometry here, so a deputy can only + * forge offsets and lengths: each must be refused against the const config, + * and the store must still read back unchanged. */ +__attribute__((used, noinline)) +int wt_platform_deputy_flash_probe(void) +{ + uint8_t ref[WT_FLASH_PHRASE_SIZE]; + uint8_t leak[WT_FLASH_PHRASE_SIZE]; + uint32_t oor; + int ok = 1; + + /* Wraps modulo 2^32 so base + offset addresses SPM-private RAM. */ + oor = (uint32_t)((uintptr_t)WT_RAM_S_BASE - g_hsm_flash_cfg.base); + + (void)memset(ref, 0, sizeof(ref)); + if (wt_deputy_read_ok(WT_SPM_KS_FLASH_READ, ref, sizeof(ref)) == 0) { + return 0; + } + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_READ, oor, 16u, leak); + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_PROGRAM, oor, 16u, leak); + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_ERASE, oor, + g_hsm_flash_cfg.sector_size, NULL); + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_VERIFY, oor, 16u, leak); + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_BLANKCHECK, oor, 16u, + NULL); + /* A length running past the store from an in-range offset. */ + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_READ, + g_hsm_flash_cfg.size - 8u, 16u, leak); + /* Misaligned program and sub-sector erase, checked on the const units. */ + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_PROGRAM, 4u, 16u, leak); + ok &= wt_deputy_expect_badargs(WT_SPM_KS_FLASH_ERASE, 0u, 16u, NULL); + + (void)memset(leak, 0, sizeof(leak)); + if (wt_deputy_read_ok(WT_SPM_KS_FLASH_READ, leak, sizeof(leak)) == 0 || + memcmp(leak, ref, sizeof(ref)) != 0) { + ok = 0; + } + if (wt_deputy_read_ok(WT_SPM_KS_FLASH_VERIFY, ref, sizeof(ref)) == 0) { + ok = 0; + } + return ok; +} +#endif diff --git a/port/mcxn947/hsm_flash.h b/port/mcxn947/hsm_flash.h new file mode 100644 index 000000000..e772db2e9 --- /dev/null +++ b/port/mcxn947/hsm_flash.h @@ -0,0 +1,53 @@ +/* hsm_flash.h + * + * 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 . + */ + +#ifndef WOLFTRUST_MCXN947_HSM_FLASH_H +#define WOLFTRUST_MCXN947_HSM_FLASH_H + +#include +#include +#include "wolftrust/port_nvm.h" + +typedef struct wt_hsm_flash_config { + uintptr_t base; + uint32_t size; + uint32_t sector_size; + uint32_t program_unit; +} wt_hsm_flash_config_t; + +typedef struct wt_hsm_flash_context { + bool write_locked; +} wt_hsm_flash_context_t; + +/* The vault's own flash bookkeeping (hsm_flash_ctx.c, in the vault's data + * band). The driver's privileged state stays in hsm_flash.c, in SPM RAM. */ +extern wt_hsm_flash_context_t g_hsm_flash_ctx; +extern volatile uint32_t g_wt_flash_gate_aborts; +extern volatile uint32_t g_wt_flash_gate_abort_info; + +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) +int wt_conf_nvm_flash_sync(uint8_t *buf, uint32_t len, int store); +#endif + +#if defined(WT_REMEASURE_PROBE) +int wt_hsm_flash_remeasure_tamper(uintptr_t secure_base); +#endif + +#endif /* WOLFTRUST_MCXN947_HSM_FLASH_H */ diff --git a/port/mcxn947/hsm_flash_ctx.c b/port/mcxn947/hsm_flash_ctx.c new file mode 100644 index 000000000..eda4a7666 --- /dev/null +++ b/port/mcxn947/hsm_flash_ctx.c @@ -0,0 +1,27 @@ +/* hsm_flash_ctx.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 . + */ + +#include "hsm_flash.h" + +wt_hsm_flash_context_t g_hsm_flash_ctx; + +/* Gate-path forensics (SWD-readable), counted in the vault's own thread. */ +volatile uint32_t g_wt_flash_gate_aborts __attribute__((used)); +volatile uint32_t g_wt_flash_gate_abort_info __attribute__((used)); diff --git a/port/mcxn947/l3_port.h b/port/mcxn947/l3_port.h new file mode 100644 index 000000000..c989dfa76 --- /dev/null +++ b/port/mcxn947/l3_port.h @@ -0,0 +1,31 @@ +/* l3_port.h + * + * 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 . + */ + +/* MCXN947 inputs to the shared Armv8-M isolation level 3 layer. */ + +#ifndef WOLFTRUST_FW_MCXN947_L3_PORT_H +#define WOLFTRUST_FW_MCXN947_L3_PORT_H + +#include "mcxn947_regs.h" + +/* A Secure peripheral only the SPM drives, probed by the periphsp negative. */ +#define WT_L3_SPM_PERIPHERAL_BASE WT_ELS_BASE_S + +#endif diff --git a/port/mcxn947/manifest-conformance.json b/port/mcxn947/manifest-conformance.json new file mode 100644 index 000000000..b0cb56afb --- /dev/null +++ b/port/mcxn947/manifest-conformance.json @@ -0,0 +1,728 @@ +{ + "format_version": 1, + "generator_version": "WOLFTRUST_GEN_1", + "features": 1, + "isolation_profile": 3, + "profile_capabilities": { + "capabilities": 127, + "max_domains": 11, + "max_memory_resources_per_domain": 3, + "max_interrupts_per_domain": 1 + }, + "domains": [ + { + "id": 0, + "domain_class": 0, + "rot_role": 1, + "security_state": 0, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 268697600, + "stack_base": 805507072, + "stack_size": 4096, + "memory_resources": [ + { + "base": 268697600, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805507072, + "size": 4096, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 2, + "restart_limit": 0, + "restart_window_ticks": 0, + "initial_delay_ticks": 0 + }, + "required_capabilities": 127, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 1, + "domain_class": 2, + "rot_role": 0, + "security_state": 1, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 524288, + "stack_base": 536870912, + "stack_size": 98304, + "memory_resources": [ + { + "base": 524288, + "size": 524288, + "attributes": 5, + "share_id": 0 + }, + { + "base": 536870912, + "size": 98304, + "attributes": 19, + "share_id": 0 + }, + { + "base": 1074479104, + "size": 4096, + "attributes": 43, + "share_id": 2 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 1, + "launch_min_version": 1 + }, + { + "id": 2, + "domain_class": 2, + "rot_role": 0, + "security_state": 1, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 1048576, + "stack_base": 536969216, + "stack_size": 98304, + "memory_resources": [ + { + "base": 1048576, + "size": 262144, + "attributes": 5, + "share_id": 0 + }, + { + "base": 536969216, + "size": 98304, + "attributes": 19, + "share_id": 0 + }, + { + "base": 1074479104, + "size": 4096, + "attributes": 43, + "share_id": 2 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 1, + "launch_min_version": 1 + }, + { + "id": 3, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268763136, + "stack_base": 805699584, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268763136, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805699584, + "size": 8192, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805603328, + "size": 2048, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 4, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268767232, + "stack_base": 805691392, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268767232, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805691392, + "size": 8192, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805605376, + "size": 32768, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 5, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268783616, + "stack_base": 805662720, + "stack_size": 16384, + "memory_resources": [ + { + "base": 268783616, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805662720, + "size": 16384, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805595136, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 6, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268787712, + "stack_base": 805654528, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268787712, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805654528, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 7, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268791808, + "stack_base": 805646336, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268791808, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805646336, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 8, + "domain_class": 1, + "rot_role": 3, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268771328, + "stack_base": 805707776, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268771328, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805707776, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 9, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268775424, + "stack_base": 805715968, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268775424, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805715968, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [ + { + "interrupt": 36, + "attributes": 0, + "share_id": 0 + } + ], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 10, + "domain_class": 1, + "rot_role": 3, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268779520, + "stack_base": 805724160, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268779520, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805724160, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + } + ], + "partitions": [ + { + "name": "PARTITION_ATTEST", + "domain_id": 3, + "framework_version": 256, + "model": 0, + "priority": 2, + "services": [ + { + "name": "SERVICE_ATTEST", + "sid": 4096, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4102 + ], + "interrupts": [] + }, + { + "name": "PARTITION_HSM", + "domain_id": 4, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_HSM", + "sid": 4102, + "version": 1, + "version_policy": 0, + "signal": 32, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "PARTITION_VAULT", + "domain_id": 5, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_VAULT", + "sid": 4098, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": false, + "connection_based": true + } + ], + "dependencies": [], + "interrupts": [] + }, + { + "name": "PARTITION_ITS", + "domain_id": 6, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_ITS", + "sid": 4099, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "PARTITION_PS", + "domain_id": 7, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_PS", + "sid": 4100, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "SERVER_PARTITION", + "domain_id": 8, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVER_TEST_DISPATCHER", + "sid": 64257, + "version": 1, + "version_policy": 1, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "SERVER_SECURE_CONNECT_ONLY", + "sid": 64258, + "version": 2, + "version_policy": 1, + "signal": 32, + "stateless_handle_index": 0, + "nonsecure_clients": false, + "connection_based": true + }, + { + "name": "SERVER_STRICT_VERSION", + "sid": 64259, + "version": 2, + "version_policy": 0, + "signal": 64, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "SERVER_UNSPECIFIED_VERSION", + "sid": 64260, + "version": 1, + "version_policy": 0, + "signal": 128, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "SERVER_RELAX_VERSION", + "sid": 64261, + "version": 2, + "version_policy": 1, + "signal": 256, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "SERVER_UNEXTERN", + "sid": 64262, + "version": 2, + "version_policy": 1, + "signal": 512, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "SERVER_CONNECTION_DROP", + "sid": 64263, + "version": 2, + "version_policy": 1, + "signal": 1024, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 64513, + 64515 + ], + "interrupts": [] + }, + { + "name": "DRIVER_PARTITION", + "domain_id": 9, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "DRIVER_UART", + "sid": 64513, + "version": 1, + "version_policy": 1, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "DRIVER_WATCHDOG", + "sid": 64514, + "version": 1, + "version_policy": 1, + "signal": 32, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "DRIVER_NVMEM", + "sid": 64515, + "version": 1, + "version_policy": 1, + "signal": 64, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + }, + { + "name": "DRIVER_TEST", + "sid": 64516, + "version": 1, + "version_policy": 1, + "signal": 128, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [], + "interrupts": [ + { + "signal_name": "DRIVER_UART_INTR_SIG", + "interrupt": 36, + "signal": 256 + } + ] + }, + { + "name": "CLIENT_PARTITION", + "domain_id": 10, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "CLIENT_TEST_DISPATCHER", + "sid": 64001, + "version": 1, + "version_policy": 1, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 64513, + 64515, + 64516, + 64257, + 64260, + 64259, + 64261, + 64258, + 64263 + ], + "interrupts": [] + } + ], + "limits": { + "max_partitions": 8, + "max_services_per_partition": 7, + "max_dependencies_per_partition": 9, + "max_stateless_handles": 32 + } +} diff --git a/port/mcxn947/manifest.json b/port/mcxn947/manifest.json new file mode 100644 index 000000000..532f56b79 --- /dev/null +++ b/port/mcxn947/manifest.json @@ -0,0 +1,501 @@ +{ + "format_version": 1, + "generator_version": "WOLFTRUST_GEN_1", + "features": 1, + "isolation_profile": 3, + "profile_capabilities": { + "capabilities": 127, + "max_domains": 9, + "max_memory_resources_per_domain": 3, + "max_interrupts_per_domain": 0 + }, + "domains": [ + { + "id": 0, + "domain_class": 0, + "rot_role": 1, + "security_state": 0, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 268697600, + "stack_base": 805507072, + "stack_size": 4096, + "memory_resources": [ + { + "base": 268697600, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805507072, + "size": 4096, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 2, + "restart_limit": 0, + "restart_window_ticks": 0, + "initial_delay_ticks": 0 + }, + "required_capabilities": 127, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 1, + "domain_class": 2, + "rot_role": 0, + "security_state": 1, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 524288, + "stack_base": 536870912, + "stack_size": 98304, + "memory_resources": [ + { + "base": 524288, + "size": 524288, + "attributes": 5, + "share_id": 0 + }, + { + "base": 536870912, + "size": 98304, + "attributes": 19, + "share_id": 0 + }, + { + "base": 1074479104, + "size": 4096, + "attributes": 43, + "share_id": 2 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 1, + "launch_min_version": 1 + }, + { + "id": 2, + "domain_class": 2, + "rot_role": 0, + "security_state": 1, + "privilege_state": 0, + "initial_lifecycle": 1, + "entry_point": 1048576, + "stack_base": 536969216, + "stack_size": 98304, + "memory_resources": [ + { + "base": 1048576, + "size": 262144, + "attributes": 5, + "share_id": 0 + }, + { + "base": 536969216, + "size": 98304, + "attributes": 19, + "share_id": 0 + }, + { + "base": 1074479104, + "size": 4096, + "attributes": 43, + "share_id": 2 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 1, + "launch_min_version": 1 + }, + { + "id": 3, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268763136, + "stack_base": 805699584, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268763136, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805699584, + "size": 8192, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805603328, + "size": 2048, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 4, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268767232, + "stack_base": 805691392, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268767232, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805691392, + "size": 8192, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805605376, + "size": 32768, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 5, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268783616, + "stack_base": 805662720, + "stack_size": 16384, + "memory_resources": [ + { + "base": 268783616, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805662720, + "size": 16384, + "attributes": 19, + "share_id": 0 + }, + { + "base": 805595136, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 6, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268787712, + "stack_base": 805654528, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268787712, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805654528, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 7, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268791808, + "stack_base": 805646336, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268791808, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805646336, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + }, + { + "id": 8, + "domain_class": 1, + "rot_role": 2, + "security_state": 0, + "privilege_state": 1, + "initial_lifecycle": 0, + "entry_point": 268795904, + "stack_base": 805638144, + "stack_size": 8192, + "memory_resources": [ + { + "base": 268795904, + "size": 4096, + "attributes": 5, + "share_id": 0 + }, + { + "base": 805638144, + "size": 8192, + "attributes": 19, + "share_id": 0 + } + ], + "interrupt_resources": [], + "restart_policy": { + "action": 1, + "restart_limit": 3, + "restart_window_ticks": 8000, + "initial_delay_ticks": 1 + }, + "required_capabilities": 80, + "launch_required": 0, + "launch_min_version": 0 + } + ], + "partitions": [ + { + "name": "PARTITION_ATTEST", + "domain_id": 3, + "framework_version": 256, + "model": 0, + "priority": 2, + "services": [ + { + "name": "SERVICE_ATTEST", + "sid": 4096, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4102 + ], + "interrupts": [] + }, + { + "name": "PARTITION_HSM", + "domain_id": 4, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_HSM", + "sid": 4102, + "version": 1, + "version_policy": 0, + "signal": 32, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "PARTITION_VAULT", + "domain_id": 5, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_VAULT", + "sid": 4098, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": false, + "connection_based": true + } + ], + "dependencies": [], + "interrupts": [] + }, + { + "name": "PARTITION_ITS", + "domain_id": 6, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_ITS", + "sid": 4099, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "PARTITION_PS", + "domain_id": 7, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_PS", + "sid": 4100, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [ + 4098 + ], + "interrupts": [] + }, + { + "name": "PARTITION_FWU", + "domain_id": 8, + "framework_version": 256, + "model": 0, + "priority": 1, + "services": [ + { + "name": "SERVICE_FWU", + "sid": 4101, + "version": 1, + "version_policy": 0, + "signal": 16, + "stateless_handle_index": 0, + "nonsecure_clients": true, + "connection_based": true + } + ], + "dependencies": [], + "interrupts": [] + } + ], + "limits": { + "max_partitions": 6, + "max_services_per_partition": 1, + "max_dependencies_per_partition": 1, + "max_stateless_handles": 32 + } +} diff --git a/port/mcxn947/mcxn947_regs.h b/port/mcxn947/mcxn947_regs.h new file mode 100644 index 000000000..f245d8e79 --- /dev/null +++ b/port/mcxn947/mcxn947_regs.h @@ -0,0 +1,72 @@ +/* mcxn947_regs.h + * + * 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 . + */ + +#ifndef WOLFTRUST_MCXN947_REGS_H +#define WOLFTRUST_MCXN947_REGS_H + +#include + +/* Secure-alias peripheral bases (MCXN947 cm33_core0, NS alias = base - + * 0x10000000). */ +#define WT_PERIPH_NS_BASE 0x40000000u +#define WT_PERIPH_S_BASE 0x50000000u +#define WT_PERIPH_ALIAS_SIZE 0x10000000u + +#define WT_SYSCON_BASE_S 0x50000000u +#define WT_FMU0_BASE_S 0x50043000u +#define WT_ELS_BASE_S 0x50054000u +#define WT_ELS_BASE_NS 0x40054000u +#define WT_LPUART4_BASE_S 0x500B4000u +#define WT_LPUART4_BASE_NS 0x400B4000u +#define WT_TRDC_BASE_S 0x500C7000u +#define WT_AHBSC_BASE_S 0x50120000u + +/* Boot ROM through its Secure alias; the ROM flash API tree sits at its end. */ +#define WT_ROM_BASE_S 0x13000000u +#define WT_ROM_SIZE 0x00040000u +#define WT_ROM_API_TREE 0x1303FC00u + +#ifndef WT_REG32 +#define WT_REG32(address) (*(volatile uint32_t*)(address)) +#endif + +/* EdgeLock (ELS, S50) command interface; register offsets per the MCUXpresso + * S50 peripheral header. */ +#define WT_ELS_STATUS WT_REG32(WT_ELS_BASE_S + 0x00u) +#define WT_ELS_CTRL WT_REG32(WT_ELS_BASE_S + 0x04u) +#define WT_ELS_CMDCFG0 WT_REG32(WT_ELS_BASE_S + 0x08u) +#define WT_ELS_DMA_RES0 WT_REG32(WT_ELS_BASE_S + 0x38u) +#define WT_ELS_DMA_RES0_LEN WT_REG32(WT_ELS_BASE_S + 0x3Cu) +#define WT_ELS_ERR_STATUS WT_REG32(WT_ELS_BASE_S + 0x4Cu) +#define WT_ELS_ERR_STATUS_CLR WT_REG32(WT_ELS_BASE_S + 0x50u) +#define WT_ELS_STATUS_BUSY (1u << 0) +#define WT_ELS_STATUS_IRQ (1u << 1) +#define WT_ELS_STATUS_ERR (1u << 2) +#define WT_ELS_STATUS_LOCKED (1u << 16) +#define WT_ELS_CTRL_EN (1u << 0) +#define WT_ELS_CTRL_START (1u << 1) +#define WT_ELS_CTRL_RESET (1u << 2) +#define WT_ELS_CTRL_CMD(cmd) (((uint32_t)(cmd) & 0x1Fu) << 3) +#define WT_ELS_CMD_RND_REQ 24u +#define WT_ELS_ERR_STATUS_CLR_ALL (1u << 0) +/* RND_REQ fills the DMA result buffer in whole words. */ +#define WT_ELS_RND_ALIGN 4u + +#endif /* WOLFTRUST_MCXN947_REGS_H */ diff --git a/port/mcxn947/memory_map.h b/port/mcxn947/memory_map.h new file mode 100644 index 000000000..4c011e185 --- /dev/null +++ b/port/mcxn947/memory_map.h @@ -0,0 +1,109 @@ +/* memory_map.h + * + * 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 . + */ + +#ifndef WOLFTRUST_FW_MCXN947_MEMORY_MAP_H +#define WOLFTRUST_FW_MCXN947_MEMORY_MAP_H + +/* MCXN947 CPU0. Bit 28 of every address selects the Secure alias: the 2 MiB + * internal flash is 0x00000000 (NS) / 0x10000000 (S), RAMA-H is 0x20000000 + * (NS) / 0x30000000 (S), peripherals 0x40000000 (NS) / 0x50000000 (S). */ +#ifndef WT_SECURE_FLASH_BASE +#define WT_SECURE_FLASH_BASE 0x10040000u +#endif +#ifndef WT_SECURE_FLASH_SIZE +#define WT_SECURE_FLASH_SIZE 0x00040000u +#endif +#ifndef WT_SECURE_IMAGE_HEADER_SIZE +#define WT_SECURE_IMAGE_HEADER_SIZE 0x400u +#endif +#define WT_FLASH_S_BASE WT_SECURE_FLASH_BASE +#define WT_FLASH_S_SIZE WT_SECURE_FLASH_SIZE +#define WT_FLASH_IMAGE_BASE (WT_FLASH_S_BASE + WT_SECURE_IMAGE_HEADER_SIZE) +/* The SG veneers sit in the image's second 1 KiB, as on the STM32H5: the + * flash is in the Code region, where the IDAU honours an SAU NSC window. */ +#define WT_NSC_BASE (WT_FLASH_IMAGE_BASE + 0x00000400u) +#define WT_NSC_END (WT_NSC_BASE + 0x000003FFu) + +#define WT_FLASH_NS_BASE 0x00000000u +#define WT_FLASH_S_ALIAS_BASE 0x10000000u +#define WT_FLASH_SIZE 0x00200000u +#define WT_FLASH_TO_S_ALIAS(address) \ + ((address) - WT_FLASH_NS_BASE + WT_FLASH_S_ALIAS_BASE) +#ifndef WT_GUEST0_FLASH_BASE +#define WT_GUEST0_FLASH_BASE 0x00080000u +#endif +#ifndef WT_GUEST1_FLASH_BASE +#define WT_GUEST1_FLASH_BASE 0x00100000u +#endif +#ifndef WT_GUEST0_FLASH_SIZE +#define WT_GUEST0_FLASH_SIZE 0x00080000u +#endif +#ifndef WT_GUEST1_FLASH_SIZE +#define WT_GUEST1_FLASH_SIZE 0x00040000u +#endif + +/* Flash geometry: 8 KiB erase sectors, 16-byte ECC phrases that are written + * once per erase, 128-byte pages the ROM API programs from. Every store below + * the Secure image lives in bank 1, so the image keeps executing from bank 0 + * while the ROM API programs them. */ +#define WT_FLASH_SECTOR_SIZE 0x00002000u +#define WT_FLASH_PAGE_SIZE 0x00000080u +#define WT_FLASH_PHRASE_SIZE 0x00000010u +#define WT_FLASH_BANK_SIZE 0x00100000u +#define WT_HSM_NVM_FLASH_BASE_NS 0x001E0000u +#define WT_HSM_NVM_FLASH_BASE_S 0x101E0000u +#define WT_HSM_NVM_FLASH_SIZE 0x00004000u + +#define WT_CONF_NVM_FLASH_BASE_NS 0x001E4000u +#define WT_CONF_NVM_FLASH_BASE_S 0x101E4000u +#define WT_CONF_NVM_FLASH_SIZE 0x00002000u + +/* Non-secure guest RAM: two 96 KiB windows at the bottom of RAMA-H. Their + * Secure alias holds the first loader's RAM, dead after the jump, which + * wt_platform_init zeroes before any guest runs. */ +#define WT_RAM_NS_BASE 0x20000000u +#define WT_PLATFORM_GUEST_STACK_WINDOW_BASE WT_RAM_NS_BASE +#define WT_PLATFORM_GUEST_STACK_WINDOW_SIZE 0x00030000u +#define WT_GUEST0_RAM_BASE 0x20000000u +#define WT_GUEST1_RAM_BASE 0x20018000u +#define WT_GUEST_RAM_SIZE 0x00018000u + +/* Secure runtime RAM: the top 220 KiB of RAMA-H through its Secure alias, + * one 4 KiB page above the wolfBoot handoff record (56 bytes, the page is + * cleared once consumed); the level 3 bands are offsets from this base. */ +#define WT_RAM_S_BASE 0x30031000u +#define WT_RAM_S_SIZE 0x00037000u +#define WT_BOOT_HANDOFF_ADDRESS 0x30030000u +#define WT_L3_SPM_RAM_SIZE 0x00015800u +#define WT_SP_HSM_DATA_SIZE 0x00008000u + +#include "../common/armv8m/l3_layout.h" + +/* wolfBoot update partition (Secure alias, bank 1). */ +#define WT_FWU_UPDATE_FLASH_BASE_S 0x10180000u +#define WT_FWU_UPDATE_FLASH_SIZE 0x00040000u + +#define WT_SHARED_STATUS_ADDR 0x20000000u + +#ifndef WT_PLATFORM_CORE_CLOCK_HZ +#define WT_PLATFORM_CORE_CLOCK_HZ 150000000u +#endif + +#endif diff --git a/port/mcxn947/partitions.c b/port/mcxn947/partitions.c new file mode 100644 index 000000000..797b97d07 --- /dev/null +++ b/port/mcxn947/partitions.c @@ -0,0 +1,502 @@ +/* partitions.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 . + */ + +#include "wolftrust/arch/armv8m/context.h" +#include "wolftrust/guest_verify.h" +#include "wolftrust/partition.h" +#include "memory_map.h" +#include "mcxn947_regs.h" + +#include + +/* Pinned guest-measurement slot: the image patcher stamps the guest digests + * by magic before wolfBoot signs the image, so the pins share its root of + * trust; an unpatched slot advertises zero records and fails launch closed. */ +#define WT_GUEST_MEAS_SLOT_MAGIC_LEN 16u +#define WT_GUEST_MEAS_SLOT_UNPATCHED 0xFFFFFFFFu + +/* Enforcement the port provides (WT-PORT-0008): the fabric filter is the + * per-dispatch SAU window, as on the MIMXRT700, isolating CPU0's guests only; + * CPU1, DMA and other bus masters are not fenced (no TRDC/AHBSC rules yet). */ +#define WT_MCXN947_PORT_CAPABILITIES \ + (WT_PORT_CAPABILITY_VECTOR_READ_ALIAS | \ + WT_PORT_CAPABILITY_NS_DOMAIN_PROGRAMMING | \ + WT_PORT_CAPABILITY_TZ_FILTER) +#if defined(__ARM_EABI__) +#define WT_GUEST_MEAS_SECTION \ + __attribute__((section(".wt_guest_meas"), used, aligned(4))) +#else +#define WT_GUEST_MEAS_SECTION +#endif + +typedef struct wt_guest_meas_slot { + uint8_t magic[WT_GUEST_MEAS_SLOT_MAGIC_LEN]; + uint32_t count; + wt_guest_measurement_t records[WT_GUEST_MEAS_MAX_RECORDS]; +} wt_guest_meas_slot_t; + +static const wt_guest_meas_slot_t g_guest_meas_slot WT_GUEST_MEAS_SECTION = { + { 0x57u, 0x54u, 0x47u, 0x4Du, 0x45u, 0x41u, 0x53u, 0x31u, + 0xA5u, 0x3Cu, 0x96u, 0xE1u, 0x78u, 0x0Fu, 0xB2u, 0x4Bu }, + WT_GUEST_MEAS_SLOT_UNPATCHED, + { { 0u, 0u, 0u, { 0u } } } +}; + +static wt_guest_measurement_t g_guest_meas_copy[WT_GUEST_MEAS_MAX_RECORDS]; + +const wt_guest_measurement_t* wt_platform_guest_measurements(size_t* count) +{ + /* The slot is stamped into the binary after linking, so this const + * object's initializer lies: every field is loaded through volatile, or + * the optimizer folds the unpatched zeros into the records it returns. */ + const volatile uint32_t* slot_count = &g_guest_meas_slot.count; + const volatile uint8_t* src = + (const volatile uint8_t*)g_guest_meas_slot.records; + uint8_t* dst = (uint8_t*)g_guest_meas_copy; + uint32_t records = *slot_count; + size_t bytes; + size_t i; + + if (count == NULL) { + return NULL; + } + + if (records == WT_GUEST_MEAS_SLOT_UNPATCHED || + records > WT_GUEST_MEAS_MAX_RECORDS) { + *count = 0u; + return NULL; + } + + bytes = (size_t)records * sizeof(wt_guest_measurement_t); + for (i = 0u; i < bytes; i++) { + dst[i] = src[i]; + } + *count = (size_t)records; + return g_guest_meas_copy; +} + +#ifndef WT_TIMESLICE_MS +#define WT_TIMESLICE_MS 2U +#endif + +#ifndef WT_SHARED_UART +#define WT_SHARED_UART 0 +#endif + +/* Both guests share LPUART4 (LP_FLEXCOMM4), the FRDM board's debug console. */ +#define WT_USART_REGION_SIZE 0x00001000U +#define WT_GUEST0_USART_BASE WT_LPUART4_BASE_NS +#define WT_GUEST1_USART_BASE WT_LPUART4_BASE_NS +/* Initial restore runs from a Secure exception and returns to a Non-secure + * Thread/MSP frame; ES stays set because the exception was taken to Secure + * state (clearing it trips INVPC on Armv8-M silicon). */ +#define WT_EXC_RETURN_NS_THREAD_MSP_FROM_SECURE 0xFFFFFFB9U + +static wt_guest_config_t g_partition_configs[] = { + { + .guest_id = 0U, + .name = "guest-a", + .vector_table = WT_GUEST0_FLASH_BASE, + .initial_psp_ns = 0x00000000U, + .initial_msp_ns = WT_GUEST0_RAM_BASE + WT_GUEST_RAM_SIZE, + .irq_mask = { + .words = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U} + }, + .memory_windows = { + {WT_GUEST0_FLASH_BASE, WT_GUEST0_FLASH_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC, WT_RESOURCE_SHARE_NONE}, + {WT_GUEST0_RAM_BASE, WT_GUEST_RAM_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE | WT_MEM_ATTR_RESTART_CLEAR, + WT_RESOURCE_SHARE_NONE} + }, + .memory_window_count = 2U, + .memory_regions = { + {WT_GUEST0_FLASH_BASE, WT_GUEST0_FLASH_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC}, + {WT_GUEST0_RAM_BASE, WT_GUEST_RAM_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE}, + {WT_GUEST0_USART_BASE, WT_USART_REGION_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE | WT_MEM_ATTR_DEVICE}, + /* NSC window: NS guests must be able to fetch the SG veneers. + * Per ARMv8-M, NSC fetches succeed when SAU marks them NSC AND + * the NS MPU grants execute permission. */ + {WT_NSC_BASE, (WT_NSC_END - WT_NSC_BASE + 1U), + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC} + }, + .memory_region_count = 4U, + .restart_policy = { + .restart_limit = 3U, + .restart_window_ticks = 64U, + .initial_delay_ticks = 1U + }, + .initial_state = WT_GUEST_READY, + .timeslice_ms = WT_TIMESLICE_MS, + .port = { + .required_capabilities = WT_MCXN947_PORT_CAPABILITIES, + .provided_capabilities = WT_MCXN947_PORT_CAPABILITIES, + .vector_read_address = + WT_FLASH_TO_S_ALIAS(WT_GUEST0_FLASH_BASE) + } + }, +#if WT_MAX_GUESTS > 1 + { + .guest_id = 1U, + .name = "guest-b", + .vector_table = WT_GUEST1_FLASH_BASE, + .initial_psp_ns = 0x00000000U, + .initial_msp_ns = WT_GUEST1_RAM_BASE + WT_GUEST_RAM_SIZE, + .irq_mask = { + .words = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U} + }, + .memory_windows = { + {WT_GUEST1_FLASH_BASE, WT_GUEST1_FLASH_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC, WT_RESOURCE_SHARE_NONE}, + {WT_GUEST1_RAM_BASE, WT_GUEST_RAM_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE | WT_MEM_ATTR_RESTART_CLEAR, + WT_RESOURCE_SHARE_NONE} + }, + .memory_window_count = 2U, + .memory_regions = { + {WT_GUEST1_FLASH_BASE, WT_GUEST1_FLASH_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC}, + {WT_GUEST1_RAM_BASE, WT_GUEST_RAM_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE}, + {WT_GUEST1_USART_BASE, WT_USART_REGION_SIZE, + WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE | WT_MEM_ATTR_DEVICE}, + /* NSC window: see guest-a above. */ + {WT_NSC_BASE, (WT_NSC_END - WT_NSC_BASE + 1U), + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC} + }, + .memory_region_count = 4U, + .restart_policy = { + .restart_limit = 3U, + .restart_window_ticks = 64U, + .initial_delay_ticks = 1U + }, + .initial_state = WT_GUEST_READY, + .timeslice_ms = WT_TIMESLICE_MS, + .port = { + .required_capabilities = WT_MCXN947_PORT_CAPABILITIES, + .provided_capabilities = WT_MCXN947_PORT_CAPABILITIES, + .vector_read_address = + WT_FLASH_TO_S_ALIAS(WT_GUEST1_FLASH_BASE) + } + } +#endif +}; + +static wt_guest_runtime_t g_partition_runtime[ + sizeof(g_partition_configs) / sizeof(g_partition_configs[0]) +]; +/* Port-owned concrete context storage; the neutral runtime holds pointers. */ +static wt_guest_context_t g_partition_contexts[ + sizeof(g_partition_configs) / sizeof(g_partition_configs[0]) +]; + +static void wt_partitions_wire_contexts(void) +{ + size_t i; + + for (i = 0; i < sizeof(g_partition_runtime) / + sizeof(g_partition_runtime[0]); ++i) { + g_partition_runtime[i].context = &g_partition_contexts[i]; + } +} +static const wt_profile_capabilities_t g_profile_capabilities = { + .capabilities = WT_CAPABILITY_SECURITY_STATE | + WT_CAPABILITY_PRIVILEGE_STATE | + WT_CAPABILITY_ROT_ISOLATION | + WT_CAPABILITY_DOMAIN_ISOLATION | + WT_CAPABILITY_MEMORY_PROTECTION | + WT_CAPABILITY_INTERRUPT_ISOLATION | + WT_CAPABILITY_RESTART, + .max_domains = 11U, + .max_memory_resources_per_domain = 3U, + .max_interrupts_per_domain = 1U, +}; +static uintptr_t g_bound_exec_bases[ + sizeof(g_partition_configs) / sizeof(g_partition_configs[0]) +]; +static size_t g_bound_exec_sizes[ + sizeof(g_partition_configs) / sizeof(g_partition_configs[0]) +]; + +static bool wt_guest_reset_handler_valid(const wt_guest_config_t* config, + uintptr_t resetHandler) +{ + uintptr_t entry = resetHandler & ~(uintptr_t)1u; + + return ((resetHandler & 1u) != 0u) && + (entry >= config->vector_table) && + (entry < (config->vector_table + config->memory_windows[0].size)); +} + +static uintptr_t wt_guest_reset_handler(const wt_guest_config_t* config) +{ + uintptr_t resetHandler; + + resetHandler = ((const uint32_t*)config->port.vector_read_address)[1]; + if (!wt_guest_reset_handler_valid(config, resetHandler)) { + /* Read the vector through the Secure alias first; fall back to the + * Non-secure alias only after rejecting the value as an invalid guest + * entry (the IDAU may return zero through the Secure alias). */ + resetHandler = ((const uint32_t*)config->vector_table)[1]; + } + + if (!wt_guest_reset_handler_valid(config, resetHandler)) { + return 0u; + } + + return resetHandler; +} + +const wt_guest_config_t* wt_partitions_config_table(size_t* count) +{ + if (count != NULL) { + *count = sizeof(g_partition_configs) / sizeof(g_partition_configs[0]); + } + + return g_partition_configs; +} + +wt_guest_runtime_t* wt_partitions_runtime_table(size_t* count) +{ + wt_partitions_wire_contexts(); + if (count != NULL) { + *count = sizeof(g_partition_runtime) / sizeof(g_partition_runtime[0]); + } + + return g_partition_runtime; +} + +const wt_profile_capabilities_t* wt_partitions_profile_capabilities(void) +{ + return &g_profile_capabilities; +} + +static const wt_domain_descriptor_t* wt_partition_manifest_domain( + const wt_system_manifest_t* manifest, wt_domain_id_t id) +{ + size_t i; + + for (i = 0U; i < manifest->domain_count; ++i) { + if (manifest->domains[i].id == id) { + return &manifest->domains[i]; + } + } + + return NULL; +} + +int wt_partitions_bind_manifest(const wt_system_manifest_t* manifest) +{ + size_t count; + size_t i; + + if (manifest == NULL || manifest->domains == NULL) { + return -1; + } + + count = sizeof(g_partition_configs) / sizeof(g_partition_configs[0]); + for (i = 0U; i < count; ++i) { + wt_guest_config_t* config = &g_partition_configs[i]; + const wt_domain_descriptor_t* domain; + + /* Domain zero is the SPM; each guest is a Non-secure application + * domain one higher in the generated manifest. The Secure Partitions + * (crypto, attestation) are separate secure domains above the guests + * and are not bound to a guest here. */ + if (config->guest_id == WT_DOMAIN_ID_INVALID) { + return -1; + } + domain = wt_partition_manifest_domain(manifest, + (wt_domain_id_t)config->guest_id + 1U); + /* Guests launch privileged (CONTROL_NS = 0), so declare PRIVILEGED. */ + if (domain == NULL || + domain->domain_class != WT_DOMAIN_CLASS_NONSECURE_APPLICATION || + domain->security_state != WT_SECURITY_STATE_NONSECURE || + domain->privilege_state != WT_PRIVILEGE_STATE_PRIVILEGED || + domain->restart_policy.action != WT_RESTART_ACTION_DOMAIN) { + return -1; + } + if (wt_partition_validate_port_binding(config, domain) != + WT_PORT_VALID) { + return -1; + } + + /* The generated manifest's restart policy is authoritative: the SPM + * honors the declared limits, not a compiled-in copy. */ + config->restart_policy.restart_limit = + domain->restart_policy.restart_limit; + config->restart_policy.restart_window_ticks = + domain->restart_policy.restart_window_ticks; + config->restart_policy.initial_delay_ticks = + domain->restart_policy.initial_delay_ticks; + /* The manifest's declared initial lifecycle drives the runtime state: + * an NS application declared READY boots runnable; STOPPED stays out + * of the schedule until an explicit lifecycle action. */ + config->initial_state = (wt_guest_state_t)domain->initial_lifecycle; + config->launch_required = domain->launch_required; + config->launch_min_version = domain->launch_min_version; + + /* Rebuild the entire NS MPU table from declared policy: memory + * resources become caller-band windows, the declared console UART is + * the only device grant accepted (pinned to the compiled address), + * and unused slots are cleared so no static grant survives. */ + if (domain->memory_resource_count > WT_MAX_MEMORY_REGIONS - 1U) { + return -1; + } + size_t window_count = 0U; + size_t region_count = 0U; + for (size_t resource = 0U; + resource < domain->memory_resource_count; ++resource) { + const wt_memory_resource_t* manifest_resource = + &domain->memory_resources[resource]; + uint32_t mpu_attributes = manifest_resource->attributes & + (WT_MEM_ATTR_READ | WT_MEM_ATTR_WRITE | WT_MEM_ATTR_EXEC | + WT_MEM_ATTR_DEVICE); + + if ((manifest_resource->attributes & WT_MEM_ATTR_DEVICE) != 0U) { + uintptr_t usart_base = (config->guest_id == 0U) ? + WT_GUEST0_USART_BASE : WT_GUEST1_USART_BASE; + + if (manifest_resource->base != usart_base || + manifest_resource->size != WT_USART_REGION_SIZE) { + return -1; + } + } + else { + if (window_count >= config->memory_window_count) { + return -1; + } + config->memory_windows[window_count] = *manifest_resource; + window_count++; + } + config->memory_regions[region_count].base = manifest_resource->base; + config->memory_regions[region_count].size = manifest_resource->size; + config->memory_regions[region_count].attributes = mpu_attributes; + region_count++; + } + if (window_count != config->memory_window_count) { + return -1; + } + /* NSC veneer fetch window: a platform policy object every NS domain + * needs to reach the SG gateway (SAU NSC + NS MPU execute). */ + config->memory_regions[region_count].base = WT_NSC_BASE; + config->memory_regions[region_count].size = + WT_NSC_END - WT_NSC_BASE + 1U; + config->memory_regions[region_count].attributes = + WT_MEM_ATTR_READ | WT_MEM_ATTR_EXEC; + region_count++; + for (size_t clear = region_count; clear < WT_MAX_MEMORY_REGIONS; + ++clear) { + config->memory_regions[clear].base = 0U; + config->memory_regions[clear].size = 0U; + config->memory_regions[clear].attributes = 0U; + } + config->memory_region_count = region_count; + + if (domain->entry_point == 0U || + domain->interrupt_resource_count > WT_MAX_IRQ_WORDS * 32U || + (domain->interrupt_resource_count != 0U && + domain->interrupt_resources == NULL)) { + return -1; + } + memset(&config->irq_mask, 0, sizeof(config->irq_mask)); + for (size_t interrupt = 0U; + interrupt < domain->interrupt_resource_count; ++interrupt) { + uint32_t irq = domain->interrupt_resources[interrupt].interrupt; + size_t word = irq / 32U; + uint32_t bit = irq % 32U; + + if (word >= WT_MAX_IRQ_WORDS) { + return -1; + } + config->irq_mask.words[word] |= (uint32_t)1U << bit; + } + g_bound_exec_bases[i] = 0U; + g_bound_exec_sizes[i] = 0U; + for (size_t executable = 0U; + executable < domain->memory_resource_count; ++executable) { + const wt_memory_resource_t* resource = + &domain->memory_resources[executable]; + + if ((resource->attributes & WT_MEM_ATTR_EXEC) != 0U) { + g_bound_exec_bases[i] = resource->base; + g_bound_exec_sizes[i] = resource->size; + break; + } + } + if (g_bound_exec_sizes[i] == 0U) { + return -1; + } + + if (domain->stack_base > (uintptr_t)-1 - domain->stack_size) { + return -1; + } + config->initial_msp_ns = domain->stack_base + domain->stack_size; + } + + return 0; +} + +void wt_partition_reset_runtime(const wt_guest_config_t* config, + wt_guest_runtime_t* runtime) +{ + uint32_t restart_count; + uint32_t first_restart_tick; + + if (config == NULL || runtime == NULL) { + return; + } + + restart_count = runtime->restart_count; + first_restart_tick = runtime->first_restart_tick; + memset(runtime, 0, sizeof(*runtime)); + /* The memset wipes the port-wired context pointer; rewire by table + * index and zero the concrete context storage instead. */ + runtime->context = + &g_partition_contexts[(size_t)(runtime - g_partition_runtime)]; + memset(runtime->context, 0, sizeof(*runtime->context)); + runtime->restart_count = restart_count; + runtime->first_restart_tick = first_restart_tick; + runtime->state = config->initial_state; + runtime->context->psp_ns = config->initial_psp_ns; + runtime->context->msp_ns = config->initial_msp_ns; + runtime->context->vector_table_ns = config->vector_table; + /* Reset PC is the guest's reset-handler pointer at vector[1]; the slot + * already carries the Thumb bit. wt_jump_to_ns strips it before BXNS. + * Read via the Secure alias of the flash: a Secure-side read of the NS + * alias may return zero, so vector_read_address is the 0x1... alias. */ + runtime->context->pc = wt_guest_reset_handler(config); + if (config->guest_id >= + sizeof(g_bound_exec_bases) / sizeof(g_bound_exec_bases[0]) || + g_bound_exec_sizes[config->guest_id] == 0U || + runtime->context->pc < g_bound_exec_bases[config->guest_id] || + runtime->context->pc >= g_bound_exec_bases[config->guest_id] + + g_bound_exec_sizes[config->guest_id]) { + runtime->context->pc = 0U; + return; + } + runtime->context->lr = 0U; + runtime->context->xpsr = 0x01000000U; + runtime->context->exc_return = WT_EXC_RETURN_NS_THREAD_MSP_FROM_SECURE; + runtime->context->frame_stacked = false; +} diff --git a/port/mcxn947/platform_mcxn947.c b/port/mcxn947/platform_mcxn947.c new file mode 100644 index 000000000..be378b062 --- /dev/null +++ b/port/mcxn947/platform_mcxn947.c @@ -0,0 +1,361 @@ +/* platform_mcxn947.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 . + */ + +#include "wolftrust/platform.h" +#include "wolftrust/arch.h" +#include "wolftrust/guest_verify.h" +#include "wolftrust/monitor.h" +#include "wolftrust/arch/armv8m/context.h" +#include "wolftrust/arch/armv8m/armv8m.h" +#include "wolftrust/arch/armv8m/core_regs.h" + +#include +#include +#include +#include + +#include "memory_map.h" +#include "mcxn947_regs.h" + +#include "wolftrust/fabric_windows.h" + +#include "wolftrust/ffm_gateway.h" +#include "wolftrust/spm_transport.h" +#include "wolftrust/ffm.h" +#include "wolftrust/ffm_boot.h" +#include "wolftrust/ffm_domain.h" +#include "psa_manifest/pid.h" +#include "wolftrust/services/hsm.h" +#include "wolftrust/boot_handoff.h" +#include "wolftrust/services/initial_attestation.h" +#include "wolftrust/arch/armv8m/cmse.h" +#include "wolftrust/sched/tasklet.h" + +static volatile uint32_t g_secure_service_depth; +static volatile uint32_t g_hsm_wait_skip_count; + +/* The SAU must agree with the IDAU or the more secure attribute wins. Guest + * RAM is not a static region: each dispatch opens only the arriving guest's + * window (dynamic regions below), so the peer's stays Secure while it runs. */ +static const wt_armv8m_sau_region_t g_sau_regions[] = { + { WT_GUEST0_FLASH_BASE, + WT_GUEST1_FLASH_BASE + WT_GUEST1_FLASH_SIZE - 1u, false }, + { WT_NSC_BASE, WT_NSC_END, true }, + /* Only the console: the manifest accepts no other Non-secure device grant, + * so a guest with its MPU off still faults on every other peripheral. */ + { WT_LPUART4_BASE_NS, WT_LPUART4_BASE_NS + 0x00000FFFu, false }, +}; + +/* Dynamic SAU regions for the per-dispatch guest RAM windows, above the static + * table (three entries above). CM33 implements eight SAU regions; a guest + * declares one writable RAM window, so this headroom covers every manifest. */ +#define WT_SAU_DYN_FIRST 3u +#define WT_SAU_DYN_LAST 6u + +static uint32_t g_sau_dyn_next; + +/* Secure-side MPU whitelist, programmed with PRIVDEFENA off by the arch + * layer and replayed after every Secure Partition domain. */ +static const wt_armv8m_mpu_region_t g_mpu_s_whitelist[] = { + /* Region 0: secure image RX (vectors, NSC stubs, .text, .rodata). */ + { WT_FLASH_S_BASE, WT_FLASH_S_BASE + WT_FLASH_S_SIZE - 1u, + WT_MPU_RBAR_AP_RO | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NORMAL }, + + /* Region 1: the Secure alias of everything above the image (guest + * images, update and swap partitions, the wolfHSM and conformance NVM + * stores): read-only XN, written only through the ROM flash API. */ + { WT_FLASH_TO_S_ALIAS(WT_GUEST0_FLASH_BASE), + WT_FLASH_S_ALIAS_BASE + WT_FLASH_SIZE - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RO | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NOCACHE }, + + /* Region 2: secure RAM RW-NX from the boot handoff record up. */ + { WT_BOOT_HANDOFF_ADDRESS, + WT_RAM_S_BASE + WT_RAM_S_SIZE - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NORMAL }, + + /* Region 3: NS guest RAM RW-NX through the 0x20000000 alias (HSM + * transport buffers and fault-response CSRs). */ + { WT_RAM_NS_BASE, + WT_RAM_NS_BASE + WT_PLATFORM_GUEST_STACK_WINDOW_SIZE - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NORMAL }, + + /* Region 4: flash NS alias, XN. Writable because the ROM flash driver + * programs a phrase by storing it through this alias for the FMU to + * capture; outside a ROM program sequence a store cannot alter flash. */ + { WT_FLASH_NS_BASE, WT_FLASH_NS_BASE + WT_FLASH_SIZE - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NOCACHE }, + + /* Region 5: peripheral aperture, both aliases (SYSCON, FMU, ELS, + * LPUART, TRDC, AHBSC). */ + { WT_PERIPH_NS_BASE, WT_PERIPH_S_BASE + WT_PERIPH_ALIAS_SIZE - 1u, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW, + WT_MPU_RLAR_ATTRIDX_DEVICE }, + + /* Region 6: Cortex-M33 private peripheral bus. */ + { 0xE0000000u, 0xE00FFFFFu, + WT_MPU_RBAR_XN | WT_MPU_RBAR_AP_RW, + WT_MPU_RLAR_ATTRIDX_DEVICE }, + + /* Region 7: the boot ROM through its Secure alias, privileged + * read-execute: the flash API the SPM programs the stores with. */ + { WT_ROM_BASE_S, WT_ROM_BASE_S + WT_ROM_SIZE - 1u, + WT_MPU_RBAR_AP_RO | WT_MPU_RBAR_SH_INNER, + WT_MPU_RLAR_ATTRIDX_NORMAL }, +}; + +/* No hardware guest-flash fence is read back yet, so a guest window is never + * reported as protected; a WT_GUEST_FLASH_WRP=1 build refuses every guest. */ +int wt_platform_guest_flash_wrp_ok(uintptr_t window_base, size_t window_size) +{ + (void)window_base; + (void)window_size; + return WT_GUEST_VERIFY_ERROR_WRP; +} + +void wt_platform_init(void) +{ + wt_ffm_gateway_install(); + WT_SCB_VTOR_S = WT_FLASH_IMAGE_BASE; + wt_armv8m_sau_init(g_sau_regions, + sizeof(g_sau_regions) / sizeof(g_sau_regions[0])); + wt_armv8m_mpu_s_init(g_mpu_s_whitelist, + sizeof(g_mpu_s_whitelist) / + sizeof(g_mpu_s_whitelist[0])); + wt_arch_init(); + /* The guest windows alias the first loader's RAM, dead after the jump. */ + wt_arch_zero_guest_memory(WT_GUEST0_RAM_BASE, WT_GUEST_RAM_SIZE); + wt_arch_zero_guest_memory(WT_GUEST1_RAM_BASE, WT_GUEST_RAM_SIZE); + g_secure_service_depth = 0u; + g_hsm_wait_skip_count = 0u; +} + +#if defined(WT_LAUNCH_DEBUG) +/* Per-guest boot launch-verification result, read over the debug port. */ +volatile int32_t g_wt_launch_debug[2] __attribute__((used)); + +void wt_platform_launch_debug(int code, uint32_t guest) +{ + if (guest < (sizeof(g_wt_launch_debug) / sizeof(g_wt_launch_debug[0]))) { + g_wt_launch_debug[guest] = (int32_t)code; + } +} +#endif + +/* SAU guest-isolation back-end: close_all drops every dynamic region (guest + * RAM falls to the SAU default, Secure), open_window marks one window + * Non-secure, so a store into the peer's window faults and is contained. */ +static void wt_sau_close_all(void) +{ + uint32_t rnr; + + for (rnr = WT_SAU_DYN_FIRST; rnr <= WT_SAU_DYN_LAST; ++rnr) { + wt_armv8m_sau_program_region(rnr, 0u, 0u, false, false); + } + g_sau_dyn_next = WT_SAU_DYN_FIRST; +} + +static void wt_sau_open_window(uintptr_t base, size_t size) +{ + if (g_sau_dyn_next <= WT_SAU_DYN_LAST) { + wt_armv8m_sau_program_region(g_sau_dyn_next, (uint32_t)base, + (uint32_t)(base + size - 1u), false, true); + ++g_sau_dyn_next; + } +} + +static void wt_sau_commit(void) +{ + wt_dsb(); + wt_isb(); +} + +void wt_platform_program_memory_windows(const wt_memory_window_t* windows, + size_t count) +{ + static const wt_fabric_windows_t fabric = { + WT_RAM_NS_BASE, + WT_RAM_NS_BASE + WT_PLATFORM_GUEST_STACK_WINDOW_SIZE, + wt_sau_close_all, + wt_sau_open_window, + wt_sau_commit + }; + + wt_fabric_apply_windows(&fabric, windows, count); +} + +#if defined(WT_CONFORMANCE) && (WT_CONFORMANCE == 1) +/* PAL interrupt source: the unprivileged DRIVER partition asks for its line + * to fire, so the privileged side pends it in the NVIC. */ +void wt_conf_uart_irq_set(int on) +{ + if (on != 0) { +#if WT_CONF_IRQ >= 32u + WT_NVIC_ISPR1 = (1u << (WT_CONF_IRQ - 32u)); +#else + WT_NVIC_ISPR0 = (1u << WT_CONF_IRQ); +#endif + } + else { + wt_arch_secure_irq_disable(WT_CONF_IRQ); + } +} + +void WT_CONF_IRQ_HANDLER(void) +{ + wt_spm_conf_irq(WT_CONF_IRQ); +} + +#endif + +void wt_platform_log_fault(wt_guest_id_t guest_id, + wt_fault_reason_t reason, + uintptr_t fault_address, + uintptr_t pc) +{ + (void)guest_id; + (void)reason; + wt_armv8m_note_fault(fault_address, pc); +} + +void wt_platform_all_guests_faulted(void) +{ + __asm volatile("bkpt #0x7D"); + for (;;) { + __asm volatile("wfi"); + } +} + +void wt_platform_panic(void) +{ + __asm volatile("bkpt #0x7E"); + for (;;) { + } +} + +void wt_platform_system_reset(void) +{ + /* Drop the Secure stack limits first: the next boot starts wolfBoot on + * its own stack below MSPLIM_S, and a core that carried the limit over + * the reset would fault its first push. */ + __asm volatile("movs r0, #0\n msr msplim, r0\n msr psplim, r0" ::: "r0"); + wt_dsb(); + WT_SCB_AIRCR_S = WT_SCB_AIRCR_SYSRESETREQ; + wt_dsb(); + for (;;) { + } +} + +#if defined(WT_REMEASURE_PROBE) +extern int wt_hsm_flash_remeasure_tamper(uintptr_t secure_base); + +void wt_platform_remeasure_probe(void) +{ + const wt_guest_config_t *configs; + size_t cfg_count; + const wt_memory_window_t *window = NULL; + uint32_t mpu_ctrl; + size_t i; + int r1; + int r2; + + configs = wt_partitions_config_table(&cfg_count); + if (configs != NULL && cfg_count > 0u) { + for (i = 0u; i < configs[0].memory_window_count; i++) { + if ((configs[0].memory_windows[i].attributes & + WT_MEM_ATTR_EXEC) != 0u) { + window = &configs[0].memory_windows[i]; + break; + } + } + } + if (window != NULL) { + int tamper; + + r1 = wt_runtime_verify_guest(0u); + mpu_ctrl = WT_MPU_S_CTRL; + WT_MPU_S_CTRL = 0u; + wt_dsb(); + wt_isb(); + tamper = wt_hsm_flash_remeasure_tamper(WT_FLASH_TO_S_ALIAS(window->base)); + WT_MPU_S_CTRL = mpu_ctrl; + wt_dsb(); + wt_isb(); + r2 = wt_runtime_verify_guest(0u); + /* The verdict needs the tamper to have landed: a driver error must + * not pass as a detected tamper. */ + if (r1 == 0 && tamper == 0 && r2 != 0) { + __asm volatile("bkpt #0x6C"); + } + } + for (;;) { + __asm volatile("wfi"); + } +} +#endif + +#if defined(WT_BOOTUPDATE_PROBE) +#include "wolftrust/services/fwu_service.h" +extern const wt_fwu_backend_t wt_fwu_flash_backend; + +void wt_platform_bootupdate_probe(uint32_t running_version) +{ + uint32_t mpu_ctrl; + int armed = -1; + + if (running_version != 1u) { + return; + } + if (wt_fwu_flash_backend.begin != NULL && + wt_fwu_flash_backend.begin(NULL) == 0 && + wt_fwu_flash_backend.arm != NULL) { + mpu_ctrl = WT_MPU_S_CTRL; + WT_MPU_S_CTRL = 0u; + wt_dsb(); + wt_isb(); + armed = wt_fwu_flash_backend.arm(NULL, 0u, 2u); + WT_MPU_S_CTRL = mpu_ctrl; + wt_dsb(); + wt_isb(); + } + if (armed == 0) { + wt_platform_system_reset(); + } +} +#endif + +#ifdef WT_ENGINE_HSM + +bool wt_platform_secure_service_active(void) +{ + return g_secure_service_depth != 0u; +} + +void wt_platform_note_hsm_wait_skip(wt_guest_id_t guest_id) +{ + (void)guest_id; + g_hsm_wait_skip_count++; +} + +#endif /* WT_ENGINE_HSM */ diff --git a/port/mcxn947/rng_entropy.c b/port/mcxn947/rng_entropy.c new file mode 100644 index 000000000..91e304e68 --- /dev/null +++ b/port/mcxn947/rng_entropy.c @@ -0,0 +1,157 @@ +/* rng_entropy.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 . + */ + +#include +#include +#include +#include + +#include "mcxn947_regs.h" + +#include "wolftrust/spm_transport.h" +#include "wolftrust/spm_gate.h" +#include "wolftrust/arch.h" + +int wolftrust_rng_generate_block(unsigned char *output, unsigned int sz); +int wolftrust_rng_generate_block_direct(unsigned char *output, + unsigned int sz); + +#define WT_ELS_SPINS 0x00200000u +#define WT_ELS_PAGE_WORDS 16u + +static bool s_rng_ready; +/* A request that outlives its deadline may still complete by DMA, so the page + * is static rather than stack and the engine is never asked again. */ +static bool s_rng_failed; +static uint32_t s_els_page[WT_ELS_PAGE_WORDS] + __attribute__((aligned(WT_ELS_RND_ALIGN))); +/* Read over the debug port by the hardware runner. */ +volatile uint32_t g_wt_els_errors __attribute__((used)); +volatile uint32_t g_wt_els_last_status __attribute__((used)); +volatile uint32_t g_wt_els_last_err __attribute__((used)); + +static int wt_els_wait_idle(void) +{ + uint32_t spins = 0u; + uint32_t status = WT_ELS_STATUS; + + while ((status & WT_ELS_STATUS_BUSY) != 0u && spins < WT_ELS_SPINS) { + ++spins; + status = WT_ELS_STATUS; + } + if ((status & (WT_ELS_STATUS_BUSY | WT_ELS_STATUS_ERR | + WT_ELS_STATUS_LOCKED)) != 0u) { + g_wt_els_errors++; + g_wt_els_last_status = status; + g_wt_els_last_err = WT_ELS_ERR_STATUS; + return -1; + } + return 0; +} + +static int wt_els_init(void) +{ + WT_ELS_CTRL = WT_ELS_CTRL_EN; + return wt_els_wait_idle(); +} + +/* One RND_REQ fills a whole page the ELS writes by DMA; words a request does + * not use are wiped, never served to a later request. */ +static int wt_els_rnd_page(uint32_t *page) +{ + int ret; + + ret = wt_els_wait_idle(); + if (ret == 0) { + WT_ELS_DMA_RES0 = (uint32_t)(uintptr_t)page; + WT_ELS_DMA_RES0_LEN = WT_ELS_PAGE_WORDS * (uint32_t)sizeof(uint32_t); + WT_ELS_CMDCFG0 = 0u; + WT_ELS_CTRL = WT_ELS_CTRL_EN | WT_ELS_CTRL_START | + WT_ELS_CTRL_CMD(WT_ELS_CMD_RND_REQ); + ret = wt_els_wait_idle(); + if (ret != 0) { + s_rng_failed = true; + WT_ELS_ERR_STATUS_CLR = WT_ELS_ERR_STATUS_CLR_ALL; + } + } + return ret; +} + +static int wt_els_fill(unsigned char *output, unsigned int sz) +{ + uint32_t *page = s_els_page; + volatile uint32_t* wipe = s_els_page; + unsigned int done = 0u; + unsigned int chunk; + unsigned int i; + int ret = 0; + + while (ret == 0 && done < sz) { + ret = wt_els_rnd_page(page); + if (ret == 0) { + chunk = sz - done; + if (chunk > (unsigned int)sizeof(s_els_page)) { + chunk = (unsigned int)sizeof(s_els_page); + } + (void)memcpy(output + done, page, chunk); + done += chunk; + } + } + for (i = 0u; i < WT_ELS_PAGE_WORDS; ++i) { + wipe[i] = 0u; + } + return ret; +} + +int wolftrust_rng_generate_block_direct(unsigned char *output, unsigned int sz) +{ + if ((output == NULL && sz != 0u) || s_rng_failed) { + return -1; + } + if (!s_rng_ready) { + if (wt_els_init() != 0) { + return -1; + } + s_rng_ready = true; + } + if (sz == 0u) { + return 0; + } + return wt_els_fill(output, sz); +} + +int wolftrust_rng_generate_block(unsigned char *output, unsigned int sz) +{ + /* A confined keystore partition cannot touch the ELS; the SVC + * dispatcher runs the direct half privileged. */ + if (wt_arch_thread_unprivileged()) { + wt_spm_call_t call; + + (void)memset(&call, 0, sizeof(call)); + call.op = WT_SPM_OP_KEYSTORE_ENTROPY; + call.buffer = output; + call.num_bytes = sz; + if (wt_spm_sp_call(&call) != WT_FFM_SUCCESS) { + return -1; + } + return call.ret_int; + } + return wolftrust_rng_generate_block_direct(output, sz); +} diff --git a/port/mcxn947/rom_flash.c b/port/mcxn947/rom_flash.c new file mode 100644 index 000000000..e2a6e3a9a --- /dev/null +++ b/port/mcxn947/rom_flash.c @@ -0,0 +1,280 @@ +/* rom_flash.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 . + */ + +/* MCXN947 internal flash program and erase through the boot ROM's flash API, + * called directly from its API tree so the port needs no vendor SDK. */ + +#include +#include + +#include "memory_map.h" +#include "mcxn947_regs.h" +#include "rom_flash.h" + +/* Layouts derived from MCUXpresso fsl_flash.c (bootloader_tree_t, the flash + * driver interface prefix) and fsl_flash.h (flash_config_t, 72 bytes). */ +typedef struct wt_rom_flash_api { + uint32_t version; + int32_t (*init)(void *config); + int32_t (*erase)(void *config, uint32_t start, uint32_t length, + uint32_t key); + int32_t (*program)(void *config, uint32_t start, uint8_t *src, + uint32_t length); + int32_t (*verify_erase)(void *config, uint32_t start, uint32_t length); + int32_t (*verify_program)(void *config, uint32_t start, uint32_t length, + const uint8_t *expected, uint32_t *failed_address, + uint32_t *failed_data); + int32_t (*get_property)(void *config, uint32_t property, uint32_t *value); +} wt_rom_flash_api_t; + +typedef struct wt_rom_api_tree { + void (*run_bootloader)(void *arg); + uint32_t version; + const char *copyright; + uint32_t reserved0; + const wt_rom_flash_api_t *flash; +} wt_rom_api_tree_t; + +#define WT_ROM_FLASH_CONFIG_WORDS 18u +#define WT_ROM_FLASH_STATUS_OK 0 +#define WT_ROM_FLASH_ERASE_KEY 0x6B65666Cu +#define WT_ROM_FLASH_PROP_SECTOR 0x00u +#define WT_ROM_FLASH_PROP_TOTAL 0x01u + +#ifndef WT_ROM_API_TREE_ADDR +#define WT_ROM_API_TREE_ADDR WT_ROM_API_TREE +#endif +#ifndef WT_ROM_FLASH_VIEW_BASE +#define WT_ROM_FLASH_VIEW_BASE WT_FLASH_S_ALIAS_BASE +#endif + +/* SYSCON LPCAC_CTRL: the code-bus cache in front of the flash is cleared + * after every change so a verify reads the array, not a stale line. */ +#define WT_SYSCON_LPCAC_CTRL WT_REG32(WT_SYSCON_BASE_S + 0x824u) +#define WT_SYSCON_LPCAC_CTRL_CLR (1u << 1) + +static uint32_t g_rom_flash_config[WT_ROM_FLASH_CONFIG_WORDS]; +static int g_rom_flash_ready; +/* Last ROM status, read over the debug port by the harness. */ +volatile uint32_t g_wt_rom_flash_last_status __attribute__((used)); + +static const wt_rom_flash_api_t *wt_rom_flash_api(void) +{ + const wt_rom_api_tree_t *tree = + (const wt_rom_api_tree_t *)(uintptr_t)WT_ROM_API_TREE_ADDR; + + return tree->flash; +} + +static const volatile uint8_t *wt_rom_flash_view(uint32_t offset) +{ + return (const volatile uint8_t *)((uintptr_t)WT_ROM_FLASH_VIEW_BASE + + offset); +} + +static uint32_t wt_rom_flash_irq_mask(void) +{ + uint32_t primask = 0u; + +#if defined(__ARM_EABI__) + __asm volatile("mrs %0, primask\n\tcpsid i" : "=r"(primask) :: "memory"); +#endif + return primask; +} + +static void wt_rom_flash_irq_restore(uint32_t primask) +{ +#if defined(__ARM_EABI__) + __asm volatile("msr primask, %0" :: "r"(primask) : "memory"); +#else + (void)primask; +#endif +} + +static void wt_rom_flash_cache_clear(void) +{ +#if defined(__ARM_EABI__) + /* CLR_LPCAC is sticky: the cache stays cleared until it is written 0. */ + WT_SYSCON_LPCAC_CTRL |= WT_SYSCON_LPCAC_CTRL_CLR; + __asm volatile("dsb sy\n\tisb" ::: "memory"); + WT_SYSCON_LPCAC_CTRL &= ~WT_SYSCON_LPCAC_CTRL_CLR; +#endif +} + +static int wt_rom_flash_range_ok(uint32_t offset, uint32_t size, + uint32_t unit) +{ + if (size == 0u || (offset % unit) != 0u || (size % unit) != 0u) { + return 0; + } + if (offset > WT_FLASH_SIZE || size > WT_FLASH_SIZE - offset) { + return 0; + } + return 1; +} + +static int wt_rom_flash_status(int32_t status) +{ + g_wt_rom_flash_last_status = (uint32_t)status; + return (status == WT_ROM_FLASH_STATUS_OK) ? WT_ROM_FLASH_OK : + WT_ROM_FLASH_ERR_ROM; +} + +int wt_rom_flash_init(void) +{ + const wt_rom_flash_api_t *api; + uint32_t sector = 0u; + uint32_t total = 0u; + uint32_t primask; + size_t i; + int ret = WT_ROM_FLASH_OK; + + if (g_rom_flash_ready != 0) { + return WT_ROM_FLASH_OK; + } + api = wt_rom_flash_api(); + if (api == NULL || api->init == NULL || api->erase == NULL || + api->program == NULL || api->verify_erase == NULL || + api->get_property == NULL) { + return WT_ROM_FLASH_ERR_ROM; + } + for (i = 0u; i < WT_ROM_FLASH_CONFIG_WORDS; i++) { + g_rom_flash_config[i] = 0u; + } + primask = wt_rom_flash_irq_mask(); + ret = wt_rom_flash_status(api->init(g_rom_flash_config)); + if (ret == WT_ROM_FLASH_OK) { + ret = wt_rom_flash_status(api->get_property(g_rom_flash_config, + WT_ROM_FLASH_PROP_SECTOR, + §or)); + } + if (ret == WT_ROM_FLASH_OK) { + ret = wt_rom_flash_status(api->get_property(g_rom_flash_config, + WT_ROM_FLASH_PROP_TOTAL, + &total)); + } + wt_rom_flash_irq_restore(primask); + /* The port's map is only valid on the geometry the ROM reports. */ + if (ret == WT_ROM_FLASH_OK && + (sector != WT_FLASH_SECTOR_SIZE || total != WT_FLASH_SIZE)) { + ret = WT_ROM_FLASH_ERR_GEOMETRY; + } + if (ret == WT_ROM_FLASH_OK) { + g_rom_flash_ready = 1; + } + return ret; +} + +int wt_rom_flash_blank(uint32_t offset, uint32_t size) +{ + const volatile uint8_t *view; + uint32_t i; + + if (offset > WT_FLASH_SIZE || size > WT_FLASH_SIZE - offset) { + return WT_ROM_FLASH_ERR_ARGS; + } + view = wt_rom_flash_view(offset); + for (i = 0u; i < size; i++) { + if (view[i] != 0xFFu) { + return WT_ROM_FLASH_ERR_NOTBLANK; + } + } + return WT_ROM_FLASH_OK; +} + +/* The ROM programs only from a page base (status 101 on silicon for any other + * start) and an all-ones phrase changes nothing, so a phrase is programmed + * from its page base behind all-ones padding, one page per ROM call. */ +int wt_rom_flash_program(uint32_t offset, const uint8_t *data, uint32_t size) +{ + uint8_t page[WT_FLASH_PAGE_SIZE] + __attribute__((aligned(WT_FLASH_PHRASE_SIZE))); + volatile uint8_t *wipe = page; + uint32_t primask; + uint32_t done = 0u; + uint32_t base; + uint32_t head; + uint32_t chunk; + uint32_t i; + int ret; + + if (data == NULL || + !wt_rom_flash_range_ok(offset, size, WT_FLASH_PHRASE_SIZE)) { + return WT_ROM_FLASH_ERR_ARGS; + } + ret = wt_rom_flash_init(); + if (ret == WT_ROM_FLASH_OK) { + ret = wt_rom_flash_blank(offset, size); + } + while (ret == WT_ROM_FLASH_OK && done < size) { + base = (offset + done) & ~(WT_FLASH_PAGE_SIZE - 1u); + head = (offset + done) - base; + chunk = WT_FLASH_PAGE_SIZE - head; + if (chunk > size - done) { + chunk = size - done; + } + for (i = 0u; i < head; i++) { + page[i] = 0xFFu; + } + for (i = 0u; i < chunk; i++) { + page[head + i] = data[done + i]; + } + primask = wt_rom_flash_irq_mask(); + ret = wt_rom_flash_status(wt_rom_flash_api()->program( + g_rom_flash_config, base, page, head + chunk)); + wt_rom_flash_cache_clear(); + wt_rom_flash_irq_restore(primask); + done += chunk; + } + for (i = 0u; i < WT_FLASH_PAGE_SIZE; i++) { + wipe[i] = 0u; + } + return ret; +} + +int wt_rom_flash_erase(uint32_t offset, uint32_t size) +{ + const wt_rom_flash_api_t *api; + uint32_t primask; + int ret; + + if (!wt_rom_flash_range_ok(offset, size, WT_FLASH_SECTOR_SIZE)) { + return WT_ROM_FLASH_ERR_ARGS; + } + ret = wt_rom_flash_init(); + if (ret == WT_ROM_FLASH_OK) { + api = wt_rom_flash_api(); + primask = wt_rom_flash_irq_mask(); + ret = wt_rom_flash_status(api->erase(g_rom_flash_config, offset, size, + WT_ROM_FLASH_ERASE_KEY)); + if (ret == WT_ROM_FLASH_OK) { + ret = wt_rom_flash_status(api->verify_erase(g_rom_flash_config, + offset, size)); + } + wt_rom_flash_cache_clear(); + wt_rom_flash_irq_restore(primask); + } + return ret; +} + +uint32_t wt_rom_flash_last_status(void) +{ + return g_wt_rom_flash_last_status; +} diff --git a/port/mcxn947/rom_flash.h b/port/mcxn947/rom_flash.h new file mode 100644 index 000000000..1642ec6d0 --- /dev/null +++ b/port/mcxn947/rom_flash.h @@ -0,0 +1,41 @@ +/* rom_flash.h + * + * 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 . + */ + +#ifndef WOLFTRUST_MCXN947_ROM_FLASH_H +#define WOLFTRUST_MCXN947_ROM_FLASH_H + +#include + +/* Internal flash through the boot ROM's flash API. Offsets are flash offsets + * (the Secure alias less 0x10000000); the privileged SPM is the only caller. + * A phrase is programmed once per erase: program refuses a non-blank phrase. */ +#define WT_ROM_FLASH_OK 0 +#define WT_ROM_FLASH_ERR_ARGS -1 +#define WT_ROM_FLASH_ERR_ROM -2 +#define WT_ROM_FLASH_ERR_NOTBLANK -3 +#define WT_ROM_FLASH_ERR_GEOMETRY -4 + +int wt_rom_flash_init(void); +int wt_rom_flash_program(uint32_t offset, const uint8_t *data, uint32_t size); +int wt_rom_flash_erase(uint32_t offset, uint32_t size); +int wt_rom_flash_blank(uint32_t offset, uint32_t size); +uint32_t wt_rom_flash_last_status(void); + +#endif /* WOLFTRUST_MCXN947_ROM_FLASH_H */ diff --git a/port/mcxn947/secure.ld b/port/mcxn947/secure.ld new file mode 100644 index 000000000..3fc303319 --- /dev/null +++ b/port/mcxn947/secure.ld @@ -0,0 +1,130 @@ +/* secure.ld + * + * 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 + */ + +ENTRY(Reset_Handler) + +MEMORY { + FLASH (rx) : ORIGIN = (WT_SECURE_FLASH_ORIGIN + WT_SECURE_IMAGE_HEADER_SIZE), + LENGTH = (WT_SECURE_FLASH_SIZE - WT_SECURE_IMAGE_HEADER_SIZE) + INCLUDE secure_l3_memory.ld +} + +INCLUDE secure_l3_symbols.ld +_sidata = LOADADDR(.data); + +SECTIONS { + .vectors (WT_SECURE_FLASH_ORIGIN + WT_SECURE_IMAGE_HEADER_SIZE) : { + KEEP(*(.vectors)) + } > FLASH + + /* Linker-synthesized SG veneers (--cmse-implib) in the image's second + * 1 KiB, the SAU NSC window (WT_NSC_* in memory_map.h); the explicit + * address keeps the section placed even with no .gnu.sgstubs input. */ + .gnu.sgstubs (WT_SECURE_FLASH_ORIGIN + WT_SECURE_IMAGE_HEADER_SIZE + 0x400) : { + *(.gnu.sgstubs) + *(.gnu.sgstubs*) + . = ALIGN(32); + } > FLASH + + .nsc ALIGN(32) : { + KEEP(*(.nsc*)) + } > FLASH + + .text (WT_SECURE_FLASH_ORIGIN + WT_SECURE_IMAGE_HEADER_SIZE + 0x800) : { + _s_secure_text = .; + *(.text*) + *(.glue_7) + *(.glue_7t) + . = ALIGN(32); + _e_secure_text = .; + } > FLASH + + /* Constant data is readable, never executable (WT-FFM-0010): the SP thread + * MPU tables grant [flash, _e_secure_text) RX and the rest of the image + * window read-only XN, so no partition can execute from constant data. */ + .rodata (READONLY) : { + . = ALIGN(4); + KEEP(*(.wt_guest_meas)) + /* wolfCrypt globals that are never written after link stay read-only so + * the crypto partition sees them without owning writable state. */ + *(.data.always_prefetch* .bss.always_prefetch*) + *(.data.sha256DrbgDisabled* .bss.sha256DrbgDisabled*) + *(.rodata*) + *(.ARM.extab* .gnu.linkonce.armextab.*) + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + *(.eh_frame) + } > FLASH + + ASSERT(WT_SECURE_IMAGE_HEADER_SIZE < WT_SECURE_FLASH_SIZE, + "secure image header consumes secure flash region") + ASSERT(. <= (WT_SECURE_FLASH_ORIGIN + WT_SECURE_FLASH_SIZE), + "secure image overflows secure flash region") + ASSERT(SIZEOF(.gnu.sgstubs) + SIZEOF(.nsc) <= 0x400, "NSC window overflow") + + /* Conformance partition .data/.bss, claimed before the general rules so it + * lands in CONFDATA (empty outside conformance builds); the server and + * driver bands stay contiguous MPU carves the gate denies to other SPs. */ + .conf_data : { + . = ALIGN(4); + _sconfdata = .; + EXCLUDE_FILE(*conf_sec_test_supp_*.o *conf_sec_driver_partition.o *conf_sec_pal_driver_intf.o) *conf_sec_*.o(.data .data.*) + . = ALIGN(32); + _s_conf_server_data = .; + *conf_sec_test_supp_*.o(.data .data.* .bss .bss.* COMMON) + . = ALIGN(32); + _e_conf_server_data = .; + _s_conf_driver_data = .; + *conf_sec_driver_partition.o(.data .data.* .bss .bss.* COMMON) + *conf_sec_pal_driver_intf.o(.data .data.* .bss .bss.* COMMON) + . = ALIGN(32); + _e_conf_driver_data = .; + _econfdata = .; + } > CONFDATA AT > FLASH + _siconfdata = LOADADDR(.conf_data); + + .conf_bss (NOLOAD) : { + . = ALIGN(4); + _sconfbss = .; + EXCLUDE_FILE(*conf_sec_test_supp_*.o *conf_sec_driver_partition.o *conf_sec_pal_driver_intf.o) *conf_sec_*.o(.bss .bss.* COMMON) + . = ALIGN(4); + _econfbss = .; + } > CONFDATA + + INCLUDE secure_l3_bands.ld + + .data : { + _sdata = .; + *(.data*) + _edata = .; + } > RAM AT > FLASH + ASSERT(LOADADDR(.data) + SIZEOF(.data) <= + (WT_SECURE_FLASH_ORIGIN + WT_SECURE_FLASH_SIZE), + "secure image load image overflows secure flash region") + + .bss (NOLOAD) : { + _sbss = .; + *(.bss*) + *(COMMON) + _ebss = .; + } > RAM + + INCLUDE secure_l3_tail.ld +} diff --git a/port/mcxn947/secure_owners.txt b/port/mcxn947/secure_owners.txt new file mode 100644 index 000000000..e1815063e --- /dev/null +++ b/port/mcxn947/secure_owners.txt @@ -0,0 +1,3 @@ +# MCXN947 objects, appended to tools/secure_owners.txt by the secure link. +sec_platform_mcxn947.o spm +wt_sec_rom_flash.o spm diff --git a/tests/firmware/mcxn947-baremetal/Makefile b/tests/firmware/mcxn947-baremetal/Makefile new file mode 100644 index 000000000..6b14e83e6 --- /dev/null +++ b/tests/firmware/mcxn947-baremetal/Makefile @@ -0,0 +1,113 @@ +# Bare-metal Non-secure guest0 for the FRDM-MCXN947. Builds the wolfTrust +# Secure image (and its CMSE import library) through the top-level Makefile, +# then links guest0 against that import library so the WolfTrust_FFM_* veneers +# resolve. Image assembly and the emulator boot are driven by +# tests/target/run_mcxn947_m33mu.sh. +TOOLPREFIX ?= arm-none-eabi- +CC := $(TOOLPREFIX)gcc +OBJCOPY := $(TOOLPREFIX)objcopy +SIZE := $(TOOLPREFIX)size + +ROOT := ../../.. +TARGET ?= mcxn947 +ARCH ?= armv8m + +BUILD ?= build +# The Secure image's build directory, relative to the repository root; the +# hardware runner points both at its own tree so an emulator run can share +# the checkout. +WT_BUILD_DIR ?= build +TOP_BUILD_DIR := $(ROOT)/$(WT_BUILD_DIR) +SECURE_CMSE_IMPLIB := $(TOP_BUILD_DIR)/secure_cmse_implib.o + +# The same guest links into both Non-secure windows (port/mcxn947/memory_map.h); +# the monitor refuses to start unless every configured guest has an image. +GUEST0_FLASH_ORIGIN ?= 0x00080000 +GUEST0_FLASH_LENGTH ?= 0x00080000 +GUEST0_RAM_ORIGIN ?= 0x20000000 +GUEST0_RAM_LENGTH ?= 0x00018000 +GUEST1_FLASH_ORIGIN ?= 0x00100000 +GUEST1_FLASH_LENGTH ?= 0x00040000 +GUEST1_RAM_ORIGIN ?= 0x20018000 +GUEST1_RAM_LENGTH ?= 0x00018000 + +CFLAGS := -mcpu=cortex-m33 -mthumb -mgeneral-regs-only -ffreestanding \ + -fno-builtin -nostdlib -Os -g -Wall -Wextra \ + -ffunction-sections -fdata-sections -mno-unaligned-access \ + -I$(ROOT)/include + +# WT_SAU_PROBE=1 builds guest0's isolation probe: it stores into guest1's RAM +# window, which the per-dispatch SAU window keeps Secure while guest0 runs. +# guest1 carries no probe; it is the peer that must keep running. +WT_SAU_PROBE ?= 0 +GUEST0_CFLAGS := -DGUEST_NAME=\"guest0\" +GUEST1_CFLAGS := -DGUEST_NAME=\"guest1\" +ifeq ($(WT_SAU_PROBE),1) +GUEST0_CFLAGS += -DWT_SAU_PROBE -DGUEST_PROBE_ADDR=0x20028000u \ + -DGUEST_PROBE_VALUE=0xDEADBEEFu +endif +# WT_PERIPH_NEG_PROBE=1 makes guest0, its MPU off, read the SPM's ELS through +# the Non-secure alias; the SAU must refuse it on every launch (periphneg). +WT_PERIPH_NEG_PROBE ?= 0 +ifeq ($(WT_PERIPH_NEG_PROBE),1) +GUEST0_CFLAGS += -DWT_PERIPH_NEG_PROBE -DGUEST_PERIPH_ADDR=0x40054000u +endif +# WT_GUEST_FAULT_PROBE=1 makes guest0 read Secure RAM on every launch, so the +# monitor spends its restart budget and quarantines it (restart). +WT_GUEST_FAULT_PROBE ?= 0 +ifeq ($(WT_GUEST_FAULT_PROBE),1) +GUEST0_CFLAGS += -DWT_GUEST_FAULT_PROBE -DGUEST_FAULT_ADDR=0x30031000u +endif + +CLIENT_OBJ := $(BUILD)/psa_ffm_client.o + +all: $(BUILD)/guest0.bin $(BUILD)/guest1.bin + +# The image's build mode comes from the environment; forcing one here would +# relink the image in another mode and drop the wolftrust.bin the runners sign. +$(SECURE_CMSE_IMPLIB): FORCE + $(MAKE) -C $(ROOT) ARCH=$(ARCH) TARGET=$(TARGET) TOOLPREFIX=$(TOOLPREFIX) \ + BUILD_DIR=$(WT_BUILD_DIR) $(WT_BUILD_DIR)/secure_cmse_implib.o + +$(BUILD)/guest0.o: guest0.c | $(BUILD) + $(CC) $(CFLAGS) $(GUEST0_CFLAGS) -c -o $@ $< + +$(BUILD)/guest1.o: guest0.c | $(BUILD) + $(CC) $(CFLAGS) $(GUEST1_CFLAGS) -c -o $@ $< + +$(BUILD)/psa_ffm_client.o: $(ROOT)/src/client/psa_ffm_client.c | $(BUILD) + $(CC) $(CFLAGS) -c -o $@ $< + +$(BUILD)/guest0.elf: $(BUILD)/guest0.o $(CLIENT_OBJ) $(SECURE_CMSE_IMPLIB) guest0.ld | $(BUILD) + $(CC) $(CFLAGS) \ + -Wl,-Tguest0.ld \ + -Wl,--defsym=GUEST_FLASH_ORIGIN=$(GUEST0_FLASH_ORIGIN) \ + -Wl,--defsym=GUEST_FLASH_LENGTH=$(GUEST0_FLASH_LENGTH) \ + -Wl,--defsym=GUEST_RAM_ORIGIN=$(GUEST0_RAM_ORIGIN) \ + -Wl,--defsym=GUEST_RAM_LENGTH=$(GUEST0_RAM_LENGTH) \ + -Wl,--gc-sections \ + -o $@ $(BUILD)/guest0.o $(CLIENT_OBJ) $(SECURE_CMSE_IMPLIB) -lgcc + $(SIZE) $@ + +$(BUILD)/guest1.elf: $(BUILD)/guest1.o $(CLIENT_OBJ) $(SECURE_CMSE_IMPLIB) guest0.ld | $(BUILD) + $(CC) $(CFLAGS) \ + -Wl,-Tguest0.ld \ + -Wl,--defsym=GUEST_FLASH_ORIGIN=$(GUEST1_FLASH_ORIGIN) \ + -Wl,--defsym=GUEST_FLASH_LENGTH=$(GUEST1_FLASH_LENGTH) \ + -Wl,--defsym=GUEST_RAM_ORIGIN=$(GUEST1_RAM_ORIGIN) \ + -Wl,--defsym=GUEST_RAM_LENGTH=$(GUEST1_RAM_LENGTH) \ + -Wl,--gc-sections \ + -o $@ $(BUILD)/guest1.o $(CLIENT_OBJ) $(SECURE_CMSE_IMPLIB) -lgcc + $(SIZE) $@ + +$(BUILD)/%.bin: $(BUILD)/%.elf + $(OBJCOPY) -O binary $< $@ + +$(BUILD): + mkdir -p $@ + +clean: + rm -rf $(BUILD) + +.PHONY: all clean FORCE +FORCE: diff --git a/tests/firmware/mcxn947-baremetal/guest0.c b/tests/firmware/mcxn947-baremetal/guest0.c new file mode 100644 index 000000000..f24b7da08 --- /dev/null +++ b/tests/firmware/mcxn947-baremetal/guest0.c @@ -0,0 +1,303 @@ +/* guest0.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 . + */ + +/* Bare-metal Non-secure bring-up guest for the MCXN947 CPU0: drives the + * WolfTrust_FFM_* veneers to prove the Secure runtime serves a Non-secure PSA + * client, reporting in a mailbox at the base of guest RAM and on LPUART4. */ + +#include +#include + +#include "psa/client.h" + +/* SERVICE_HSM from the platform manifest (port/mcxn947/manifest.json). */ +#define GUEST0_SERVICE_HSM_SID 4102u +#define GUEST0_SERVICE_ITS_SID 4099u +#define GUEST0_SERVICE_VERSION 1u + +/* One source builds both guests; the Makefile names each on the console. */ +#ifndef GUEST_NAME +#error "GUEST_NAME must name the guest" +#endif + +/* Debugger-readable status words at the base of the guest RAM window. */ +#define GUEST0_SIGNATURE 0x47543030u /* debugger tag, "guest0 ran" */ +#define GUEST0_STATUS_RUNNING 0x00000000u +#define GUEST0_STATUS_DONE 0x600D600Du +#define GUEST0_STATUS_FAIL 0xBAD00000u + +/* LPUART4 (LP_FLEXCOMM4) through its Non-secure alias. The first loader + * configured the clock, pins, and baud, so the guest only keeps the + * transmitter enabled, polls TDRE and writes DATA. */ +#define GUEST0_LPUART_BASE 0x400B4000u +#define GUEST0_LPUART_VERID (*(volatile uint32_t*)(GUEST0_LPUART_BASE + 0x00u)) +#define GUEST0_LPUART_STAT (*(volatile uint32_t*)(GUEST0_LPUART_BASE + 0x14u)) +#define GUEST0_LPUART_CTRL (*(volatile uint32_t*)(GUEST0_LPUART_BASE + 0x18u)) +#define GUEST0_LPUART_DATA (*(volatile uint32_t*)(GUEST0_LPUART_BASE + 0x1Cu)) +#define GUEST0_LPUART_STAT_TDRE 0x00800000u +#define GUEST0_LPUART_CTRL_TE_RE 0x000C0000u +#define GUEST0_UART_TX_POLL_LIMIT 100000u + +typedef struct wt_guest0_mailbox { + uint32_t signature; + uint32_t step; + uint32_t framework; + uint32_t hsm_version; + int32_t hsm_handle; + uint32_t status; + uint32_t console; + uint32_t probe; + uint32_t probe_read; + uint32_t beat; /* advances while the guest idles once done */ +} wt_guest0_mailbox_t; + +__attribute__((section(".shared"), used)) +volatile wt_guest0_mailbox_t g_guest0_mailbox; + +#if defined(WT_PERIPH_NEG_PROBE) +static void guest0_uart_puts(const char* s); + +/* A privileged guest switches off its own MPU and reads the SPM's ELS through + * the Non-secure alias. The SAU must fault the read; returning at all leaks. */ +static void guest0_periph_probe(volatile wt_guest0_mailbox_t* mb) +{ + *(volatile uint32_t*)0xE000ED94u = 0u; + __asm volatile("dsb\n isb" ::: "memory"); + mb->probe = 1u; + __asm volatile("dsb" ::: "memory"); + mb->probe_read = *(volatile const uint32_t*)GUEST_PERIPH_ADDR; + mb->probe = 3u; + guest0_uart_puts("wolfTrust MCXN947 " GUEST_NAME ": periph probe LEAKED\r\n"); +} +#endif + +#if defined(WT_SAU_PROBE) +/* Isolation probe: GUEST_PROBE_ADDR is the peer's RAM, Secure to the SAU + * while this guest runs; with the guest's own MPU off, attribution alone + * must stop the store. Latch: 1 attempted (faulted), 2 blocked, 3 leaked. */ +#define GUEST0_MPU_CTRL (*(volatile uint32_t*)0xE000ED94u) +#define GUEST0_PROBE_ATTEMPTED 1u +#define GUEST0_PROBE_BLOCKED 2u +#define GUEST0_PROBE_LEAKED 3u + +static void guest0_sau_probe(volatile wt_guest0_mailbox_t* mb) +{ + volatile uint32_t* target = (volatile uint32_t*)GUEST_PROBE_ADDR; + uint32_t readback; + + GUEST0_MPU_CTRL = 0u; + __asm volatile("dsb\n isb" ::: "memory"); + /* Latched only once the MPU is off, so 1 means the peer store was issued. */ + mb->probe = GUEST0_PROBE_ATTEMPTED; + __asm volatile("dsb" ::: "memory"); + *target = GUEST_PROBE_VALUE; + __asm volatile("dsb" ::: "memory"); + readback = *target; + mb->probe_read = readback; + if (readback == GUEST_PROBE_VALUE) { + mb->probe = GUEST0_PROBE_LEAKED; + } + else { + mb->probe = GUEST0_PROBE_BLOCKED; + } +} +#endif + +extern uint32_t _estack; +extern uint32_t _sidata; +extern uint32_t _sdata; +extern uint32_t _edata; +extern uint32_t _sbss; +extern uint32_t _ebss; + +void Reset_Handler(void); +static void Default_Handler(void); + +__attribute__((section(".vectors"), used)) +const uint32_t g_vectors[16] = { + (uint32_t)&_estack, + (uint32_t)Reset_Handler, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + 0u, 0u, 0u, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, + 0u, + (uint32_t)Default_Handler, + (uint32_t)Default_Handler, +}; + +static void Default_Handler(void) +{ + for (;;) { + } +} + +static void guest0_uart_putc(char c) +{ + uint32_t t = GUEST0_UART_TX_POLL_LIMIT; + + while (((GUEST0_LPUART_STAT & GUEST0_LPUART_STAT_TDRE) == 0u) && + (t > 0u)) { + t--; + } + if (t != 0u) { + GUEST0_LPUART_DATA = (uint32_t)(uint8_t)c; + } +} + +static void guest0_uart_puts(const char* s) +{ + while (*s != '\0') { + guest0_uart_putc(*s); + s++; + } +} + +void Reset_Handler(void) +{ + volatile wt_guest0_mailbox_t* mb = &g_guest0_mailbox; + uint32_t* src; + uint32_t* dst; + uint32_t fw; + uint32_t tries; + psa_handle_t handle; + int ok = 1; + + src = &_sidata; + for (dst = &_sdata; dst < &_edata; ) { + *dst = *src; + dst++; + src++; + } + for (dst = &_sbss; dst < &_ebss; ) { + *dst = 0u; + dst++; + } + + mb->signature = GUEST0_SIGNATURE; + mb->step = 1u; + mb->framework = 0u; + mb->hsm_version = 0u; + mb->hsm_handle = 0; + mb->status = GUEST0_STATUS_RUNNING; + mb->console = GUEST0_LPUART_VERID; + mb->probe = 0u; + mb->probe_read = 0u; + mb->beat = 0u; + + GUEST0_LPUART_CTRL |= GUEST0_LPUART_CTRL_TE_RE; + guest0_uart_puts("wolfTrust MCXN947 " GUEST_NAME ": start\r\n"); + +#if defined(WT_GUEST_FAULT_PROBE) + /* A Non-secure read of Secure RAM faults on every launch. */ + mb->probe_read = *(volatile const uint32_t*)GUEST_FAULT_ADDR; +#endif + + fw = psa_framework_version(); + mb->framework = fw; + mb->step = 2u; + if (fw != PSA_FRAMEWORK_VERSION) { + ok = 0; + } + + mb->hsm_version = psa_version(GUEST0_SERVICE_HSM_SID); + mb->step = 3u; + + /* A failed connect to the mandatory service is retried: each attempt + * re-enters the relay, so a faulting relay spends its restart budget + * (spbudgetneg) instead of surviving on one entry per guest. */ + handle = PSA_NULL_HANDLE; + for (tries = 0u; tries < 4u; tries++) { + handle = psa_connect(GUEST0_SERVICE_HSM_SID, GUEST0_SERVICE_VERSION); + if (PSA_HANDLE_IS_VALID(handle)) { + break; + } + } + mb->hsm_handle = (int32_t)handle; + mb->step = 4u; + if (PSA_HANDLE_IS_VALID(handle)) { + psa_close(handle); + mb->step = 5u; + } + else { + ok = 0; + } + + if (ok != 0) { + mb->status = GUEST0_STATUS_DONE; + guest0_uart_puts("wolfTrust MCXN947 " GUEST_NAME + ": FF-M connect ok, done\r\n"); + } + else { + mb->status = GUEST0_STATUS_FAIL; + guest0_uart_puts("wolfTrust MCXN947 " GUEST_NAME ": FAIL\r\n"); + } + + /* Touch a second service so the storage SP wakes: the crossdomain and + * keystoreneg probes fault on its entry, so this connect failing is their + * verdict, never this guest's. */ + handle = psa_connect(GUEST0_SERVICE_ITS_SID, GUEST0_SERVICE_VERSION); + if (PSA_HANDLE_IS_VALID(handle)) { + psa_close(handle); + guest0_uart_puts("wolfTrust MCXN947 " GUEST_NAME + ": storage connect ok\r\n"); + } + +#if defined(WT_SAU_PROBE) + guest0_sau_probe(mb); +#endif +#if defined(WT_PERIPH_NEG_PROBE) + guest0_periph_probe(mb); +#endif + + for (;;) { + mb->beat++; + } +} + +/* The freestanding client core marshals its iovec through memset; -fno-builtin + * keeps these loops from being turned into calls to themselves. */ +void* memset(void* dst, int c, size_t n) +{ + uint8_t* p = (uint8_t*)dst; + size_t i; + + for (i = 0u; i < n; i++) { + p[i] = (uint8_t)c; + } + return dst; +} + +void* memcpy(void* dst, const void* src, size_t n) +{ + uint8_t* d = (uint8_t*)dst; + const uint8_t* s = (const uint8_t*)src; + size_t i; + + for (i = 0u; i < n; i++) { + d[i] = s[i]; + } + return dst; +} diff --git a/tests/firmware/mcxn947-baremetal/guest0.ld b/tests/firmware/mcxn947-baremetal/guest0.ld new file mode 100644 index 000000000..e561cd2f8 --- /dev/null +++ b/tests/firmware/mcxn947-baremetal/guest0.ld @@ -0,0 +1,57 @@ +/* guest0.ld + * + * 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 . + */ + +ENTRY(Reset_Handler) + +MEMORY { + FLASH (rx) : ORIGIN = GUEST_FLASH_ORIGIN, LENGTH = GUEST_FLASH_LENGTH + RAM (rwx): ORIGIN = GUEST_RAM_ORIGIN, LENGTH = GUEST_RAM_LENGTH +} + +_estack = ORIGIN(RAM) + LENGTH(RAM); + +SECTIONS { + .shared ORIGIN(RAM) (NOLOAD) : { + *(.shared*) + } > RAM + + .vectors GUEST_FLASH_ORIGIN : { + KEEP(*(.vectors)) + } > FLASH + + .text : { + *(.text*) + *(.rodata*) + } > FLASH + + .data : { + _sdata = .; + *(.data*) + _edata = .; + } > RAM AT > FLASH + _sidata = LOADADDR(.data); + + .bss (NOLOAD) : { + _sbss = .; + *(.bss*) + *(COMMON) + _ebss = .; + } > RAM +} diff --git a/tests/firmware/psa-guest/Makefile b/tests/firmware/psa-guest/Makefile index abe07cb73..79b052350 100644 --- a/tests/firmware/psa-guest/Makefile +++ b/tests/firmware/psa-guest/Makefile @@ -43,6 +43,9 @@ WT_FWU_PROBE ?= 0 WT_FWU_PROBE_STREAM_BYTES ?= # The backend under test refuses a write that skips ahead of the staged extent. WT_FWU_PROBE_SEQUENTIAL ?= 0 +# Bytes of the FWU probe's final write; a size off the backend alignment proves +# FINISH accepts a padded last block. +WT_FWU_PROBE_TAIL_BYTES ?= # The attestation verify pins the token measurement to wolfBoot's measurement # of the signed Secure image and the lifecycle the Secure side reports. WT_EXPECTED_MEASUREMENT_HEX ?= @@ -286,6 +289,9 @@ endif ifeq ($(WT_FWU_PROBE_SEQUENTIAL),1) PROBE_CFLAGS += -DWT_FWU_PROBE_SEQUENTIAL endif +ifneq ($(WT_FWU_PROBE_TAIL_BYTES),) +PROBE_CFLAGS += -DWT_FWU_PROBE_TAIL_BYTES=$(WT_FWU_PROBE_TAIL_BYTES) +endif # guest0 carries the probes; guest1 is the peer that must keep running. GUEST0_CFLAGS := -DGUEST_NAME=\"guest0\" -DWT_WOLFHSM_CLIENT_ID=1u \ diff --git a/tests/firmware/psa-guest/boards/mcxn947/board.h b/tests/firmware/psa-guest/boards/mcxn947/board.h new file mode 100644 index 000000000..bcd38a8eb --- /dev/null +++ b/tests/firmware/psa-guest/boards/mcxn947/board.h @@ -0,0 +1,58 @@ +/* board.h + * + * 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 . + */ + +/* MCXN947 (FRDM-MCXN947, CPU0) board seams for the portable PSA guest: the + * console is LPUART4 through its Non-secure alias, configured by the first + * loader, so the guest keeps the transmitter on, polls TDRE and writes DATA. */ + +#ifndef WOLFTRUST_PSA_GUEST_BOARD_H +#define WOLFTRUST_PSA_GUEST_BOARD_H + +#include + +#define GUEST_UART_BASE 0x400B4000u +#define GUEST_UART_STAT (*(volatile uint32_t*)(GUEST_UART_BASE + 0x14u)) +#define GUEST_UART_CTRL (*(volatile uint32_t*)(GUEST_UART_BASE + 0x18u)) +#define GUEST_UART_DATA (*(volatile uint32_t*)(GUEST_UART_BASE + 0x1Cu)) +#define GUEST_UART_STAT_TDRE 0x00800000u +#define GUEST_UART_CTRL_TE_RE 0x000C0000u +#define GUEST_UART_POLL_LIMIT 100000u + +static inline void guest_board_uart_init(void) +{ + GUEST_UART_CTRL |= GUEST_UART_CTRL_TE_RE; +} + +static inline void guest_board_uart_putc(char c) +{ + uint32_t t = GUEST_UART_POLL_LIMIT; + + while (((GUEST_UART_STAT & GUEST_UART_STAT_TDRE) == 0u) && (t > 0u)) { + t--; + } + if (t != 0u) { + GUEST_UART_DATA = (uint32_t)(uint8_t)c; + } +} + +/* Secure RAM the restart probe reads (WT_RAM_S_BASE in port/mcxn947). */ +#define GUEST_SECURE_RAM_ADDR 0x30031000u + +#endif /* WOLFTRUST_PSA_GUEST_BOARD_H */ diff --git a/tests/firmware/psa-guest/boards/mcxn947/board.mk b/tests/firmware/psa-guest/boards/mcxn947/board.mk new file mode 100644 index 000000000..ae642a17f --- /dev/null +++ b/tests/firmware/psa-guest/boards/mcxn947/board.mk @@ -0,0 +1,9 @@ +# MCXN947 Non-secure guest windows (port/mcxn947/memory_map.h). +GUEST0_FLASH_ORIGIN ?= 0x00080000 +GUEST0_FLASH_LENGTH ?= 0x00080000 +GUEST0_RAM_ORIGIN ?= 0x20000000 +GUEST0_RAM_LENGTH ?= 0x00018000 +GUEST1_FLASH_ORIGIN ?= 0x00100000 +GUEST1_FLASH_LENGTH ?= 0x00040000 +GUEST1_RAM_ORIGIN ?= 0x20018000 +GUEST1_RAM_LENGTH ?= 0x00018000 diff --git a/tests/firmware/psa-guest/main.c b/tests/firmware/psa-guest/main.c index af9d15191..7c7ae8f25 100644 --- a/tests/firmware/psa-guest/main.c +++ b/tests/firmware/psa-guest/main.c @@ -848,6 +848,18 @@ static void exercise_hsm_attack_probe(void) * wolfBoot update partition and verifies each block by read-back, then the * PSA FWU 1.0 reject/clean tail restores READY. */ #define GUEST_FWU_OP_QUERY 1u +#if !defined(WT_FWU_PROBE_TAIL_BYTES) +#define WT_FWU_PROBE_TAIL_BYTES 32u +#endif +#if WT_FWU_PROBE_TAIL_BYTES < 1 || WT_FWU_PROBE_TAIL_BYTES > 512 +#error "WT_FWU_PROBE_TAIL_BYTES must fit the probe's 512-byte write payload" +#endif +#if defined(WT_FWU_PROBE_STREAM_BYTES) +#define WT_FWU_PROBE_PAYLOAD_BYTES \ + ((uint32_t)WT_FWU_PROBE_STREAM_BYTES - 1024u + WT_FWU_PROBE_TAIL_BYTES) +#else +#define WT_FWU_PROBE_PAYLOAD_BYTES ((uint32_t)WT_FWU_PROBE_TAIL_BYTES) +#endif #define GUEST_FWU_OP_START 2u #define GUEST_FWU_OP_WRITE 3u #define GUEST_FWU_OP_FINISH 4u @@ -935,7 +947,7 @@ static void exercise_fwu(void) memset(writebuf + 16, 0xFF, 512u); word = 0x464C4F57u; memcpy(writebuf + 16, &word, 4u); - word = 32u; + word = WT_FWU_PROBE_PAYLOAD_BYTES; memcpy(writebuf + 20, &word, 4u); writebuf[24] = 0x01u; writebuf[25] = 0x00u; @@ -986,11 +998,11 @@ static void exercise_fwu(void) memset(&req, 0, sizeof(req)); req.offset = tail_off; - req.size = 32u; + req.size = WT_FWU_PROBE_TAIL_BYTES; memcpy(writebuf, &req, sizeof(req)); - memset(writebuf + 16, 0x22, 32u); - st = guest_fwu_call(handle, GUEST_FWU_OP_WRITE, writebuf, 16u + 32u, - NULL, 0u); + memset(writebuf + 16, 0x22, WT_FWU_PROBE_TAIL_BYTES); + st = guest_fwu_call(handle, GUEST_FWU_OP_WRITE, writebuf, + 16u + WT_FWU_PROBE_TAIL_BYTES, NULL, 0u); if (st != 0) { guest_line_i32("wolfTrust FWU write#2 failed st=", st); ok = 0; diff --git a/tests/host/Makefile b/tests/host/Makefile index 95a9108b9..9596c8b70 100644 --- a/tests/host/Makefile +++ b/tests/host/Makefile @@ -33,6 +33,7 @@ endif UNIT_SUITES := domain manifest lifecycle monitor guest_verify rollback sp_recovery \ fabric_windows periph irq_claim rt700_trng rt700_xspi rt700_lifecycle \ + mcxn947_flash mcxn947_lifecycle mcxn947_rng \ ipc ffm spm \ vnet wolfhsm_loopback wolfhsm_relay native_wire native_wire_client \ keystore_isolation \ diff --git a/tests/host/mcxn947_flash/Makefile b/tests/host/mcxn947_flash/Makefile new file mode 100644 index 000000000..7161fece3 --- /dev/null +++ b/tests/host/mcxn947_flash/Makefile @@ -0,0 +1,97 @@ +# 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, see . + +ROOT := ../../.. +WOLFHSM := $(ROOT)/lib/wolfHSM + +CC ?= cc +BUILD_DIR ?= build +# The port's unit (16, the phrase) is the default run; `run` repeats the +# suite with wolfHSM's 8-byte default to show the hazard it would carry. +WT_FLASH_UNIT ?= 16 + +CFLAGS := \ + -DWOLFHSM_CFG_NO_CRYPTO \ + -DWOLFHSM_CFG_NO_SYS_TIME \ + -DWOLFHSM_CFG_FLASH_UNIT_SIZE=$(WT_FLASH_UNIT) \ + -I$(ROOT)/include \ + -I$(ROOT)/port/mcxn947 \ + -I$(WOLFHSM) \ + -include mock_rom_api.h \ + -std=c99 -O0 -g -Wall -Wextra -pedantic +CFLAGS += $(EXTRA_CFLAGS) + +_PORT_SRCS := $(ROOT)/port/mcxn947/rom_flash.c +_WH_SRCS := \ + $(WOLFHSM)/src/wh_nvm.c \ + $(WOLFHSM)/src/wh_nvm_flash.c \ + $(WOLFHSM)/src/wh_flash_unit.c \ + $(WOLFHSM)/src/wh_utils.c +_TEST_SRCS := main.c + +_PORT_OBJS := $(patsubst %.c,wt_%.o,$(notdir $(_PORT_SRCS))) +_WH_OBJS := $(patsubst %.c,wh_%.o,$(notdir $(_WH_SRCS))) +_TEST_OBJS := $(patsubst %.c,%.o,$(notdir $(_TEST_SRCS))) + +ALL_OBJS := $(addprefix $(BUILD_DIR)/,$(_PORT_OBJS) $(_WH_OBJS) $(_TEST_OBJS)) +TEST_BIN := $(BUILD_DIR)/test_mcxn947_flash + +.PHONY: all run run-one compilers sanitize valgrind clean + +all: $(TEST_BIN) + +$(BUILD_DIR): + mkdir -p $@ + +$(BUILD_DIR)/wt_%.o: $(ROOT)/port/mcxn947/%.c mock_rom_api.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(BUILD_DIR)/wh_%.o: $(WOLFHSM)/src/%.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -Wno-unused-parameter -c -o $@ $< + +$(BUILD_DIR)/%.o: %.c mock_rom_api.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(TEST_BIN): $(ALL_OBJS) + $(CC) $(CFLAGS) $(EXTRA_LDFLAGS) -o $@ $(ALL_OBJS) + +run: $(TEST_BIN) + $(TEST_BIN) + $(MAKE) --no-print-directory run-one \ + BUILD_DIR=$(BUILD_DIR)/unit8 WT_FLASH_UNIT=8 + +run-one: $(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/mcxn947_flash/main.c b/tests/host/mcxn947_flash/main.c new file mode 100644 index 000000000..23988ad7c --- /dev/null +++ b/tests/host/mcxn947_flash/main.c @@ -0,0 +1,703 @@ +/* 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 . + */ + +/* Host suite for the MCXN947 ROM flash driver and the wolfHSM store over a + * model of the ECC flash and its ROM (page-based programs, all-ones phrases + * untouched, a phrase programmed twice between erases counted); per unit. */ + +#include +#include +#include + +#include "memory_map.h" +#include "rom_flash.h" + +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_flash.h" +#include "wolfhsm/wh_flash_unit.h" +#include "wolfhsm/wh_nvm.h" +#include "wolfhsm/wh_nvm_flash.h" + +/* Layouts mirrored from rom_flash.c (the driver's private view of the ROM). */ +typedef struct mock_flash_api { + uint32_t version; + int32_t (*init)(void *config); + int32_t (*erase)(void *config, uint32_t start, uint32_t length, + uint32_t key); + int32_t (*program)(void *config, uint32_t start, uint8_t *src, + uint32_t length); + int32_t (*verify_erase)(void *config, uint32_t start, uint32_t length); + int32_t (*verify_program)(void *config, uint32_t start, uint32_t length, + const uint8_t *expected, uint32_t *failed_address, + uint32_t *failed_data); + int32_t (*get_property)(void *config, uint32_t property, uint32_t *value); +} mock_flash_api_t; + +typedef struct mock_api_tree { + void (*run_bootloader)(void *arg); + uint32_t version; + const char *copyright; + uint32_t reserved0; + const mock_flash_api_t *flash; +} mock_api_tree_t; + +#define PHRASE WT_FLASH_PHRASE_SIZE +#define PAGE WT_FLASH_PAGE_SIZE +#define SECTOR WT_FLASH_SECTOR_SIZE +#define PHRASES (WT_FLASH_SIZE / PHRASE) +#define ERASE_KEY 0x6B65666Cu +#define STATUS_OK 0 +#define STATUS_ALIGN 101 +#define STATUS_KEY 107 +#define STATUS_COMPARE 117 +#define NVM_OFF (WT_HSM_NVM_FLASH_BASE_S - WT_FLASH_S_ALIAS_BASE) +#define NVM_SIZE WT_HSM_NVM_FLASH_SIZE + +static uint8_t g_flash[WT_FLASH_SIZE]; +static uint8_t g_programmed[PHRASES]; + +/* Model counters and knobs. */ +static uint32_t g_double_programs; +static uint32_t g_first_double_offset; +static uint32_t g_program_calls; +static uint32_t g_unaligned_programs; +static uint32_t g_erase_calls; +static uint32_t g_verify_erase_calls; +static uint32_t g_init_calls; +static uint32_t g_prop_sector = SECTOR; +static uint32_t g_prop_total = WT_FLASH_SIZE; +static int32_t g_program_status = STATUS_OK; +static int32_t g_erase_status = STATUS_OK; + +static int g_failures; +static int g_checks; + +static void check(int cond, const char *name) +{ + g_checks++; + if (cond) { + printf("PASS: %s\n", name); + } + else { + printf("FAIL: %s\n", name); + g_failures++; + } +} + +/* Program as the array does: bits clear, an all-ones phrase changes nothing, + * and a phrase written twice since its erase no longer carries a valid ECC. */ +static void model_program(uint32_t start, const uint8_t *src, uint32_t len) +{ + uint32_t first = start / PHRASE; + uint32_t last = (start + len - 1u) / PHRASE; + uint32_t lo; + uint32_t hi; + uint32_t p; + uint32_t i; + int touched; + + for (p = first; p <= last; p++) { + lo = (p * PHRASE > start) ? p * PHRASE : start; + hi = ((p + 1u) * PHRASE < start + len) ? (p + 1u) * PHRASE : + start + len; + touched = 0; + for (i = lo; i < hi; i++) { + if (src[i - start] != 0xFFu) { + touched = 1; + } + } + if (touched == 0) { + continue; + } + if (g_programmed[p] != 0u) { + if (g_double_programs == 0u) { + g_first_double_offset = p * PHRASE; + } + g_double_programs++; + } + g_programmed[p] = 1u; + } + for (i = 0u; i < len; i++) { + g_flash[start + i] &= src[i]; + } +} + +static void model_erase(uint32_t start, uint32_t len) +{ + uint32_t p; + + (void)memset(g_flash + start, 0xFF, len); + for (p = start / PHRASE; p < (start + len) / PHRASE; p++) { + g_programmed[p] = 0u; + } +} + +static void model_reset(void) +{ + (void)memset(g_flash, 0xFF, sizeof(g_flash)); + (void)memset(g_programmed, 0, sizeof(g_programmed)); + g_double_programs = 0u; + g_first_double_offset = 0u; +} + +static int32_t mock_init(void *config) +{ + uint32_t *words = (uint32_t *)config; + + g_init_calls++; + words[0] = 0u; + words[1] = g_prop_total; + words[2] = 2u; + words[3] = 128u; + words[4] = g_prop_sector; + return STATUS_OK; +} + +static int32_t mock_erase(void *config, uint32_t start, uint32_t length, + uint32_t key) +{ + (void)config; + g_erase_calls++; + if (key != ERASE_KEY) { + return STATUS_KEY; + } + if ((start % 4u) != 0u || (length % 4u) != 0u || + start + length > WT_FLASH_SIZE) { + return STATUS_ALIGN; + } + if (g_erase_status != STATUS_OK) { + return g_erase_status; + } + model_erase(start, length); + return STATUS_OK; +} + +static int32_t mock_program(void *config, uint32_t start, uint8_t *src, + uint32_t length) +{ + (void)config; + g_program_calls++; + /* As on silicon: only a page base is accepted as the start. */ + if ((start % PAGE) != 0u) { + g_unaligned_programs++; + return STATUS_ALIGN; + } + if ((length % 4u) != 0u || start + length > WT_FLASH_SIZE) { + return STATUS_ALIGN; + } + if (g_program_status != STATUS_OK) { + return g_program_status; + } + model_program(start, src, length); + return STATUS_OK; +} + +static int32_t mock_verify_erase(void *config, uint32_t start, uint32_t length) +{ + uint32_t i; + + (void)config; + g_verify_erase_calls++; + for (i = 0u; i < length; i++) { + if (g_flash[start + i] != 0xFFu) { + return STATUS_COMPARE; + } + } + return STATUS_OK; +} + +static int32_t mock_verify_program(void *config, uint32_t start, + uint32_t length, const uint8_t *expected, + uint32_t *failed_address, + uint32_t *failed_data) +{ + (void)config; + (void)failed_address; + (void)failed_data; + return (memcmp(g_flash + start, expected, length) == 0) ? STATUS_OK : + STATUS_COMPARE; +} + +static int32_t mock_get_property(void *config, uint32_t property, + uint32_t *value) +{ + (void)config; + if (property == 0u) { + *value = g_prop_sector; + } + else if (property == 1u) { + *value = g_prop_total; + } + else { + return 106; + } + return STATUS_OK; +} + +static const mock_flash_api_t g_mock_api = { + 0x46010000u, mock_init, mock_erase, mock_program, mock_verify_erase, + mock_verify_program, mock_get_property +}; +static const mock_api_tree_t g_mock_tree = { + NULL, 0x4B010000u, "mock", 0u, &g_mock_api +}; + +uintptr_t wt_mock_rom_tree(void) +{ + return (uintptr_t)&g_mock_tree; +} + +uintptr_t wt_mock_flash_base(void) +{ + return (uintptr_t)g_flash; +} + +static void test_driver(void) +{ + uint8_t phrase[PHRASE]; + uint8_t two[2u * PHRASE]; + uint32_t calls; + int rc; + + model_reset(); + (void)memset(phrase, 0x5A, sizeof(phrase)); + (void)memset(two, 0xC3, sizeof(two)); + + /* The ROM's geometry must be the one the map was written for. */ + g_prop_sector = 0x1000u; + check(wt_rom_flash_init() == WT_ROM_FLASH_ERR_GEOMETRY, + "init refuses a ROM reporting another sector size"); + g_prop_sector = SECTOR; + check(wt_rom_flash_init() == WT_ROM_FLASH_OK && g_init_calls == 2u, + "init accepts the 8 KiB sector, 2 MiB geometry"); + check(wt_rom_flash_init() == WT_ROM_FLASH_OK && g_init_calls == 2u, + "init is idempotent"); + + check(wt_rom_flash_program(NVM_OFF + 8u, phrase, PHRASE) == + WT_ROM_FLASH_ERR_ARGS, "program refuses a phrase-misaligned offset"); + check(wt_rom_flash_program(NVM_OFF, phrase, 8u) == WT_ROM_FLASH_ERR_ARGS, + "program refuses a sub-phrase length"); + check(wt_rom_flash_program(NVM_OFF, NULL, PHRASE) == WT_ROM_FLASH_ERR_ARGS, + "program refuses a null source"); + check(wt_rom_flash_program(WT_FLASH_SIZE - PHRASE, two, sizeof(two)) == + WT_ROM_FLASH_ERR_ARGS, "program refuses a range past the array"); + check(g_program_calls == 0u, "refused programs never reach the ROM"); + + check(wt_rom_flash_program(NVM_OFF, two, sizeof(two)) == WT_ROM_FLASH_OK && + memcmp(g_flash + NVM_OFF, two, sizeof(two)) == 0, + "two blank phrases program in one ROM call"); + calls = g_program_calls; + rc = wt_rom_flash_program(NVM_OFF + PHRASE, phrase, PHRASE); + check(rc == WT_ROM_FLASH_ERR_NOTBLANK && g_program_calls == calls, + "a programmed phrase is refused before the ROM is called"); + check(g_double_programs == 0u, "no phrase was programmed twice"); + check(memcmp(g_flash + NVM_OFF + PHRASE, two, PHRASE) == 0, + "the refused write left the phrase as it was"); + check(wt_rom_flash_blank(NVM_OFF, PHRASE) == WT_ROM_FLASH_ERR_NOTBLANK && + wt_rom_flash_blank(NVM_OFF + 2u * PHRASE, PHRASE) == WT_ROM_FLASH_OK, + "blank check reads the array through the view"); + + check(wt_rom_flash_erase(NVM_OFF + PHRASE, SECTOR) == WT_ROM_FLASH_ERR_ARGS, + "erase refuses a sector-misaligned offset"); + check(wt_rom_flash_erase(NVM_OFF, PHRASE) == WT_ROM_FLASH_ERR_ARGS, + "erase refuses a sub-sector length"); + check(g_erase_calls == 0u, "refused erases never reach the ROM"); + check(wt_rom_flash_erase(NVM_OFF, SECTOR) == WT_ROM_FLASH_OK && + g_verify_erase_calls == 1u && + wt_rom_flash_blank(NVM_OFF, SECTOR) == WT_ROM_FLASH_OK, + "sector erase runs the ROM erase and its verify"); + check(wt_rom_flash_program(NVM_OFF + PHRASE, phrase, PHRASE) == + WT_ROM_FLASH_OK && g_double_programs == 0u, + "an erased phrase programs again, once"); + + g_program_status = 103; + check(wt_rom_flash_program(NVM_OFF + 4u * PHRASE, phrase, PHRASE) == + WT_ROM_FLASH_ERR_ROM && wt_rom_flash_last_status() == 103u, + "a ROM program failure is reported with its status"); + g_program_status = STATUS_OK; + g_erase_status = 105; + check(wt_rom_flash_erase(NVM_OFF, SECTOR) == WT_ROM_FLASH_ERR_ROM && + wt_rom_flash_last_status() == 105u, + "a ROM erase failure is reported with its status"); + g_erase_status = STATUS_OK; + + check(wt_rom_flash_erase(NVM_OFF, SECTOR) == WT_ROM_FLASH_OK, + "the sector erases before the page checks"); + check(g_mock_api.program(NULL, NVM_OFF + PHRASE, phrase, PHRASE) == + STATUS_ALIGN, "the model refuses a start off a page base"); + g_program_calls = 0u; + g_unaligned_programs = 0u; + check(wt_rom_flash_program(NVM_OFF + 3u * PHRASE, phrase, PHRASE) == + WT_ROM_FLASH_OK && g_program_calls == 1u && + g_unaligned_programs == 0u, + "a lone phrase is programmed by one page-based ROM call"); + check(wt_rom_flash_blank(NVM_OFF, 3u * PHRASE) == WT_ROM_FLASH_OK && + memcmp(g_flash + NVM_OFF + 3u * PHRASE, phrase, PHRASE) == 0, + "the all-ones padding left the phrases before it blank"); + check(wt_rom_flash_program(NVM_OFF + 4u * PHRASE, two, PHRASE) == + WT_ROM_FLASH_OK && g_double_programs == 0u && + memcmp(g_flash + NVM_OFF + 3u * PHRASE, phrase, PHRASE) == 0 && + memcmp(g_flash + NVM_OFF + 4u * PHRASE, two, PHRASE) == 0, + "padding over a programmed phrase is not a program of it"); + g_program_calls = 0u; + check(wt_rom_flash_program(NVM_OFF + PAGE - PHRASE, two, sizeof(two)) == + WT_ROM_FLASH_OK && g_program_calls == 2u && + g_unaligned_programs == 0u && + memcmp(g_flash + NVM_OFF + PAGE - PHRASE, two, sizeof(two)) == 0, + "a program crossing a page boundary takes one ROM call per page"); +} + +/* The port backend's rules (port/mcxn947/hsm_flash.c) over the real driver: + * a 16-byte program unit, sector erases, reads through the alias. */ +static int port_init(void *context, const void *config) +{ + (void)context; + (void)config; + return WH_ERROR_OK; +} + +static int port_cleanup(void *context) +{ + (void)context; + return WH_ERROR_OK; +} + +static uint32_t port_partition_size(void *context) +{ + (void)context; + return SECTOR; +} + +static int port_lock(void *context, uint32_t offset, uint32_t size) +{ + (void)context; + (void)offset; + (void)size; + return WH_ERROR_OK; +} + +static int port_range_ok(uint32_t offset, uint32_t size) +{ + return (offset <= NVM_SIZE && size <= NVM_SIZE - offset) ? 1 : 0; +} + +static int port_read(void *context, uint32_t offset, uint32_t size, + uint8_t *data) +{ + (void)context; + if (!port_range_ok(offset, size)) { + return WH_ERROR_BADARGS; + } + (void)memcpy(data, g_flash + NVM_OFF + offset, size); + return WH_ERROR_OK; +} + +static int port_result(int rc) +{ + if (rc == WT_ROM_FLASH_OK) { + return WH_ERROR_OK; + } + if (rc == WT_ROM_FLASH_ERR_NOTBLANK) { + return WH_ERROR_NOTBLANK; + } + if (rc == WT_ROM_FLASH_ERR_ARGS) { + return WH_ERROR_BADARGS; + } + return WH_ERROR_ABORTED; +} + +static int port_program(void *context, uint32_t offset, uint32_t size, + const uint8_t *data) +{ + (void)context; + if (!port_range_ok(offset, size) || (offset % PHRASE) != 0u || + (size % PHRASE) != 0u) { + return WH_ERROR_BADARGS; + } + if (size == 0u) { + return WH_ERROR_OK; + } + return port_result(wt_rom_flash_program(NVM_OFF + offset, data, size)); +} + +static int port_erase(void *context, uint32_t offset, uint32_t size) +{ + (void)context; + if (!port_range_ok(offset, size) || (offset % SECTOR) != 0u || + (size % SECTOR) != 0u) { + return WH_ERROR_BADARGS; + } + if (size == 0u) { + return WH_ERROR_OK; + } + return port_result(wt_rom_flash_erase(NVM_OFF + offset, size)); +} + +static int port_verify(void *context, uint32_t offset, uint32_t size, + const uint8_t *data) +{ + (void)context; + if (!port_range_ok(offset, size)) { + return WH_ERROR_BADARGS; + } + return (memcmp(g_flash + NVM_OFF + offset, data, size) == 0) ? + WH_ERROR_OK : WH_ERROR_NOTVERIFIED; +} + +static int port_blank_check(void *context, uint32_t offset, uint32_t size) +{ + int rc; + + (void)context; + if (!port_range_ok(offset, size)) { + return WH_ERROR_BADARGS; + } + rc = wt_rom_flash_blank(NVM_OFF + offset, size); + if (rc == WT_ROM_FLASH_ERR_NOTBLANK) { + return WH_ERROR_NOTBLANK; + } + return (rc == WT_ROM_FLASH_OK) ? WH_ERROR_OK : WH_ERROR_BADARGS; +} + +static const whFlashCb g_port_cb = { + .Init = port_init, + .Cleanup = port_cleanup, + .PartitionSize = port_partition_size, + .WriteLock = port_lock, + .WriteUnlock = port_lock, + .Read = port_read, + .Program = port_program, + .Erase = port_erase, + .Verify = port_verify, + .BlankCheck = port_blank_check, +}; + +/* A backend that programs whatever unit it is handed, as a port that took + * the wolfHSM unit for the flash's own would. */ +static int raw_program(void *context, uint32_t offset, uint32_t size, + const uint8_t *data) +{ + (void)context; + if (!port_range_ok(offset, size) || (offset % 4u) != 0u || + (size % 4u) != 0u) { + return WH_ERROR_BADARGS; + } + model_program(NVM_OFF + offset, data, size); + return WH_ERROR_OK; +} + +static int raw_erase(void *context, uint32_t offset, uint32_t size) +{ + (void)context; + if (!port_range_ok(offset, size) || (offset % SECTOR) != 0u || + (size % SECTOR) != 0u) { + return WH_ERROR_BADARGS; + } + model_erase(NVM_OFF + offset, size); + return WH_ERROR_OK; +} + +static int raw_blank_check(void *context, uint32_t offset, uint32_t size) +{ + uint32_t i; + + (void)context; + if (!port_range_ok(offset, size)) { + return WH_ERROR_BADARGS; + } + for (i = 0u; i < size; i++) { + if (g_flash[NVM_OFF + offset + i] != 0xFFu) { + return WH_ERROR_NOTBLANK; + } + } + return WH_ERROR_OK; +} + +static const whFlashCb g_raw_cb = { + .Init = port_init, + .Cleanup = port_cleanup, + .PartitionSize = port_partition_size, + .WriteLock = port_lock, + .WriteUnlock = port_lock, + .Read = port_read, + .Program = raw_program, + .Erase = raw_erase, + .Verify = port_verify, + .BlankCheck = raw_blank_check, +}; + +#define OBJECTS 20u +#define OBJECT_MAX 300u +#define ROUNDS 3u + +static whNvmFlashContext g_nf_ctx; +static whNvmFlashConfig g_nf_cfg; +static whNvmContext g_nvm; +static whNvmConfig g_nvm_cfg; +static whNvmCb g_nvm_cb = WH_NVM_FLASH_CB; +static uint8_t g_pattern[OBJECT_MAX]; + +static void fill_object(uint8_t *out, uint32_t id, uint32_t len) +{ + uint32_t i; + + for (i = 0u; i < len; i++) { + out[i] = (uint8_t)(id * 7u + i * 13u + 1u); + } +} + +static int nvm_open(const whFlashCb *cb) +{ + (void)memset(&g_nf_ctx, 0, sizeof(g_nf_ctx)); + (void)memset(&g_nvm, 0, sizeof(g_nvm)); + g_nf_cfg.cb = cb; + g_nf_cfg.context = NULL; + g_nf_cfg.config = NULL; + g_nvm_cfg.cb = &g_nvm_cb; + g_nvm_cfg.context = &g_nf_ctx; + g_nvm_cfg.config = &g_nf_cfg; + return wh_Nvm_Init(&g_nvm, &g_nvm_cfg); +} + +/* Fill, read back, destroy, refill until the store compacts into its other + * partition, then reopen the store as a reboot would and read everything + * back. Every sector erase after the open is one such compaction. Returns 0 + * when every object read back as written. */ +static int nvm_lifecycle(const whFlashCb *cb, uint32_t *compactions) +{ + whNvmMetadata meta; + uint8_t readback[OBJECT_MAX]; + whNvmId victims[OBJECTS / 2u]; + uint32_t round; + uint32_t i; + uint32_t len; + uint32_t erases_at_open; + int ret; + + *compactions = 0u; + ret = nvm_open(cb); + erases_at_open = g_erase_calls; + for (round = 0u; ret == WH_ERROR_OK && round < ROUNDS; round++) { + for (i = 0u; ret == WH_ERROR_OK && i < OBJECTS; i++) { + len = 1u + ((i * 37u + round * 11u) % OBJECT_MAX); + (void)memset(&meta, 0, sizeof(meta)); + meta.id = (whNvmId)(0x100u + i); + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = (whNvmSize)len; + fill_object(g_pattern, meta.id + round, len); + ret = wh_Nvm_AddObjectWithReclaim(&g_nvm, &meta, (whNvmSize)len, + g_pattern); + } + for (i = 0u; ret == WH_ERROR_OK && i < OBJECTS; i++) { + len = 1u + ((i * 37u + round * 11u) % OBJECT_MAX); + fill_object(g_pattern, 0x100u + i + round, len); + (void)memset(readback, 0, sizeof(readback)); + ret = wh_Nvm_Read(&g_nvm, (whNvmId)(0x100u + i), 0u, + (whNvmSize)len, readback); + if (ret == WH_ERROR_OK && memcmp(readback, g_pattern, len) != 0) { + ret = WH_ERROR_NOTVERIFIED; + } + } + for (i = 0u; i < OBJECTS / 2u; i++) { + victims[i] = (whNvmId)(0x100u + 2u * i); + } + if (ret == WH_ERROR_OK) { + ret = wh_Nvm_DestroyObjects(&g_nvm, OBJECTS / 2u, victims); + } + } + *compactions = g_erase_calls - erases_at_open; + if (ret == WH_ERROR_OK) { + ret = wh_Nvm_Cleanup(&g_nvm); + } + if (ret == WH_ERROR_OK) { + ret = nvm_open(cb); + } + for (i = 1u; ret == WH_ERROR_OK && i < OBJECTS; i += 2u) { + len = 1u + ((i * 37u + (ROUNDS - 1u) * 11u) % OBJECT_MAX); + fill_object(g_pattern, 0x100u + i + ROUNDS - 1u, len); + (void)memset(readback, 0, sizeof(readback)); + ret = wh_Nvm_Read(&g_nvm, (whNvmId)(0x100u + i), 0u, (whNvmSize)len, + readback); + if (ret == WH_ERROR_OK && memcmp(readback, g_pattern, len) != 0) { + ret = WH_ERROR_NOTVERIFIED; + } + } + return ret; +} + +static void test_store(void) +{ + uint32_t compactions = 0u; + int ret; + + model_reset(); + g_program_calls = 0u; + g_erase_calls = 0u; +#if WOLFHSM_CFG_FLASH_UNIT_SIZE == 16 + ret = nvm_lifecycle(&g_port_cb, &compactions); + check(ret == WH_ERROR_OK, + "16-byte units: three fill/read/destroy rounds and a reopen"); + check(compactions >= 1u, + "16-byte units: the store compacted into its other partition"); + check(g_program_calls > 100u, + "16-byte units: the lifecycle programmed many phrases"); + printf(" programs=%u compactions=%u double=%u\n", + (unsigned)g_program_calls, (unsigned)compactions, + (unsigned)g_double_programs); + check(g_double_programs == 0u, + "16-byte units: no phrase was programmed twice between erases"); + check(g_unaligned_programs == 0u, + "16-byte units: every ROM program started on a page base"); + /* The unit, not the port's guard, is what keeps every program whole: + * a backend with no phrase rule stays clean at 16 bytes. */ + model_reset(); + ret = nvm_lifecycle(&g_raw_cb, &compactions); + check(ret == WH_ERROR_OK && g_double_programs == 0u, + "16-byte units: the raw backend never writes a phrase twice"); +#else + ret = nvm_open(&g_port_cb); + check(ret != WH_ERROR_OK, + "8-byte units: the port backend refuses the sub-phrase unit"); + check(g_program_calls == 0u && g_double_programs == 0u, + "8-byte units: nothing reached the ROM through the port backend"); + model_reset(); + ret = nvm_lifecycle(&g_raw_cb, &compactions); + check(ret == WH_ERROR_OK, + "8-byte units: the raw backend completes the same lifecycle"); + printf(" compactions=%u double=%u first=0x%08X\n", + (unsigned)compactions, (unsigned)g_double_programs, + (unsigned)g_first_double_offset); + check(g_double_programs > 0u, + "8-byte units: two units sharing a phrase were written at " + "different times, which this flash would corrupt"); +#endif +} + +int main(void) +{ + printf("MCXN947 flash: WOLFHSM_CFG_FLASH_UNIT_SIZE=%d\n", + (int)WOLFHSM_CFG_FLASH_UNIT_SIZE); + test_driver(); + test_store(); + printf("%s: %d checks, %d failures\n", + (g_failures == 0) ? "PASS" : "FAIL", g_checks, g_failures); + return (g_failures == 0) ? 0 : 1; +} diff --git a/tests/host/mcxn947_flash/mock_rom_api.h b/tests/host/mcxn947_flash/mock_rom_api.h new file mode 100644 index 000000000..e45fc55c5 --- /dev/null +++ b/tests/host/mcxn947_flash/mock_rom_api.h @@ -0,0 +1,34 @@ +/* mock_rom_api.h + * + * 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 . + */ + +/* Force-included ahead of rom_flash.c: the boot ROM API tree and the flash + * read view come from the test's model instead of the part's addresses. */ +#ifndef WT_TEST_MOCK_ROM_API_H +#define WT_TEST_MOCK_ROM_API_H + +#include + +uintptr_t wt_mock_rom_tree(void); +uintptr_t wt_mock_flash_base(void); + +#define WT_ROM_API_TREE_ADDR (wt_mock_rom_tree()) +#define WT_ROM_FLASH_VIEW_BASE (wt_mock_flash_base()) + +#endif /* WT_TEST_MOCK_ROM_API_H */ diff --git a/tests/host/mcxn947_lifecycle/Makefile b/tests/host/mcxn947_lifecycle/Makefile new file mode 100644 index 000000000..04ad36331 --- /dev/null +++ b/tests/host/mcxn947_lifecycle/Makefile @@ -0,0 +1,70 @@ +# 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, see . + +ROOT := ../../.. + +CC ?= cc +BUILD_DIR ?= build + +# The mapping header ships inside the carried wolfBoot patch; test that copy. +PATCH := $(ROOT)/tests/target/wolfboot-mcxn947-lifecycle.patch +HEADER := $(BUILD_DIR)/mcxn_lifecycle.h + +CFLAGS := \ + -I$(BUILD_DIR) \ + -std=c11 -O0 -g -Wall -Wextra -Werror -pedantic +CFLAGS += $(EXTRA_CFLAGS) + +TEST_BIN := $(BUILD_DIR)/test_mcxn947_lifecycle + +.PHONY: all run compilers sanitize valgrind clean + +all: $(TEST_BIN) + +$(BUILD_DIR): + mkdir -p $@ + +$(HEADER): $(PATCH) | $(BUILD_DIR) + awk '/^\+\+\+ b\/hal\/mcxn_lifecycle\.h$$/ { f = 1; next } \ + f && /^diff / { 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/mcxn947_lifecycle/main.c b/tests/host/mcxn947_lifecycle/main.c new file mode 100644 index 000000000..723925e0a --- /dev/null +++ b/tests/host/mcxn947_lifecycle/main.c @@ -0,0 +1,172 @@ +/* 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 MCXN947 life cycle mapping wolfBoot hands wolfTrust, tested from the + * copy in the carried patch: only an agreeing OEM state maps to PSA, and no + * fault or open-debug state reads as SECURED. */ + +#include "mcxn_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(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_OPEN, MCXN_LC_OEM_OPEN), + 0x1000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_SECURE_WORLD_CLOSED, + MCXN_LC_OEM_SECURE_WORLD_CLOSED), 0x2000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_CLOSED, + MCXN_LC_OEM_CLOSED), 0x3000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_LOCKED, + MCXN_LC_OEM_LOCKED), 0x3000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_FIELD_RETURN, + MCXN_LC_OEM_FIELD_RETURN), 0x6000u); + + /* NXP-internal, blank, shredded, and every unlisted code are unknown. */ + for (lc = 0u; lc <= 0xFFu; lc++) { + if (lc == MCXN_LC_OEM_OPEN || lc == MCXN_LC_OEM_SECURE_WORLD_CLOSED || + lc == MCXN_LC_OEM_CLOSED || lc == MCXN_LC_OEM_LOCKED || + lc == MCXN_LC_OEM_FIELD_RETURN) { + continue; + } + EXPECT_LC(mcxn_lc_to_psa_lifecycle(lc, lc), 0x0000u); + } + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_NXP_BLANK, MCXN_LC_NXP_BLANK), + 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_NXP_FAB, MCXN_LC_NXP_FAB), + 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_NXP_DEV, MCXN_LC_NXP_DEV), + 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_NXP_FIELD_RETURN, + MCXN_LC_NXP_FIELD_RETURN), 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_SHREDDED, MCXN_LC_SHREDDED), + 0x0000u); +} + +static void test_redundancy(void) +{ + /* The duplicate must agree, and nothing above the state byte may be set. */ + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_OPEN, + MCXN_LC_OEM_SECURE_WORLD_CLOSED), 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_CLOSED, MCXN_LC_OEM_OPEN), + 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(0x00000103u, 0x00000103u), 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(0xFFFC0003u, 0xFFFC0003u), 0x0000u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(MCXN_LC_OEM_OPEN, 0x00010003u), + 0x0000u); +} + +static void test_debug_status(void) +{ + uint32_t lc = MCXN_LC_OEM_CLOSED; + + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_CLOSED), 0x3000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_NS_ONLY), 0x4000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_NSNID), 0x4000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_SECURE), 0x5000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_SNID), 0x5000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, DAUTH_ALL), 0x5000u); + /* A malformed field (00 or 01) voids the whole register. */ + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, 0x00u), 0x0000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, 0xA9u), 0x0000u); + EXPECT_LC(mcxn_attestation_lifecycle(lc, lc, 0x6Au), 0x0000u); + /* Debug state only refines SECURED; the open states ignore it. */ + EXPECT_LC(mcxn_attestation_lifecycle(MCXN_LC_OEM_OPEN, MCXN_LC_OEM_OPEN, + 0x00u), 0x1000u); + EXPECT_LC(mcxn_attestation_lifecycle(MCXN_LC_OEM_SECURE_WORLD_CLOSED, + MCXN_LC_OEM_SECURE_WORLD_CLOSED, DAUTH_ALL), 0x2000u); + EXPECT_LC(mcxn_attestation_lifecycle(MCXN_LC_OEM_FIELD_RETURN, + MCXN_LC_OEM_FIELD_RETURN, DAUTH_ALL), 0x6000u); + EXPECT_LC(mcxn_attestation_lifecycle(MCXN_LC_OEM_LOCKED, + MCXN_LC_OEM_LOCKED, DAUTH_CLOSED), 0x3000u); +} + +/* The OTP fuse map SPSDK carries for the part (fuses.json, register + * LIFE_CYCLE, index 1): LIFECYCLE[7:0] at bit 0 and its duplicate at bit 24 + * use the same codes the ROM reflects into SYSCON, and every ladder step only + * sets fuse bits, which is what makes it one way. */ +#define FUSE_LC_DEVELOP 3u +#define FUSE_LC_DEVELOP2 7u +#define FUSE_LC_IN_FIELD 15u +#define FUSE_LC_IN_FIELD_LOCKED 207u +#define FUSE_LC_FIELD_RETURN_OEM 31u +#define FUSE_LC_WORD(lc) ((uint32_t)(lc) | ((uint32_t)(lc) << 24)) + +static void test_fuse_map(void) +{ + static const uint32_t ladder[][2] = { + { MCXN_LC_OEM_OPEN, MCXN_LC_OEM_SECURE_WORLD_CLOSED }, + { MCXN_LC_OEM_SECURE_WORLD_CLOSED, MCXN_LC_OEM_CLOSED }, + { MCXN_LC_OEM_CLOSED, MCXN_LC_OEM_LOCKED }, + { MCXN_LC_OEM_CLOSED, MCXN_LC_OEM_FIELD_RETURN }, + }; + size_t i; + + EXPECT_LC(MCXN_LC_OEM_OPEN, FUSE_LC_DEVELOP); + EXPECT_LC(MCXN_LC_OEM_SECURE_WORLD_CLOSED, FUSE_LC_DEVELOP2); + EXPECT_LC(MCXN_LC_OEM_CLOSED, FUSE_LC_IN_FIELD); + EXPECT_LC(MCXN_LC_OEM_LOCKED, FUSE_LC_IN_FIELD_LOCKED); + EXPECT_LC(MCXN_LC_OEM_FIELD_RETURN, FUSE_LC_FIELD_RETURN_OEM); + EXPECT_LC(FUSE_LC_WORD(MCXN_LC_OEM_SECURE_WORLD_CLOSED), 0x07000007u); + EXPECT_LC(FUSE_LC_WORD(MCXN_LC_OEM_CLOSED), 0x0F00000Fu); + for (i = 0; i < sizeof(ladder) / sizeof(ladder[0]); i++) { + EXPECT_LC(FUSE_LC_WORD(ladder[i][0]) & ~FUSE_LC_WORD(ladder[i][1]), + 0u); + EXPECT_LC(mcxn_lc_to_psa_lifecycle(ladder[i][1], ladder[i][1]) != 0u, + 1u); + } +} + +int main(void) +{ + test_state_table(); + test_redundancy(); + test_debug_status(); + test_fuse_map(); + printf("%s: %d checks, %d failures\n", + (failures == 0) ? "PASS" : "FAIL", checks, failures); + return (failures == 0) ? 0 : 1; +} diff --git a/tests/host/mcxn947_rng/Makefile b/tests/host/mcxn947_rng/Makefile new file mode 100644 index 000000000..e2bdbbd4b --- /dev/null +++ b/tests/host/mcxn947_rng/Makefile @@ -0,0 +1,74 @@ +# 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, see . + + +ROOT := ../../.. + +CC ?= cc +BUILD_DIR ?= build + +CFLAGS := \ + -I$(ROOT)/include \ + -I$(ROOT)/port/mcxn947 \ + -include mock_els.h \ + -std=c99 -O0 -g -Wall -Wextra -pedantic -fno-pie +CFLAGS += $(EXTRA_CFLAGS) + +ALL_OBJS := $(BUILD_DIR)/wt_rng_entropy.o $(BUILD_DIR)/main.o +TEST_BIN := $(BUILD_DIR)/test_mcxn947_rng +# The driver latches failure, so each case runs in its own process. +CASES := default locked error + +.PHONY: all run compilers sanitize valgrind clean + +all: $(TEST_BIN) + +$(BUILD_DIR): + mkdir -p $@ + +$(BUILD_DIR)/wt_%.o: $(ROOT)/port/mcxn947/%.c mock_els.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(BUILD_DIR)/%.o: %.c mock_els.h | $(BUILD_DIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(TEST_BIN): $(ALL_OBJS) + $(CC) $(CFLAGS) -no-pie $(EXTRA_LDFLAGS) -o $@ $(ALL_OBJS) + +run: $(TEST_BIN) + for c in $(CASES); do $(TEST_BIN) $$c || exit 1; done + +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) + for c in $(CASES); do \ + valgrind --error-exitcode=1 --leak-check=full \ + --show-leak-kinds=all $(TEST_BIN) $$c || exit 1; \ + done + +clean: + rm -rf $(BUILD_DIR) diff --git a/tests/host/mcxn947_rng/main.c b/tests/host/mcxn947_rng/main.c new file mode 100644 index 000000000..f436c836a --- /dev/null +++ b/tests/host/mcxn947_rng/main.c @@ -0,0 +1,280 @@ +/* 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 . + */ + + +/* Host suite for the MCXN947 ELS entropy driver over a model of the RND_REQ + * command: requests serve fresh DMA pages and wipe them, and a request that + * misses its deadline must target memory outside the stack and leave the + * source refusing every later call, as must a request the engine fails; a + * locked engine refuses before any request. Each case runs in its own process + * (the driver latches failure). Built without PIE so the 32-bit DMA register + * holds a host address of static storage. */ + +#include +#include +#include + +#include "mcxn947_regs.h" +#include "wolftrust/spm_transport.h" + +#define WT_ELS_PAGE_BYTES 64u +#define WT_ELS_PAGE_WORDS_TEST (WT_ELS_PAGE_BYTES / 4u) + +int wolftrust_rng_generate_block(unsigned char *output, unsigned int sz); +extern volatile uint32_t g_wt_els_errors; + +static int checks; +static int failures; + +#define EXPECT(cond, what) \ + do { \ + checks++; \ + if (!(cond)) { \ + (void)fprintf(stderr, "line %d: %s\n", __LINE__, (what)); \ + failures++; \ + } \ + } while (0) + +static volatile uint32_t g_status; +static volatile uint32_t g_ctrl; +static volatile uint32_t g_scratch; +static volatile uint32_t g_err_clr; +static int g_fail_request; +static uint32_t g_dma_addr; +static uint32_t g_dma_len; +static uint32_t g_requests; +static uint32_t g_last_dma_addr; +static int g_hang; +static uint32_t g_ctrl_seen; +static uintptr_t g_stack_marker; + +static int on_stack(uint32_t addr) +{ + uint32_t m = (uint32_t)g_stack_marker; + uint32_t d = (addr > m) ? addr - m : m - addr; + + return d < 0x10000u; +} + +static uint32_t page_word(uint32_t request, uint32_t word) +{ + return 0xA5000000u | (request << 8) | word; +} + +/* A START seen on the next status read issues the request: it fills the page + * at once, or with g_hang stays BUSY past the driver's deadline. */ +static void wt_mock_step(void) +{ + volatile uint32_t *dst; + uint32_t i; + + if (g_ctrl != g_ctrl_seen) { + if ((g_ctrl & WT_ELS_CTRL_START) != 0u) { + /* START self-clears, so an identical later write is a new one. */ + g_ctrl &= ~WT_ELS_CTRL_START; + g_requests++; + g_last_dma_addr = g_dma_addr; + if (g_hang) { + g_status = WT_ELS_STATUS_BUSY; + } + else if (g_fail_request) { + g_status = WT_ELS_STATUS_ERR; + } + else { + g_status = 0u; + if (!on_stack(g_dma_addr)) { + dst = (volatile uint32_t *)(uintptr_t)g_dma_addr; + for (i = 0u; i < g_dma_len / 4u; ++i) { + dst[i] = page_word(g_requests, i); + } + } + } + } + g_ctrl_seen = g_ctrl; + } +} + +volatile uint32_t* wt_mock_reg(uintptr_t address) +{ + switch (address) { + case WT_ELS_BASE_S + 0x00u: + wt_mock_step(); + return &g_status; + case WT_ELS_BASE_S + 0x04u: + return &g_ctrl; + case WT_ELS_BASE_S + 0x38u: + return (volatile uint32_t *)&g_dma_addr; + case WT_ELS_BASE_S + 0x3Cu: + return (volatile uint32_t *)&g_dma_len; + case WT_ELS_BASE_S + 0x50u: + return &g_err_clr; + default: + return &g_scratch; + } +} + +int wt_spm_sp_call(struct wt_spm_call* call) +{ + (void)call; + return -1; +} + +/* out[off..off+len) must hold request's page words starting at word 0. */ +static int served_from(const unsigned char *out, unsigned int len, + uint32_t request) +{ + uint32_t w; + unsigned int i; + + for (i = 0u; i < len; ++i) { + w = page_word(request, i / 4u); + if (out[i] != (unsigned char)(w >> (8u * (i % 4u)))) { + return 0; + } + } + return 1; +} + +static int page_wiped(void) +{ + const volatile uint32_t *page = + (const volatile uint32_t *)(uintptr_t)g_last_dma_addr; + uint32_t i; + + for (i = 0u; i < WT_ELS_PAGE_WORDS_TEST; ++i) { + if (page[i] != 0u) { + return 0; + } + } + return 1; +} + +static void test_completed_requests(void) +{ + unsigned char out[100]; + uint32_t first; + + g_hang = 0; + memset(out, 0, sizeof(out)); + EXPECT(wolftrust_rng_generate_block(out, 40u) == 0, + "a completed RND_REQ serves its words"); + EXPECT(g_requests == 1u, "40 bytes take one page"); + EXPECT(g_dma_len == WT_ELS_PAGE_BYTES, "a request covers a whole page"); + EXPECT(!on_stack(g_last_dma_addr), + "the DMA page is not on the caller's stack"); + EXPECT(served_from(out, 40u, 1u), "the 40 bytes are the page's words"); + EXPECT(page_wiped(), "the page is wiped after the request"); + + first = g_requests; + memset(out, 0, sizeof(out)); + EXPECT(wolftrust_rng_generate_block(out, 16u) == 0, + "a second request succeeds"); + EXPECT(g_requests == first + 1u, "every call issues a fresh request"); + EXPECT(served_from(out, 16u, first + 1u), + "unused words of the earlier page are never served"); + + first = g_requests; + memset(out, 0, sizeof(out)); + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) == 0, + "a request spanning two pages succeeds"); + EXPECT(g_requests == first + 2u, "100 bytes take two pages"); + EXPECT(served_from(out, WT_ELS_PAGE_BYTES, first + 1u), + "the first 64 bytes are the first page"); + EXPECT(served_from(out + WT_ELS_PAGE_BYTES, + (unsigned int)sizeof(out) - WT_ELS_PAGE_BYTES, + first + 2u), + "the remaining 36 bytes are the second page"); + EXPECT(page_wiped(), "the page is wiped after a multi-page request"); +} + +static void test_late_completion(void) +{ + unsigned char out[16]; + uint32_t before; + + g_hang = 1; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, + "a request past its deadline fails"); + EXPECT(!on_stack(g_last_dma_addr), + "the timed-out request's DMA target is not on the stack"); + /* The request completes after the driver gave up. */ + g_status = 0u; + before = g_requests; + g_hang = 0; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, + "the source refuses every call after a timeout"); + EXPECT(g_requests == before, "no new request is issued after a timeout"); +} + +static void test_locked_engine(void) +{ + unsigned char out[16]; + + g_status = WT_ELS_STATUS_LOCKED; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, + "a locked engine refuses"); + EXPECT(g_requests == 0u, "a locked engine is never sent a request"); + EXPECT(g_wt_els_errors == 1u, "the refusal is counted"); + g_status = 0u; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) == 0, + "the engine serves once it is no longer locked"); + EXPECT(served_from(out, sizeof(out), 1u), "and serves its page"); +} + +static void test_failed_request(void) +{ + unsigned char out[16]; + uint32_t before; + + g_fail_request = 1; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, + "a request the engine fails is refused"); + EXPECT(g_requests == 1u, "the failed request was issued"); + EXPECT(g_wt_els_errors == 1u, "the failure is counted"); + EXPECT((g_err_clr & WT_ELS_ERR_STATUS_CLR_ALL) != 0u, + "the error status is cleared"); + g_fail_request = 0; + g_status = 0u; + before = g_requests; + EXPECT(wolftrust_rng_generate_block(out, sizeof(out)) != 0, + "the source refuses every call after a failed request"); + EXPECT(g_requests == before, "no new request is issued after a failure"); +} + +int main(int argc, char **argv) +{ + uint8_t marker = 0u; + const char *which = (argc > 1) ? argv[1] : "default"; + + g_stack_marker = (uintptr_t)▮ + if (strcmp(which, "locked") == 0) { + test_locked_engine(); + } + else if (strcmp(which, "error") == 0) { + test_failed_request(); + } + else { + test_completed_requests(); + test_late_completion(); + } + (void)printf("mcxn947 rng %s: %d checks, %d failures\n", which, checks, + failures); + return failures == 0 ? 0 : 1; +} diff --git a/tests/host/mcxn947_rng/mock_els.h b/tests/host/mcxn947_rng/mock_els.h new file mode 100644 index 000000000..08db7d627 --- /dev/null +++ b/tests/host/mcxn947_rng/mock_els.h @@ -0,0 +1,34 @@ +/* mock_els.h + * + * 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 . + */ + + +/* Every MCXN947 register access in the driver under test goes through the + * model in main.c, which plays the ELS command interface. */ + +#ifndef WT_TEST_MOCK_ELS_H +#define WT_TEST_MOCK_ELS_H + +#include + +volatile uint32_t* wt_mock_reg(uintptr_t address); + +#define WT_REG32(address) (*wt_mock_reg((uintptr_t)(address))) + +#endif /* WT_TEST_MOCK_ELS_H */ diff --git a/tests/target/detect_mcxn947.sh b/tests/target/detect_mcxn947.sh new file mode 100755 index 000000000..6a071d04f --- /dev/null +++ b/tests/target/detect_mcxn947.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Board-present check for the FRDM-MCXN947 rig: the on-board MCU-Link probe +# (the one pyOCD lists as "MCU-LINK FRDM-MCXN947", or MCXN947_PROBE_UID), its +# VCOM, a pyOCD with the MCXN947 pack, and SPSDK's blhost for the ISP +# life-cycle path. Exit 0 when usable; otherwise print one SKIP reason and +# exit 1. +set -u + +MCXN947_PYOCD="${MCXN947_PYOCD:-pyocd}" +MCXN947_SPSDK_VENV="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" + +if ! command -v "$MCXN947_PYOCD" >/dev/null 2>&1; then + if [ -x "$MCXN947_SPSDK_VENV/bin/pyocd" ]; then + MCXN947_PYOCD="$MCXN947_SPSDK_VENV/bin/pyocd" + PATH="$MCXN947_SPSDK_VENV/bin:$PATH" + export PATH + else + echo "pyocd not installed" + exit 1 + fi +fi +probes="$("$MCXN947_PYOCD" list 2>/dev/null | grep -F "FRDM-MCXN947" || true)" +if [ -n "${MCXN947_PROBE_UID:-}" ]; then + probes="$("$MCXN947_PYOCD" list 2>/dev/null | grep -F "$MCXN947_PROBE_UID" || true)" +fi +if [ -z "$probes" ]; then + echo "MCU-Link FRDM-MCXN947 probe not attached (set MCXN947_PROBE_UID for another)" + exit 1 +fi +console="${MCXN947_CONSOLE:-}" +if [ -z "$console" ]; then + for tty in /dev/serial/by-id/*FRDM-MCXN947*-if02; do + [ -c "$tty" ] && console="$tty" && break + done +fi +if [ -z "$console" ] || [ ! -c "$console" ]; then + echo "board console not present (MCXN947_CONSOLE)" + exit 1 +fi +if ! "$MCXN947_PYOCD" pack find mcxn947 2>/dev/null | grep -q "True"; then + echo "pyocd MCXN947 pack not installed (pyocd pack install MCXN947)" + exit 1 +fi +if ! command -v blhost >/dev/null 2>&1; then + echo "spsdk (blhost) not installed" + exit 1 +fi +exit 0 diff --git a/tests/target/lib/mcxn947_wolfboot.sh b/tests/target/lib/mcxn947_wolfboot.sh new file mode 100644 index 000000000..988b26884 --- /dev/null +++ b/tests/target/lib/mcxn947_wolfboot.sh @@ -0,0 +1,71 @@ +# shellcheck shell=sh +# The pinned MCXN947 wolfBoot first stage the MCXN947 runner boots: the +# secure-application handoff branch (wolfSSL/wolfBoot#948, pinned at its +# fork ref until it merges; bump MCXN947_WOLFBOOT_REPO/REF to upstream then), +# built from the mcxn-tz-wolftrust config. Sourced; needs $here (tests/target). +MCXN947_WOLFBOOT_REPO=https://github.com/aidangarske/wolfBoot.git +MCXN947_WOLFBOOT_REF=1d47fa7b622b654a8e6a53cf53e7811c42618505 +MCXN947_WOLFBOOT_CONFIG=config/examples/mcxn-tz-wolftrust.config + +# What a cached tree must match: the ref, the config, the carried life cycle +# patch (the MCXN HAL at the pin reports a fixed SECURED life cycle; the +# patch reads the part's), and the make overrides. +mcxn947_wolfboot_stamp() { + # shellcheck disable=SC2154 # $here is set by the sourcing runner + printf '%s %s %s [%s]' "$MCXN947_WOLFBOOT_REF" "$MCXN947_WOLFBOOT_CONFIG" \ + "$(cksum "$here/wolfboot-mcxn947-lifecycle.patch" | cut -d' ' -f1)" "$*" +} + +mcxn947_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" ] +} + +# The MCUXpresso SDK the wolfBoot MCXN HAL compiles against: the environment +# (as the CI stages it), else the sibling checkout wolfBoot's config expects. +mcxn947_wolfboot_sdk() { + if [ -z "${MCUXPRESSO:-}" ] && [ -d "$HOME/NXP/mcuxpresso-sdk/mcuxsdk" ]; then + MCUXPRESSO="$HOME/NXP/mcuxpresso-sdk/mcuxsdk" + export MCUXPRESSO + fi + if [ -z "${MCUXPRESSO_CMSIS:-}" ] && [ -d "$HOME/NXP/CMSIS_5/CMSIS" ]; then + MCUXPRESSO_CMSIS="$HOME/NXP/CMSIS_5/CMSIS" + export MCUXPRESSO_CMSIS + fi + [ -n "${MCUXPRESSO:-}" ] && [ -d "$MCUXPRESSO" ] && + [ -n "${MCUXPRESSO_CMSIS:-}" ] && [ -d "$MCUXPRESSO_CMSIS" ] +} + +# mcxn947_wolfboot_build [make VAR=value...]: builds /wolfboot.bin +# at the pin unless the tree there already matches; nonzero on any failure. +mcxn947_wolfboot_build() { + mcxn947_dir="$1" + shift + mcxn947_stamp=$(mcxn947_wolfboot_stamp "$@") + if mcxn947_wolfboot_current "$mcxn947_dir" "$mcxn947_stamp"; then + return 0 + fi + mcxn947_wolfboot_sdk || { + echo "MCXN947 wolfBoot needs the MCUXpresso SDK: set MCUXPRESSO and MCUXPRESSO_CMSIS" >&2 + return 1 + } + rm -rf "$mcxn947_dir" && + git clone --no-checkout "$MCXN947_WOLFBOOT_REPO" "$mcxn947_dir" && + git -C "$mcxn947_dir" fetch --depth 1 origin "$MCXN947_WOLFBOOT_REF" && + git -C "$mcxn947_dir" checkout --detach "$MCXN947_WOLFBOOT_REF" && + git -C "$mcxn947_dir" apply "$here/wolfboot-mcxn947-lifecycle.patch" && + git -C "$mcxn947_dir" submodule update --init --single-branch --depth 1 && + cp "$mcxn947_dir/$MCXN947_WOLFBOOT_CONFIG" "$mcxn947_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 "$mcxn947_dir" && + make keytools && + make -j1 wolfboot_signing_private_key.der && + make -j1 "$@" wolfboot.bin + ) && + printf '%s\n' "$mcxn947_stamp" > "$mcxn947_dir/.wt_first_stage" +} diff --git a/tests/target/lib/scenario_matrix.py b/tests/target/lib/scenario_matrix.py index 992a2613d..ccdf77500 100755 --- a/tests/target/lib/scenario_matrix.py +++ b/tests/target/lib/scenario_matrix.py @@ -158,6 +158,50 @@ ), "l3_exempt": {}, }, + "mcxn947": { + "label": "ci:mcxn947", + "arm": "mcxn947-m33mu", + "image": "ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.25", + "smoke": ("positive", "sauneg", "crossdomain", "bothpsa", "confboot", + "devcrypto"), + "groups": ( + ("positive", "MCXN947 positive lifecycle (SAU guest windows)"), + ("sauneg", "MCXN947 cross-guest store faults and is contained"), + ("restart authneg", "MCXN947 guest restart budget and launch refusal"), + ("wrpoff", "MCXN947 fence-required build launches no guest"), + ("rollbackneg remeasureneg manifestneg spbudgetneg", + "MCXN947 secure verdict negatives"), + ("crossdomain keystoreneg", "MCXN947 SP domain isolation negatives"), + ("spfaultneg panicneg", "MCXN947 SP fault and panic recovery"), + ("fpneg", "MCXN947 FP isolation (partition FP faults, contained)"), + ("sealbootneg", "MCXN947 damaged main-stack seal refuses to boot"), + ("bothpsa bothiso", "MCXN947 both-guest PSA lifecycle and isolation"), + ("attestneg fwustage", + "MCXN947 attestation negatives and FWU staging"), + ("hsmattackneg", + "MCXN947 wolfHSM cross-namespace + NVM relay negatives"), + ("confboot", "MCXN947 FF-M IPC conformance (85/4)"), + ("devstorage devattest devattestqcbor", + "MCXN947 dev_apis storage and attestation conformance"), + ("devcrypto vaultrecover vaultrecoversec", + "MCXN947 dev_apis crypto conformance and vault recovery"), + ("periphneg", "MCXN947 NS read of the SPM's ELS refused by the SAU"), + ("deputyneg", "MCXN947 keystore-flash privileged deputy refused (L3)"), + ("hsmpinneg hsmfaultneg", + "MCXN947 wolfHSM pointer pin and tasklet fault containment (L3)"), + ("bandneg1 bandneg2 bandneg3 bandneg4 bandneg5 bandneg6", + "MCXN947 partition band-to-band isolation negatives (L3)"), + ("restartneg1 restartneg2 restartneg3", + "MCXN947 partitions restart on a reset band (L3)"), + ("periphspneg manifestneg2 manifestneg3", + "MCXN947 SPM peripheral and manifest level negatives (L3)"), + ("sealneg sealhaltneg sealpivotneg", + "MCXN947 stack seal containment and halt (L3)"), + ("mspovfneg xnneg svcneg", + "MCXN947 SPM stack, execute-never and SVC misuse (L3)"), + ), + "l3_exempt": {}, + }, } @@ -218,18 +262,29 @@ def ci_plan(event, labels, port_input): SELFTEST = ( - ("pull_request", "", "", "mimxrt700:smoke stm32h563:smoke"), - ("pull_request", "ci:rt700", "", "mimxrt700:full stm32h563:smoke"), - ("pull_request", "ci:h5", "", "mimxrt700:smoke stm32h563:full"), - ("pull_request", "ci:all", "", "mimxrt700:full stm32h563:full"), - ("pull_request", "ci:m33mu", "", "mimxrt700:full stm32h563:full"), - ("pull_request", "ci:h5 ci:rt700", "", "mimxrt700:full stm32h563:full"), - ("push", "", "", "mimxrt700:full stm32h563:full"), - ("schedule", "", "", "mimxrt700:full stm32h563:full"), - ("workflow_dispatch", "", "", "mimxrt700:full stm32h563:full"), - ("workflow_dispatch", "", "all", "mimxrt700:full stm32h563:full"), + ("pull_request", "", "", + "mcxn947:smoke mimxrt700:smoke stm32h563:smoke"), + ("pull_request", "ci:rt700", "", + "mcxn947:smoke mimxrt700:full stm32h563:smoke"), + ("pull_request", "ci:h5", "", + "mcxn947:smoke mimxrt700:smoke stm32h563:full"), + ("pull_request", "ci:mcxn947", "", + "mcxn947:full mimxrt700:smoke stm32h563:smoke"), + ("pull_request", "ci:all", "", + "mcxn947:full mimxrt700:full stm32h563:full"), + ("pull_request", "ci:m33mu", "", + "mcxn947:full mimxrt700:full stm32h563:full"), + ("pull_request", "ci:h5 ci:rt700", "", + "mcxn947:smoke mimxrt700:full stm32h563:full"), + ("push", "", "", "mcxn947:full mimxrt700:full stm32h563:full"), + ("schedule", "", "", "mcxn947:full mimxrt700:full stm32h563:full"), + ("workflow_dispatch", "", "", + "mcxn947:full mimxrt700:full stm32h563:full"), + ("workflow_dispatch", "", "all", + "mcxn947:full mimxrt700:full stm32h563:full"), ("workflow_dispatch", "", "mimxrt700", "mimxrt700:full"), ("workflow_dispatch", "", "stm32h563", "stm32h563:full"), + ("workflow_dispatch", "", "mcxn947", "mcxn947:full"), ) diff --git a/tests/target/m33mu-mcxn947.patch b/tests/target/m33mu-mcxn947.patch new file mode 100644 index 000000000..4049df45b --- /dev/null +++ b/tests/target/m33mu-mcxn947.patch @@ -0,0 +1,547 @@ +diff --git a/cpu/mcxn947/mcxn947_mmio.c b/cpu/mcxn947/mcxn947_mmio.c +index 831de76..e279d77 100644 +--- a/cpu/mcxn947/mcxn947_mmio.c ++++ b/cpu/mcxn947/mcxn947_mmio.c +@@ -4,6 +4,7 @@ + */ + #include + #include ++#include + #include "mcxn947/mcxn947_mmio.h" + #include "mcxn947/mcxn947_romapi.h" + #include "mcxn947/mcxn947_secure.h" +@@ -102,8 +103,12 @@ + + /* SYSCON register offsets */ + #define SYSCON_PRESETCTRL0 0x100u ++#define SYSCON_PRESETCTRL2 0x108u + #define SYSCON_AHBCLKCTRL0 0x200u ++#define SYSCON_AHBCLKCTRL2 0x208u + #define SYSCON_AHBCLKCTRL1 0x204u ++#define SYSCON_ELS_OTP_LC_STATE 0x980u ++#define SYSCON_ELS_OTP_LC_STATE_DP 0x984u + + struct gpio_bank { + mm_u32 regs[GPIO_SIZE / 4]; +@@ -674,8 +679,16 @@ void mm_mcxn947_mmio_reset(void) + g_gpio_nvic = 0; + + /* Initialize SYSCON reset states */ +- syscon.regs[SYSCON_AHBCLKCTRL0 / 4] = 0x021FFE603u; /* + ELS clock bit */ +- syscon.regs[SYSCON_PRESETCTRL0 / 4] = 0x021FFE000u; /* + ELS reset released */ ++ syscon.regs[SYSCON_AHBCLKCTRL0 / 4] = 0x021FFE603u; ++ syscon.regs[SYSCON_PRESETCTRL0 / 4] = 0x021FFE000u; ++ /* The ROM leaves the ELS (CSS, AHBCLKCTRL2/PRESETCTRL2 bit 18) clocked ++ * and out of reset. */ ++ syscon.regs[SYSCON_AHBCLKCTRL2 / 4] = (1u << 18); ++ syscon.regs[SYSCON_PRESETCTRL2 / 4] = (1u << 18); ++ /* ELS_OTP_LC_STATE and its duplicate: OEM_OPEN (0x03), the life cycle a ++ * FRDM board ships in, as the ROM reflects it from OTP. */ ++ syscon.regs[SYSCON_ELS_OTP_LC_STATE / 4] = 0x03u; ++ syscon.regs[SYSCON_ELS_OTP_LC_STATE_DP / 4] = 0x03u; + + scg.regs[SCG_VERID / 4u] = 0x00000000u; + scg.regs[SCG_PARAM / 4u] = 0x000001FEu; +@@ -774,6 +787,10 @@ static mm_bool syscon_write(void *opaque, mm_u32 offset, mm_u32 size_bytes, mm_u + if (s == 0 || size_bytes == 0 || size_bytes > 4) return MM_FALSE; + if ((offset + size_bytes) > SYSCON_SIZE) return MM_FALSE; + if (size_bytes != 4) return MM_FALSE; ++ if (offset < 0x250u && getenv("M33MU_MCXN947_SYSCON_TRACE")) { ++ printf("[SYSCON] write offset=0x%03lx value=0x%08lx\n", ++ (unsigned long)offset, (unsigned long)value); ++ } + s->regs[offset / 4] = value; + return MM_TRUE; + } +diff --git a/cpu/mcxn947/mcxn947_romapi.c b/cpu/mcxn947/mcxn947_romapi.c +index e98f570..3ac8b98 100644 +--- a/cpu/mcxn947/mcxn947_romapi.c ++++ b/cpu/mcxn947/mcxn947_romapi.c +@@ -112,8 +112,11 @@ + + #define ROMAPI_FLASH_ERASE_KEY 0x6b65666cu + ++#define ROMAPI_FLASH_PAGE_SIZE 128u ++ + #define ROMAPI_STATUS_FLASH_SUCCESS 0u + #define ROMAPI_STATUS_FLASH_INVALID_ARGUMENT 4u ++#define ROMAPI_STATUS_FLASH_ALIGNMENT_ERROR 101u + #define ROMAPI_STATUS_FLASH_ADDRESS_ERROR 102u + #define ROMAPI_STATUS_FLASH_ACCESS_ERROR 103u + #define ROMAPI_STATUS_FLASH_ERASE_KEY_ERROR 107u +@@ -320,6 +323,23 @@ static mm_bool romapi_memcpy_from_map(const struct mm_memmap *map, enum mm_sec_s + return MM_TRUE; + } + ++/* Programming only clears bits, so an all-ones phrase leaves the array as it ++ * is; that is how a phrase gets programmed from its page base on silicon. */ ++static mm_bool romapi_flash_program_from_map(const struct mm_memmap *map, enum mm_sec_state sec, ++ mm_u32 dst, mm_u32 src, mm_u32 len, struct mm_memmap *wmap) ++{ ++ mm_u32 i; ++ mm_u8 b; ++ mm_u8 old; ++ if (map == 0 || wmap == 0) return MM_FALSE; ++ for (i = 0; i < len; ++i) { ++ if (!romapi_read_mem8(map, sec, src + i, &b)) return MM_FALSE; ++ if (!romapi_read_mem8(map, sec, dst + i, &old)) return MM_FALSE; ++ if (!romapi_write_mem8(wmap, sec, dst + i, (mm_u8)(old & b))) return MM_FALSE; ++ } ++ return MM_TRUE; ++} ++ + static mm_bool romapi_memset_map(struct mm_memmap *map, enum mm_sec_state sec, mm_u32 dst, mm_u8 value, mm_u32 len) + { + mm_u32 i; +@@ -493,10 +513,14 @@ static mm_u32 romapi_flash_program(struct mm_cpu *cpu, struct mm_memmap *map) + src = cpu->r[2]; + len = cpu->r[3]; + if (len == 0u) return ROMAPI_STATUS_FLASH_SUCCESS; ++ /* The ROM takes a program only from a page base (status 101 on silicon). */ ++ if ((start % ROMAPI_FLASH_PAGE_SIZE) != 0u || (len % 4u) != 0u) { ++ return ROMAPI_STATUS_FLASH_ALIGNMENT_ERROR; ++ } + if (!romapi_flash_offset(map, start, len, &flash_off)) { + return ROMAPI_STATUS_FLASH_ADDRESS_ERROR; + } +- if (!romapi_memcpy_from_map(map, cpu->sec_state, start, src, len, map)) { ++ if (!romapi_flash_program_from_map(map, cpu->sec_state, start, src, len, map)) { + return ROMAPI_STATUS_FLASH_ACCESS_ERROR; + } + romapi_persist_flush(flash_off, len); +diff --git a/cpu/mcxn947/mcxn947_secure.c b/cpu/mcxn947/mcxn947_secure.c +index 148ef22..d15bd66 100644 +--- a/cpu/mcxn947/mcxn947_secure.c ++++ b/cpu/mcxn947/mcxn947_secure.c +@@ -26,6 +26,36 @@ + #define MCXN947_SEC_ALIAS 0x10000000u + #define MCXN947_SEC_SIZE 0x1000u + ++/* The EdgeLock (S50) command interface at its documented base: enough of the ++ * register map for a Secure caller to enable the block and run RND_REQ, which ++ * fills the DMA result buffer. The block refuses Non-secure accesses. */ ++#define MCXN947_S50_BASE 0x40054000u ++#define S50_REG_STATUS 0x000u ++#define S50_REG_CTRL 0x004u ++#define S50_REG_CMDCFG0 0x008u ++#define S50_REG_CFG 0x00Cu ++#define S50_REG_DMA_RES0 0x038u ++#define S50_REG_DMA_RES0_LEN 0x03Cu ++#define S50_REG_INT_ENABLE 0x040u ++#define S50_REG_INT_STATUS_CLR 0x044u ++#define S50_REG_INT_STATUS_SET 0x048u ++#define S50_REG_ERR_STATUS 0x04Cu ++#define S50_REG_ERR_STATUS_CLR 0x050u ++#define S50_REG_VERSION 0x054u ++#define S50_REG_PRNG_DATOUT 0x05Cu ++#define S50_REG_WORDS (0x80u / 4u) ++#define S50_STATUS_ERR (1u << 2) ++#define S50_STATUS_DRBG_ENT_LVL (3u << 8) ++#define S50_CTRL_EN (1u << 0) ++#define S50_CTRL_START (1u << 1) ++#define S50_CTRL_RESET (1u << 2) ++#define S50_CTRL_CMD_SHIFT 3u ++#define S50_CTRL_CMD_MASK 0x1Fu ++#define S50_CMD_RND_REQ 24u ++#define S50_ERR_OPN (1u << 1) ++#define S50_ERR_ALG (1u << 2) ++#define S50_RND_MAX 4096u ++ + #define SEC_REG_CMD 0x000u + #define SEC_REG_STATUS 0x004u + #define SEC_REG_ARG0 0x008u +@@ -96,13 +126,21 @@ struct mcxn947_secure_persist { + struct mcxn947_secure_slot slots[MCXN947_ELS_SLOTS]; + }; + ++struct mcxn947_s50_state { ++ mm_u32 regs[S50_REG_WORDS]; ++ mm_u32 err_status; ++ mm_bool enabled; ++}; ++ + struct mcxn947_secure_state { + struct mcxn947_secure_regs puf; + struct mcxn947_secure_regs els; + struct mcxn947_secure_regs pkc; + struct mcxn947_secure_regs otpc; + struct mcxn947_secure_regs prince; ++ struct mcxn947_s50_state s50; + struct mcxn947_secure_persist persist; ++ struct mm_memmap *map; + mm_u8 *flash; + mm_u32 flash_size; + mm_u32 lifecycle; +@@ -686,6 +724,141 @@ static mm_bool secure_region_write(void *opaque, mm_u32 offset, mm_u32 size_byte + return MM_FALSE; + } + ++/* RND_REQ: the DMA result buffer is filled a word at a time through the ++ * memory map, as the ELS DMA master would write it. */ ++static void s50_rnd_req(struct mcxn947_s50_state *s) ++{ ++ mm_u8 block[S50_RND_MAX]; ++ mm_u32 addr = s->regs[S50_REG_DMA_RES0 / 4u]; ++ mm_u32 len = s->regs[S50_REG_DMA_RES0_LEN / 4u]; ++ mm_u32 i; ++ if (g_sec.map == 0 || len == 0u || len > S50_RND_MAX || ++ (len % 4u) != 0u || (addr % 4u) != 0u) { ++ s->err_status |= S50_ERR_OPN; ++ if (getenv("M33MU_S50_TRACE")) { ++ printf("[S50] RND_REQ refused addr=0x%08lx len=%lu\n", ++ (unsigned long)addr, (unsigned long)len); ++ } ++ return; ++ } ++ mm_mcxn947_secure_rng_fill(block, len); ++ for (i = 0u; i < len; i += 4u) { ++ if (!mm_memmap_write(g_sec.map, MM_SECURE, addr + i, 4u, ++ read32le(block + i))) { ++ s->err_status |= S50_ERR_OPN; ++ if (getenv("M33MU_S50_TRACE")) { ++ printf("[S50] RND_REQ DMA write failed addr=0x%08lx\n", ++ (unsigned long)(addr + i)); ++ } ++ return; ++ } ++ } ++ if (getenv("M33MU_S50_TRACE")) { ++ printf("[S50] RND_REQ addr=0x%08lx len=%lu ok\n", ++ (unsigned long)addr, (unsigned long)len); ++ } ++} ++ ++static void s50_ctrl_write(struct mcxn947_s50_state *s, mm_u32 value) ++{ ++ mm_u32 cmd = (value >> S50_CTRL_CMD_SHIFT) & S50_CTRL_CMD_MASK; ++ if ((value & S50_CTRL_RESET) != 0u) { ++ s->err_status = 0u; ++ s->enabled = MM_FALSE; ++ return; ++ } ++ s->enabled = ((value & S50_CTRL_EN) != 0u) ? MM_TRUE : MM_FALSE; ++ if ((value & S50_CTRL_START) == 0u) { ++ return; ++ } ++ if (!s->enabled || !mm_mcxn947_secure_els_clock_ready()) { ++ s->err_status |= S50_ERR_OPN; ++ if (getenv("M33MU_S50_TRACE")) { ++ printf("[S50] START refused enabled=%d clock_ready=%d\n", ++ (int)s->enabled, (int)mm_mcxn947_secure_els_clock_ready()); ++ } ++ return; ++ } ++ if (cmd == S50_CMD_RND_REQ) { ++ s50_rnd_req(s); ++ } else { ++ s->err_status |= S50_ERR_ALG; ++ } ++} ++ ++static mm_bool s50_read(void *opaque, mm_u32 offset, mm_u32 size_bytes, ++ mm_u32 *value_out) ++{ ++ struct mcxn947_s50_state *s = (struct mcxn947_s50_state *)opaque; ++ mm_u8 word[4]; ++ if (!secure_access_ok()) return MM_FALSE; ++ if (s == 0 || value_out == 0 || size_bytes != 4u) return MM_FALSE; ++ if (offset >= S50_REG_WORDS * 4u) return MM_FALSE; ++ switch (offset) { ++ case S50_REG_STATUS: ++ *value_out = S50_STATUS_DRBG_ENT_LVL | ++ ((s->err_status != 0u) ? S50_STATUS_ERR : 0u); ++ return MM_TRUE; ++ case S50_REG_ERR_STATUS: ++ *value_out = s->err_status; ++ return MM_TRUE; ++ case S50_REG_VERSION: ++ *value_out = 0x00050000u; ++ return MM_TRUE; ++ case S50_REG_PRNG_DATOUT: ++ mm_mcxn947_secure_rng_fill(word, sizeof(word)); ++ *value_out = read32le(word); ++ return MM_TRUE; ++ default: ++ *value_out = s->regs[offset / 4u]; ++ return MM_TRUE; ++ } ++} ++ ++static mm_bool s50_write(void *opaque, mm_u32 offset, mm_u32 size_bytes, ++ mm_u32 value) ++{ ++ struct mcxn947_s50_state *s = (struct mcxn947_s50_state *)opaque; ++ if (!secure_access_ok()) return MM_FALSE; ++ if (s == 0 || size_bytes != 4u) return MM_FALSE; ++ if (offset >= S50_REG_WORDS * 4u) return MM_FALSE; ++ switch (offset) { ++ case S50_REG_STATUS: ++ case S50_REG_ERR_STATUS: ++ case S50_REG_VERSION: ++ case S50_REG_PRNG_DATOUT: ++ return MM_FALSE; ++ case S50_REG_CTRL: ++ s->regs[offset / 4u] = value & ~S50_CTRL_START; ++ s50_ctrl_write(s, value); ++ return MM_TRUE; ++ case S50_REG_ERR_STATUS_CLR: ++ if ((value & 1u) != 0u) s->err_status = 0u; ++ return MM_TRUE; ++ case S50_REG_INT_STATUS_CLR: ++ case S50_REG_INT_STATUS_SET: ++ return MM_TRUE; ++ default: ++ s->regs[offset / 4u] = value; ++ return MM_TRUE; ++ } ++} ++ ++static mm_bool secure_register_s50(struct mmio_bus *bus) ++{ ++ struct mmio_region reg; ++ memset(®, 0, sizeof(reg)); ++ reg.base = MCXN947_S50_BASE; ++ reg.size = MCXN947_SEC_SIZE; ++ reg.opaque = &g_sec.s50; ++ reg.read = s50_read; ++ reg.write = s50_write; ++ if (!mmio_bus_register_region(bus, ®)) return MM_FALSE; ++ reg.base = MCXN947_S50_BASE + MCXN947_SEC_ALIAS; ++ if (!mmio_bus_register_region(bus, ®)) return MM_FALSE; ++ return MM_TRUE; ++} ++ + static mm_bool secure_register_pair(struct mmio_bus *bus, mm_u32 base, + struct mcxn947_secure_regs *regs) + { +@@ -710,6 +883,7 @@ void mm_mcxn947_secure_reset(void) + secure_reset_regs(&g_sec.els); + secure_reset_regs(&g_sec.pkc); + secure_reset_regs(&g_sec.prince); ++ memset(&g_sec.s50, 0, sizeof(g_sec.s50)); + g_sec.puf.status |= SEC_STATUS_DONE; + g_sec.otpc.status |= SEC_STATUS_DONE; + g_sec.els.status |= SEC_STATUS_DONE; +@@ -730,6 +904,7 @@ mm_bool mm_mcxn947_secure_register_mmio(struct mmio_bus *bus) + if (!secure_register_pair(bus, MCXN947_ELS_BASE, &g_sec.els)) return MM_FALSE; + if (!secure_register_pair(bus, MCXN947_PKC_BASE, &g_sec.pkc)) return MM_FALSE; + if (!secure_register_pair(bus, MCXN947_PRINCE_BASE, &g_sec.prince)) return MM_FALSE; ++ if (!secure_register_s50(bus)) return MM_FALSE; + return MM_TRUE; + } + +@@ -739,9 +914,9 @@ void mm_mcxn947_secure_flash_bind(struct mm_memmap *map, + const struct mm_flash_persist *persist, + mm_u32 flags) + { +- (void)map; + (void)persist; + (void)flags; ++ g_sec.map = map; + g_sec.flash = flash; + g_sec.flash_size = flash_size; + secure_refresh_identity(&g_sec); +@@ -778,10 +953,11 @@ void mm_mcxn947_secure_rng_fill(mm_u8 *out, mm_u32 len) + } + } + ++/* The ELS (CSS) gate is AHBCLKCTRL2/PRESETCTRL2 bit 18 (SDK kCLOCK_Css). */ + mm_bool mm_mcxn947_secure_els_clock_ready(void) + { +- return mm_mcxn947_syscon_clock_bit_on(0x200u, 25u) && +- mm_mcxn947_syscon_reset_bit_released(0x100u, 25u); ++ return mm_mcxn947_syscon_clock_bit_on(0x208u, 18u) && ++ mm_mcxn947_syscon_reset_bit_released(0x108u, 18u); + } + + mm_bool mm_mcxn947_secure_rom_call_allowed(enum mm_sec_state sec) +diff --git a/include/m33mu/decode.h b/include/m33mu/decode.h +index 9f5739c..382fce2 100644 +--- a/include/m33mu/decode.h ++++ b/include/m33mu/decode.h +@@ -124,6 +124,8 @@ enum mm_op_kind { + MM_OP_LDRSB_POST_IMM, + MM_OP_LDRSH_REG, + MM_OP_LDRSH_IMM, ++ MM_OP_LDRSH_PRE_IMM, ++ MM_OP_LDRSH_POST_IMM, + MM_OP_LDRSHT, + MM_OP_STRHT, + MM_OP_LDRHT, +diff --git a/src/execute.c b/src/execute.c +index 9f06d51..aaddc0f 100644 +--- a/src/execute.c ++++ b/src/execute.c +@@ -4492,6 +4492,36 @@ enum mm_exec_status mm_execute_decoded(struct mm_execute_ctx *ctx) + cpu.r[d.rn] = base + d.imm; + } + } break; ++ case MM_OP_LDRSH_POST_IMM: { ++ mm_u32 base = cpu.r[d.rn]; ++ mm_u32 val = 0; ++ EXEC_CHECK_UNALIGN(base, 2u); ++ if (!exec_read_signed(&map, cpu.sec_state, base, 2u, &val)) { ++ if (!raise_mem_fault(&cpu, &map, &scs, f.pc_fetch, cpu.xpsr, base, MM_FALSE)) done = MM_TRUE; ++ return MM_EXEC_CONTINUE; ++ } ++ cpu.r[d.rd] = val; ++ if (d.rn == 13u) { ++ EXEC_SET_SP(base + d.imm); ++ } else { ++ cpu.r[d.rn] = base + d.imm; ++ } ++ } break; ++ case MM_OP_LDRSH_PRE_IMM: { ++ mm_u32 addr = cpu.r[d.rn] + d.imm; ++ mm_u32 val = 0; ++ EXEC_CHECK_UNALIGN(addr, 2u); ++ if (!exec_read_signed(&map, cpu.sec_state, addr, 2u, &val)) { ++ if (!raise_mem_fault(&cpu, &map, &scs, f.pc_fetch, cpu.xpsr, addr, MM_FALSE)) done = MM_TRUE; ++ return MM_EXEC_CONTINUE; ++ } ++ cpu.r[d.rd] = val; ++ if (d.rn == 13u) { ++ EXEC_SET_SP(addr); ++ } else { ++ cpu.r[d.rn] = addr; ++ } ++ } break; + case MM_OP_LDRSH_IMM: + case MM_OP_LDRSHT: { + mm_u32 addr = cpu.r[d.rn] + d.imm; +diff --git a/src/m33mu/capstone.c b/src/m33mu/capstone.c +index bbfad17..b401d68 100644 +--- a/src/m33mu/capstone.c ++++ b/src/m33mu/capstone.c +@@ -550,7 +550,8 @@ static mm_bool cross_check_kind(const cs_insn *insn, const struct mm_decoded *de + case ARM_INS_LDRSB: return (dec->kind == MM_OP_LDRSB_IMM || + dec->kind == MM_OP_LDRSB_POST_IMM || + dec->kind == MM_OP_LDRSB_REG); +- case ARM_INS_LDRSH: return (dec->kind == MM_OP_LDRSH_IMM || dec->kind == MM_OP_LDRSH_REG); ++ case ARM_INS_LDRSH: return (dec->kind == MM_OP_LDRSH_IMM || dec->kind == MM_OP_LDRSH_REG || ++ dec->kind == MM_OP_LDRSH_POST_IMM || dec->kind == MM_OP_LDRSH_PRE_IMM); + case ARM_INS_DSB: return dec->kind == MM_OP_DSB; + case ARM_INS_DMB: return dec->kind == MM_OP_DMB; + case ARM_INS_ISB: return dec->kind == MM_OP_ISB; +@@ -1288,12 +1289,13 @@ static mm_bool cross_check_operands(const struct mm_fetch_result *fetch, const c + if (dec->kind == MM_OP_LDR_POST_IMM || dec->kind == MM_OP_STR_POST_IMM || + dec->kind == MM_OP_LDRB_POST_IMM || dec->kind == MM_OP_STRB_POST_IMM || + dec->kind == MM_OP_LDRH_POST_IMM || dec->kind == MM_OP_STRH_POST_IMM || +- dec->kind == MM_OP_LDRSB_POST_IMM) { ++ dec->kind == MM_OP_LDRSB_POST_IMM || dec->kind == MM_OP_LDRSH_POST_IMM) { + expect_wb = MM_TRUE; + expect_post = MM_TRUE; + } else if (dec->kind == MM_OP_LDR_PRE_IMM || dec->kind == MM_OP_STR_PRE_IMM || + dec->kind == MM_OP_LDRB_PRE_IMM || dec->kind == MM_OP_STRB_PRE_IMM || +- dec->kind == MM_OP_LDRH_PRE_IMM || dec->kind == MM_OP_STRH_PRE_IMM) { ++ dec->kind == MM_OP_LDRH_PRE_IMM || dec->kind == MM_OP_STRH_PRE_IMM || ++ dec->kind == MM_OP_LDRSH_PRE_IMM) { + expect_wb = MM_TRUE; + expect_post = MM_FALSE; + } +diff --git a/src/m33mu/decode.c b/src/m33mu/decode.c +index 6882d58..0f5eed2 100644 +--- a/src/m33mu/decode.c ++++ b/src/m33mu/decode.c +@@ -1564,6 +1564,36 @@ static struct mm_decoded decode_32(mm_u32 insn) + d.undefined = MM_FALSE; + return d; + } ++ /* Post-index writeback form: LDRSH Rt, [Rn], #+/-imm8 */ ++ if (p == 0u && w == 1u) { ++ mm_u8 rn = (mm_u8)((insn >> 16) & 0x0fu); ++ mm_u8 rt = (mm_u8)((insn >> 12) & 0x0fu); ++ mm_u32 imm8 = insn & 0xffu; ++ if (rt == 15u || rn == 15u) { ++ return d; ++ } ++ d.kind = MM_OP_LDRSH_POST_IMM; ++ d.rn = rn; ++ d.rd = rt; ++ d.imm = u ? imm8 : (mm_u32)(0u - imm8); ++ d.undefined = MM_FALSE; ++ return d; ++ } ++ /* Pre-index writeback form: LDRSH Rt, [Rn, #+/-imm8]! */ ++ if (p == 1u && w == 1u) { ++ mm_u8 rn = (mm_u8)((insn >> 16) & 0x0fu); ++ mm_u8 rt = (mm_u8)((insn >> 12) & 0x0fu); ++ mm_u32 imm8 = insn & 0xffu; ++ if (rt == 15u || rn == 15u) { ++ return d; ++ } ++ d.kind = MM_OP_LDRSH_PRE_IMM; ++ d.rn = rn; ++ d.rd = rt; ++ d.imm = u ? imm8 : (mm_u32)(0u - imm8); ++ d.undefined = MM_FALSE; ++ return d; ++ } + } + + /* LDAEX (word) */ +diff --git a/src/main.c b/src/main.c +index aad0697..9457fa4 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -6382,8 +6382,11 @@ int main(int argc, char **argv) + mm_prot_add_region(&prot, cfg.flash_base_ns, cfg.flash_size_ns, + MM_PROT_PERM_READ | MM_PROT_PERM_WRITE | MM_PROT_PERM_EXEC, MM_SECURE); + if (cpu_name != 0 && strcmp(cpu_name, "mcxn947") == 0) { +- mm_prot_add_region(&prot, 0x1303fc00u, 0x00000200u, MM_PROT_PERM_READ, MM_SECURE); +- mm_prot_add_region(&prot, 0x1303fc00u, 0x00000200u, MM_PROT_PERM_READ, MM_NONSECURE); ++ /* MCXN947 ROM at 0x03000000 (NS alias) / 0x13000000 (S alias): ++ * the ROM API stubs are fetched from it, so without an ++ * executable region every ROM call left IACCVIOL in CFSR. */ ++ mm_prot_add_region(&prot, 0x03000000u, 0x00040000u, MM_PROT_PERM_READ | MM_PROT_PERM_EXEC, MM_NONSECURE); ++ mm_prot_add_region(&prot, 0x13000000u, 0x00040000u, MM_PROT_PERM_READ | MM_PROT_PERM_EXEC, MM_SECURE); + } + if (cpu_name != 0 && strcmp(cpu_name, "lpc55s69") == 0) { + /* LPC55S69 ROM is at 0x03000000 (NS alias) / 0x13000000 (S alias). +diff --git a/tests/decode_phase3_test.c b/tests/decode_phase3_test.c +index 2731177..4564284 100644 +--- a/tests/decode_phase3_test.c ++++ b/tests/decode_phase3_test.c +@@ -293,6 +293,33 @@ static int test_ldrsh_w_imm8_pre(void) + return 0; + } + ++static int test_ldrsh_w_imm8_post(void) ++{ ++ struct mm_decoded dec; ++ /* LDRSH.W fp, [r8], #2 : hw1=0xf938, hw2=0xbb02 (P=0,U=1,W=1) */ ++ if (decode_insn(0xf938u, 0xbb02u, &dec) != 0) return 1; ++ if (dec.kind != MM_OP_LDRSH_POST_IMM) return 1; ++ if (dec.rd != 11u || dec.rn != 8u) return 1; ++ if (dec.imm != 2u) return 1; ++ /* LDRSH.W r1, [r2], #-4 : hw1=0xf932, hw2=0x1904 (P=0,U=0,W=1) */ ++ if (decode_insn(0xf932u, 0x1904u, &dec) != 0) return 1; ++ if (dec.kind != MM_OP_LDRSH_POST_IMM) return 1; ++ if (dec.rd != 1u || dec.rn != 2u) return 1; ++ if (dec.imm != 0xfffffffcu) return 1; ++ return 0; ++} ++ ++static int test_ldrsh_w_imm8_pre_wb(void) ++{ ++ struct mm_decoded dec; ++ /* LDRSH.W r3, [r4, #0x20]! : hw1=0xf934, hw2=0x3f20 (P=1,U=1,W=1) */ ++ if (decode_insn(0xf934u, 0x3f20u, &dec) != 0) return 1; ++ if (dec.kind != MM_OP_LDRSH_PRE_IMM) return 1; ++ if (dec.rd != 3u || dec.rn != 4u) return 1; ++ if (dec.imm != 0x20u) return 1; ++ return 0; ++} ++ + static int test_clz_w_decode(void) + { + struct mm_decoded dec; +@@ -604,6 +631,8 @@ int main(void) + { "ldrsb_w_imm8_pre", test_ldrsb_w_imm8_pre }, + { "ldrsh_w_imm12", test_ldrsh_w_imm12 }, + { "ldrsh_w_imm8_pre", test_ldrsh_w_imm8_pre }, ++ { "ldrsh_w_imm8_post", test_ldrsh_w_imm8_post }, ++ { "ldrsh_w_imm8_pre_wb", test_ldrsh_w_imm8_pre_wb }, + { "clz_w_decode", test_clz_w_decode }, + { "rbit_w_decode", test_rbit_w_decode }, + { "tt_decode", test_tt_decode }, diff --git a/tests/target/provisioning/provisioning_ctrl_mcxn947.sh b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh new file mode 100644 index 000000000..b036e5fcf --- /dev/null +++ b/tests/target/provisioning/provisioning_ctrl_mcxn947.sh @@ -0,0 +1,501 @@ +# shellcheck shell=bash +# provisioning_ctrl_mcxn947.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 . + +# MCXN947 (FRDM-MCXN947) port for provisioning_ctrl.sh: the OTP life cycle +# the ROM reflects into SYSCON, rehearsed in the boot handoff record; the +# lock is previewed from the vendor tools (SPSDK pfr and nxpfuses) and +# refused, hard-wired off in this phase. Sourced, never run. See +# docs/MCXN947-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=mcxn947 tests/target/provisioning/provisioning_ctrl.sh, not this port file." >&2 + exit 2 +fi + +PORT_NAME="MCXN947" +target_dir="$here/.." +pyocd_target="${MCXN947_TARGET:-mcxn947vdf}" +spsdk_venv="${MCXN947_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 pyOCD: 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="${MCXN947_GUEST_MASK:-0x3}" +case "$guest_mask" in + 0x1|0x2|0x3|1|2|3) ;; + *) refuse "MCXN947_GUEST_MASK must be 0x1, 0x2, or 0x3 (both guests), not '$guest_mask'." ;; +esac +elf="$repo/build/wolftrust.elf" +wolfboot_dir="${MCXN947_WOLFBOOT_DIR:-$HOME/.cache/wolftrust/wolfboot-mcxn947}" +# The blhost connection of the part in ISP mode, named only in the preview. +isp="${MCXN947_ISP:-}" +# The life cycle write stays hard-wired off in this phase: the lock previews +# the exact vendor write and refuses it whatever the caller supplies. +MCXN947_LOCK_WRITE_ENABLED=0 + +# SYSCON life cycle state the ROM reflects from OTP (read-only), its +# duplicate, the debug authentication status, the device UUID (IPC1), the +# BOOT partition's reset vector, the handoff record, and the PFR pages +# (SPSDK device database: CMPA 0x01004000, CFPA 0x01000000, 512 bytes; the +# CFPA ping and pong copies follow it). +LC_STATE=0x50000980 +LC_STATE_DP=0x50000984 +DAUTHSTATUS=0xE000EFB8 +UUID_BASE=0x01100000 +WOLFTRUST_RESET_VECTOR=0x10040404 +HANDOFF=0x30030000 +LC_OEM_OPEN=0x03 +CMPA_ADDR=0x01004000 +CFPA_SCRATCH_ADDR=0x01000000 +CFPA_PING_ADDR=0x01002000 +CFPA_PONG_ADDR=0x01003000 +PFR_PAGE_SIZE=512 +# The OTP fuse SPSDK's fuse map names LIFE_CYCLE (index 1): LIFECYCLE[7:0] +# and its duplicate LIFECYCLE_DP[7:0] carry the same code the ROM reflects. +FUSE_LIFE_CYCLE=1 + +port_ladder() { + cat <<'EOF' +0x03 oem-open no no permanent - +0x07 oem-secure-world yes yes permanent 0x03 +0x0F oem-closed yes no permanent 0x07 +0xCF oem-locked yes no permanent 0x0F +0x1F oem-field-return yes no permanent 0x0F +EOF +} +port_usage_extra() { + cat <<'EOF' + verify-wrp report the guest flash fence (none on this port yet) + verify-pages re-read the CMPA and CFPA pages and compare them with the last lock preview +EOF +} + +lc_hex() { printf '0x%02X' $(( $1 & 0xFF )); } +# SPSDK's fuse map name for a life cycle code (nxpfuses LIFECYCLE[7:0]). +fuse_lc_name() { + case "$1" in + 0x03) echo DEVELOP ;; 0x07) echo DEVELOP2 ;; 0x0F) echo IN_FIELD ;; + 0xCF) echo IN_FIELD_LOCKED ;; 0x1F) echo FIELD_RETURN_OEM ;; + esac +} +ensure_pyocd() { + if ! command -v pyocd >/dev/null 2>&1; then + [ -x "$spsdk_venv/bin/pyocd" ] || fail "tools" "pyocd not found (set MCXN947_SPSDK_VENV)" + PATH="$spsdk_venv/bin:$PATH" + export PATH + fi +} +ensure_spsdk() { + ensure_pyocd + command -v pfr >/dev/null 2>&1 && command -v nxpfuses >/dev/null 2>&1 || + fail "tools" "SPSDK pfr and nxpfuses not found (set MCXN947_SPSDK_VENV)" +} +# The probe this host names, else the one attached debug probe; the FRDM +# board's MCU-Link is soldered on. Resolved once, outside any subshell, so a +# missing id refuses loudly instead of leaving a read empty. +probe_uid() { + local ids + if [ -n "${MCXN947_PROBE_UID:-}" ]; then echo "$MCXN947_PROBE_UID"; return 0; fi + ids="$(timeout 30 pyocd list 2>/dev/null | awk '$1 ~ /^[0-9]+$/ { print $(NF-1) }')" + [ "$(printf '%s\n' "$ids" | grep -c .)" = "1" ] && echo "$ids" +} +ensure_probe() { + ensure_pyocd + [ -n "${PROBE:-}" ] && return 0 + PROBE="$(probe_uid)" || + refuse "this host has more than one debug probe: set MCXN947_PROBE_UID to the FRDM-MCXN947 MCU-Link's unique id (pyocd list)." +} +# pyocd bound to that probe: . +pyocd_probe() { + local secs="$1" + shift + [ -n "${PROBE:-}" ] || refuse "no debug probe bound (ensure_probe was not called)." + timeout "$secs" pyocd "$@" -u "$PROBE" +} +# 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 + pyocd_probe 60 cmd -t cortex_m "${cmds[@]}" 2>&1 | awk '/^[0-9a-f]+:/ { print $2 }' +} +# One PFR page (512 bytes) over SWD into a file; the IFR is memory mapped. +# The commander exits 0 after a failed command, so its output is checked and +# a stale file from an earlier read never stands in for this one. +read_page() { + local out + rm -f "$2" + out="$(pyocd_probe 60 cmd -t cortex_m -c "savemem $1 $PFR_PAGE_SIZE $2" 2>&1)" || return 1 + ! grep -qiE 'error|exception|fault|failure' <<<"$out" && + [ -f "$2" ] && [ "$(wc -c < "$2" | tr -d ' ')" = "$PFR_PAGE_SIZE" ] +} +page_digest() { sha256 < "$1" | cut -c1-64; } +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 PSA life cycle the mock hands wolfTrust for state $1: the production +# value, with debug closed, which only a fused part can show on its own. +mock_psa() { + case "$1" in + 0x07) echo 0x2000 ;; 0x0F|0xCF) echo 0x3000 ;; 0x1F) echo 0x6000 ;; + esac +} +# The life cycle fused when discover ran. +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 "$HANDOFF" $((HANDOFF + 4)) $((HANDOFF + 12)) | 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 four images the chain flashes, as "address file" lines (physical +# offsets; wolfBoot at 0, the BOOT partition and both guest windows), the +# ones run_mcxn947_hardware.sh builds and flashes. +flashed_images() { + printf '%s %s\n' \ + 0x00000000 "$wolfboot_dir/wolfboot.bin" \ + 0x00040000 "$repo/build/wolftrust_v1_signed.bin" \ + 0x00080000 "$repo/tests/firmware/mcxn947-baremetal/build/guest0.bin" \ + 0x00100000 "$repo/tests/firmware/mcxn947-baremetal/build/guest1.bin" +} +port_image_digest() { framed_digest; } +images_on_device() { + local a f + while read -r a f; do + pyocd_probe 120 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 device UUID, readable over SWD in every state short of closed debug. +chip_uuid() { + local w + w="$(read_words "$UUID_BASE" $((UUID_BASE + 4)) $((UUID_BASE + 8)) $((UUID_BASE + 12)) | tr -d '\n')" + [ ${#w} -eq 32 ] && [ "$w" != "00000000000000000000000000000000" ] && + [ "$w" != "ffffffffffffffffffffffffffffffff" ] && echo "$w" +} +# The MCXN947 has no guest flash fence yet: the array is written only through +# the ROM API from the Secure side, and no hardware write fence is claimed. +fence_line() { + echo "none (no guest flash fence on this port; a WT_GUEST_FLASH_WRP=1 image refuses every guest)" + return 1 +} + +port_status() { + local lc lcd dauth hl vm rf uuid + ensure_probe + read -r lc lcd dauth < <(read_words "$LC_STATE" "$LC_STATE_DP" "$DAUTHSTATUS" | tr '\n' ' '; echo) + [ -n "${lc:-}" ] && [ -n "${lcd:-}" ] || fail "status" "cannot read the life cycle over SWD through probe $PROBE" + echo "debug probe $PROBE" + echo "OTP life cycle LC_STATE=0x${lc: -2} ($(state_name "$(lc_hex "0x$lc")" || true)) LC_STATE_DP=0x${lcd: -2}" + echo "DAUTHSTATUS 0x$dauth" + echo "device UUID $(chip_uuid || echo unreadable)" + echo "guest fence $(fence_line || true)" + echo "lock write hard-wired off (MCXN947_LOCK_WRITE_ENABLED=$MCXN947_LOCK_WRITE_ENABLED): lock previews only" + 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 lcd dauth hl uuid + ensure_probe + mkdir -p "$state_dir" + rm -f "$state_dir/discovery" "$state_dir/regress-ok" + read -r lc lcd dauth < <(read_words "$LC_STATE" "$LC_STATE_DP" "$DAUTHSTATUS" | tr '\n' ' '; echo) + [ -n "${lc:-}" ] && [ -n "${lcd:-}" ] || fail "discover" "cannot read the life cycle over SWD through probe $PROBE" + [ "$(lc_hex "0x$lc")" = "$(lc_hex "0x$lcd")" ] || fail "discover" "life cycle copies disagree (LC 0x$lc, DP 0x$lcd)" + [ $((0x$lc & 0xFFFFFF00)) -eq 0 ] && [ $((0x$lcd & 0xFFFFFF00)) -eq 0 ] || + fail "discover" "life cycle words 0x$lc/0x$lcd carry bits above the state byte" + case "$(lc_hex "0x$lc")" in + 0x03|0x07|0x0F) ;; + *) fail "discover" "life cycle $(lc_hex "0x$lc") has no further rehearsal step" ;; + esac + pass "life cycle is $(label "$(lc_hex "0x$lc")") and its duplicate agrees (probe $PROBE)" + uuid="$(chip_uuid)" || fail "discover" "cannot read the device UUID at $UUID_BASE" + pass "device UUID $uuid" + hl="$(handoff_lifecycle)" || fail "discover" "no readable boot handoff: boot the wolfTrust chain first" + pass "the boot handoff life cycle is readable (0x$hl, $(psa_name "$hl"))" + printf 'LC=0x%s DP=0x%s DAUTH=0x%s UUID=%s PROBE=%s\n' "$lc" "$lcd" "$dauth" "$uuid" "$PROBE" > "$state_dir/discovery" + echo "PASS: discovery stamped ($state_dir/discovery)" +} +# The production images, through the hardware runner bound to this probe. +port_restore() { + ensure_probe + MCXN947_PROBE_UID="$PROBE" "$target_dir/run_mcxn947_hardware.sh" positive || + fail "restore" "the hardware runner did not pass; see its output" +} +port_extra() { + local line + case "$1" in + verify-wrp) + line="$(fence_line)" || fail "verify-wrp" "$line" + pass "guest fence $line" ;; + verify-pages) + ensure_probe + verify_pages ;; + provision-da|burn) + refuse "'$1' would program OTP: this port rehearses life cycles only and writes none of them; see the MCXN947 Guide." ;; + set-perimeter|set-wrp|clear-wrp) + refuse "'$1' has no MCXN947 form: wolfTrust programs the TrustZone perimeter on every boot, every reset clears it, and the port has no guest flash fence yet." ;; + *) 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 oem-secure-world before advancing to $1." +} +# Halt at wolfTrust's reset vector, after wolfBoot wrote the handoff record +# and before wolfTrust consumes it, replace the record's PSA life cycle with +# the target state's, and resume. The fuses and SYSCON are untouched. +port_advance() { + local entry vm rf psa + ensure_probe + 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 "$WOLFTRUST_RESET_VECTOR")" + [ -n "$entry" ] && [ "$entry" != "00000000" ] && [ "$entry" != "ffffffff" ] || + fail "advance" "no wolfTrust reset vector at $WOLFTRUST_RESET_VECTOR (flash the chain first)" + psa="$(mock_psa "$1")" + echo "ADVANCING the boot handoff life cycle to $psa for $1 ($(state_name "$1")) through probe $PROBE; regress or any reset undoes it" + "$spsdk_py" - "0x$entry" "$psa" "$HANDOFF" "$pyocd_target" "$vm" "$rf" "$PROBE" <<'PYEOF' +import sys +import time +from pyocd.core.helpers import ConnectHelper +from pyocd.core.target import Target + +HANDOFF_MAGIC = 0x5742484F +entry, psa, handoff = (int(a, 0) for a in sys.argv[1:4]) +with ConnectHelper.session_with_chosen_probe( + unique_id=sys.argv[7], + options={"target_override": sys.argv[4], "resume_on_disconnect": True, + "reset_type": "hw"}) as s: + t = s.target + t.reset_and_halt() + 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("wolfTrust entry breakpoint not reached") + time.sleep(0.02) + t.remove_breakpoint(entry & ~1) + if t.read32(handoff) != HANDOFF_MAGIC or \ + t.read32(handoff + 4) != (HANDOFF_MAGIC ^ 0xFFFFFFFF): + sys.exit("no intact handoff record at 0x%08x" % handoff) + t.write32(handoff + 12, psa) + got = t.read32(handoff + 12) + # SRAM survives the reset: clear the launch masks so only this boot's + # wolfTrust can set them. + for a in sys.argv[5:7]: + 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 at wolfTrust's entry 0x%08x; handoff life cycle=0x%04x" % (entry & ~1, got)) +sys.exit(0 if got == psa else 3) +PYEOF + sleep 3 +} +# The rehearsal evidence: the life cycle wolfTrust saw, every guest launched +# verified, the images read back, the device UUID, and the probe. +port_booted() { + local hl want vm rf digest uuid + ensure_probe + # advance cleared the launch masks, so both are this boot's. + hl="$(handoff_lifecycle)" || { echo "no boot handoff life cycle readable 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 + # 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 + 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: flash the chain again"; return 1; } + pass "the images on the part match the host build (${digest:0:16})" + uuid="$(chip_uuid)" || { echo "cannot read the device UUID"; return 1; } + EVIDENCE="id=$uuid image=$digest fused=$(fused_lc) fence=none probe=$PROBE" +} +port_regress() { + local fused lc lcd hl + ensure_probe + pyocd_probe 60 reset -t "$pyocd_target" -m hw >/dev/null 2>&1 || + fail "regress" "hardware reset through pyocd failed" + sleep 3 + fused="$(fused_lc || echo "$LC_OEM_OPEN")" + read -r lc lcd < <(read_words "$LC_STATE" "$LC_STATE_DP" | tr '\n' ' '; echo) + [ "$(lc_hex "0x$lc")" = "$fused" ] && [ "$(lc_hex "0x$lcd")" = "$fused" ] || + fail "regress" "life cycle after reset is 0x$lc/0x$lcd, 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 lcd + ensure_probe + read -r lc lcd < <(read_words "$LC_STATE" "$LC_STATE_DP" | tr '\n' ' '; echo) + [ -n "${lc:-}" ] && [ -n "${lcd:-}" ] || refuse "cannot read the life cycle over SWD." + [ "$(lc_hex "0x$lc")" = "$(lc_hex "0x$lcd")" ] || refuse "the life cycle copies disagree (LC 0x$lc, DP 0x$lcd)." + lc_hex "0x$lc" +} +port_lock_identity() { chip_uuid || true; } +port_rehearsal_for() { echo "$1"; } +port_record_ok() { + local rfused + ensure_probe + rfused="$(field fused "$1")" + [ -n "$rfused" ] && [ $((rfused & ~cur & 0xFF)) -eq 0 ] && + [ "$(field fence "$1")" = "none" ] && + [ "$(field probe "$1")" = "$PROBE" ] && + [ "$(chip_uuid || true)" = "$(field id "$1")" ] +} + +# --- The lock preview: the live pages, parsed; the state's fuse script and +# CMPA page from the vendor tools; the diff. Nothing here writes. --- +preview_dir() { echo "$state_dir/lock-$1"; } +# Read the four PFR pages into $1/, parse each, and digest them. +read_pages() { + local d="$1" n a + mkdir -p "$d" + for n in cmpa:$CMPA_ADDR cfpa-scratch:$CFPA_SCRATCH_ADDR cfpa-ping:$CFPA_PING_ADDR cfpa-pong:$CFPA_PONG_ADDR; do + a="${n##*:}"; n="${n%%:*}" + read_page "$a" "$d/$n.bin" || return 1 + pfr parse -f mcxn947 -t "${n%%-*}" -b "$d/$n.bin" -o "$d/$n.yaml" >/dev/null 2>&1 || return 1 + printf '%s %s\n' "$(page_digest "$d/$n.bin")" "$n.bin" + done +} +# The state's exact OTP write from SPSDK's fuse map, and its CMPA page from +# the live configuration (the life cycle lives in OTP, so the page is kept). +generate_lock() { + local d="$1" state="$2" name + name="$(fuse_lc_name "$state")" + [ -n "$name" ] || return 1 + printf 'family: mcxn947\nrevision: latest\nregisters:\n LIFE_CYCLE:\n "LIFECYCLE[7:0]": %s\n "LIFECYCLE_DP[7:0]": %s\n' \ + "$name" "$name" > "$d/fuses-$state.yaml" + nxpfuses fuses-script -c "$d/fuses-$state.yaml" -o "$d/burn-$state.bls" >/dev/null 2>&1 || return 1 + grep -q "^efuse-program-once $FUSE_LIFE_CYCLE " "$d/burn-$state.bls" || return 1 + pfr export -c "$d/cmpa.yaml" -o "$d/cmpa-$state.bin" >/dev/null 2>&1 || return 1 + pfr parse -f mcxn947 -t cmpa -b "$d/cmpa-$state.bin" -o "$d/cmpa-$state.yaml" >/dev/null 2>&1 || return 1 + { diff "$d/cmpa.yaml" "$d/cmpa-$state.yaml" | grep '^[<>]' | grep -v '^. *#' || true; } > "$d/cmpa-$state.diff" +} +port_ready() { + local d digests + ensure_spsdk + ensure_probe + case "$1" in + 0x0F|0xCF|0x1F) + refuse "$(label "$1") needs the BootROM to authenticate wolfBoot (a signed image under the fused root key table hash, SEC_BOOT_EN in the CMPA), which this port does not build yet; see the MCXN947 Guide." ;; + esac + d="$(preview_dir "$1")" + digests="$(read_pages "$d")" || refuse "cannot read and parse the CMPA and CFPA pages over SWD (probe $PROBE)." + printf '%s\n' "$digests" > "$d/pages.sha256" + generate_lock "$d" "$1" || refuse "SPSDK could not generate the $(label "$1") fuse script and CMPA page." + CHECKED="$CHECKED, same debug probe $PROBE, CMPA/CFPA read and parsed ($(awk 'NR == 1 { print substr($1, 1, 16) }' "$d/pages.sha256")), fuse script and page generated in $d" +} +port_lock_plan() { + local d + d="$(preview_dir "$1")" + echo "blhost $isp batch $d/burn-$1.bls" + grep -v '^#' "$d/burn-$1.bls" | grep -v '^$' | sed 's/^/ /' + echo "pfr write: nothing (the life cycle is OTP; the CMPA re-exported from the live page differs in $(cmp -l "$d/cmpa.bin" "$d/cmpa-$1.bin" 2>/dev/null | wc -l | tr -d ' ') bytes, field diff in $d/cmpa-$1.diff)" + echo "REFUSED at the write: MCXN947_LOCK_WRITE_ENABLED=$MCXN947_LOCK_WRITE_ENABLED, hard-wired off in this port" +} +port_consequence() { echo "the OTP life cycle cannot be unburned, and the part never returns to $(label "$cur")."; } +port_lock_write() { + [ "$MCXN947_LOCK_WRITE_ENABLED" = "1" ] || + refuse "the MCXN947 life cycle write is hard-wired off in this port (MCXN947_LOCK_WRITE_ENABLED=0): the preview is the whole of what this phase does, and nothing was written." + refuse "no MCXN947 life cycle write path is implemented even when enabled." +} +port_lock_verify() { refuse "no MCXN947 life cycle write is implemented."; } +# The pages as they are now against the last preview's digests. +verify_pages() { + local d n now was + d="$(ls -d "$state_dir"/lock-0x* 2>/dev/null | tail -1)" + [ -n "$d" ] && [ -s "$d/pages.sha256" ] || fail "verify-pages" "no lock preview recorded yet: run 'lock ' first" + mkdir -p "$d/after" + for n in cmpa:$CMPA_ADDR cfpa-scratch:$CFPA_SCRATCH_ADDR cfpa-ping:$CFPA_PING_ADDR cfpa-pong:$CFPA_PONG_ADDR; do + read_page "${n##*:}" "$d/after/${n%%:*}.bin" || fail "verify-pages" "cannot read ${n%%:*} over SWD" + now="$(page_digest "$d/after/${n%%:*}.bin")" + was="$(awk -v f="${n%%:*}.bin" '$2 == f { print $1 }' "$d/pages.sha256")" + [ "$now" = "$was" ] || fail "verify-pages" "${n%%:*} changed since the preview (was $was, now $now)" + pass "${n%%:*} unchanged since the preview (${now:0:16})" + done +} diff --git a/tests/target/provisioning/test_provisioning_gates.sh b/tests/target/provisioning/test_provisioning_gates.sh index 0792f9268..88921a9f4 100755 --- a/tests/target/provisioning/test_provisioning_gates.sh +++ b/tests/target/provisioning/test_provisioning_gates.sh @@ -93,7 +93,28 @@ esac EOF cat > "$T/venv/bin/pyocd" </dev/null; exit 0; fi +echo "\$*" >> $T/pyocd_calls +if [ "\$1" = cmd ]; then + # With a register file (MCXN947), answer each read32 from it and give a + # savemem a blank page; else the RT700 SFP dump. + if [ -s "$T/mcxn_regs" ]; then + for a in "\$@"; do + case "\$a" in + "read32 "*) k=\$(printf '%08x' "\${a#read32 }" 2>/dev/null || echo 0) + v=\$(awk -v k="\$k" '\$1 == k { print \$2 }' "$T/mcxn_regs") + echo "\$k: \${v:-00000000}" ;; + "savemem "*) set -- \$a + head -c "\$3" /dev/zero | tr '\\000' '\\377' > "\$4" + # A failed read: the file is written but the commander reports an + # error, ahead of a long log, and still exits 0. + if [ -e "$T/savemem_err" ]; then echo "Error: memory transfer fault" + printf '%02000000d\\n' 0; fi ;; + esac + done + exit 0 + fi + cat "$T/sfp" 2>/dev/null; exit 0 +fi [ "\$1" = list ] || exit 0 echo " # Probe/Board Unique ID Target" echo "-----------------------------------" @@ -105,7 +126,43 @@ printf '#!/bin/sh\nexec %s "$@"\n' "$(command -v python3)" > "$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" +# SPSDK stubs for the MCXN947 lock preview: pfr parses a page into a YAML +# that names its source and exports that source back (a byte-true round +# trip); nxpfuses writes the fuse script for the named life cycle; every +# write-class command of pfr, nxpfuses and nxpdebugmbox is logged and fails. +cat > "$T/venv/bin/pfr" < "\$o" ;; + export) while [ \$# -gt 0 ]; do case "\$1" in -c) c="\$2"; shift;; -o) o="\$2"; shift;; esac; shift; done + cp "\$(sed -n 's/^source: //p' "\$c")" "\$o" ;; + get-template) while [ \$# -gt 0 ]; do case "\$1" in -o) o="\$2"; shift;; esac; shift; done + printf 'family: mcxn947\\n' > "\$o" ;; + *) echo "pfr \$sub \$*" >> $T/writes; exit 1 ;; +esac +EOF +cat > "$T/venv/bin/nxpfuses" < "\$o" ;; + *) echo "nxpfuses \$sub \$*" >> $T/writes; exit 1 ;; +esac +EOF +cat > "$T/venv/bin/nxpdebugmbox" <> $T/writes; exit 1 ;; +esac +EOF +chmod +x "$T/bin/stcli" "$T/venv/bin/blhost" "$T/venv/bin/pyocd" "$T/venv/bin/pfr" "$T/venv/bin/nxpfuses" "$T/venv/bin/nxpdebugmbox" pass=0; failn=0 check() { # check -- cmd... @@ -360,5 +417,98 @@ check "the next step after the burn" 2 "runs only from in-field (0x0F)" -- env " 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 "== MCXN947" +MX=(env TARGET=mcxn947 MCXN947_SPSDK_VENV="$T/venv" MCXN947_PROBE_UID=PROBEA MCXN947_WOLFBOOT_DIR="$R/wolfBoot" WT_PROVISION_STATE="$T/st" PATH="$T/venv/bin:$PATH" "$P") +MS="$T/st/mcxn947" +mkdir -p "$R/tests/firmware/mcxn947-baremetal/build" +echo mg0 > "$R/tests/firmware/mcxn947-baremetal/build/guest0.bin" +echo mg1 > "$R/tests/firmware/mcxn947-baremetal/build/guest1.bin" +# restore runs the hardware runner; here a stub that reports the probe it got. +printf '#!/bin/sh\necho "stub runner $1 probe=${MCXN947_PROBE_UID:-none}"\n' > "$R/tests/target/run_mcxn947_hardware.sh" +chmod +x "$R/tests/target/run_mcxn947_hardware.sh" +mdig() { + local a f + for a in 0x00000000:wolfBoot/wolfboot.bin 0x00040000:build/wolftrust_v1_signed.bin \ + 0x00080000:tests/firmware/mcxn947-baremetal/build/guest0.bin \ + 0x00100000:tests/firmware/mcxn947-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 +} +# mrec [probe]: a rehearsal record of this part +mrec() { mkdir -p "$MS"; echo "state=$1 run=r id=11111111222222223333333344444444 image=$(mdig) fused=$2 fence=none probe=${4:-PROBEA} time=$3 cred=none completed=$3" > "$MS/rehearsal-$1"; } +# regs [DAUTHSTATUS]: the words the pyocd stub answers +# (SYSCON life cycle, debug status, UUID, an intact handoff, the reset vector). +regs() { + printf '50000980 %08x\n50000984 %08x\ne000efb8 %08x\n' $(($1)) $(($2)) $((${3:-0xAA})) > "$T/mcxn_regs" + printf '01100000 11111111\n01100004 22222222\n01100008 33333333\n0110000c 44444444\n' >> "$T/mcxn_regs" + printf '30030000 5742484f\n30030004 a8bdb7b0\n3003000c 00001000\n10040404 10040801\n' >> "$T/mcxn_regs" +} +regs 0x03 0x03 +check "help lists the port's states" 0 "oem-secure-world" -- "${MX[@]}" help +check "the MCXN947 port file refuses to run alone" 2 "not this port file" -- bash "$R/tests/target/provisioning/provisioning_ctrl_mcxn947.sh" lock 0x07 +for m in 0 0x4 junk; do + check "MCXN947_GUEST_MASK=$m refused" 2 "MCXN947_GUEST_MASK must be" -- env MCXN947_GUEST_MASK="$m" WT_LOCK_CONFIRM=1 "${MX[@]}" advance 0x07 +done +check "no guest flash fence is reported" 1 "no guest flash fence" -- "${MX[@]}" verify-wrp +check "lock refused: oem-closed by name" 2 "oem-closed (0x0F) is not a lock target" -- "${MX[@]}" lock oem-closed +check "lock refused: oem-locked" 2 "is not a lock target" -- "${MX[@]}" lock 0xCF +check "lock refused: oem-field-return" 2 "is not a lock target" -- "${MX[@]}" lock 0x1F +check "advance needs discover" 2 "run 'discover' first" -- env WT_LOCK_CONFIRM=1 "${MX[@]}" advance 0x07 +check "status reads the life cycle and the UUID" 0 "device UUID 11111111222222223333333344444444" -- "${MX[@]}" status +regs 0x03 0x07 +check "discover: copies disagree" 1 "life cycle copies disagree" -- "${MX[@]}" discover +regs 0x103 0x103 +check "discover: bits above the state byte" 1 "above the state byte" -- "${MX[@]}" discover +regs 0xCF 0xCF +check "discover: no further rehearsal step" 1 "has no further rehearsal step" -- "${MX[@]}" discover +regs 0x03 0x03 +check "discover stamps the life cycle and UUID" 0 "discovery stamped" -- "${MX[@]}" discover +check "advance past oem-secure-world needs a regress" 2 "prove 'regress' from oem-secure-world" -- env WT_LOCK_CONFIRM=1 "${MX[@]}" advance 0x0F +check "OTP writes refused" 2 "would program OTP" -- "${MX[@]}" burn +check "perimeter commands refused" 2 "has no MCXN947 form" -- "${MX[@]}" set-wrp +check "restore runs the hardware runner bound to the probe" 0 "stub runner positive probe=PROBEA" -- env WT_LOCK_CONFIRM=1 "${MX[@]}" restore +# Three probes and no id: every device command refuses loudly, never a +# silent empty read. +printf 'PROBEA\nPROBEB\nPROBEC\n' > "$T/probe" +check "status with three probes and no id refuses" 2 "set MCXN947_PROBE_UID" -- env -u MCXN947_PROBE_UID TARGET=mcxn947 MCXN947_SPSDK_VENV="$T/venv" WT_PROVISION_STATE="$T/st" PATH="$T/venv/bin:$PATH" "$P" status +check "status names the bound probe" 0 "debug probe PROBEA" -- "${MX[@]}" status +check "discover stamps the probe" 0 "probe PROBEA" -- "${MX[@]}" discover +grep -q "PROBE=PROBEA" "$MS/discovery" && { pass=$((pass+1)); echo "ok discovery records the probe"; } || { failn=$((failn+1)); echo "FAIL discovery lacks the probe"; } +: > "$T/pyocd_calls" +# The lock: previews the vendor write from the live pages and refuses it, +# with every confirmation supplied, on the hard-wired port constant. +check "lock needs a rehearsal" 2 "no rehearsal for oem-secure-world" -- "${MX[@]}" lock 0x07 +mrec 0x07 0x03 "$(now)" +out="$("${MX[@]}" lock 0x07 2>&1)"; rc=$? +[ $rc = 2 ] && grep -q "preview only" <<<"$out" && grep -q "efuse-program-once 1 0x7000007 --no-verify" <<<"$out" && grep -q "pfr write: nothing" <<<"$out" && grep -q "hard-wired off" <<<"$out" && grep -q "CMPA/CFPA read and parsed" <<<"$out" && { pass=$((pass+1)); echo "ok lock previews the fuse script and the pages, then stops"; } || { failn=$((failn+1)); echo "FAIL lock preview rc=$rc"; echo "$out" | tail -8; } +[ -s "$MS/lock-0x07/burn-0x07.bls" ] && [ -s "$MS/lock-0x07/cmpa.yaml" ] && [ -s "$MS/lock-0x07/pages.sha256" ] && { pass=$((pass+1)); echo "ok the preview keeps the script, the parsed pages and their digests"; } || { failn=$((failn+1)); echo "FAIL preview artifacts missing"; } +if grep -q . "$T/pyocd_calls" && ! grep -v -- "-u PROBEA" "$T/pyocd_calls" | grep -q .; then pass=$((pass+1)); echo "ok every pyocd call of the preview carried the probe id"; else failn=$((failn+1)); echo "FAIL a pyocd call ran without -u PROBEA: $(grep -v -- '-u PROBEA' "$T/pyocd_calls" | head -2)"; fi +mrec 0x07 0x03 "$(now)" PROBEB +check "a rehearsal on another probe is refused" 2 "no rehearsal for oem-secure-world" -- "${MX[@]}" lock 0x07 +mrec 0x07 0x03 "$(now)" +check "lock refused with the production opt-in but no terminal" 2 "needs an interactive terminal" -- env WT_FIXTURE_BOUND=1 WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 "${MX[@]}" lock 0x07 +if [ "$have_expect" = 1 ]; then +MXX="env TARGET=mcxn947 MCXN947_SPSDK_VENV=$T/venv MCXN947_PROBE_UID=PROBEA MCXN947_WOLFBOOT_DIR=$R/wolfBoot WT_PROVISION_STATE=$T/st PATH=$T/venv/bin:$PATH WT_LOCK_CONFIRM=1 WT_PRODUCTION_LOCK=1 $P" +out="$(expect -c "set timeout 300; spawn $MXX lock 0x07; expect \"to continue: \"; send \"I ACCEPT 0x07\r\"; expect eof; catch close; catch wait r; exit [lindex \$r 3]")" +rc=$?; [ $rc = 2 ] && grep -q "hard-wired off in this port (MCXN947_LOCK_WRITE_ENABLED=0)" <<<"$out" && [ -e "$MS/rehearsal-0x07" ] && { pass=$((pass+1)); echo "ok the exact phrase with every confirmation is still refused at the write"; } || { failn=$((failn+1)); echo "FAIL typed acceptance rc=$rc"; echo "$out" | tail -5; } +else + echo "skip typed lock refusal (no expect)" +fi +nowrite +check "verify-pages: unchanged since the preview" 0 "cfpa-pong unchanged since the preview" -- "${MX[@]}" verify-pages +# A savemem that fails while the commander exits 0 must not let the previous +# readback stand in for the page. +touch "$T/savemem_err" +check "verify-pages: a failed read is never the earlier readback" 1 "cannot read cmpa over SWD" -- "${MX[@]}" verify-pages +rm -f "$T/savemem_err" +# From a part fused one step further, the closed states are still no lock +# target: the ladder stops them before any preview. +regs 0x07 0x07; mrec 0x0F 0x07 "$(now)" +check "lock refused: oem-closed from oem-secure-world" 2 "oem-closed (0x0F) is not a lock target" -- "${MX[@]}" lock 0x0F +regs 0x0F 0x0F; mrec 0xCF 0x0F "$(now)" +check "lock refused: oem-locked from oem-closed" 2 "oem-locked (0xCF) is not a lock target" -- "${MX[@]}" lock 0xCF +nowrite +rm -f "$T/mcxn_regs" echo "provisioning gates: $pass passed, $failn failed" [ "$failn" = 0 ] diff --git a/tests/target/run_mcxn947_hardware.sh b/tests/target/run_mcxn947_hardware.sh new file mode 100755 index 000000000..440b81d94 --- /dev/null +++ b/tests/target/run_mcxn947_hardware.sh @@ -0,0 +1,511 @@ +#!/usr/bin/env bash +# FRDM-MCXN947 hardware runner (sibling of run_rt700_hardware.sh and +# run_mcxn947_m33mu.sh; one CLI shape). Runs on the host that owns the probe. +# +# run_mcxn947_hardware.sh +# +# Scenarios: +# positive the full chain: wolfBoot (TrustZone loader) authenticates the +# wolfTrust Secure image, which launches both Non-secure guests. +# Builds wolfTrust + the guests, pins the guest measurements, +# wolfBoot-signs the Secure image, flashes the chain, erases the +# update, swap and NVM sectors so the run starts from a fresh +# vault, resets, verifies every image by readback, and asserts the +# guest mailboxes, the launch masks, the ROM flash status and the +# vault's first phrase over SWD. +# sauneg positive plus the guest isolation negative: guest0 stores into +# guest1's RAM, which the per-dispatch SAU window keeps Secure; the +# store must fault on every launch until guest0 is quarantined, +# guest1's RAM never holds the sentinel, and guest1 keeps running. +# periphneg guest0, its MPU off, reads the SPM's ELS through the Non-secure +# alias on every launch; the SAU must refuse it (no leak) until +# guest0 is quarantined, and guest1 keeps running. +# restart guest0 reads Secure RAM on every launch: the monitor spends its +# restart budget and quarantines it; guest1 is untouched. +# psa both Non-secure windows run the portable PSA client +# (tests/firmware/psa-guest): every lifecycle stage must complete +# in both mailboxes, with the ELS behind psa_generate_random. +# wrpoff wolfTrust built with WT_GUEST_FLASH_WRP=1 requires a guest flash +# fence, which this port does not have: it must refuse both guests +# (launch refused mask 0x3, no mailbox written). +# +# The probe is the board's MCU-Link, the one pyOCD lists as +# "MCU-LINK FRDM-MCXN947" (MCXN947_PROBE_UID picks one of several); the +# console is its VCOM (MCXN947_CONSOLE). The wolfBoot first stage is +# MCXN947_WOLFBOOT_REF plus the carried patch (lib/mcxn947_wolfboot.sh) +# unless MCXN947_WOLFBOOT_DIR names a tree that library built. +set -euo pipefail + +scenario="${1:-}" +here="$(cd "$(dirname "$0")" && pwd)" +repo="$(cd "$here/../.." && pwd)" +# Retained evidence (console, readback, wolfBoot build log) lives outside +# build/, which every run cleans before it builds. +work="${MCXN947_WORK:-$repo/logs/mcxn947-hw}" +lock="${MCXN947_HW_LOCK:-$repo/logs/mcxn947-hw.lock}" +engine="${WT_ENGINE:-native}" +case "$scenario" in + wrpoff) export WT_GUEST_FLASH_WRP=1 ;; +esac +# Each run's console, readbacks and SWD dump, kept per scenario and engine. +evidence="$work/$scenario-$engine" +target="${MCXN947_TARGET:-mcxn947vdf}" +probe_uid="${MCXN947_PROBE_UID:-}" +console="${MCXN947_CONSOLE:-}" +wolfboot_dir="${MCXN947_WOLFBOOT_DIR:-}" +spsdk_venv="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" +pyocd="${MCXN947_PYOCD:-pyocd}" +console_seconds="${MCXN947_CONSOLE_SECONDS:-25}" +# Physical flash offsets (port/mcxn947/memory_map.h, wolfBoot's +# mcxn-tz-wolftrust config): the debugger and the ROM see the array at 0. +wolfboot_flash_addr=0x00000000 +secure_flash_addr=0x00040000 +guest0_flash_addr=0x00080000 +guest1_flash_addr=0x00100000 +# 8 KiB sectors: update and swap 0x180000-0x1C2000, NVM 0x1E0000-0x1E6000. +update_erase="0x00180000 33" +nvm_erase="0x001E0000 3" +hsm_nvm_addr=0x001E0000 +flash_sector=0x2000 +guest0_ram=0x20000000 +guest1_ram=0x20018000 +probe_addr=0x20028000 +# What guest0's negatives touch (tests/firmware/mcxn947-baremetal/Makefile): +# the SPM's ELS through its NS alias, and Secure RAM for the restart probe. +periph_probe_addr=0x40054000 +restart_probe_addr=0x30031000 +handoff_addr=0x30030000 +syscon_lc_state=0x50000980 +# The guests' manifest restart budget (port/mcxn947/partitions.c). +restart_limit=3 +baremetal_dir="$repo/tests/firmware/mcxn947-baremetal" +psa_dir="$repo/tests/firmware/psa-guest" + +# shellcheck source=lib/mcxn947_wolfboot.sh disable=SC1091 +. "$here/lib/mcxn947_wolfboot.sh" + +# One run at a time owns the board; a suite batch takes the same lock once +# for all of its scenarios and says so. +if [ -z "${MCXN947_HW_LOCKED:-}" ]; then + mkdir -p "$(dirname "$lock")" + exec 9>"$lock" + flock 9 +fi + +log() { printf '%s\n' "$*"; } +stage() { printf ' ... %s\n' "$*"; } +fail() { log "FAIL: $*"; exit 1; } +check() { + if [ "$1" -eq 0 ]; then log " [check] PASS $2"; else log " [check] FAIL $2"; fail "$2"; fi +} + +ensure_spsdk() { + if ! command -v "$pyocd" >/dev/null 2>&1; then + [ -x "$spsdk_venv/bin/pyocd" ] || fail "pyocd not found (set MCXN947_PYOCD or MCXN947_SPSDK_VENV)" + pyocd="$spsdk_venv/bin/pyocd" + PATH="$spsdk_venv/bin:$PATH" + export PATH + fi +} + +# The board's own MCU-Link, unless the caller named a probe; other probes on +# the host belong to other boards and are never touched. +ensure_probe() { + local listed + if [ -z "$probe_uid" ]; then + listed="$("$pyocd" list 2>/dev/null | grep -F "MCU-LINK FRDM-MCXN947" || true)" + [ "$(printf '%s\n' "$listed" | grep -c .)" -eq 1 ] || \ + fail "expected one MCU-Link FRDM-MCXN947 probe (set MCXN947_PROBE_UID)" + probe_uid="$(printf '%s\n' "$listed" | awk '{ for (i = 1; i <= NF; i++) if ($i ~ /^[A-Z0-9]{8,}$/) { print $i; exit } }')" + [ -n "$probe_uid" ] || fail "could not read the probe's unique id" + fi + if [ -z "$console" ]; then + for tty in /dev/serial/by-id/*FRDM-MCXN947*-if02; do + [ -c "$tty" ] && console="$tty" && break + done + fi + [ -n "$console" ] && [ -c "$console" ] || fail "board console not found (set MCXN947_CONSOLE)" +} + +# Observation uses the generic Cortex-M attach: the device-pack target resets +# the chip on connect, so a read would land in a fresh boot, not the one under +# test. +dap() { + timeout 60 "$pyocd" cmd -u "$probe_uid" -t cortex_m "$@" 2>&1 | + grep -viE "rom table|APB-AP|coresight|cidr" +} + +# pyOCD's flash routine runs in RAM and faults under wolfTrust's Secure MPU +# whitelist, so the core is parked and the MPU disabled first. The pack resets +# the chip on every connect, so the park and every step that depends on it +# share one pyOCD session; a second session would find the chain running. +park_cmds=(-O reset_type=hw -c "reset halt" -c "write32 0xE000ED94 0") +board_cmds=() + +# A failed or partial flash must stop the run: a stale image either boots old +# code or fails its pinned measurement and looks like a port bug. +flash_at() { + local addr="$1" image="$2" + [ -s "$image" ] || fail "flash image missing: $image" + board_cmds+=(-c "load $image $addr") +} + +# The board keeps the update, swap and NVM sectors across runs where the +# emulator starts fresh; erasing them puts every run on a blank vault. +erase_ranges() { + local range + for range in "$@"; do + board_cmds+=(-c "erase $range") + done +} + +# 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 "$refused" ] || fail "launch masks not found in wolftrust.elf" + # A WT_GUEST_FLASH_WRP=1 image here verifies no guest, so the linker drops + # the verified mask; every other image must carry it. + [ -n "$verified" ] || [ "${WT_GUEST_FLASH_WRP:-0}" = "1" ] || + fail "launch masks not found in wolftrust.elf" + board_cmds+=(-c "write32 $guest0_ram 0 0 0 0 0 0 0 0 0 0 0 0" \ + -c "write32 $guest1_ram 0 0 0 0 0 0 0 0 0 0 0 0" \ + -c "write32 $probe_addr 0" -c "write32 $refused 0") + [ -z "$verified" ] || board_cmds+=(-c "write32 $verified 0") +} + +# The commander exits 0 whatever a command did, so its output is checked too. +program_board() { + local out rc=0 + [ "${#board_cmds[@]}" -gt 0 ] || fail "nothing to program" + out="$(timeout 900 "$pyocd" cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ + "${park_cmds[@]}" "${board_cmds[@]}" 2>&1)" || rc=$? + if [ "$rc" -ne 0 ]; then + printf '%s\n' "$out" | tail -6 + fail "programming the board failed (pyocd exit $rc)" + fi + # A here-string, not a pipe: under pipefail an early grep -q exit can + # SIGPIPE the writer and hide a match in a long log. + if grep -qiE 'error|exception|not halted|failure|not within|not in flash' <<<"$out"; then + printf '%s\n' "$out" | grep -iE 'error|exception|not halted|failure|not within|not in flash' | tail -6 + fail "programming the board failed" + fi + board_cmds=() +} + +# Boot from the parked state: the pack's halt leaves a DWT comparator and the +# debug fault status armed, which would stop the chain again, so both are +# cleared before the core is released. The console is read for the whole boot; +# the MCU-Link forwards early-boot UART only while the debug link stays active, +# so the capture is kept company by spaced reads. +release_core() { + local n + stty -F "$console" 115200 raw -echo -crtscts -ixon 2>/dev/null || true + ( timeout "$console_seconds" cat "$console" > "$evidence/console.log" 2>/dev/null ) & + sleep 0.5 + timeout 60 "$pyocd" cmd -u "$probe_uid" -t "$target" -O resume_on_disconnect=false \ + -O reset_type=hw -c "reset halt" -c "write32 0xE0001028 0" \ + -c "write32 0xE0001038 0" -c "write32 0xE000ED30 0x1F" -c "go" \ + >/dev/null 2>&1 || fail "could not release the core" + for n in 1 2 3 4 5 6; do + sleep 1.5 + dap -c "read32 $guest0_ram" >/dev/null 2>&1 || true + done + wait +} + +# Read an image back over the debug port once the chain has booted and +# compare it. +verify_at() { + local addr="$1" image="$2" size back + size="$(wc -c < "$image" | tr -d ' ')" + back="$evidence/readback-$(basename "$image")" + timeout 120 "$pyocd" cmd -u "$probe_uid" -t cortex_m \ + -c "savemem $addr $size $back" >/dev/null 2>&1 || \ + fail "readback of $addr failed" + cmp -s "$back" "$image" || \ + fail "flash verify mismatch at $addr ($(basename "$image"))" + log " [flash] verified $(basename "$image") @ $addr" +} + +ensure_wolfboot() { + if [ -n "$wolfboot_dir" ]; then + mcxn947_wolfboot_current "$wolfboot_dir" "$(mcxn947_wolfboot_stamp)" || \ + fail "MCXN947_WOLFBOOT_DIR=$wolfboot_dir was not built by lib/mcxn947_wolfboot.sh at $MCXN947_WOLFBOOT_REF" + else + wolfboot_dir="$HOME/.cache/wolftrust/wolfboot-mcxn947" + mkdir -p "$(dirname "$wolfboot_dir")" + stage "wolfBoot $MCXN947_WOLFBOOT_REF (mcxn-tz-wolftrust)" + mcxn947_wolfboot_build "$wolfboot_dir" > "$work/wolfboot-build.log" 2>&1 || { + tail -20 "$work/wolfboot-build.log" + fail "wolfBoot build failed" + } + fi + [ -s "$wolfboot_dir/wolfboot.bin" ] || \ + fail "wolfBoot image missing at $wolfboot_dir/wolfboot.bin (MCXN947_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_dir="$1" guest_flags="$2" guest_build + + ensure_spsdk + ensure_probe + mkdir -p "$work" + ensure_wolfboot + + # MCXN 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 + # secure image and the guest CMSE import library agree. + export WT_SECURE_IMAGE_HEADER_SIZE=0x400 + rm -rf "$repo/build" "$evidence" + mkdir -p "$evidence" + + stage "build wolfTrust secure image + CMSE import library (${WT_ENGINE:-native})" + make -s -C "$repo" TARGET=mcxn947 secure-image TOOLPREFIX=arm-none-eabi- + + stage "build the Non-secure guests from ${guest_dir#"$repo/"} ${guest_flags:-(no probes)}" + make -s -C "$guest_dir" clean + # The board is OEM_OPEN, the ASSEMBLY_AND_TEST life cycle the attestation + # token reports; the PSA guest's verify pins it. + # shellcheck disable=SC2086 + make -s -C "$guest_dir" TARGET=mcxn947 WT_EXPECTED_LIFECYCLE=0x1000u $guest_flags + guest_build="$guest_dir/build" + + stage "pin both guest measurements, then wolfBoot-sign wolfTrust" + python3 "$repo/tools/measure/patch_guest_digests.py" "$repo/build/wolftrust.bin" \ + "0:1:$guest_build/guest0.bin" "1:1:$guest_build/guest1.bin" + IMAGE_HEADER_SIZE=1024 WOLFBOOT_PARTITION_SIZE=0x40000 WOLFBOOT_SECTOR_SIZE=$flash_sector \ + "$wolfboot_dir/tools/keytools/sign" --ecc256 --sha256 \ + "$repo/build/wolftrust.bin" \ + "$wolfboot_dir/wolfboot_signing_private_key.der" 1 + + stage "flash the chain, erase the update, swap and NVM sectors, clear SRAM" + flash_at "$wolfboot_flash_addr" "$wolfboot_dir/wolfboot.bin" + flash_at "$secure_flash_addr" "$repo/build/wolftrust_v1_signed.bin" + flash_at "$guest0_flash_addr" "$guest_build/guest0.bin" + flash_at "$guest1_flash_addr" "$guest_build/guest1.bin" + erase_ranges "$update_erase" "$nvm_erase" + clear_mailboxes + program_board + + stage "boot the chain, reading the console for ${console_seconds}s" + release_core + verify_at "$wolfboot_flash_addr" "$wolfboot_dir/wolfboot.bin" + verify_at "$secure_flash_addr" "$repo/build/wolftrust_v1_signed.bin" + verify_at "$guest0_flash_addr" "$guest_build/guest0.bin" + verify_at "$guest1_flash_addr" "$guest_build/guest1.bin" + dump_evidence + log " [evidence] $evidence (console.log, readback-*.bin, swd.txt)" +} + +# The words every check reads, kept as text beside the console so a verdict +# can be re-read without the board. +dump_evidence() { + { + echo "scenario=$scenario engine=$engine image=$(sha256sum "$repo/build/wolftrust_v1_signed.bin" | cut -c1-16)" + dap -c "read32 $guest0_ram 48" -c "read32 $guest1_ram 48" -c "read32 $probe_addr 4" \ + -c "read32 $(elf_sym g_wt_launch_verified_mask)" -c "read32 $(elf_sym g_wt_launch_refused_mask)" \ + -c "read32 $(elf_sym g_wt_restart_events)" -c "read32 $(elf_sym g_wt_quarantine_events)" \ + -c "read32 $(elf_sym g_wt_rom_flash_last_status)" -c "read32 $(elf_sym g_wt_flash_last_error)" \ + -c "read32 $(elf_sym g_wt_els_errors) 12" -c "read32 $(elf_sym g_wt_boot_handoff) 56" \ + -c "read32 $hsm_nvm_addr 32" -c "read32 $((hsm_nvm_addr + flash_sector)) 16" \ + -c "read32 $handoff_addr 16" -c "read32 $syscon_lc_state" \ + -c halt -c "reg pc" -c "reg xpsr" -c go + } > "$evidence/swd.txt" 2>&1 || true +} + +# One 32-bit word at base+offset over SWD, as eight lowercase hex digits. +word_at() { + dap -c "read32 $(printf '0x%x' $(($1 + $2)))" | grep -oiE '[0-9a-f]{8}' | tail -1 | + tr 'A-F' 'a-f' +} + +# 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 }' +} + +sym_word() { word_at "$(elf_sym "$1")" 0; } + +# Each bare-metal guest records its progress at the base of its own RAM +# window (tests/firmware/mcxn947-baremetal/guest0.c). +check_guest() { + local id="$1" base="$2" sig fw st uart + sig="$(word_at "$base" 0)" + fw="$(word_at "$base" 8)" + st="$(word_at "$base" 20)" + uart="$(word_at "$base" 24)" + check "$([ "$sig" = "47543030" ]; echo $?)" "guest$id launched: signature 0x47543030 ($sig)" + check "$([ "$fw" = "00000100" ]; echo $?)" "guest$id psa_framework_version 0x0100 ($fw)" + check "$([ "$st" = "600d600d" ]; echo $?)" \ + "guest$id done: FF-M connect verified, status 0x600D600D ($st)" + check "$([ -n "$uart" ] && [ "$uart" != "00000000" ]; echo $?)" \ + "guest$id reaches its Non-secure console (LPUART4 VERID 0x$uart)" +} + +# The peer keeps running: its idle beat advances between two reads. +check_beating() { + local id="$1" base="$2" b1 b2 + b1="$(word_at "$base" 36)" + sleep 1 + b2="$(word_at "$base" 36)" + check "$([ -n "$b1" ] && [ "$b1" != "$b2" ]; echo $?)" \ + "guest$id still running (beat 0x$b1 -> 0x$b2)" +} + +check_launch_masks() { + local want_verified="$1" want_refused="$2" verified refused + verified="$(sym_word g_wt_launch_verified_mask)" + refused="$(sym_word g_wt_launch_refused_mask)" + 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)" +} + +# The Secure side of every run: the ROM flash API answered from the SVC gate, +# the vault was created in the ROM-programmed NVM, an erased phrase reads as +# 0xFF, wolfBoot's handoff was consumed, and the scheduler is not in a fault +# handler. +check_secure_side() { + local rom err els phrase blank handoff ipsr lc handed + rom="$(sym_word g_wt_rom_flash_last_status)" + err="$(sym_word g_wt_flash_last_error)" + check "$([ "$rom" = "00000000" ]; echo $?)" \ + "ROM flash API reached from the SVC gate, last status 0x$rom" + check "$([ "$err" = "00000000" ]; echo $?)" "wolfHSM flash backend last error 0x$err" + els="$(sym_word g_wt_els_errors)" + check "$([ "$els" = "00000000" ]; echo $?)" \ + "the ELS served every RND_REQ (refusals 0x$els, last status 0x$(sym_word g_wt_els_last_status), error 0x$(sym_word g_wt_els_last_err))" + phrase="$(word_at "$hsm_nvm_addr" 4)" + check "$([ -n "$phrase" ] && [ "$phrase" != "ffffffff" ]; echo $?)" \ + "the vault's first state phrase is programmed (0x$phrase)" + blank="$(word_at "$hsm_nvm_addr" "$flash_sector")" + check "$([ "$blank" = "ffffffff" ]; echo $?)" \ + "an erased phrase reads as 0xFF through the bus (0x$blank)" + handoff="$(word_at "$handoff_addr" 0)" + check "$([ "$handoff" = "00000000" ]; echo $?)" \ + "wolfBoot's handoff record was consumed and cleared (0x$handoff)" + ipsr="$(dap -c halt -c 'reg xpsr' -c go | sed -n 's/^xpsr = 0x\([0-9a-fA-F]*\).*/\1/p')" + ipsr=$((0x${ipsr:-3} & 0x1ff)) + check "$([ "$ipsr" -lt 2 ] || [ "$ipsr" -gt 7 ]; echo $?)" \ + "system still scheduling (IPSR $ipsr, not a fault handler)" + lc="$(word_at "$syscon_lc_state" 0)" + log " [lifecycle] ELS_OTP_LC_STATE 0x$lc" + handed="$(word_at "$(elf_sym g_wt_boot_handoff)" 12)" + if [ "$lc" = "00000003" ]; then + check "$([ "$handed" = "00001000" ]; echo $?)" \ + "wolfBoot handed OEM_OPEN as PSA ASSEMBLY_AND_TEST (lifecycle 0x$handed)" + else + log " [lifecycle] handoff lifecycle 0x$handed" + fi + check "$(grep -q "Verifying signature...done" "$evidence/console.log"; echo $?)" \ + "wolfBoot verified the wolfTrust image signature (console)" +} + +# guest0 faulted at $1 on every launch (the Secure fault handler records the +# address), so a fault anywhere else fails the scenario; the monitor spent its +# restart budget and quarantined guest0, while guest1 ran untouched. +check_guest0_quarantined() { + local want="$1" restarts quarantines st beat fault + restarts="$(sym_word g_wt_restart_events)" + quarantines="$(sym_word g_wt_quarantine_events)" + fault="$(sym_word g_last_fault_address)" + check "$([ "$((0x$fault))" -eq "$((want))" ]; echo $?)" \ + "guest0's last fault was its probe's access to $want (0x$fault)" + check "$([ "$((0x$restarts))" -eq "$restart_limit" ]; echo $?)" \ + "guest0 relaunched through its restart budget ($((0x$restarts)) of $restart_limit)" + check "$([ "$((0x$quarantines))" -eq 1 ]; echo $?)" \ + "guest0 quarantined once ($((0x$quarantines)))" + st="$(word_at "$guest0_ram" 20)" + beat="$(word_at "$guest0_ram" 36)" + check "$([ "$st" != "600d600d" ]; echo $?)" "guest0 never reported done (status 0x$st)" + check "$([ "$beat" = "00000000" ]; echo $?)" "guest0 never reached its idle loop (beat 0x$beat)" + check_launch_masks 00000003 00000000 + check_guest 1 "$guest1_ram" + check_beating 1 "$guest1_ram" +} + +# The PSA guest's mailbox (tests/firmware/psa-guest/main.c): a signature and +# the lifecycle stages it completed. +check_psa_guest() { + local id="$1" base="$2" sig lc neg + sig="$(word_at "$base" 0)" + lc="$(word_at "$base" 4)" + neg="$(word_at "$base" 8)" + check "$([ "$sig" = "50534147" ]; echo $?)" "guest$id launched: signature PSAG ($sig)" + check "$([ $((0x$lc & 0xBF)) -eq $((0xBF)) ]; echo $?)" \ + "guest$id completed the TEE, crypto, ITS, PS, key-ops, SHA KAT and done stages (0x$lc)" + check "$([ $((0x$lc & 0x40)) -ne 0 ]; echo $?)" \ + "guest$id completed the attestation COSE stage (0x$lc)" + check "$([ "$neg" = "0000000f" ]; echo $?)" \ + "guest$id FF-M negatives all rejected: forged handle, oversized vector, cross-guest vector, unknown SID (0x$neg)" +} + +case "$scenario" in +positive) + run_chain "$baremetal_dir" "" + check_guest 0 "$guest0_ram" + check_guest 1 "$guest1_ram" + check_launch_masks 00000003 00000000 + check "$([ "$(sym_word g_wt_restart_events)" = "00000000" ]; echo $?)" "no guest restarted" + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +sauneg) + run_chain "$baremetal_dir" "WT_SAU_PROBE=1" + check_guest0_quarantined "$probe_addr" + peer="$(word_at "$probe_addr" 0)" + check "$([ "$peer" != "deadbeef" ]; echo $?)" \ + "guest1 RAM never received guest0's sentinel (0x$peer)" + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +periphneg) + run_chain "$baremetal_dir" "WT_PERIPH_NEG_PROBE=1" + check_guest0_quarantined "$periph_probe_addr" + check "$(! grep -q "periph probe LEAKED" "$evidence/console.log"; echo $?)" \ + "no NS read of the SPM's ELS ever returned (console)" + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +restart) + run_chain "$baremetal_dir" "WT_GUEST_FAULT_PROBE=1" + check_guest0_quarantined "$restart_probe_addr" + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +psa) + run_chain "$psa_dir" "" + check_psa_guest 0 "$guest0_ram" + check_psa_guest 1 "$guest1_ram" + check_launch_masks 00000003 00000000 + check "$([ "$(sym_word g_wt_restart_events)" = "00000000" ]; echo $?)" "no guest restarted" + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +wrpoff) + run_chain "$baremetal_dir" "" + check "$([ -z "$(elf_sym g_wt_launch_verified_mask)" ] || + [ "$(sym_word g_wt_launch_verified_mask)" = "00000000" ]; echo $?)" \ + "no guest was launch-verified (the image has no path that marks one)" + check "$([ "$(sym_word g_wt_launch_refused_mask)" = "00000003" ]; echo $?)" \ + "launch-refused guest mask 0x$(sym_word g_wt_launch_refused_mask) (want 0x00000003)" + for g in "0:$guest0_ram" "1:$guest1_ram"; do + sig="$(word_at "${g##*:}" 0)" + check "$([ "$sig" = "00000000" ]; echo $?)" \ + "guest${g%%:*} never entered its domain (mailbox 0x$sig)" + done + check_secure_side + log "PASS: hardware/mcxn947/$scenario" + ;; +*) + log "usage: $0 positive|sauneg|periphneg|restart|psa|wrpoff" + exit 2 + ;; +esac diff --git a/tests/target/run_mcxn947_m33mu.sh b/tests/target/run_mcxn947_m33mu.sh new file mode 100755 index 000000000..abdf94cc7 --- /dev/null +++ b/tests/target/run_mcxn947_m33mu.sh @@ -0,0 +1,626 @@ +#!/usr/bin/env bash +# wolfTrust on the NXP MCXN947 under the M33MU emulator (--cpu mcxn947): the +# same chain the FRDM-MCXN947 boots (wolfBoot -> wolfTrust secure -> two +# baremetal guests), the MIMXRT700 scenario names, asserted from the emulator +# log. This is emulator evidence, never silicon evidence. +# +# positive wolfBoot verifies wolfTrust; both guests launch, reach the SPM +# through the SG veneers and finish with no fault anywhere. +# sauneg positive plus the CPU isolation negative: guest0 stores into +# guest1's RAM window, which the per-dispatch SAU window keeps +# Secure, so the store faults. The monitor contains it, relaunches +# guest0 through its restart budget, quarantines it, and guest1 +# keeps running. +# +# rollbackneg, remeasureneg, manifestneg, spbudgetneg, and sealbootneg are the +# shared Secure-verdict scenarios from lib/scenario.sh: each ends on the +# verdict breakpoint its probe emits, asserted by the port-independent table. +# crossdomain and keystoreneg fault the storage SP on an out-of-domain read; +# spfaultneg and panicneg fault an SP on its first entry and prove the SPM +# restarts it in place while both guests finish; fpneg proves only that the +# relay's FP instruction takes a NOCP UsageFault. restart spends guest0's +# restart budget on a launch-time fault; authneg refuses a tampered guest0 at +# launch. guest1 runs on through all of them. wrpoff builds wolfTrust to +# require a guest flash fence this port does not have, so neither guest runs. +# +# bothpsa, bothiso, attestneg, hsmattackneg, and fwustage run the portable +# PSA test guest (tests/firmware/psa-guest) in both windows; hsmattackneg +# drives the raw wolfHSM client wire, so it exists only under WT_ENGINE=hsm. +# +# 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; guest1 is the small bare-metal +# guest so the boot cycles of the IPC suite's panic tests stay cheap. +# +# Environment (all optional): +# M33MU prebuilt emulator carrying lib's MCXN947 model patch +# (tests/target/m33mu-mcxn947.patch); otherwise +# M33MU_REF plus that patch is built under /tmp +# MCXN947_WOLFBOOT_DIR wolfBoot tree holding wolfboot.bin, tools/keytools/ +# sign and wolfboot_signing_private_key.der, built by +# lib/mcxn947_wolfboot.sh from the mcxn-tz-wolftrust +# config with the carried life cycle patch; otherwise +# the pinned ref is built under /tmp (the MCUXpresso +# SDK must be reachable: MCUXPRESSO and +# MCUXPRESSO_CMSIS, or ~/NXP as wolfBoot expects) +# MCXN947_M33MU_TIMEOUT emulator wall-clock budget in seconds (default 60, +# 180 for the PSA guest scenarios) +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. +unset TARGET MAKEFLAGS MFLAGS + +scenario="${1:-}" +# The scenario table in lib/scenario_matrix.py is the one list of names. +known="$(python3 "$(dirname "$0")/lib/scenario_matrix.py" --port mcxn947 \ + --tier full --flat)" +case " $known " in + *" $scenario "*) ;; + *) echo "usage: $0 $(echo "$known" | tr ' ' '|')" >&2; exit 2 ;; +esac +case "$scenario" in + hsmattackneg|hsmpinneg|hsmfaultneg) + if [ "${WT_ENGINE:-native}" != "hsm" ]; then + echo "$scenario probes the wolfHSM engine; run it with WT_ENGINE=hsm" >&2 + exit 2 + fi ;; +esac + +here="$(cd "$(dirname "$0")" && pwd)" +repo="$(cd "$here/../.." && pwd)" +case "$scenario" in + bothpsa|bothiso|attestneg|hsmattackneg|fwustage|deputyneg|hsmpinneg|hsmfaultneg|bandneg[1-6]|restartneg[1-3]|periphspneg|sealneg|sealpivotneg|svcneg) + guest_dir="tests/firmware/psa-guest" + guest1_dir="$guest_dir" + timeout_s="${MCXN947_M33MU_TIMEOUT:-180}" ;; + confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec) + guest_dir="tests/firmware/psa-guest" + guest1_dir="tests/firmware/mcxn947-baremetal" + timeout_s="${MCXN947_M33MU_TIMEOUT:-900}" ;; + *) + guest_dir="tests/firmware/mcxn947-baremetal" + guest1_dir="$guest_dir" + timeout_s="${MCXN947_M33MU_TIMEOUT:-60}" ;; +esac +guest_build="$repo/$guest_dir/build" +guest1_build="$repo/$guest1_dir/build" + +# shellcheck source=lib/mcxn947_wolfboot.sh disable=SC1091 +. "$here/lib/mcxn947_wolfboot.sh" +wolfboot_dir="${MCXN947_WOLFBOOT_DIR:-/tmp/wolfboot_mcxn947}" +log="$repo/build/mcxn947_m33mu_$scenario.log" + +# The upstream M33MU commit with the MCXN947 RAM attribution fix; the carried +# patch adds the EdgeLock RND_REQ register model the SPM seeds its DRBG from. +M33MU_REF=87f693a3cbc4c1315ed3676cef962c092eb2f7be + +# The guests' manifest restart budget (port/mcxn947/partitions.c): sauneg +# relaunches guest0 this many times before quarantining it. +restart_limit=3 +# Where guest0's probe stores: inside guest1's RAM window (GUEST_PROBE_ADDR in +# tests/firmware/mcxn947-baremetal/Makefile). +probe_addr=0x20028000 +# Where guest0's restart probe reads: Secure RAM (GUEST_FAULT_ADDR there). +fault_addr=0x30031000 + +# shellcheck source=lib/scenario.sh disable=SC1091 +. "$here/lib/scenario.sh" +l3_layout_load "$repo" mcxn947 + +# Port markers the shared verdict assertions match against. +# shellcheck disable=SC2034 # matched inside lib/scenario.sh +GUEST_STARTED_RE='wolfTrust MCXN947 guest[01]: start' +# shellcheck disable=SC2034 +GUEST_DONE_RE='wolfTrust MCXN947 guest[01]: FF-M connect ok, done' + +# --- The pinned upstream M33MU plus the carried MCXN947 model patch. --- +m33mu_stamp="$M33MU_REF $(cksum "$here/m33mu-mcxn947.patch" | cut -d' ' -f1)" +if [ -n "${M33MU:-}" ] && [ -x "$M33MU" ]; then + log "Using prebuilt M33MU: $M33MU" +elif [ -x /tmp/m33mu_mcxn947_src/build/m33mu ] && + [ "$(cat /tmp/m33mu_mcxn947_src/.wt_m33mu 2>/dev/null)" = "$m33mu_stamp" ]; then + M33MU=/tmp/m33mu_mcxn947_src/build/m33mu + log "Reusing M33MU from a prior scenario: $M33MU" +else + stage "build M33MU $M33MU_REF" + rm -rf /tmp/m33mu_mcxn947_src + git clone --no-checkout https://github.com/danielinux/m33mu.git /tmp/m33mu_mcxn947_src + git -C /tmp/m33mu_mcxn947_src fetch --depth 1 origin "$M33MU_REF" + git -C /tmp/m33mu_mcxn947_src checkout --detach "$M33MU_REF" + git -C /tmp/m33mu_mcxn947_src apply "$here/m33mu-mcxn947.patch" + cmake -S /tmp/m33mu_mcxn947_src -B /tmp/m33mu_mcxn947_src/build \ + -DM33MU_ENABLE_WOLFSSL=OFF -DM33MU_BUILD_TESTS=OFF \ + -DM33MU_ENABLE_RUST_PLUGINS=OFF + cmake --build /tmp/m33mu_mcxn947_src/build --target m33mu -j"$(nproc)" + printf '%s\n' "$m33mu_stamp" > /tmp/m33mu_mcxn947_src/.wt_m33mu + M33MU=/tmp/m33mu_mcxn947_src/build/m33mu +fi + +# --- Secure-app wolfBoot first stage from the pinned mcxn-tz-wolftrust +# config: wolfBoot at the start of the flash, BOOT at 0x40000, the +# handoff record at 0x30030000, every image offset physical. --- +if [ -n "${MCXN947_WOLFBOOT_DIR:-}" ]; then + mcxn947_wolfboot_current "$wolfboot_dir" "$(mcxn947_wolfboot_stamp)" || \ + fail "wolfboot.bin in MCXN947_WOLFBOOT_DIR=$wolfboot_dir was not built from $MCXN947_WOLFBOOT_REF with $MCXN947_WOLFBOOT_CONFIG and the carried life cycle patch; rebuild it there (tests/target/lib/mcxn947_wolfboot.sh) or unset it" +else + stage "wolfBoot $MCXN947_WOLFBOOT_REF (mcxn-tz-wolftrust)" + mcxn947_wolfboot_build "$wolfboot_dir" || fail "wolfBoot build failed" +fi + +# --- wolfTrust and both guests, measurements pinned, signed for the boot +# partition. --- +cd "$repo" +git submodule update --init --single-branch +# MCXN wolfBoot uses a 1024-byte image header, so wolfTrust links at the boot +# base + 0x400 and is signed with a matching header. +export WT_SECURE_IMAGE_HEADER_SIZE=0x400 +rm -rf build +mkdir -p build + +secure_flags="$(scenario_secure_flags "$scenario")" +# Exported, not passed once: the guest build below re-enters the secure +# Makefile for the CMSE implib, and a build-mode mismatch there would relink +# the image and drop the wolftrust.bin this runner goes on to patch and sign. +if [ -n "$secure_flags" ]; then + # shellcheck disable=SC2086,SC2163 + export $secure_flags +fi +stage "build wolfTrust secure image + CMSE import library ${secure_flags:-(production)}" +make -s TARGET=mcxn947 secure-image TOOLPREFIX=arm-none-eabi- + +guest_flags="" +case "$scenario" in + sauneg) guest_flags="WT_SAU_PROBE=1" ;; + periphneg) guest_flags="WT_PERIPH_NEG_PROBE=1" ;; + # The secure probe fires inside a partition; guest0 ends the run on its + # breakpoint once its lifecycle is done, as the H5 level 3 runs do. + deputyneg|hsmpinneg|hsmfaultneg|bandneg[1-6]|restartneg[1-3]|periphspneg|sealneg|sealpivotneg|svcneg) + guest_flags="WT_M33MU_EXPECT_BKPT=1" + # guest0's own tasklet faults, so the peer ends the run. + if [ "$scenario" = "hsmfaultneg" ]; then + guest_flags="$guest_flags WT_M33MU_BKPT_GUEST=1" + fi ;; + restart) guest_flags="WT_GUEST_FAULT_PROBE=1" ;; + attestneg) guest_flags="WT_ATTEST_NEG_PROBE=1" ;; + hsmattackneg) guest_flags="WT_HSM_ATTACK_PROBE=1" ;; + # 0x21000 bytes of body reach sector 16 of the 32-sector update partition; + # the 27-byte tail ends off a phrase, so FINISH sees a padded last block. + fwustage) guest_flags="WT_FWU_PROBE=1 WT_FWU_PROBE_STREAM_BYTES=0x21000u WT_FWU_PROBE_SEQUENTIAL=1 WT_FWU_PROBE_TAIL_BYTES=27u" ;; + confboot) guest_flags="WT_RUN_CONFORMANCE=1 WT_M33MU_EXPECT_BKPT=1" ;; + devstorage) guest_flags="WT_RUN_CONFORMANCE=1 WT_CONF_SUITE=storage WT_M33MU_EXPECT_BKPT=1" ;; + devcrypto|vaultrecover|vaultrecoversec) + guest_flags="WT_RUN_CONFORMANCE=1 WT_CONF_SUITE=crypto WT_M33MU_EXPECT_BKPT=1" ;; + devattest) guest_flags="WT_RUN_CONFORMANCE=1 WT_CONF_SUITE=attestation WT_M33MU_EXPECT_BKPT=1" ;; + devattestqcbor) + tests/upstream/fetch_qcbor.sh >/dev/null + guest_flags="WT_RUN_CONFORMANCE=1 WT_CONF_SUITE=attestation WT_ATTEST_CBOR=qcbor WT_M33MU_EXPECT_BKPT=1" ;; +esac +stage "build the Non-secure guests from $guest_dir ${guest_flags:-(no probes)}" +make -s -C "$guest_dir" clean +# The emulator boots in OEM_OPEN, the ASSEMBLY_AND_TEST life cycle the +# attestation token reports; the guest's verify pins it. +# shellcheck disable=SC2086 +make -s -C "$guest_dir" TARGET=mcxn947 WT_EXPECTED_LIFECYCLE=0x1000u $guest_flags +if [ "$guest1_dir" != "$guest_dir" ]; then + make -s -C "$guest1_dir" clean + make -s -C "$guest1_dir" TARGET=mcxn947 +fi + +stage "pin both guest measurements, then wolfBoot-sign wolfTrust" +python3 tools/measure/patch_guest_digests.py build/wolftrust.bin \ + "0:1:$guest_build/guest0.bin" "1:1:$guest1_build/guest1.bin" +IMAGE_HEADER_SIZE=1024 WOLFBOOT_PARTITION_SIZE=0x40000 WOLFBOOT_SECTOR_SIZE=0x2000 \ + "$wolfboot_dir/tools/keytools/sign" --ecc256 --sha256 build/wolftrust.bin \ + "$wolfboot_dir/wolfboot_signing_private_key.der" 1 +[ -s build/wolftrust_v1_signed.bin ] || fail "signing produced no image" +# The harness, not the guest, holds wolfBoot's measurement of the signed image +# and matches the attestation token's reported value against it. +expected_measurement="$(python3 tests/scripts/read_wolfboot_measurement.py \ + build/wolftrust_v1_signed.bin)" + +if [ "$scenario" = "authneg" ]; then + # One byte of guest0 flips after its digest was pinned: launch verification + # must refuse guest0 while guest1 and the platform keep running. + python3 - "$guest_build/guest0.bin" <<'PYEOF' +import sys +with open(sys.argv[1], "r+b") as f: + f.seek(0x40) + byte = f.read(1) + f.seek(0x40) + f.write(bytes([byte[0] ^ 0x01])) +PYEOF +fi + +# The guests idle forever once done, so a console boot ends on the wall-clock +# budget (M33MU status 127) and a --quit-on-faults boot at the fault (0). +# Flash file offsets are physical: wolfBoot at 0, BOOT at 0x40000, the guest +# windows at 0x80000 and 0x100000. +boot_chain() { + local want="$1" + local out="$2" + local status + shift 2 + set +e + "$M33MU" --cpu mcxn947 "$wolfboot_dir/wolfboot.bin" \ + build/wolftrust_v1_signed.bin:0x40000 \ + "$guest_build/guest0.bin:0x80000" \ + "$guest1_build/guest1.bin:0x100000" \ + --timeout "$timeout_s" "$@" > "$out" 2>&1 + status=$? + set -e + check "$(printf '%s' "$status" | grep -qE "^($want)$"; echo $?)" \ + "M33MU exited with status $want (got $status)" +} + +end="$(scenario_end "$scenario")" +case "$scenario" in + confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover|vaultrecoversec) + # The val guest ends on its own breakpoint; the suite's panic tests reset + # the whole chain mid-run and val resumes off its flash boot flag. + end="bkpt:0x7f" ;; + fpneg|xnneg|mspovfneg) + # M33MU ends these runs at the fault (NOCP, the fetch denial, STKOF). + end="fault" ;; + deputyneg|hsmpinneg|hsmfaultneg|bandneg[1-6]|restartneg[1-3]|periphspneg|sealneg|sealpivotneg|svcneg) + end="bkpt:0x7f" ;; +esac +stage "boot the chain under M33MU (--cpu mcxn947, ${timeout_s}s budget)" +case "$end" in + bkpt:*) boot_chain 0 "$log" --uart-stdout --expect-bkpt "${end#bkpt:}" ;; + fault) boot_chain "0|1" "$log" --uart-stdout --quit-on-faults ;; + *) boot_chain 127 "$log" --uart-stdout ;; +esac + +# wolfBoot's MCXN HAL prints nothing of its own; its generic loader output +# shows the measured handoff path ran. +expect "wolfBoot verified the wolfTrust image signature" "Verifying signature...done" +if [ "$end" = "idle" ]; then + refute_re "wolfTrust never panicked (bkpt 0x7e)" '\[BKPT\] imm=0x7e' + refute_re "the monitor never lost every guest (bkpt 0x7d)" '\[BKPT\] imm=0x7d' + refute_re "no HardFault escalation" '\[HARDFLT\]' + refute_re "no guest reported a failed FF-M handshake" 'guest[01]: FAIL' + refute_re "no PSA guest lifecycle step failed" \ + 'guest[01]: .*(FAILED|failed|NOT re|SUCCEEDED|accepted|unavailable|wrong data|not refused|not rejected)' + expect "the run ended on the wall-clock budget, not a trap" "wall-clock limit" +elif [ "${end}" = "bkpt:0x7f" ]; then + expect "the val guest ran to its clean exit breakpoint" "[EXPECT BKPT] Success" +elif [ "$end" = "fault" ]; then + expect "the run stopped at the first delivered fault" "Execution stopped" +else + expect "the emulator stopped on the expected verdict breakpoint" \ + "[EXPECT BKPT] Success" + scenario_assert_verdict "$scenario" +fi + +# The Arm suite prints its report once at the end; guest1's lines can splice +# it, so the totals are read from the log with guest1 stripped and unwrapped. +conf_totals() { + local flat + flat="$(sed 's/guest1:.*$//' "$log" | tr -d '\r\n')" + conf_passed=$(printf '%s' "$flat" | grep -oE 'TOTAL PASSED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true) + conf_skipped=$(printf '%s' "$flat" | grep -oE 'TOTAL SKIPPED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true) + conf_failed=$(printf '%s' "$flat" | grep -oE 'TOTAL FAILED[[:space:]]*:[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | tail -1 || true) + : "${conf_passed:=-1}"; : "${conf_skipped:=-1}"; : "${conf_failed:=-1}" +} + +# A guest fault relaunches that guest, so exact launch counts also prove that +# nothing faulted where nothing should have. +case "$scenario" in + positive|crossdomain|keystoreneg|spfaultneg|panicneg) + for guest in guest0 guest1; do + expect_n "$guest launched exactly once (no fault, no relaunch)" 1 \ + "wolfTrust MCXN947 $guest: start" + expect_n "$guest reached the SPM through the SG veneers and finished" 1 \ + "wolfTrust MCXN947 $guest: FF-M connect ok, done" + done + # The deliberate SP probe faults are Secure faults the guests above prove + # the system rode out. M33MU's own fault dumps go to a buffered stdout + # that its wall-clock exit drops, so each probe is shown by a second boot + # that stops at the first delivered fault. + if [ "$scenario" = "positive" ] || [ "$scenario" = "spfaultneg" ]; then + expect_n "both guests reached the storage service" 2 ": storage connect ok" + elif [ "$scenario" = "panicneg" ]; then + # The panic unblocks the client it was serving with an error; the + # restarted SP serves the other guest. + expect_n "the restarted storage SP served the guest after the panic" 1 \ + ": storage connect ok" + fi + if [ "$scenario" = "spfaultneg" ] || [ "$scenario" = "panicneg" ]; then + # The relay (spfaultneg) runs an undefined instruction on its first + # entry; the storage SP (panicneg) closes an error handle, which the SPM + # must panic it for by resuming it on one. + stage "boot again, stopping at the first delivered fault" + log="$repo/build/mcxn947_m33mu_${scenario}_trace.log" + boot_chain 0 "$log" --quit-on-faults + expect "the traced run stopped at a delivered fault" "Execution stopped" + expect_n_re "the probed SP took a Secure-Thread UsageFault" 1 \ + '\[USGFLT\] enter sec=1 mode=0' + expect "the fault was the undefined instruction" "[USGFLT] CFSR=0x00010000" + elif [ "$scenario" != "positive" ]; then + # The storage SP's probe faults on its first wake, so the guests' + # storage connect never succeeds while their own lifecycle survives. + refute_re "the probed storage SP never served a guest connect" \ + ": storage connect ok" + case "$scenario" in + crossdomain) neg_addr="$L3_SPM_RAM" ;; + keystoreneg) neg_addr="$L3_VAULT_BAND" ;; + esac + stage "boot again with the protection-unit trace, stopping at the first fault" + log="$repo/build/mcxn947_m33mu_${scenario}_trace.log" + M33MU_PROT_TRACE=1 boot_chain 0 "$log" --quit-on-faults + expect "the traced run stopped at a delivered fault" "Execution stopped" + expect_re "the fault was the SP's out-of-domain read at the band address" \ + "\[MEMFAULT_CAUSE\] sec=S type=READ addr=$neg_addr reason=mpu-ap" + fi + ;; + deputyneg|hsmpinneg|hsmfaultneg|bandneg[1-6]|restartneg[1-3]|periphspneg|sealneg|sealpivotneg|svcneg) + expect_n "guest0 launched exactly once" 1 "guest0: alive" + if [ "$scenario" = "hsmfaultneg" ]; then + expect "guest1 ran its whole PSA lifecycle to the end breakpoint" \ + "guest1: done" + else + expect "guest0 ran its whole PSA lifecycle to the end breakpoint" \ + "guest0: done" + expect "guest1 ran beside the partition under test" "guest1: alive" + fi + case "$scenario" in + deputyneg) + l3_assert_probe_linked build/wolftrust.elf \ + wt_platform_deputy_flash_probe ;; + hsmpinneg) + l3_assert_probe_linked build/wolftrust.elf wt_platform_hsm_pin_probe ;; + *) + scenario_assert_l3 "$scenario" ;; + esac + case "$scenario" in + deputyneg|hsmpinneg|bandneg[1-6]|restartneg[1-3]) + # The probed partition is restarted (or never faults) and serves on. + if [ "$scenario" = "deputyneg" ] || [ "$scenario" = "hsmpinneg" ]; then + refute_re "no fault markers in the boot log" \ + '^(\[MEMFAULT\]|\[USGFLT\]|\[HARDFLT\]|HardFault|SecureFault)' + fi + expect "mediated crypto dispatch verified" \ + "guest0: wolfTrust FF-M mediated crypto dispatch verified" + expect "ITS set/get verified" "guest0: wolfTrust ITS set/get verified" + expect "PS sealed set/get verified" \ + "guest0: wolfTrust PS sealed set/get verified" + expect "key-ops sign/verify verified" \ + "guest0: wolfTrust key-ops sign/verify verified" + expect "attestation COSE_Sign1 verified" \ + "guest0: wolfTrust attestation: COSE_Sign1 verified" + ;; + hsmfaultneg) + expect "the other guest kept being served" \ + "guest1: psa_hash_compute(SHA-256) KAT verified" ;; + svcneg) + # The stateless ITS client reports the unblocked call as an error. + expect_re "the pinned ITS client was unblocked with an error" \ + 'guest[01]: psa_its_set failed st=-1(32|45)' + expect "the restarted ITS partition served the other guest" \ + "wolfTrust ITS set/get verified" + expect "sealed storage path unaffected" \ + "guest0: wolfTrust PS sealed set/get verified" ;; + periphspneg) + refute_re "the probed storage SP never served ITS" \ + 'guest[01]: wolfTrust ITS set/get verified' ;; + esac + ;; + xnneg|mspovfneg) + scenario_assert_verdict "$scenario" + ;; + fpneg) + # The relay's FP instruction must hit a disabled coprocessor: NOCP, not the + # fallback undefined-instruction fault. Restart and guest survival are the + # silicon leg's to show (docs/Testing.md). + expect_n_re "the relay took a Secure-Thread UsageFault" 1 \ + '\[USGFLT\] enter sec=1 mode=0' + expect "the fault was the NOCP trap (Secure FP disabled)" \ + "[USGFLT] CFSR=0x00080000" + refute_re "no HardFault escalation" '\[HARDFLT\]' + ;; + restart) + faults=$((restart_limit + 1)) + expect_n "guest0 relaunched through its restart budget, then quarantined" \ + "$faults" "wolfTrust MCXN947 guest0: start" + refute_re "guest0 never got past its launch-time fault" \ + "wolfTrust MCXN947 guest0: FF-M connect ok" + expect_n "guest1 launched once, untouched by guest0's faults" 1 \ + "wolfTrust MCXN947 guest1: start" + expect_n "guest1 completed the FF-M handshake" 1 \ + "wolfTrust MCXN947 guest1: FF-M connect ok, done" + stage "boot again with the protection-unit trace, stopping at the first fault" + log="$repo/build/mcxn947_m33mu_${scenario}_trace.log" + M33MU_PROT_TRACE=1 boot_chain 0 "$log" --quit-on-faults + expect "the traced run stopped at a delivered fault" "Execution stopped" + refused="\[MEMFAULT_CAUSE\] sec=NS type=READ addr=$fault_addr reason=secure-attr" + expect_n_re "the fault was guest0's read of Secure RAM, refused as Secure" \ + 1 "$refused" + sau_refusals="$(grep -A1 -E -- "$refused" "$log" | grep -c "src=SAU" || true)" + check "$([ "$sau_refusals" -eq 1 ]; echo $?)" \ + "the refusal came from the SAU" + ;; + wrpoff) + refute_re "no guest entered its domain without a guest flash fence" \ + "wolfTrust MCXN947 guest[01]: start" + ;; + authneg) + refute_re "the tampered guest0 never entered its domain" \ + "wolfTrust MCXN947 guest0: start" + expect_n "guest1 launched once after guest0's refusal" 1 \ + "wolfTrust MCXN947 guest1: start" + expect_n "guest1 completed the FF-M handshake" 1 \ + "wolfTrust MCXN947 guest1: FF-M connect ok, done" + expect_n "guest1 reached the storage service" 1 \ + "wolfTrust MCXN947 guest1: storage connect ok" + ;; + bothpsa|bothiso|attestneg|hsmattackneg|fwustage) + for guest in guest0 guest1; do + expect_n "$guest launched exactly once (no fault, no relaunch)" 1 \ + "$guest: alive" + expect_n "$guest ran its whole PSA lifecycle" 1 "$guest: done" + done + case "$scenario" in + bothpsa) + # The same PSA client behaviour from both guests in one boot. + for guest in guest0 guest1; do + expect "$guest: mediated SERVICE_CRYPTO SHA-256 KAT" \ + "$guest: wolfTrust FF-M mediated crypto dispatch verified" + expect "$guest: PSA psa_generate_random" \ + "$guest: psa_generate_random st=0" + expect "$guest: PSA psa_hash_compute(SHA-256) KAT" \ + "$guest: psa_hash_compute(SHA-256) KAT verified" + expect "$guest: PSA AES-CTR through a volatile key" \ + "$guest: psa_cipher_encrypt(AES-CTR) st=0" + expect "$guest: ITS set/get through SERVICE_ITS" \ + "$guest: wolfTrust ITS set/get verified" + expect "$guest: PS sealed set/get through SERVICE_PS" \ + "$guest: wolfTrust PS sealed set/get verified" + expect "$guest: key-ops sign/verify through the mediated path" \ + "$guest: wolfTrust key-ops sign/verify verified" + expect "$guest: cross-key verify refused" \ + "$guest: wolfTrust key negatives verified" + expect "$guest: initial attestation token issued" \ + "$guest: psa_initial_attestation st=0" + expect "$guest: COSE_Sign1 token verified against the IAK" \ + "$guest: wolfTrust attestation: COSE_Sign1 verified" + expect "$guest: token measurement equals wolfBoot's measurement of the signed image" \ + "$guest: wolfTrust attestation: token measurement=$expected_measurement" + done + ;; + bothiso) + # The SPM rejects the same abuse from both guests, and each guest + # still gets served afterwards. + for guest in guest0 guest1; do + expect "$guest: forged-handle call rejected" \ + "$guest: wolfTrust FF-M forged-handle call rejected" + expect "$guest: oversized-vector call rejected" \ + "$guest: wolfTrust FF-M oversized-vector call rejected" + expect "$guest: cross-guest vector refused (caller-banded memcheck)" \ + "$guest: wolfTrust FF-M cross-guest vector rejected" + expect "$guest: unknown-SID connect refused" \ + "$guest: wolfTrust FF-M unknown-SID connect refused" + expect "$guest: still served after the negatives" \ + "$guest: psa_hash_compute(SHA-256) KAT verified" + done + ;; + attestneg) + expect "attestation baseline token verified" \ + "guest0: wolfTrust attestation: COSE_Sign1 verified" + expect "oversized challenge rejected st=-135" \ + "guest0: attestneg oversized challenge rejected st=-135" + expect "zero token buffer rejected st=-135" \ + "guest0: attestneg zero token buffer rejected st=-135" + expect "tampered token fails the guest verify" \ + "guest0: attestneg tampered token rejected" + expect "misattributed lifecycle fails the guest verify" \ + "guest0: attestneg lifecycle mismatch rejected" + expect "attestation negatives verified" \ + "guest0: wolfTrust attestation negatives verified" + ;; + hsmattackneg) + expect "forged COMM_INIT attempted" \ + "guest0: hsmattackneg forged COMM_INIT" + expect "IAK sign refused" "guest0: hsmattackneg IAK sign refused" + expect "rollback NVM group refused" \ + "guest0: hsmattackneg rollback NVM group refused" + expect "own-namespace crypto still works" \ + "guest0: hsmattackneg own-namespace crypto still works" + ;; + fwustage) + expect "write before start refused on target" \ + "guest0: wolfTrust FWU write-before-start refused" + expect "candidate body streamed across the update partition's data sectors" \ + "guest0: wolfTrust FWU streamed candidate body across the update partition" + expect "candidate staged to the update partition and armed" \ + "guest0: wolfTrust FWU staged signed-header candidate to update partition, armed, verified" + expect "reject disarms and clean restores READY" \ + "guest0: wolfTrust FWU reject disarmed and clean restored READY" + expect "a write that skips ahead is refused and the session cleans" \ + "guest0: wolfTrust FWU out-of-order write refused and cleaned" + expect "unrelated storage partitions unaffected" \ + "guest0: wolfTrust ITS set/get verified" + ;; + esac + ;; + confboot|devstorage|devcrypto|devattest|devattestqcbor|vaultrecover) + expect "guest0 booted the PSA client" "guest0: wolfTrust FF-M psa_framework_version=0x0100" + expect "conformance val_entry start" "guest0: wolfTrust FF-M conformance: val_entry start" + conf_totals + case "$scenario" in + confboot) + # 89 scheduled: 85 pass, the 4 heap tests report SKIPPED on this + # zero-allocation image, as on the H5 and the RT700. + check "$([ "$conf_failed" -eq 0 ] && [ "$conf_passed" -eq 85 ] && [ "$conf_skipped" -eq 4 ]; echo $?)" \ + "Arm FF-M IPC suite: ${conf_passed} passed, ${conf_skipped} skipped, ${conf_failed} failed (want 85/4/0)" + ;; + devstorage) + check "$([ "$conf_failed" -eq 0 ] && [ "$((conf_passed + conf_skipped))" -eq 17 ]; echo $?)" \ + "dev_apis storage: ${conf_passed} passed, ${conf_skipped} skipped, ${conf_failed} failed (17 total)" + ;; + devcrypto|vaultrecover) + check "$([ "$conf_failed" -eq 0 ] && [ "$((conf_passed + conf_skipped))" -eq 77 ]; echo $?)" \ + "dev_apis crypto: ${conf_passed} passed, ${conf_skipped} skipped, ${conf_failed} failed (77 scheduled)" + ;; + devattest|devattestqcbor) + check "$([ "$conf_failed" -eq 0 ] && [ "$conf_passed" -eq 1 ]; echo $?)" \ + "dev_apis initial_attestation: ${conf_passed} passed, ${conf_failed} failed" + ;; + esac + ;; + vaultrecoversec) + refute_re "no fault (fail closed, not a brick)" '\[HARDFLT\]|HardFault|SecureFault' + expect "guest starts under fail-closed attestation" \ + "guest0: wolfTrust FF-M conformance: val_entry start" + ;; + periphneg) + # The CPU leg of the H5 periphneg; the M33MU MCXN947 model has no DMA, so + # Non-secure DMA copies out of Secure memory are silicon work. + faults=$((restart_limit + 1)) + expect_n "guest0 relaunched through its restart budget" \ + "$faults" "wolfTrust MCXN947 guest0: start" + refute_re "no NS read of the SPM's ELS ever returned" "periph probe LEAKED" + expect_n "guest1 launched once, untouched by guest0's faults" 1 \ + "wolfTrust MCXN947 guest1: start" + expect_n "guest1 completed the FF-M handshake" 1 \ + "wolfTrust MCXN947 guest1: FF-M connect ok, done" + stage "boot again with the protection-unit trace, stopping at the first fault" + log="$repo/build/mcxn947_m33mu_${scenario}_trace.log" + M33MU_PROT_TRACE=1 boot_chain 0 "$log" --quit-on-faults + expect "the traced run stopped at a delivered fault" "Execution stopped" + periph_ns="$(printf '0x%08x' $((L3_SPM_PERIPHERAL - 0x10000000)))" + refused="\[MEMFAULT_CAUSE\] sec=NS type=READ addr=$periph_ns reason=secure-attr" + expect_n_re "the fault was guest0's NS read of the SPM's ELS, refused as Secure" \ + 1 "$refused" + sau_refusals="$(grep -A1 -E -- "$refused" "$log" | grep -c "src=SAU" || true)" + check "$([ "$sau_refusals" -eq 1 ]; echo $?)" "the refusal came from the SAU" + ;; + sauneg) + faults=$((restart_limit + 1)) + expect_n "guest0 relaunched through its restart budget" \ + "$faults" "wolfTrust MCXN947 guest0: start" + expect_n "every guest0 launch completed the FF-M handshake before probing" \ + "$faults" "wolfTrust MCXN947 guest0: FF-M connect ok, done" + expect_n "guest1 launched once, untouched by guest0's faults" 1 \ + "wolfTrust MCXN947 guest1: start" + expect_n "guest1 completed the FF-M handshake" 1 \ + "wolfTrust MCXN947 guest1: FF-M connect ok, done" + + # The launch count above shows every attempt faulted; why it faulted comes + # from M33MU's protection-unit trace, which names the attribution behind a + # fault. The trace runs between every console byte, so this boot leaves + # the console off, and it stops at the first delivered fault: the emulator + # is deterministic, so that is guest0's first store. + stage "boot again with the protection-unit trace, stopping at the first fault" + log="$repo/build/mcxn947_m33mu_${scenario}_trace.log" + M33MU_PROT_TRACE=1 boot_chain 0 "$log" --quit-on-faults + expect "the traced run stopped at a delivered fault" "Execution stopped" + refused="\[MEMFAULT_CAUSE\] sec=NS type=WRITE addr=$probe_addr reason=secure-attr" + expect_n_re "the fault was guest0's store into guest1's RAM, refused as Secure" \ + 1 "$refused" + sau_refusals="$(grep -A1 -E -- "$refused" "$log" | grep -c "src=SAU" || true)" + check "$([ "$sau_refusals" -eq 1 ]; echo $?)" \ + "the refusal came from the SAU: the peer window was Secure at dispatch" + ;; +esac + +log "PASS: mcxn947-m33mu/$scenario" diff --git a/tests/target/run_mcxn947_suite.sh b/tests/target/run_mcxn947_suite.sh new file mode 100755 index 000000000..47b7ed435 --- /dev/null +++ b/tests/target/run_mcxn947_suite.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# wolfTrust MCXN947 hardware suite, the FRDM-MCXN947 sibling of +# run_rt700_suite.sh: the positive chain, the guest isolation and peripheral +# negatives, the restart budget, and the PSA client lifecycle, each built and +# flashed to the board on the host that owns its MCU-Link. Without a board +# detect_mcxn947.sh reports why and the suite SKIPs; it never silently passes. +# +# Its logs live under logs/, never build/: every runner cleans build/ before +# it builds, so evidence there would not survive the next scenario. One batch +# at a time owns the board, through a lock the runner shares. +set -euo pipefail + +repo="$(cd "$(dirname "$0")/../.." && pwd)" +runner="${MCXN947_SUITE_RUNNER:-$repo/tests/target/run_mcxn947_hardware.sh}" +detect="${MCXN947_SUITE_DETECT:-$repo/tests/target/detect_mcxn947.sh}" +scenarios="${WT_MCXN947_SCENARIOS:-positive sauneg periphneg restart psa wrpoff}" +spsdk_venv="${MCXN947_SPSDK_VENV:-$HOME/spsdk-venv}" +logs="${MCXN947_SUITE_LOGS:-$repo/logs/mcxn947-suite}" +lock="${MCXN947_HW_LOCK:-$repo/logs/mcxn947-hw.lock}" + +# 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 ! "$detect" >/dev/null 2>&1; then + echo "SKIP: MCXN947 hardware suite ($("$detect" 2>&1))" + exit 0 +fi + +mkdir -p "$logs" "$(dirname "$lock")" +exec 9>"$lock" +if ! flock -n 9; then + echo "WAIT: another MCXN947 hardware batch holds $lock" + flock 9 +fi +export MCXN947_HW_LOCKED=1 + +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 +echo "LOGS: $logs" + +if [ "$rc" -eq 0 ]; then echo "PASS: hardware/all"; else echo "FAIL: hardware/all"; exit 1; fi diff --git a/tests/target/run_suite.sh b/tests/target/run_suite.sh index d5a506ce5..cfa1a6bf0 100755 --- a/tests/target/run_suite.sh +++ b/tests/target/run_suite.sh @@ -6,16 +6,18 @@ # tests/target/run_suite.sh m33mu ... -> run_m33mu_scenario.sh # tests/target/run_suite.sh qemu-a ... -> run_qemu_a_scenario.sh # tests/target/run_suite.sh rt700-m33mu ... -> run_rt700_m33mu.sh +# tests/target/run_suite.sh mcxn947-m33mu ... -> run_mcxn947_m33mu.sh # WT_SUITE_LOG_DIR where target-.log lands (default logs) set -u -usage() { echo "usage: $0 m33mu|qemu-a|rt700-m33mu ..." >&2; exit 2; } +usage() { echo "usage: $0 m33mu|qemu-a|rt700-m33mu|mcxn947-m33mu ..." >&2; exit 2; } model="${1:-}" case "$model" in - m33mu) runner=run_m33mu_scenario.sh ;; - qemu-a) runner=run_qemu_a_scenario.sh ;; - rt700-m33mu) runner=run_rt700_m33mu.sh ;; + m33mu) runner=run_m33mu_scenario.sh ;; + qemu-a) runner=run_qemu_a_scenario.sh ;; + rt700-m33mu) runner=run_rt700_m33mu.sh ;; + mcxn947-m33mu) runner=run_mcxn947_m33mu.sh ;; *) usage ;; esac shift diff --git a/tests/target/test_mcxn947_suite_evidence.sh b/tests/target/test_mcxn947_suite_evidence.sh new file mode 100755 index 000000000..eb884117e --- /dev/null +++ b/tests/target/test_mcxn947_suite_evidence.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Regression check for the MCXN947 hardware suite: its logs and the runner's +# retained evidence never live under build/ (every run cleans build/ before +# it builds, so a batch would lose the previous scenario's log there), and a +# batch serializes on the board lock the runner shares. Runs without a board: +# the suite is driven with a stub runner and a stub detector. +set -euo pipefail + +repo="$(cd "$(dirname "$0")/../.." && pwd)" +suite="$repo/tests/target/run_mcxn947_suite.sh" +runner="$repo/tests/target/run_mcxn947_hardware.sh" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +fails=0 +check() { + if [ "$1" -eq 0 ]; then echo "PASS: $2"; else echo "FAIL: $2"; fails=$((fails + 1)); fi +} + +# The defaults, read from the scripts: nothing retained lands under build/. +check "$(grep -qE '^logs="\$\{MCXN947_SUITE_LOGS:-\$repo/logs/' "$suite"; echo $?)" \ + "the suite's logs default to logs/, not build/" +check "$(grep -qE '^work="\$\{MCXN947_WORK:-\$repo/logs/' "$runner"; echo $?)" \ + "the runner's retained evidence defaults to logs/, not build/" +check "$(! grep -qE '(logs|work|lock)="\$\{[A-Z0-9_]+:-\$repo/build' "$suite" "$runner"; echo $?)" \ + "no evidence path defaults to build/" +check "$(grep -q 'flock' "$suite" && grep -q 'flock' "$runner"; echo $?)" \ + "the suite and the runner both take the board lock" +check "$(grep -qE '^evidence="\$work/\$scenario-\$engine"' "$runner"; echo $?)" \ + "the runner keeps each run's console and dumps by scenario and engine" + +# A stub runner that cleans build/ the way a real run does (the suite's old +# log home), then reports; its log must survive in the suite's log dir. +stub="$tmp/runner.sh" +cat > "$stub" <<'EOF' +#!/usr/bin/env bash +rm -rf "${STUB_REPO}/build/mcxn947-suite" +echo " [check] PASS stub $1 ran with lock=${MCXN947_HW_LOCKED:-unset}" +echo "PASS: hardware/mcxn947/$1" +EOF +chmod +x "$stub" +export STUB_REPO="$repo" MCXN947_SUITE_RUNNER="$stub" MCXN947_SUITE_DETECT=/bin/true +export MCXN947_SUITE_LOGS="$tmp/logs" MCXN947_HW_LOCK="$tmp/hw.lock" +export WT_MCXN947_SCENARIOS="alpha beta" +out="$("$suite" 2>&1)"; rc=$? +check "$([ "$rc" -eq 0 ]; echo $?)" "the stub batch passes (rc $rc)" +check "$(grep -q "PASS: hardware/alpha" <<<"$out" && grep -q "PASS: hardware/beta" <<<"$out"; echo $?)" \ + "both scenarios reported" +check "$([ -s "$tmp/logs/alpha.log" ] && [ -s "$tmp/logs/beta.log" ]; echo $?)" \ + "every scenario's log survived the runner cleaning build/" +check "$(grep -q "lock=1" "$tmp/logs/beta.log"; echo $?)" \ + "the runner saw the batch's lock and did not take its own" +check "$(grep -q "^LOGS: $tmp/logs" <<<"$out"; echo $?)" "the suite names its log dir" + +# Serialization: while another batch holds the lock, a new batch waits. +( flock "$tmp/hw.lock" sleep 4 ) & +holder=$! +sleep 0.5 +rm -rf "$tmp/logs" +set +e +waited="$(timeout 2 "$suite" 2>&1)"; wrc=$? +set -e +check "$([ "$wrc" -eq 124 ]; echo $?)" "a batch waits for the lock holder (rc $wrc)" +check "$(grep -q "^WAIT:" <<<"$waited"; echo $?)" "the waiting batch says so" +check "$([ ! -e "$tmp/logs/alpha.log" ]; echo $?)" "the waiting batch ran no scenario" +wait "$holder" +"$suite" >/dev/null 2>&1 +check "$([ -s "$tmp/logs/beta.log" ]; echo $?)" "the batch runs once the lock is released" + +# program_board must stop before the core is released when pyOCD exits nonzero +# without saying why (a timeout) or exits 0 reporting a failed command. +sed -n '/^program_board() {/,/^}/p' "$runner" > "$tmp/program_board.sh" +program_case() { + bash -c ' +set -euo pipefail +fail() { echo "FAIL: $*"; exit 1; } +timeout() { shift; "$@"; } +pyocd() { case "$PB_MODE" in + silent) return 124 ;; error) echo "Error: transfer failed"; return 0 ;; + longerror) echo "Error: transfer failed"; printf "%02000000d\n" 0; return 0 ;; + *) echo "loaded"; return 0 ;; esac; } +probe_uid=X; target=mcxn947vdf; pyocd=pyocd +park_cmds=(-c "reset halt"); board_cmds=(-c "load a.bin 0x0") +. "$1" +program_board && echo RELEASED' _ "$tmp/program_board.sh" 2>&1 +} +check "$(! PB_MODE=silent program_case | grep -q RELEASED; echo $?)" \ + "a silent pyOCD timeout stops programming" +check "$(! PB_MODE=error program_case | grep -q RELEASED; echo $?)" \ + "a command error reported with exit 0 stops programming" +check "$(! PB_MODE=longerror program_case | grep -q RELEASED; echo $?)" \ + "a command error ahead of a long log stops programming" +check "$(PB_MODE=ok program_case | grep -q RELEASED; echo $?)" \ + "a clean session releases the core" + +echo "mcxn947 suite evidence: $fails failures" +[ "$fails" -eq 0 ] diff --git a/tests/target/wolfboot-mcxn947-lifecycle.patch b/tests/target/wolfboot-mcxn947-lifecycle.patch new file mode 100644 index 000000000..b77e45d8a --- /dev/null +++ b/tests/target/wolfboot-mcxn947-lifecycle.patch @@ -0,0 +1,195 @@ +diff --git a/hal/mcxn.c b/hal/mcxn.c +index 318eeed5..951955c3 100644 +--- a/hal/mcxn.c ++++ b/hal/mcxn.c +@@ -25,6 +25,7 @@ + #include "fsl_common.h" + #include "image.h" + #include "hal.h" ++#include "mcxn_lifecycle.h" + + #include "clock_config.h" + #include "fsl_clock.h" +@@ -529,6 +530,25 @@ void uart_write(const char *buf, unsigned int sz) + } + } + ++/* The PSA security lifecycle the Secure application attests, from the OTP ++ * life cycle state the ROM reflects into SYSCON and the debug authentication ++ * state. */ ++int hal_attestation_get_lifecycle(uint32_t *lifecycle) ++{ ++ uint32_t lcState; ++ uint32_t lcStateDp; ++ uint32_t debugAuthStatus; ++ ++ if (lifecycle == NULL) ++ return -1; ++ lcState = *(volatile uint32_t *)MCXN_SYSCON_ELS_OTP_LC_STATE; ++ lcStateDp = *(volatile uint32_t *)MCXN_SYSCON_ELS_OTP_LC_STATE_DP; ++ debugAuthStatus = *(volatile uint32_t *)MCXN_DAUTHSTATUS_ADDRESS; ++ *lifecycle = mcxn_attestation_lifecycle(lcState, lcStateDp, ++ debugAuthStatus); ++ return 0; ++} ++ + #if defined(WOLFCRYPT_TZ_PSA) && defined(WOLFBOOT_DICE_HW) && defined(__WOLFBOOT) + + /* Holds the raw 64-byte P-256 public key (X||Y) written by hal_dice_create_attest_key(). +@@ -605,14 +625,6 @@ int hal_attestation_get_ueid(uint8_t *buf, size_t *len) + return ret; + } + +-int hal_attestation_get_lifecycle(uint32_t *lifecycle) +-{ +- if (lifecycle == NULL) +- return -1; +- *lifecycle = 0x3000u; /* PSA_LIFECYCLE_SECURED (default) */ +- return 0; +-} +- + /* Counts actual hardware CDI derivations performed (wolfBoot is always skipped). + * Shared with hal_dice_create_attest_key so it can reset after each complete sequence. */ + static int cdi_derivation_count = 0; +diff --git a/hal/mcxn_lifecycle.h b/hal/mcxn_lifecycle.h +new file mode 100644 +index 00000000..aada6089 +--- /dev/null ++++ b/hal/mcxn_lifecycle.h +@@ -0,0 +1,136 @@ ++/* mcxn_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 . ++ */ ++ ++/* MCXN947 life cycle to PSA security lifecycle. The ROM reflects the OTP ++ * life cycle state into SYSCON (ELS_OTP_LC_STATE and its duplicate); the ++ * state codes are the low byte of the ROM API's nboot_lc_* values. */ ++ ++#ifndef MCXN_LIFECYCLE_H ++#define MCXN_LIFECYCLE_H ++ ++#include ++ ++#define MCXN_SYSCON_ELS_OTP_LC_STATE 0x50000980u ++#define MCXN_SYSCON_ELS_OTP_LC_STATE_DP 0x50000984u ++#define MCXN_LC_STATE_MASK 0xFFu ++#define MCXN_LC_RESERVED_MASK 0xFFFFFF00u ++ ++#define MCXN_LC_NXP_BLANK 0x00u ++#define MCXN_LC_NXP_FAB 0x01u ++#define MCXN_LC_OEM_OPEN 0x03u ++#define MCXN_LC_OEM_SECURE_WORLD_CLOSED 0x07u ++#define MCXN_LC_OEM_CLOSED 0x0Fu ++#define MCXN_LC_OEM_FIELD_RETURN 0x1Fu ++#define MCXN_LC_NXP_FIELD_RETURN 0x7Fu ++#define MCXN_LC_OEM_LOCKED 0xCFu ++#define MCXN_LC_NXP_DEV 0xFCu ++#define MCXN_LC_SHREDDED 0xFFu ++ ++#define MCXN_DAUTHSTATUS_ADDRESS 0xE000EFB8u ++#define MCXN_DAUTHSTATUS_NSID_SHIFT 0u ++#define MCXN_DAUTHSTATUS_NSNID_SHIFT 2u ++#define MCXN_DAUTHSTATUS_SID_SHIFT 4u ++#define MCXN_DAUTHSTATUS_SNID_SHIFT 6u ++#define MCXN_DAUTHSTATUS_FIELD_MASK 0x3u ++#define MCXN_DAUTHSTATUS_ENABLED 0x3u ++#define MCXN_DAUTHSTATUS_DISABLED 0x2u ++ ++static inline uint32_t mcxn_lc_to_psa_lifecycle(uint32_t lcState, ++ uint32_t lcStateDp) ++{ ++ uint32_t lifecycle; ++ ++ if ((lcState & MCXN_LC_RESERVED_MASK) != 0u || ++ (lcStateDp & MCXN_LC_RESERVED_MASK) != 0u || ++ lcState != lcStateDp) { ++ return 0x0000u; /* PSA_LIFECYCLE_UNKNOWN */ ++ } ++ switch (lcState & MCXN_LC_STATE_MASK) { ++ case MCXN_LC_OEM_OPEN: ++ lifecycle = 0x1000u; /* PSA_LIFECYCLE_ASSEMBLY_AND_TEST */ ++ break; ++ case MCXN_LC_OEM_SECURE_WORLD_CLOSED: ++ lifecycle = 0x2000u; /* PSA_LIFECYCLE_PSA_ROT_PROVISIONING */ ++ break; ++ case MCXN_LC_OEM_CLOSED: ++ case MCXN_LC_OEM_LOCKED: ++ lifecycle = 0x3000u; /* PSA_LIFECYCLE_SECURED */ ++ break; ++ case MCXN_LC_OEM_FIELD_RETURN: ++ lifecycle = 0x6000u; /* PSA_LIFECYCLE_DECOMMISSIONED */ ++ break; ++ default: ++ lifecycle = 0x0000u; /* PSA_LIFECYCLE_UNKNOWN */ ++ break; ++ } ++ return lifecycle; ++} ++ ++static inline int mcxn_debug_status_field_is(uint32_t debugAuthStatus, ++ uint32_t shift, uint32_t value) ++{ ++ return (((debugAuthStatus >> shift) & MCXN_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 mcxn_debug_status_field_valid(uint32_t debugAuthStatus, ++ uint32_t shift) ++{ ++ return mcxn_debug_status_field_is(debugAuthStatus, shift, ++ MCXN_DAUTHSTATUS_ENABLED) || ++ mcxn_debug_status_field_is(debugAuthStatus, shift, ++ MCXN_DAUTHSTATUS_DISABLED); ++} ++ ++static inline uint32_t mcxn_attestation_lifecycle(uint32_t lcState, ++ uint32_t lcStateDp, uint32_t debugAuthStatus) ++{ ++ uint32_t lifecycle = mcxn_lc_to_psa_lifecycle(lcState, lcStateDp); ++ ++ if (lifecycle == 0x3000u) { ++ if (!mcxn_debug_status_field_valid(debugAuthStatus, ++ MCXN_DAUTHSTATUS_NSID_SHIFT) || ++ !mcxn_debug_status_field_valid(debugAuthStatus, ++ MCXN_DAUTHSTATUS_NSNID_SHIFT) || ++ !mcxn_debug_status_field_valid(debugAuthStatus, ++ MCXN_DAUTHSTATUS_SID_SHIFT) || ++ !mcxn_debug_status_field_valid(debugAuthStatus, ++ MCXN_DAUTHSTATUS_SNID_SHIFT)) { ++ lifecycle = 0x0000u; /* PSA_LIFECYCLE_UNKNOWN */ ++ } ++ else if (mcxn_debug_status_field_is(debugAuthStatus, ++ MCXN_DAUTHSTATUS_SID_SHIFT, MCXN_DAUTHSTATUS_ENABLED) || ++ mcxn_debug_status_field_is(debugAuthStatus, ++ MCXN_DAUTHSTATUS_SNID_SHIFT, MCXN_DAUTHSTATUS_ENABLED)) { ++ lifecycle = 0x5000u; /* PSA_LIFECYCLE_RECOVERABLE_PSA_ROT_DEBUG */ ++ } ++ else if (mcxn_debug_status_field_is(debugAuthStatus, ++ MCXN_DAUTHSTATUS_NSID_SHIFT, MCXN_DAUTHSTATUS_ENABLED) || ++ mcxn_debug_status_field_is(debugAuthStatus, ++ MCXN_DAUTHSTATUS_NSNID_SHIFT, MCXN_DAUTHSTATUS_ENABLED)) { ++ lifecycle = 0x4000u; /* PSA_LIFECYCLE_NON_PSA_ROT_DEBUG */ ++ } ++ } ++ return lifecycle; ++} ++ ++#endif /* MCXN_LIFECYCLE_H */ diff --git a/tools/ci-static-analysis-build.sh b/tools/ci-static-analysis-build.sh index ac629390d..1b340d64c 100644 --- a/tools/ci-static-analysis-build.sh +++ b/tools/ci-static-analysis-build.sh @@ -1,13 +1,13 @@ #!/bin/sh -# Compile the host and both Armv8-M ports under a static-analysis build tracer. +# Compile the host and every Armv8-M port under a static-analysis build tracer. set -eu make test -for target in stm32h563 mimxrt700; do +for target in stm32h563 mimxrt700 mcxn947; do for engine in native hsm; do case "$target" in - mimxrt700) attest_cose=0 ;; + mimxrt700|mcxn947) attest_cose=0 ;; *) attest_cose=1 ;; esac make TARGET="$target" WT_ENGINE="$engine" \