diff --git a/.github/workflows/cross-compile.yml b/.github/workflows/cross-compile.yml
index 0e80b033..9a073288 100644
--- a/.github/workflows/cross-compile.yml
+++ b/.github/workflows/cross-compile.yml
@@ -33,3 +33,7 @@ jobs:
run: make -C tests/firmware/stm32h563 all TOOLPREFIX=arm-none-eabi-
- name: Compile-check the no-attestation secure image (WT_ATTEST_COSE=0)
run: make TARGET=stm32h563 WT_ATTEST_COSE=0 secure-image TOOLPREFIX=arm-none-eabi-
+ - name: Build the hsm-engine secure image (WT_ENGINE=hsm)
+ run: make TARGET=stm32h563 WT_ENGINE=hsm secure-image TOOLPREFIX=arm-none-eabi- BUILD_DIR=build_hsm
+ - name: Build the native-engine secure image (WT_ENGINE=native)
+ run: make TARGET=stm32h563 WT_ENGINE=native secure-image TOOLPREFIX=arm-none-eabi- BUILD_DIR=build_native
diff --git a/.github/workflows/m33mu.yml b/.github/workflows/m33mu.yml
index c69ee8f4..b7c2b182 100644
--- a/.github/workflows/m33mu.yml
+++ b/.github/workflows/m33mu.yml
@@ -80,12 +80,13 @@ jobs:
./test-app/emu-test-apps/test.sh
wolfboot-wolftrust-m33mu:
- name: wolfTrust ${{ matrix.guest }} lifecycle
+ name: wolfTrust ${{ matrix.guest }} lifecycle (${{ matrix.engine }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
guest: [zephyr, freertos]
+ engine: [native, hsm]
timeout-minutes: 45
container:
image: ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.15
@@ -93,6 +94,7 @@ jobs:
# Keep the validated H5 secure-app handoff until that configuration is
# upstreamed; upstream master does not yet ship this wolfTrust config.
WOLFBOOT_REF: d85fa9dbdf6c36f47b7e96eba5c9df750ad3c963
+ WT_ENGINE: ${{ matrix.engine }}
CROSS_COMPILE: /usr/local/bin/arm-none-eabi-
ZEPHYR_TOOLCHAIN_VARIANT: cross-compile
# The wolfBoot configuration reserves the wolfTrust partition at the
@@ -273,11 +275,12 @@ jobs:
fi
wolfboot-wolftrust-m33mu-scenarios:
- name: ${{ matrix.scenario.name }}
+ name: ${{ matrix.scenario.name }} (${{ matrix.engine }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
+ engine: [native, hsm]
scenario:
- { key: positive, name: "Positive lifecycle" }
- { key: bothpsa, name: "Both-OS PSA parity (Zephyr + FreeRTOS)" }
@@ -305,9 +308,17 @@ jobs:
- { key: vnetneg, name: "Confined VNET isolation negatives" }
- { key: manifestneg, name: "Corrupted-manifest activation refused" }
- { key: gtzcneg, name: "NS MPU bypass cannot reach peer guest RAM" }
+ - { key: spbudgetneg, name: "SP restart-budget exhaustion escalates" }
+ exclude:
+ # hsmattackneg drives the raw wolfHSM client wire from the guest;
+ # the native engine does not link that wire, so the attack surface
+ # under test does not exist there.
+ - { engine: native, scenario: { key: hsmattackneg, name: "wolfHSM cross-namespace + NVM relay negatives" } }
timeout-minutes: 45
container:
image: ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.15
+ env:
+ WT_ENGINE: ${{ matrix.engine }}
steps:
- uses: actions/checkout@v4
diff --git a/docs/Architecture.md b/docs/Architecture.md
index 7bce0000..a006aba1 100644
--- a/docs/Architecture.md
+++ b/docs/Architecture.md
@@ -21,8 +21,8 @@ contain Armv8-M-specific types and assumptions.
| wolfBoot | Performs the BL2 secure-boot role, authenticates wolfTrust, passes the measured-boot handoff, and swaps authenticated update images. |
| wolfTrust | Configures isolation, validates the manifest and guest images, schedules guests and Secure Partitions, implements FF-M IPC, and manages lifecycle and recovery. |
| wolfPSA | Implements PSA Crypto entry points over wolfCrypt for the Non-secure reference guests. |
-| wolfCrypt | Supplies cryptographic implementations used by wolfPSA and Secure services. |
-| wolfHSM | Owns protected key operations and the persistent NVM backend. |
+| wolfCrypt | Supplies cryptographic implementations used by guest wolfPSA and Secure services. The native engine dispatches it directly. |
+| wolfHSM | Optional crypto engine providing the client/server key-management model and external-HSM integration path. Both engines use its NVM object-store subset. |
| wolfCOSE | Encodes and signs COSE_Sign1 attestation tokens. |
| wolfIP | Supplies the TCP/IP stack for the optional bare-metal reference guests; the Secure virtual Ethernet switch itself is implemented by wolfTrust. |
@@ -52,9 +52,9 @@ preserve the public manifest, service, IPC, and PSA API contracts. See
and memory policies; validates and binds the generated manifest; registers
the FF-M services; seeds each guest context; and performs an initial
signed-record, image-size, manifest-version, digest, and optional WRP check.
-3. wolfTrust consumes the measured-boot handoff, initializes wolfHSM and the
- persistent vault backends, and checks the persistent Secure-image and guest
- version floors.
+3. wolfTrust consumes the measured-boot handoff, initializes the selected
+ [crypto engine](Crypto-Engines.md) and shared persistent vault backend, and
+ checks the persistent Secure-image and guest version floors.
4. wolfTrust starts the scheduled Secure Partitions.
5. Immediately before an accepted guest's first dispatch, the monitor repeats
its signed-record, image-size, manifest-version, digest, and optional WRP
diff --git a/docs/Building.md b/docs/Building.md
index 331966ce..699ff479 100644
--- a/docs/Building.md
+++ b/docs/Building.md
@@ -31,6 +31,19 @@ reference is the mutable `main` branch, not a pinned workspace.
make
```
+The default uses the native crypto engine. Keep separate output directories
+when comparing or retaining both engine builds:
+
+```sh
+make secure-image WT_ENGINE=native BUILD_DIR=build-native
+make secure-image WT_ENGINE=hsm BUILD_DIR=build-hsm
+```
+
+`WT_ENGINE_HSM=0` and `WT_ENGINE_HSM=1` remain as legacy aliases for
+`native` and `hsm`, respectively. `WT_ENGINE` is the public selector for new
+builds. See [Crypto Engines](Crypto-Engines.md) for the behavior, key model,
+and measured footprint of each choice.
+
The default target builds:
| Output | Purpose |
@@ -99,6 +112,31 @@ make -C tests/firmware/zephyr-stm32h5 \
build-guest0-psa build-freertos-guest1
```
+`WT_ENGINE` must match the Secure image and every guest image. The guest build
+scripts default to `native` and pass the same selector through the Secure and
+guest builds:
+
+```sh
+WT_ENGINE=native make -C tests/firmware/zephyr-stm32h5 \
+ build-guest0-psa build-freertos-guest1
+WT_ENGINE=hsm make -C tests/firmware/zephyr-stm32h5 \
+ build-guest0-psa build-freertos-guest1
+```
+
+For a direct Zephyr configuration rather than the wrapper script:
+
+- native uses `CONFIG_WOLFTRUST_NATIVE_CLIENT=y` and
+ `CONFIG_WOLFTRUST_WOLFHSM_CLIENT=n`; and
+- hsm uses `CONFIG_WOLFTRUST_WOLFHSM_CLIENT=y` and leaves
+ `CONFIG_WOLFTRUST_NATIVE_CLIENT` disabled.
+
+The native Zephyr module links guest wolfCrypt plus
+`src/client/crypto_native_client.c`. The wolfHSM module instead links the
+wolfHSM client, crypto-callback glue, and
+`src/client/hsm_psa_transport.c`. The FreeRTOS
+`build_freertos_guest.sh` script makes the same source and preprocessor choice
+from its `WT_ENGINE` environment variable.
+
This produces:
- `tests/firmware/zephyr-stm32h5/build/guest0_psa/zephyr/zephyr.bin`
@@ -117,6 +155,19 @@ The same Makefile also provides:
| `run-tui` | Same pair with the M33MU TUI |
| `zephyr-freertos-uarts` | Zephyr and FreeRTOS PSA guests under M33MU |
+## Engine coverage in CI
+
+The cross-compile workflow links a Secure image with each engine. The M33MU
+lifecycle job crosses `guest: [zephyr, freertos]` with
+`engine: [native, hsm]`. The scenario job also adds the engine as a matrix
+dimension, and the label-selected pull-request workflow runs each requested
+scenario under both engines.
+
+The `hsmattackneg` scenario is intentionally hsm-only. It injects raw wolfHSM
+protocol packets and attacks a wolfHSM namespace and NVM relay surface that is
+not linked into the native engine. All other scenario rows run under both
+engines. See [Testing](Testing.md) for the commands and validation scope.
+
## Authenticated image assembly
`make` alone produces an unsigned flat Secure binary. The target
diff --git a/docs/Crypto-Engines.md b/docs/Crypto-Engines.md
new file mode 100644
index 00000000..9444dc91
--- /dev/null
+++ b/docs/Crypto-Engines.md
@@ -0,0 +1,298 @@
+# Crypto Engines
+
+wolfTrust provides two Secure crypto engines behind the same Platform Security
+Architecture (PSA) Firmware Framework for M (FF-M) service boundary. The native
+crypto engine is the default. The wolfHSM engine is an opt-in add-on for
+deployments that need the wolfHSM client/server key model or its external-HSM
+integration path.
+
+The engine choice does not change the actual Non-secure-to-Secure boundary.
+Both builds use the same five CMSE veneers, generated manifest, service IDs,
+SPM-owned caller identity, copied IOVEC rules, isolation bands, storage
+services, attestation service, firmware-update service, and Secure Partition
+recovery path. The STM32H563 manifest requests isolation profile 3 in both
+builds. That value is wolfTrust's validated policy profile, not proof of
+independent TF-M Level 3 code and data isolation.
+
+## At a glance
+
+| | Native crypto engine | wolfHSM engine |
+| --- | --- | --- |
+| Selector | `WT_ENGINE=native` (default) | `WT_ENGINE=hsm` |
+| Guest PSA Crypto | wolfPSA and wolfCrypt execute in the Non-secure guest; DRBG seed requests cross into the Secure vault | wolfPSA routes supported operations through the wolfHSM client and `SERVICE_HSM` to a per-guest Secure wolfHSM server |
+| Secure key service | Native request format dispatches wolfCrypt directly; explicitly vault-backed keys are NVM objects | wolfHSM request format dispatches the wolfHSM server and its keystore |
+| Guest volatile PSA keys | Held in the guest's Non-secure memory | Keys for supported offloaded operations are held by the Secure wolfHSM server |
+| Persistent Secure keys | Native vault objects marked `SENSITIVE` and `NONEXPORTABLE` | wolfHSM server-keystore objects with wolfHSM key policy |
+| Attestation IAK | Vault-backed P-256 key used directly by wolfCrypt | Committed wolfHSM server-keystore key |
+| External HSM path | Not provided by this engine | Available through the wolfHSM server model when a deployment configures a backend; the reference build uses software wolfCrypt |
+| Secure-image footprint | Lower | Adds the wolfHSM protocol, server, per-guest contexts, and per-guest server stacks |
+
+The native engine is not a mode in which every guest PSA key automatically
+moves into the Secure vault. In the reference guests, ordinary wolfPSA
+operations and volatile keys remain local to the Non-secure guest. The native
+wire is a separate explicit interface for vault-backed key operations. Secure
+services such as attestation use that vault backend directly.
+
+## One service seam
+
+`SERVICE_HSM` keeps its existing name and SID in both builds. The service
+copies one request into Secure memory, obtains the caller identity stamped by
+the SPM, and passes the opaque packet to the engine selected at build time:
+
+```text
+Non-secure guest
+ |
+ | psa_connect / psa_call / psa_close
+ v
+five WolfTrust_FFM_* CMSE veneers
+ |
+ v
+SPM: caller identity + manifest policy + copied IOVECs
+ |
+ v
+SERVICE_HSM protocol-opaque relay
+ |
+ +-- WT_ENGINE=native --> native request --> wolfCrypt + vault key backend
+ |
+ `-- WT_ENGINE=hsm ----> wolfHSM packet --> per-guest wolfHSM server
+```
+
+The binding is the `wt_hsm_relay_set_submit()` call in
+`src/spm_partitions.c`. It selects `wt_native_submit()` or
+`wt_hsm_relay_submit()`. `src/services/hsm_relay_service.c` does not interpret
+either protocol. It enforces the copied request and response bounds and passes
+the SPM-stamped client ID to the selected backend.
+
+## Native crypto engine
+
+The native engine links wolfCrypt and the shared flash-backed NVM object store,
+but not the wolfHSM server, communication layer, or message layer.
+
+The reference guest configuration behaves as follows:
+
+- wolfPSA and wolfCrypt execute locally in each Non-secure guest;
+- wolfCrypt DRBG seed material comes from the Secure vault RNG through one
+ `SERVICE_HSM` call;
+- ITS, Protected Storage, attestation, and firmware update continue to use
+ their normal Secure services; and
+- clients that need a vault-backed key can use the native request format
+ explicitly.
+
+Guest-created vault-backed P-256 and AES-256 key objects are indexed by the
+`SERVICE_HSM` partition identity, the SPM-stamped client identity, and a
+64-bit UID. They are stored with the wolfHSM NVM library's `SENSITIVE` and
+`NONEXPORTABLE` flags. The storage face refuses key-flagged objects, checked
+NVM reads reject non-exportable objects, and the native request format has no
+private-key export operation. Private-key computations run in the Secure
+key-vault domain and temporary plaintext key buffers are zeroized after use.
+The attestation IAK is a separate fixed vault object used only by the
+attestation path.
+
+### Native request format
+
+One request is a 24-byte `wt_crypto_wire_req_t` followed by an optional
+payload. One response is a 32-bit PSA status followed by an optional payload.
+Each complete request and response is bounded by the 384-byte
+`WT_HSM_RELAY_MSG_MAX` copied buffer.
+
+| Request field | Type | Meaning |
+| --- | --- | --- |
+| `uid` | `uint64_t` | Key UID for key operations |
+| `op` | `uint32_t` | Operation number |
+| `usage` | `uint32_t` | Key-usage bits, or requested length for `RANDOM` |
+| `key_type` | `uint32_t` | Native P-256 or AES-256 key encoding |
+| `reserved` | `uint32_t` | Reserved; must be zero. The Secure parser rejects a nonzero value with `PSA_ERROR_INVALID_ARGUMENT` |
+| payload | bytes | Imported key, digest, signature, plaintext, ciphertext, or hash input as required by the operation |
+
+The defined operations are key generate, import, public export, sign, verify,
+encrypt, decrypt, and destroy, plus random generation and SHA-256 hashing.
+Random responses are limited to 256 bytes per request. P-256 signatures use a
+fixed 64-byte `r || s` form and public keys use the 65-byte uncompressed X9.63
+form. AES-256 encrypt and decrypt use AES-GCM and return or consume
+`nonce || ciphertext || tag`.
+
+The structures are copied directly between the reference Cortex-M client and
+Secure image. This is a target-local ABI, not a versioned network protocol.
+The header and operation values are declared in
+`include/wolftrust/services/crypto_native.h`.
+
+## Shared NVM object store
+
+wolfHSM is not removed in the native engine. Its self-contained flash-backed
+object store is linked in **both** engines and owns every persistent object:
+the vault, Internal Trusted Storage, Protected Storage, the firmware-update
+staging metadata, the anti-rollback version floors, and the native engine's
+vault key objects. Only wolfHSM's server, communication, and message layers
+are dropped in native. The store's on-flash format is identical in both
+engines, so persistent storage objects survive a switch between engines.
+
+The Initial Attestation Key is the exception. The hsm engine keeps the IAK in
+the wolfHSM server keystore; the native engine keeps it as a vault key object.
+Neither engine migrates the other's, so switching engines on an
+already-provisioned device does not carry the attestation identity: a
+provisioning lifecycle mints a fresh IAK, and a SECURED device with no IAK for
+the running engine fails closed rather than adopting a new one. Choose the
+crypto engine at provisioning time and keep it fixed for the device's life.
+
+| Source | Role |
+| --- | --- |
+| `wh_nvm.c` | Object-store API: add, read, metadata, destroy, and the access-policy checks (`WRITE_ONCE`, `SENSITIVE`, `NONEXPORTABLE`) |
+| `wh_nvm_flash.c` | Log-structured object store implemented over a flash callback |
+| `wh_flash_unit.c` | Program-unit-aligned read, program, erase, and blank-check helpers under the store |
+| `wh_lock.c` | Serialization lock so the shared store is safe across the confined keystore partitions |
+| `wh_utils.c` | Endian, constant-time compare, and force-zero helpers the store depends on |
+| `wh_keyid.c` | Key-id namespace translation between client and server key identifiers |
+
+These files carry no server, communication, message, or wolfCrypt dependency,
+so linking them costs only the store itself. Reusing the proven store rather
+than reimplementing it keeps the on-flash format stable and avoids re-testing a
+storage rewrite; the flash-backed object store is not where either engine's
+size difference lives.
+
+## wolfHSM engine
+
+The wolfHSM engine links the wolfHSM client/server protocol and creates one
+Secure server context for each configured guest. Guest wolfPSA calls use
+wolfCrypt's crypto-callback path, the wolfHSM client serializes the request,
+and the request crosses the same `SERVICE_HSM` FF-M door used by the native
+engine.
+
+The relay derives guest `N` from SPM client ID `-(N + 1)` and forces wolfHSM
+server client ID `N + 1`. It rejects guest-facing wolfHSM NVM message groups,
+so a guest cannot use the crypto door to read vault, rollback, storage-counter,
+or attestation objects. The server keystore provides wolfHSM's key lifecycle,
+namespace, and non-exportable-key behavior.
+
+The reference engine runs wolfCrypt in the Secure image. Choosing
+`WT_ENGINE=hsm` does not by itself select an external device; it retains the
+wolfHSM server integration point for a deployment that supplies one.
+
+## Choosing an engine
+
+Use the native crypto engine when:
+
+- Secure flash or SRAM is constrained;
+- guest-local wolfPSA and wolfCrypt execution is acceptable;
+- the application only needs Secure entropy, the explicit vault-key
+ interface, and the other wolfTrust Secure services; or
+- the deployment does not need the wolfHSM client/server protocol.
+
+Choose the wolfHSM engine (`WT_ENGINE=hsm`) over the native engine for one of
+three reasons, in rough order of how often they apply:
+
+- **External hardware-HSM or secure-element offload.** This is the main reason
+ to enable it: the wolfHSM server can front an external device, so crypto and
+ keys are delegated off-core rather than run by on-chip wolfCrypt. The native
+ engine has no such path.
+- **The full wolfHSM server-keystore key-management model**, when a
+ deployment's tooling or provisioning flow already expects wolfHSM key
+ lifecycle, namespaces, and non-exportable-key semantics as the server
+ presents them.
+- **Backward compatibility** with existing Non-secure guest code built against
+ the wolfHSM client wire (`wh_Client_CryptoCb`), where reworking the guest to
+ the native request format is not worth it.
+
+If none of those apply, prefer the native engine: it is smaller, keeps keys
+non-exportable in the vault, and needs no wolfHSM server. The wolfHSM engine
+adds this key-management and offload model on top of the same isolation
+boundary. The native engine is not a weaker FF-M gateway or a reduced-isolation
+build.
+
+## Selecting the engine
+
+Build the Secure image with a fresh output directory for each engine:
+
+```sh
+make secure-image WT_ENGINE=native BUILD_DIR=build-native
+make secure-image WT_ENGINE=hsm BUILD_DIR=build-hsm
+```
+
+`WT_ENGINE=native` is the default, so an unset selector builds the native
+engine. The legacy selector remains accepted:
+
+| Legacy setting | Equivalent selector |
+| --- | --- |
+| `WT_ENGINE_HSM=0` | `WT_ENGINE=native` |
+| `WT_ENGINE_HSM=1` | `WT_ENGINE=hsm` |
+
+The Secure image and both guest images must use the same engine. See
+[Building](Building.md) for the Zephyr and FreeRTOS guest settings.
+
+## Measured Secure-image cost
+
+These Secure-image measurements were reproduced on 2026-09-18 from the source
+tree containing this page. The pinned dependency revisions and versions are
+listed in
+[TF-M Compatibility](TF-M-Compatibility.md). The builds ran on
+`wolf-prec5560` with `arm-none-eabi-gcc` 13.2.1, `-Os`, and the repository
+defaults other than the engine and output directory:
+
+```sh
+make BUILD_DIR=build_size_native WT_ENGINE=native secure-image
+make BUILD_DIR=build_size_hsm WT_ENGINE=hsm secure-image
+arm-none-eabi-size build_size_native/wolftrust.elf \
+ build_size_hsm/wolftrust.elf
+```
+
+Flash is `text + data`; static RAM is `data + bss`. Both images use the
+STM32H563 reference manifest with `isolation_profile` set to 3 and include two
+guests, ITS, Protected Storage, firmware update, vault services, and COSE
+attestation. The wolfHSM image also contains two per-guest wolfHSM tasklet
+stacks configured at 10 KiB each. wolfBoot and Non-secure guest images are not
+included.
+
+| Engine | `text` | `data` | `bss` | Flash | Static RAM |
+| --- | ---: | ---: | ---: | ---: | ---: |
+| Native | 86,140 bytes | 708 bytes | 26,545 bytes | 86,848 bytes | 27,253 bytes |
+| wolfHSM | 105,712 bytes | 720 bytes | 58,353 bytes | 106,432 bytes | 59,073 bytes |
+| wolfHSM overhead | 19,572 bytes | 12 bytes | 31,808 bytes | 19,584 bytes | 31,820 bytes |
+
+### Stack contribution
+
+With the default `WT_MAX_GUESTS=2`, the wolfHSM engine adds one server tasklet
+stack slot per guest:
+
+```text
+2 * (10,240-byte stack + 256-byte underflow guard) = 20,992 bytes
+```
+
+The stack payload is therefore 20,480 bytes and the guards add 512 bytes. The
+linked wolfHSM image reports `g_co_stack_slots` as `0x5200` bytes, matching the
+calculation. A one-guest build allocates one 10,496-byte slot. The remaining
+10,828 bytes of the 31,820-byte static-RAM difference are server, protocol,
+crypto, and per-guest context state.
+
+`WT_CO_STACK_SIZE` defaults to 10,240 bytes. The positive, Crypto-validation,
+and FF-M conformance M33MU workloads also passed with an 8 KiB configured stack
+and PSPLIM overflow detection enabled. That threshold test establishes a peak
+below 8 KiB for those workloads, so the 10 KiB default provides at least 2 KiB
+of allocation headroom over the tested peak. It is not a precise high-water
+measurement or a guarantee for different workloads. This is a fixed
+allocation, not a heap or a claim that every run consumes all 10 KiB.
+
+See [TF-M Compatibility](TF-M-Compatibility.md) for the complete local
+footprint comparison and methodology.
+
+## Build invariants
+
+Both engine builds enforce the following after linking:
+
+1. `mk/arch-armv8m.mk` runs `arm-none-eabi-nm` and writes the complete symbol
+ list to `BUILD_DIR/nsc-syms.txt`.
+2. The link check rejects any `__acle_se_*` symbol outside this exact `nm`
+ set: `__acle_se_WolfTrust_FFM_FrameworkVersion`,
+ `__acle_se_WolfTrust_FFM_ServiceVersion`,
+ `__acle_se_WolfTrust_FFM_Connect`, `__acle_se_WolfTrust_FFM_Call`, and
+ `__acle_se_WolfTrust_FFM_Close`.
+3. A separate count check requires exactly five `__acle_se_*` symbols, so a
+ missing veneer also fails the build.
+4. The same symbol list is searched for `malloc`, `free`, `calloc`,
+ `realloc`, `_sbrk`, `_malloc_r`, and `_free_r`; finding one fails the
+ zero-heap Secure-image build.
+
+The measured native and wolfHSM images contain exactly those five veneers and
+none of the guarded heap symbols. The source profile also defines
+`NO_WOLFSSL_MEMORY` and `WOLFSSL_NO_MALLOC`.
+
+See [Security Model](Security-Model.md) for the common boundary and
+[Testing](Testing.md) for the engine test matrix.
diff --git a/docs/Home.md b/docs/Home.md
index ff7d8818..6916ccc9 100644
--- a/docs/Home.md
+++ b/docs/Home.md
@@ -37,7 +37,7 @@ flowchart TB
subgraph WT[wolfTrust policy and service runtime]
SPM[Secure Partition Manager
policy, identity, IPC, scheduling, lifecycle, recovery]
subgraph SP[Secure services]
- CR["Cryptography and hardware
security module (HSM)"]
+ CR["Selected crypto engine
native or wolfHSM"]
ST["Internal Trusted Storage (ITS),
Protected Storage, and vault"]
AT[Initial Attestation]
FW[Firmware Update]
@@ -51,8 +51,8 @@ flowchart TB
end
subgraph LIBS[wolfSSL ecosystem components]
- PSA[wolfPSA
guest wolfCrypt and wolfHSM client]
- WC[Secure wolfCrypt and wolfHSM]
+ PSA[wolfPSA
guest wolfCrypt; optional wolfHSM client]
+ WC[Secure wolfCrypt
optional wolfHSM server]
COSE[wolfCOSE]
HAL[wolfHAL]
IP[wolfIP
optional bare-metal reference networking]
@@ -93,8 +93,8 @@ port's five CMSE gateway veneers.
| One mediated client boundary | Application domains reach services only through the client gateway supplied by the architecture port. The current Armv8-M image exports exactly five `WolfTrust_FFM_*` veneers. |
| Caller-bound IPC | wolfTrust derives the PSA client identity from the active application domain, copies vector descriptors, checks every range, and enforces manifest access policy. |
| Port-defined isolation | Each port declares and enforces the protection capabilities required by its manifest. The STM32H563 reference uses TrustZone, the Secure MPU, and GTZC MPCBB attribution; its exact limits are documented in [Security Model](Security-Model.md). |
-| PSA cryptography | Zephyr and FreeRTOS reference guests call wolfPSA's PSA Crypto API; operations are mediated to wolfCrypt and per-guest wolfHSM namespaces. |
-| Secure services | Connection-based services provide attestation, hardware security module (HSM) access, Internal Trusted Storage (ITS), Protected Storage, firmware update, and an optional Secure virtual Ethernet switch. The optional bare-metal networking guests run wolfIP outside the Secure image. |
+| PSA cryptography | Zephyr and FreeRTOS reference guests call wolfPSA's PSA Crypto API. The default native engine runs wolfCrypt in each guest and obtains DRBG seeds from the Secure vault; the optional wolfHSM engine routes supported operations to per-guest Secure server namespaces. |
+| Secure services | Connection-based services provide the selected crypto engine, attestation, Internal Trusted Storage (ITS), Protected Storage, firmware update, and an optional Secure virtual Ethernet switch. The optional bare-metal networking guests run wolfIP outside the Secure image. |
| Fault containment | A guest fault either restarts the guest within policy limits or leaves it quarantined. A Secure Partition fault releases synchronization state before failing affected calls. Restart paths scrub declared private writable memory before rearming; forbidden, exhausted, or failed recovery escalates to the port's fail-closed path. |
| Static Secure memory | The Secure image is built with `WOLFSSL_NO_MALLOC` and `NO_WOLFSSL_MEMORY`; service buffers, stacks, and state are statically allocated. |
@@ -104,6 +104,7 @@ port's five CMSE gateway veneers.
| --- | --- |
| [Getting Started](Getting-Started.md) | Prerequisites, checkout, first builds, emulator use, and hardware entry points |
| [Architecture](Architecture.md) | Boot flow, isolation layers, FF-M IPC, services, and scheduling |
+| [Crypto Engines](Crypto-Engines.md) | Native and wolfHSM engine behavior, selection, key models, and measured cost |
| [Security Model](Security-Model.md) | Trust boundaries and enforced security properties |
| [Threat Model](Threat-Model.md) | Protected assets, attacker capabilities, controls, and residual risks |
| [API Reference](API-Reference.md) | PSA client, service, storage, update, lifecycle, attestation, and gateway APIs |
diff --git a/docs/Macros.md b/docs/Macros.md
index 85fe15ee..51e9df5b 100644
--- a/docs/Macros.md
+++ b/docs/Macros.md
@@ -12,6 +12,7 @@ selected values into C preprocessor defines. Defaults below come from
| `TARGET` | Target build selector; default `stm32h563`. | Must match an `mk/target-.mk` fragment; the root Makefile includes it, the architecture fragment, and `mk/common.mk`. |
| `TOOLPREFIX` | Cross-tool prefix; default `arm-none-eabi-`. | The prefixed GCC, objcopy, nm, and size tools must be available. |
| `BUILD_DIR` | Secure build output directory; default `build`. | Must be writable. |
+| `WT_ENGINE` | Secure crypto engine: `native` (default) dispatches wolfCrypt directly behind the SERVICE_HSM door with explicitly vault-backed keys stored as `SENSITIVE` and `NONEXPORTABLE` NVM objects; `hsm` links the wolfHSM server as a key-management add-on (server-keystore semantics and an external-HSM offload path). Legacy `WT_ENGINE_HSM=0/1` maps onto the selector. | Both engines share the identical FF-M surface (5 veneers, SIDs, manifest, and L3 bands) and run every applicable CI scenario. Guest builds must use the same engine as the Secure image. See [Crypto Engines](Crypto-Engines.md). |
## Core target configuration
@@ -19,7 +20,7 @@ selected values into C preprocessor defines. Defaults below come from
| --- | --- | --- |
| `WT_MAX_GUESTS` | Selects one or two compiled STM32H563 guest contexts; default `2`. | The current port supports only `1` or `2`. Larger values require extending the partition tables and matching manifest, linker, emulator, flash, and measurement configuration. |
| `WT_TIMESLICE_MS` | Guest scheduler interval in milliseconds; default `2`. | Must be nonzero and supported by the target timer. |
-| `WT_CO_STACK_SIZE` | Default fixed coroutine stack size in bytes, including each per-guest wolfHSM tasklet; default `24576`. Manifest-sized Secure Partition stacks use their declared sizes instead. | Size from measured stack high-water marks and keep at least the scheduler minimum. |
+| `WT_CO_STACK_SIZE` | Default fixed coroutine stack size in bytes, including each per-guest wolfHSM server tasklet in the hsm engine; default `10240` (measured: the deep M33MU workloads pass at 8K with PSPLIM overflow detection armed, so 10K carries at least 2K margin). With two guests, the hsm-only server slots total 20,480 stack bytes plus 512 guard bytes. Manifest-sized Secure Partition stacks use their declared sizes instead. | Size from measured stack high-water marks and keep at least the scheduler minimum. |
| `WT_SHARED_UART` | Reference guest UART selection; default `3`. | Guest and Secure builds must use a consistent value. |
| `WT_GUEST_CORE_CLOCK_HZ` | Guest core-clock value; default `240000000`. | Must match the configured target clock. |
| `WT_GUEST_UART_CLOCK_HZ` | Guest UART-clock value; default `120000000`. | Must match the selected UART clock source. |
@@ -29,7 +30,7 @@ selected values into C preprocessor defines. Defaults below come from
| Define | Description | Requirement |
| --- | --- | --- |
| `WT_GUEST_FLASH_WRP` | When `1`, verify full STM32 guest-window WRP coverage before launch; default `0`. | Set to `1` for the hardened STM32H563 image and provision WRP after flashing. M33MU does not model WRP. |
-| `WT_ENGINE_HSM` | Value passed to the wolfCrypt engine and Secure HSM integration; default `1`. | Keep `1` for the reference service configuration. |
+| `WT_ENGINE_HSM` | Legacy engine selector; unset by default. `0` maps to `WT_ENGINE=native` and `1` maps to `WT_ENGINE=hsm` when the public selector is not supplied. The build also derives this internal value from `WT_ENGINE`. | Prefer `WT_ENGINE` for new builds and do not supply conflicting selectors. The guest and Secure image must select the same engine. |
| `WT_ATTEST_COSE` | Must remain `1` in the current STM32H563 reference build; default `1`. The `0` configuration does not compile because the reset path still references attestation-gated handoff variables. | Requires the wolfCOSE submodule and the configured attestation key backend. |
| `WT_WOLFCRYPT_SP_ASM` | Enable wolfCrypt SP Cortex-M assembly; default `1`. | Requires compatible Armv8-M assembly sources and toolchain. |
| `WT_WOLFCRYPT_ARMASM` | Enable additional wolfCrypt Thumb-2 assembly; default `1`. | Requires a compatible GNU Arm toolchain. |
diff --git a/docs/Porting.md b/docs/Porting.md
index f9f81178..8badef87 100644
--- a/docs/Porting.md
+++ b/docs/Porting.md
@@ -110,9 +110,11 @@ const void* wt_hsm_flash_config(void);
int wt_hsm_flash_format(void);
```
-The implementation must preserve the wolfHSM flash-log semantics, distinguish
-foreign or corrupt media, honor checked object flags, and erase only the
-dedicated vault region when lifecycle policy allows reformat.
+Both crypto engines use this object store. The implementation must preserve
+the wolfHSM NVM flash-log semantics, distinguish foreign or corrupt media,
+honor checked object flags, and erase only the dedicated vault region when
+lifecycle policy allows reformat. See [Crypto Engines](Crypto-Engines.md) for
+the engine boundary above the common store.
### Entropy
diff --git a/docs/Security-Model.md b/docs/Security-Model.md
index e77045f7..f733124a 100644
--- a/docs/Security-Model.md
+++ b/docs/Security-Model.md
@@ -14,7 +14,8 @@ The reference trusted computing base includes:
- wolfBoot and its verification key
- the wolfTrust Secure image and generated manifest
-- wolfCrypt, wolfHSM, wolfHAL, and wolfCOSE code linked into that image
+- wolfCrypt, wolfHAL, wolfCOSE, the shared wolfHSM NVM components, and the
+ full wolfHSM server when the optional hsm engine is selected
- Armv8-M exception, TrustZone, and MPU behavior
- STM32H563 GTZC and flash option-byte configuration
- privileged wolfTrust SVC and fault handlers
@@ -34,7 +35,7 @@ trusted to access arbitrary SPM or peer-partition writable state.
| Guest to service | Manifest service policy, connection ownership, generated handles, and SPM-stamped client identity |
| Secure Partition writable state | Unprivileged Secure threads and a per-partition Secure MPU table |
| Secure Partition to hardware backend | Operation-specific SVC gates pinned to the expected partition identity |
-| Persistent objects | Vault ownership tuple, checked wolfHSM NVM operations, and key/storage type separation |
+| Persistent objects | Vault ownership tuple, checked NVM operations, engine-specific non-exportable key policy, and key/storage type separation |
## The only Non-secure entry path
@@ -111,21 +112,37 @@ operations are available only through narrow SVC operations that check the
originating partition.
This is writable-state isolation inside one linked image. Shared executable
-text is not per-partition code isolation, and the HSM, vault, and attestation
+text is not per-partition code isolation, and the crypto, vault, and attestation
domains share the keystore data band required by their backends.
## Per-guest cryptographic keys
-The HSM service receives one copied wolfHSM request packet through FF-M IPC.
-The SPM-stamped negative client ID selects guest `N`, and the relay
-forces wolfHSM server client ID `N + 1` before processing the packet.
-A client-provided communication ID therefore cannot select another guest's
-key namespace.
-
-The guest-facing HSM relay rejects wolfHSM NVM message groups. Guests can use
-the intended cryptographic protocol but cannot directly reach vault objects,
-the firmware-version floor, the Protected Storage counter table, or the
-attestation key.
+The SERVICE_HSM door carries the selected crypto engine's wire (`WT_ENGINE`):
+the native engine (default) dispatches wolfCrypt directly, with explicitly
+vault-backed keys stored as `SENSITIVE` and `NONEXPORTABLE` NVM objects whose
+private material never leaves the Secure key-vault domain; the hsm engine
+relays wolfHSM server packets. The FF-M surface, SIDs, and isolation bands are
+identical in both engines.
+
+In the hsm engine the service receives one copied wolfHSM request packet
+through FF-M IPC. The SPM-stamped negative client ID selects guest `N`, and
+the relay forces wolfHSM server client ID `N + 1` before processing the
+packet. In the native engine the same SPM-stamped identity becomes the vault
+key namespace's delegated sub-owner. A client-provided communication ID
+therefore cannot select another guest's key namespace in either engine.
+
+The native reference guests normally run wolfPSA and wolfCrypt locally. Their
+ordinary volatile PSA keys therefore live in Non-secure guest RAM; only DRBG
+seed requests and explicit native-wire vault-key requests cross the Secure
+boundary. In the hsm engine, supported guest PSA operations and their private
+key state are routed to the Secure wolfHSM server.
+
+The hsm engine's guest-facing relay rejects wolfHSM NVM message groups. The
+native request format exposes no general NVM operation. Guests can use the
+intended engine protocol but cannot directly reach storage objects, the
+firmware-version floor, the Protected Storage counter table, or the
+attestation key. See [Crypto Engines](Crypto-Engines.md) for the complete
+selection and key-model comparison.
## Storage protection
@@ -144,7 +161,7 @@ The current `psa_ps_get_info()` implementation echoes those requested hint
flags instead of reporting the stronger protection actually applied.
Sealing uses AES-256-GCM with:
-- a device-local non-exportable key stored in wolfHSM NVM;
+- a device-local non-exportable key stored in the shared Secure NVM store;
- the object label as authenticated data;
- a 12-byte nonce derived from a persisted per-write counter; and
- a 16-byte authentication tag.
@@ -167,9 +184,10 @@ The vault requires wolfHSM's
rejects incompatible backends at initialization. The flash HAL may be supplied
by the target port or the host RAM simulator.
-Vault storage rejects its reserved key-object type. Guest cryptographic keys
-instead use the separate wolfHSM keystore behind `SERVICE_HSM`, where the
-relay binds operations to the caller's namespace.
+Vault storage rejects its reserved key-object type. In the native engine,
+explicit key requests use the vault's separate key face. In the hsm engine,
+guest cryptographic keys instead use the wolfHSM server keystore behind
+`SERVICE_HSM`. Both paths bind operations to the SPM-stamped caller namespace.
## Authenticated guest launch
@@ -199,9 +217,10 @@ the option bytes as described in [STM32H5 Guide](STM32H5-Guide.md).
## Static memory
The Secure wolfCrypt settings define both `NO_WOLFSSL_MEMORY` and
-`WOLFSSL_NO_MALLOC`. Stacks, IPC transfers, service state, HSM
+`WOLFSSL_NO_MALLOC`. Stacks, IPC transfers, service state, selected-engine
contexts, and cryptographic scratch space use fixed storage. Oversized requests
-fail instead of allocating.
+fail instead of allocating. The link also rejects allocator symbols in both
+engine images.
## Source anchors
@@ -209,6 +228,8 @@ fail instead of allocating.
- [Secure Partition scheduler and SVC gates](../src/arch/armv8m/spm_svc.c)
- [Guest verification](../src/guest_verify.c)
- [HSM relay binding](../src/services/wolfhsm/wt_hsm.c)
+- [Native crypto dispatch](../src/services/native/crypto_native.c)
+- [Native vault key backend](../src/services/native/keyvault.c)
- [Vault storage](../src/services/wolfhsm/wt_hsm_vault.c)
See [Threat Model](Threat-Model.md) for assumptions and residual risks.
diff --git a/docs/Services.md b/docs/Services.md
index a5d93b9b..e4943dd0 100644
--- a/docs/Services.md
+++ b/docs/Services.md
@@ -13,7 +13,7 @@ manifest with a seventh service.
| `SERVICE_ITS` | `0x1003` | Yes | PSA Internal Trusted Storage |
| `SERVICE_PS` | `0x1004` | Yes | PSA Protected Storage |
| `SERVICE_FWU` | `0x1005` | Yes | PSA Firmware Update staging for wolfBoot |
-| `SERVICE_HSM` | `0x1006` | Yes | Copied wolfHSM cryptographic request relay |
+| `SERVICE_HSM` | `0x1006` | Yes | Copied request relay for the selected native or wolfHSM crypto engine |
| `SERVICE_VNET` | `0x1007` | Yes, optional | Secure virtual Ethernet switch implemented by wolfTrust for optional Non-secure wolfIP guests |
All service versions are `1`. ITS and Protected Storage declare a
@@ -29,25 +29,32 @@ replies. Each thread receives shared Secure text plus its own stack and
manifest-declared data. Privileged hardware operations are performed by narrow
SVC gates that verify which partition issued the request.
-## HSM and PSA Crypto
+## Crypto engines and PSA Crypto
-Zephyr and FreeRTOS reference guests expose wolfPSA's PSA Crypto API. wolfPSA
-uses a wolfHSM client transport that places one complete wire packet in an
-FF-M `psa_call` to `SERVICE_HSM`.
+Zephyr and FreeRTOS reference guests expose wolfPSA's PSA Crypto API. The
+`WT_ENGINE` build selector chooses how those guests and `SERVICE_HSM` are
+wired. Both engines use one input and one output vector, bounded to 384 bytes,
+through the same copied FF-M call.
-The service:
+With the default native engine, wolfPSA and wolfCrypt execute in the
+Non-secure guest. DRBG seed requests use the native client to reach the Secure
+vault RNG. The native wire also exposes explicit vault-backed P-256 and
+AES-256 key operations, random generation, and SHA-256. Vault key objects are
+namespaced by the SPM-stamped client ID and stored `SENSITIVE` and
+`NONEXPORTABLE`.
-- accepts one input packet and one output packet, each bounded to 512 bytes;
-- uses the SPM-stamped PSA client ID rather than an ID in the packet;
-- maps guest `N` to its dedicated wolfHSM server and forces server
- client ID `N + 1`;
-- rejects all guest-facing wolfHSM NVM message groups; and
-- returns the copied response through the original output vector.
+With the wolfHSM engine, wolfPSA uses a wolfHSM client transport that places
+one complete wolfHSM packet in the FF-M call. The relay maps guest `N` to its
+dedicated server, forces server client ID `N + 1`, and rejects guest-facing
+wolfHSM NVM message groups. The reference build executes the server with
+software wolfCrypt; deployments can configure wolfHSM's external-HSM path.
The reference cryptographic profile is controlled by each guest's wolfCrypt
`user_settings.h`. Consult the vendored
`lib/wolfPSA/wolfpsa/psa/crypto.h` and the active guest configuration
-before assuming a particular algorithm is available.
+before assuming a particular algorithm is available. See
+[Crypto Engines](Crypto-Engines.md) for the protocol, key-protection, and
+footprint differences.
## Vault
@@ -62,11 +69,12 @@ object is indexed by:
This lets ITS and Protected Storage forward a guest's stamped client ID without
allowing either front end to escape its own vault namespace.
-The vault uses checked wolfHSM NVM operations for write-once and object
-metadata policy. Storage calls cannot create, read, or overwrite the reserved
-key-object type. The STM32H563 integration does not install the vault's
-optional key-operation backend; guest cryptographic keys are managed through
-`SERVICE_HSM` instead.
+The vault uses the wolfHSM NVM object-store library and checked operations for
+write-once and object-metadata policy in both engines. Storage calls cannot
+create, read, or overwrite the reserved key-object type. The native engine
+installs the vault key backend used by its explicit key wire. The wolfHSM
+engine instead manages guest cryptographic keys in its server keystore behind
+`SERVICE_HSM`.
## Internal Trusted Storage
@@ -103,9 +111,10 @@ limited to 512 bytes. The underlying Secure vault still uses a 1024-byte object
buffer and caps one copied vault response at 1000 bytes.
Every stored object is AES-256-GCM sealed in the vault under a device-local
-non-exportable key. A persisted write counter supplies the nonce and the object
-label is authenticated data. Confidentiality and replay-protection hint flags do
-not weaken storage: Protected Storage still seals and counter-binds the object.
+non-exportable key in the shared NVM store. A persisted write counter supplies
+the nonce and the object label is authenticated data. Confidentiality and
+replay-protection hint flags do not weaken storage: Protected Storage still
+seals and counter-binds the object.
The current `psa_ps_get_info()` behavior echoes the requested flags instead of
reporting the stronger protection actually applied, which differs from the PSA
Secure Storage 1.0 recommendation.
@@ -115,7 +124,8 @@ Secure Storage 1.0 recommendation.
`SERVICE_ATTEST` implements the token and exact-size API operations from PSA
Initial Attestation 1.0. It accepts a 32-, 48-, or 64-byte challenge and returns
a tagged COSE_Sign1 token signed with ES256 through an external signer backed
-by the protected attestation key. wolfCOSE performs the COSE encoding.
+by the selected engine's protected attestation key. wolfCOSE performs the COSE
+encoding.
The emitted token is derived from [RFC 9783](https://www.rfc-editor.org/rfc/rfc9783.html)
but is not conformant with its advertised
diff --git a/docs/TF-M-Compatibility.md b/docs/TF-M-Compatibility.md
index a40099e5..a2f87c33 100644
--- a/docs/TF-M-Compatibility.md
+++ b/docs/TF-M-Compatibility.md
@@ -9,12 +9,104 @@ This register describes the code in the repository. It is not a certification
statement, and a declaration in a vendored header does not mean every optional
algorithm or feature is enabled in every build.
+## Measured Secure-image footprint
+
+The following table records local builds, not published reference figures.
+The wolfTrust rows use the STM32H563 reference manifest with
+`isolation_profile` set to 3 and include Internal Trusted Storage, Protected
+Storage, Firmware Update, vault services, and COSE attestation. The TF-M rows
+are the standard Small, Medium, and Large profiles built for AN521.
+
+| Secure image | Profile and enabled services | Flash | Static RAM |
+| --- | --- | ---: | ---: |
+| wolfTrust native | wolfTrust profile 3; Crypto, Internal Trusted Storage, Protected Storage, Firmware Update, vault, COSE attestation | 86,848 bytes | 27,253 bytes |
+| wolfTrust wolfHSM | Same services plus the wolfHSM server | 106,432 bytes | 59,073 bytes |
+| TF-M Small | Level 1; Crypto, Internal Trusted Storage, Initial Attestation; Protected Storage and Firmware Update off | 50,968 bytes | 14,296 bytes |
+| TF-M Medium | Level 2; Crypto, Internal Trusted Storage, Protected Storage, Initial Attestation; Firmware Update off | 67,332 bytes | 42,468 bytes |
+| TF-M Large | Level 3; Crypto, Internal Trusted Storage, Protected Storage, Initial Attestation; Firmware Update off | 115,460 bytes | 45,756 bytes |
+
+In these builds, native wolfTrust uses about 25% less flash than TF-M Large
+and less static RAM than TF-M Medium while also including firmware update.
+The wolfHSM engine remains smaller in flash than TF-M Large but uses more
+static RAM because it adds per-guest server state and stacks. These results do
+not imply that the projects, platforms, or enabled feature sets are identical.
+
+### Methodology
+
+All five images were built on `wolf-prec5560` with
+`arm-none-eabi-gcc (15:13.2.rel1-2) 13.2.1 20231009`. Footprint was read from
+the linked Secure ELF with `arm-none-eabi-size` and calculated as:
+
+```text
+Flash = text + data
+Static RAM = data + bss
+```
+
+The wolfTrust rows were reproduced on 2026-09-18 from the source tree
+containing this page with the exact dependency set below.
+
+| Component | Pinned revision | Git description |
+| --- | --- | --- |
+| wolfCOSE | `f907071b10127f3ae2dd7719749a91b039ff04a1` | `v2.0.0` |
+| wolfHSM | `a0323156606282448f00473a3fcb7aaa69361921` | `wolfHSM-v1.4.0-171-ga032315` |
+| wolfIP | `146de4b6362c3a076787e27332f50daa0a445cf5` | `v1.0-91-g146de4b` |
+| wolfPSA | `1b9ec29706bc63f785682ad688350195a33b22e8` | `v5.9.1-129-g1b9ec29` |
+| wolfSSL | `22e505bcfad8ce21067ee4232128728543767a95` | `v5.9.1-stable-1088-g22e505bcf` |
+| wolfHAL | `2bc2938b0bbcc977177153a7f38393710702bf70` | No reachable tag |
+
+The wolfTrust images used `-Os` and the following commands; all other build
+variables retained their repository defaults:
+
+```sh
+git submodule update --init --recursive
+make BUILD_DIR=build_size_native WT_ENGINE=native secure-image
+make BUILD_DIR=build_size_hsm WT_ENGINE=hsm secure-image
+arm-none-eabi-size build_size_native/wolftrust.elf \
+ build_size_hsm/wolftrust.elf
+```
+
+The measured wolfTrust files were the two `wolftrust.elf` outputs. Their raw
+`text`, `data`, and `bss` values are recorded in
+[Crypto Engines](Crypto-Engines.md).
+
+The TF-M source was the `TF-Mv2.1.1-LTS` tag at commit
+`02bf279913439a07082dd581df033f370a8fbb92`. The following commands reproduce
+the AN521 GNU Arm builds with BL2 enabled and no regression tests:
+
+```sh
+for profile in small medium large; do
+ cmake -S . -B "build_${profile}" \
+ -DTFM_PLATFORM=arm/mps2/an521 \
+ -DTFM_TOOLCHAIN_FILE=toolchain_GNUARM.cmake \
+ -DTFM_PROFILE="profile_${profile}" \
+ -DCMAKE_BUILD_TYPE=MinSizeRel \
+ -DBL2=ON
+ cmake --build "build_${profile}" --parallel
+done
+arm-none-eabi-size build_small/bin/tfm_s.elf \
+ build_medium/bin/tfm_s.elf \
+ build_large/bin/tfm_s.elf
+```
+
+The measured TF-M file in each case was `tfm_s.elf`.
+
+Only the Secure runtime ELF is counted. wolfBoot and Non-secure wolfTrust
+guests are excluded; TF-M BL2 and its Non-secure application are likewise
+excluded. Although the TF-M configurations had `BL2=ON`, the separate BL2
+image is not part of `tfm_s.elf` and therefore is not in the table.
+
+The TF-M builds target AN521 while wolfTrust targets STM32H563, and their
+profiles do not enable the same services. Treat the table as a reproducible
+local build comparison, not a platform-normalized benchmark. See
+[Crypto Engines](Crypto-Engines.md) for the measured cost within wolfTrust,
+where the platform and feature set are held constant.
+
## Compatibility register
| API or behavior | Version | Status | Repository evidence |
| --- | --- | --- | --- |
| FF-M client API | 1.0 with the scoped deviations below | Connection-based IPC is implemented | `include/psa/client.h` and `src/client/psa_ffm_client.c` |
-| Secure Partition IPC API | FF-M 1.0 plus a wolfTrust-specific backport of `psa_irq_enable()` from Arm's FF-M 1.1 Extension Beta, Issue 0 | Supported for scheduled IPC partitions; wolfTrust still reports framework version `0x0100` and does not accept 1.1 manifests | `include/psa/service.h`, `include/psa/client.h`, and `src/arch/armv8m/spm_sp_api.c` |
+| Secure Partition IPC API | FF-M 1.0 plus a wolfTrust-specific backport of `psa_irq_enable()` from Arm's FF-M 1.1 Extension Beta, Issue 0 | Supported for scheduled IPC partitions; wolfTrust still reports framework version `0x0100` and does not accept 1.1 manifests | `include/psa/service.h`, `include/psa/client.h`, and `src/arch/common/spm_sp_api.c` |
| Framework and service discovery | 1.0 | Supported | `psa_framework_version` and `psa_version` |
| Copied input and output vectors | FF-M 1.0 | Supported, with at most four vectors total and a 1024-byte aggregate budget across input bytes and declared output capacity | `include/wolftrust/ffm.h` and `src/ffm.c` |
| Manifest validation | wolfTrust format 1 | Supported for immutable generated C data | `tools/manifest/generate.py`, `src/manifest.c`, and `port/stm32h563/manifest.json` |
@@ -23,7 +115,7 @@ algorithm or feature is enabled in every build.
| Protected Storage | 1.0 | Core set/get/get-info/remove subset; optional create/set-extended absent and the `WRITE_ONCE` lifecycle deviation below applies | `include/psa/protected_storage.h` and `src/services/storage_service.c` |
| Initial Attestation | 1.0 API subset with a nonconformant RFC 9783-derived token | Token and exact-size operations are supported, but the advertised TF-M profile has the claim-semantic deviations below | `lib/wolfPSA/wolfpsa/psa/initial_attestation.h` and `src/services/initial_attestation.c` |
| Firmware Update | 1.0 subset | Single-component staging and authenticated reboot supported, with the alignment and status deviations below | `include/psa/update.h` and `src/services/fwu_service.c` |
-| RoT lifecycle query | FF-M 1.0 | Secure Partition only; there is no Non-secure adapter or veneer | `include/psa/lifecycle.h` and `src/arch/armv8m/spm_sp_api.c` |
+| RoT lifecycle query | FF-M 1.0 | Secure Partition only; there is no Non-secure adapter or veneer | `include/psa/lifecycle.h` and `src/arch/common/spm_sp_api.c` |
| Secure Partition signals and IRQ APIs | FF-M 1.0 plus one wolfTrust-specific beta-extension backport | The 1.0 signal APIs and `psa_eoi` are supported; only `psa_irq_enable()` is backported from the FF-M 1.1 Extension Beta, Issue 0, while `psa_irq_status_t`, `psa_irq_is_enabled`, `psa_irq_disable`, and `psa_irq_restore` are absent | `include/psa/service.h` and the Armv8-M SVC implementation |
| Guest identity | FF-M convention | Non-secure guest `N` is client `-(N + 1)` | `src/arch/armv8m/ffm_nsc.c` |
@@ -40,7 +132,7 @@ algorithm or feature is enabled in every build.
| Abnormal Non-secure guest termination reclaims the guest's connections without delivering `PSA_IPC_DISCONNECT` to the affected services. | Scoped deviation | Fault-handler cleanup cannot dispatch a Secure service inline without re-entering the scheduler. Shipped services do not use `psa_set_rhandle()` for per-connection cleanup, but a ported service that depends on disconnect cleanup must account for this behavior. |
| PSA Crypto mechanisms are build-selected. | Standard profile behavior | The wolfPSA 1.4 header is present, while each guest's wolfCrypt settings determine available keys and algorithms. |
| Protected Storage does not implement create or set-extended. | Scoped | `psa_ps_get_support()` returns zero and both optional operations return `PSA_ERROR_NOT_SUPPORTED`. |
-| Protected Storage always applies confidentiality and replay protection even when `NO_CONFIDENTIALITY` or `NO_REPLAY_PROTECTION` is requested. | Known metadata deviation | Objects remain sealed and counter-bound, but `psa_ps_get_info()` echoes the requested hint flags instead of reporting the stronger protection actually applied, which differs from the PSA Secure Storage 1.0 recommendation. |
+| Protected Storage always applies confidentiality and replay protection even when `NO_CONFIDENTIALITY` or `NO_REPLAY_PROTECTION` is requested. | Known metadata deviation | Objects remain sealed and counter-bound, but `psa_ps_get_info()` echoes the requested hint flags instead of reporting the stronger protection actually applied, which differs from the PSA Secure Storage 1.0 requirement. |
| ITS and Protected Storage always enforce `PSA_STORAGE_FLAG_WRITE_ONCE`. | Known lifecycle deviation | The request path does not receive lifecycle state and always rejects modification or removal. PSA Secure Storage 1.0 requires the flag not to be enforced during `PSA_ROT_PROVISIONING`. |
| Initial Attestation's public header omits `PSA_INITIAL_ATTEST_MAX_TOKEN_SIZE`. | Known header deviation | The service limit is 640 bytes, but callers cannot obtain that maximum from the public PSA header. |
| A non-NULL attestation token buffer with zero capacity returns `PSA_ERROR_INVALID_ARGUMENT`. | Known status deviation | PSA Initial Attestation 1.0 specifies `PSA_ERROR_BUFFER_TOO_SMALL` for an undersized token buffer. Nonzero undersized buffers return `PSA_ERROR_BUFFER_TOO_SMALL`. |
@@ -64,7 +156,7 @@ guest RAM, and unprivileged Secure threads use per-partition Secure MPU regions.
The guest Non-secure MPU and interrupt masks are scheduling policy because a
privileged guest can reprogram them.
-The current single-image layout still shares Secure executable text, and HSM,
+The current single-image layout still shares Secure executable text, and crypto,
vault, and attestation share a keystore data band. Treat the profile field as a
requested and validated wolfTrust policy level, not by itself as proof of
independent TF-M isolation certification. [Security Model](Security-Model.md) describes the
@@ -75,9 +167,12 @@ actual boundary.
1. Keep application calls on standard PSA headers where wolfTrust provides the
corresponding Non-secure adapter: FF-M client, Crypto, ITS, Protected
Storage, and Firmware Update. The lifecycle function is Secure-Partition-only.
-2. Link `src/client/psa_ffm_client.c` and
- `build/secure_cmse_implib.o`, then add the adapter required by each API:
- wolfPSA plus `src/client/hsm_psa_transport.c` for Crypto,
+2. Link `src/client/psa_ffm_client.c` and the generated
+ `secure_cmse_implib.o` from the selected Secure-image `BUILD_DIR`, then add
+ the adapter required by each API:
+ wolfPSA plus `src/client/crypto_native_client.c` for the native Crypto
+ configuration, or wolfPSA, the wolfHSM client, and
+ `src/client/hsm_psa_transport.c` for the hsm configuration;
`src/client/psa_storage_client.c` for ITS and Protected Storage,
`src/client/psa_fwu_client.c` for Firmware Update, and
`src/client/vnet_psa_transport.c` for optional VNET.
@@ -89,8 +184,10 @@ actual boundary.
4. Include the `psa_manifest/sid.h` generated for the selected target
and manifest.
5. Check data-size assumptions against the copied IPC and service limits.
- Stream update images in blocks no larger than
- `PSA_FWU_MAX_WRITE_SIZE`.
+ Stream update images in blocks no larger than `PSA_FWU_MAX_WRITE_SIZE`.
+ For wolfTrust, both the image offset and block size must be aligned to
+ `1 << PSA_FWU_LOG2_WRITE_ALIGN`, including the final block. This is stricter
+ than PSA Firmware Update 1.0, which permits padding an unaligned final block.
6. Check optional APIs before use. In particular, treat Protected Storage
create/set-extended and Firmware Update accept as unsupported.
7. Express Secure services, dependencies, memory, interrupts, restart policy,
diff --git a/docs/Testing.md b/docs/Testing.md
index f3e5e05c..831fa144 100644
--- a/docs/Testing.md
+++ b/docs/Testing.md
@@ -28,10 +28,10 @@ make -s -C tests/host print-suites
Current suites cover domain and manifest validation, lifecycle, guest
verification, rollback decisions, IPC and FF-M behavior, SPM policy, gateway
-vectors, Secure Partition layout and recovery, HSM relay and key isolation,
-vault and storage services, attestation and COSE integration, firmware update,
-runtime remeasurement, VNET, public PSA headers, boot-handoff record
-consumption, and negative paths.
+vectors, Secure Partition layout and recovery, crypto-engine relay and key
+isolation, vault and storage services, attestation and COSE integration,
+firmware update, runtime remeasurement, VNET, public PSA headers, boot-handoff
+record consumption, and negative paths.
The attestation IAK suite runs wolfHSM NVM with both the default 8-byte and
STM32H5 16-byte flash programming units.
@@ -71,6 +71,14 @@ The baseline target command is:
make test-target
```
+The engine defaults to `native`. Set `WT_ENGINE` to exercise the same target
+path with either backend:
+
+```sh
+WT_ENGINE=native make test-target
+WT_ENGINE=hsm make test-target
+```
+
It runs the positive lifecycle, guest restart, cross-domain Secure fault, and
FF-M conformance scenarios. Detection accepts `m33mu` on
`PATH` or a path in `M33MU`. If the emulator is unavailable,
@@ -79,7 +87,8 @@ the target reports a skip rather than a pass.
Additional focused runs use:
```sh
-tests/target/run_m33mu_scenario.sh positive
+WT_ENGINE=native tests/target/run_m33mu_scenario.sh positive
+WT_ENGINE=hsm tests/target/run_m33mu_scenario.sh positive
```
The runner's usage output is the authoritative scenario list. It includes
@@ -97,6 +106,35 @@ make test-vnet-target
`test-vnet` is host-only. `test-vnet-target` launches two
authenticated wolfIP guests under M33MU.
+### Engine matrix
+
+The full CI scenario list contains 27 scenarios and adds
+`engine: [native, hsm]` as a matrix dimension. There are 53 applicable
+engine/scenario cells: every scenario runs with both engines except
+`hsmattackneg` under native.
+
+`hsmattackneg` drives the raw wolfHSM protocol from a compromised-guest probe.
+It checks that a forged wolfHSM client ID cannot select the attestation key and
+that a wolfHSM NVM-group packet cannot reach the rollback store. The native
+engine does not link the wolfHSM client wire, server, or message handlers, so
+that exact attack surface does not exist there. Native key and namespace
+behavior remains covered by the common positive, cross-domain, keystore,
+storage, attestation, and Crypto-validation rows.
+
+Validation of the engine split completed under both engines with:
+
+- the applicable M33MU scenario matrix;
+- the Arm FF-M IPC suite at 85 passed, 4 heap-dependent tests skipped, and
+ 0 failed;
+- the current dev_apis Crypto schedule at 64 passed, 13 skipped, and 0 failed
+ (77 scheduled tests; c047 is configuration-skipped in addition to the
+ upstream schedule); and
+- the STM32H563 positive, restart, cross-domain, and conformance hardware
+ suite.
+
+The engine dimension changes crypto dispatch, not what M33MU proves. Emulator
+results still do not establish STM32 attribution or physical flash behavior.
+
## STM32H563 hardware
The published hardware run requires:
@@ -119,12 +157,18 @@ than an initial skip.
Run the default hardware set with:
```sh
-WT_H5_DOCKER_IMAGE=ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.15 make test-hardware
+WT_ENGINE=native \
+WT_H5_DOCKER_IMAGE=ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.15 \
+make test-hardware
+WT_ENGINE=hsm \
+WT_H5_DOCKER_IMAGE=ghcr.io/wolfssl/wolfboot-ci-m33mu:v1.15 \
+make test-hardware
```
The target skips if hardware detection fails. It builds and flashes the
-positive, restart, cross-domain, and conformance scenarios by default. Select
-a narrower set with `WT_H5_SCENARIOS`:
+positive, restart, cross-domain, and conformance scenarios by default. The
+suite wrapper forwards `WT_ENGINE` into its build container. Select a narrower
+set with `WT_H5_SCENARIOS`:
```sh
WT_H5_SCENARIOS="positive bootupdate" \
@@ -175,7 +219,7 @@ The workflows under `.github/workflows/` separately run:
- host unit tests;
- compiler variants, sanitizers, and Valgrind;
-- Cortex-M33 cross-compilation;
+- Cortex-M33 cross-compilation of both crypto engines;
- dependency integration;
- the core/port split guard and the docs guard (no internal-ledger or
home-directory references in the published docs);
@@ -189,10 +233,11 @@ core/port split checks run on every pull request, including drafts. The fuzz
target also runs on pull requests as a 60-second libFuzzer smoke pass; the
nightly schedule and manual dispatch run the 600-second soak instead.
-The full M33MU matrix (the `M33MU` workflow: wolfBoot plus both guest lifecycles
-and every scenario) runs on every pull request, on a push to `master`, `main`,
-or `wolfTrust-dev`, on the nightly schedule, and on manual dispatch. Every PR
-gets the full emulator matrix automatically — no label or opt-in step.
+The full M33MU matrix (the `M33MU` workflow: wolfBoot plus both guest
+lifecycles, both crypto engines, and every scenario) runs on every pull
+request, on a push to `master`, `main`, or `wolfTrust-dev`, on the nightly
+schedule, and on manual dispatch. Every PR gets the full emulator matrix
+automatically — no label or opt-in step.
### Running M33MU off a pull request
diff --git a/docs/Threat-Model.md b/docs/Threat-Model.md
index c8307787..bd31e3e6 100644
--- a/docs/Threat-Model.md
+++ b/docs/Threat-Model.md
@@ -43,7 +43,8 @@ The primary adversary controls all software in one Non-secure guest. It may:
- issue arbitrary PSA requests, call types, handles, vector counts, addresses,
lengths, and payloads;
- race its own memory while a gateway call is in progress;
-- spoof protocol fields, including wolfHSM communication IDs;
+- spoof selected-engine protocol fields, including wolfHSM communication IDs
+ when the hsm engine is linked;
- trigger faults, interrupt activity, repeated connects, and abandoned update
sessions;
- attempt to access peer RAM, Secure memory, peripherals, and inactive guest
@@ -79,8 +80,8 @@ images.
| Stale or stolen handles | Handle ownership, type, generation, and state transitions are checked by the SPM. |
| Cross-guest RAM access | GTZC MPCBB attribution closes the full guest-RAM extent and reopens only the scheduled guest's writable SRAM blocks. Per-guest Non-secure MPU and interrupt state are restored scheduling policy, not adversarial boundaries against privileged guests. |
| Inactive-guest flash modification | Signature-covered guest digests and runtime verification detect changes; hardened STM32H563 builds also require complete WRP coverage. |
-| Cross-guest key use | The relay maps guest `N` to forced wolfHSM client ID `N + 1`. |
-| Direct NVM access through HSM protocol | The guest HSM relay rejects NVM message groups. |
+| Cross-guest key use | The SPM-stamped identity selects the native vault sub-owner; the hsm relay maps guest `N` to forced wolfHSM client ID `N + 1`. |
+| Direct NVM access through a crypto protocol | The native format exposes no general NVM operation; the hsm relay rejects wolfHSM NVM message groups. |
| Storage object confusion | The vault namespaces objects by front-end partition, stamped client, and UID, and storage requests cannot use its reserved key-object type. |
| Protected Storage disclosure or edit | AES-256-GCM sealing, a non-exportable device key, authenticated labels, and checked NVM operations. |
| Stale sealed-object replay | Each sealed write advances a persisted counter used in its nonce; the current slot counter authenticates reads. |
@@ -105,7 +106,7 @@ peer secrets in guest images or claim peer-image confidentiality.
### Persistent counters use the same flash trust boundary
Protected Storage counter records and firmware-version floors reside in the
-wolfHSM NVM flash pool. The design detects stale object data when its live
+shared NVM flash pool. The design detects stale object data when its live
counter remains current and fails closed on malformed records. It does not
claim resistance to a physical adversary that can restore a mutually
consistent historical snapshot of the entire NVM pool. A target requiring
@@ -114,9 +115,10 @@ that property needs rollback-resistant monotonic storage in its port.
### Secure code is shared
Secure Partition writable state is narrowed by the Secure MPU, but all service
-threads execute shared read/execute text from one linked image. HSM, vault, and
-attestation also share a keystore data band. A defect in trusted shared code or
-an allowed shared backend can therefore affect more than one service.
+threads execute shared read/execute text from one linked image. The selected
+crypto engine, vault, and attestation also share a keystore data band. A defect
+in trusted shared code or an allowed shared backend can therefore affect more
+than one service.
### Privileged handlers remain security-critical
diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md
index cc2bf41f..a0ac5357 100644
--- a/docs/_Sidebar.md
+++ b/docs/_Sidebar.md
@@ -3,6 +3,7 @@
- [[Home]]
- [[Getting Started]]
- [[Architecture]]
+- [[Crypto Engines]]
- [[Security Model]]
- [[Threat Model]]
- [[API Reference]]
diff --git a/include/wolftrust/crypto_native_client.h b/include/wolftrust/crypto_native_client.h
new file mode 100644
index 00000000..5548ba7b
--- /dev/null
+++ b/include/wolftrust/crypto_native_client.h
@@ -0,0 +1,51 @@
+/* crypto_native_client.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_CRYPTO_NATIVE_CLIENT_H
+#define WOLFTRUST_CRYPTO_NATIVE_CLIENT_H
+
+#include
+#include
+
+#include "psa/error.h"
+#include "wolftrust/services/crypto_native.h"
+
+/* Non-secure client for the native crypto wire (WT_ENGINE=native): each
+ * request is one synchronous psa_call to the SERVICE_HSM door carrying
+ * [wt_crypto_wire_req_t][payload]; the response is [int32_t status][payload].
+ * The connection is made lazily and healed on demand, mirroring the wolfHSM
+ * client glue's restart tolerance. */
+
+/* One native wire round trip. out may be NULL when the op returns no
+ * payload. Returns the secure-side psa_status_t, or a client-side
+ * PSA_ERROR_* when the door is unreachable. */
+psa_status_t wt_crypto_native_call(const wt_crypto_wire_req_t* hdr,
+ const uint8_t* payload,
+ size_t payload_len, uint8_t* out,
+ size_t out_cap, size_t* out_len);
+
+/* Vault-domain randomness over the wire, chunked to WT_CRYPTO_RANDOM_MAX. */
+psa_status_t wt_crypto_native_random(uint8_t* out, size_t len);
+
+/* wolfCrypt CUSTOM_RAND_GENERATE_BLOCK hook: NS DRBG seeds come from the
+ * secure vault RNG, never a local entropy source. Returns 0 on success. */
+int wolftrust_guest_rng_stub(unsigned char* output, unsigned int sz);
+
+#endif /* WOLFTRUST_CRYPTO_NATIVE_CLIENT_H */
diff --git a/include/wolftrust/nvm_store.h b/include/wolftrust/nvm_store.h
new file mode 100644
index 00000000..36fa2fd2
--- /dev/null
+++ b/include/wolftrust/nvm_store.h
@@ -0,0 +1,50 @@
+/* nvm_store.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_NVM_STORE_H
+#define WOLFTRUST_NVM_STORE_H
+
+#include "wolfhsm/wh_nvm.h"
+#include "wolftrust/sync/mutex.h"
+
+/* Engine-independent secure NVM store: the shared flash-backed object store
+ * and its serialisation lock, linked in both crypto engines. The lifecycle
+ * latch and rollback floors (wt_hsm_set_boot_lifecycle, wt_hsm_rollback_*)
+ * ride this store and keep their public names in services/hsm.h. */
+
+/* The single shared NVM context and its serialisation mutex. The mutex must
+ * be initialised (wt_mutex_init) before wt_nvm_store_bind wires it in. */
+extern whNvmContext g_wt_nvm_ctx;
+extern wt_mutex_t g_wt_nvm_lock_mutex;
+
+/* Wire the port flash callbacks and the lock config, then initialise the
+ * shared NVM context. Re-callable: the vault format/recovery path runs it
+ * again against a freshly erased pool. Returns a WH_ERROR_* code. */
+int wt_nvm_store_bind(void);
+
+/* Nonzero when the wolfBoot-reported lifecycle permits destructive store
+ * recovery (ASSEMBLY_AND_TEST or PSA_ROT_PROVISIONING); unset stays locked. */
+int wt_nvm_reformat_allowed(void);
+
+/* Record that the store was reformatted this boot (observability, reported
+ * by wt_hsm_vault_was_reformatted). */
+void wt_nvm_mark_reformatted(void);
+
+#endif /* WOLFTRUST_NVM_STORE_H */
diff --git a/include/wolftrust/services/crypto_native.h b/include/wolftrust/services/crypto_native.h
new file mode 100644
index 00000000..d8686187
--- /dev/null
+++ b/include/wolftrust/services/crypto_native.h
@@ -0,0 +1,97 @@
+/* crypto_native.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_SERVICES_CRYPTO_NATIVE_H
+#define WOLFTRUST_SERVICES_CRYPTO_NATIVE_H
+
+#include "wolftrust/services/vault_service.h"
+
+#include
+#include
+
+/* Native crypto engine (WT_ENGINE=native): wolfCrypt dispatch behind the
+ * unchanged SERVICE_HSM door. The relay hands one opaque packet to
+ * wt_native_submit; keys are vault NVM objects served by the wolfCrypt key
+ * backend inside the privileged vault domain — the wolfHSM server, comm,
+ * and message layers are not linked. */
+
+/* Native wire ops carried in wt_crypto_wire_req_t.op. Key ops address vault
+ * key objects namespaced (owner = SERVICE_HSM partition, sub = SPM-stamped
+ * NS client), so a client only ever reaches its own keys. */
+#define WT_CRYPTO_OP_KEY_GENERATE 1U
+#define WT_CRYPTO_OP_KEY_IMPORT 2U
+#define WT_CRYPTO_OP_KEY_EXPORT_PUBLIC 3U
+#define WT_CRYPTO_OP_KEY_SIGN 4U
+#define WT_CRYPTO_OP_KEY_VERIFY 5U
+#define WT_CRYPTO_OP_KEY_ENCRYPT 6U
+#define WT_CRYPTO_OP_KEY_DECRYPT 7U
+#define WT_CRYPTO_OP_KEY_DESTROY 8U
+#define WT_CRYPTO_OP_RANDOM 9U
+#define WT_CRYPTO_OP_HASH 10U
+
+#define WT_CRYPTO_RANDOM_MAX 256U
+
+/* One request packet: [wt_crypto_wire_req_t][payload], bounded by the relay
+ * copy buffer (WT_HSM_RELAY_MSG_MAX). usage carries the WT_VAULT_KEY_USAGE_*
+ * bits on generate/import and the requested byte count on RANDOM; key_type
+ * uses the WT_VAULT_KEY_* encodings. The response packet is
+ * [int32_t psa_status][payload]. */
+typedef struct wt_crypto_wire_req {
+ uint64_t uid;
+ uint32_t op;
+ uint32_t usage;
+ uint32_t key_type;
+ uint32_t reserved;
+} wt_crypto_wire_req_t;
+
+/* Boot bring-up for the native engine: wolfCrypt, the port flash, the shared
+ * NVM store, and the vault storage/seal/key/RNG bindings. The native peer of
+ * wt_hsm_init; failure is fatal — the caller should panic. */
+int wt_native_init(void);
+
+/* Fault-recovery entry for the native engine: invalidate operation-scoped
+ * crypto state (the shared vault DRBG) so a restarted relay partition serves
+ * from clean state instead of a generator torn mid-draw. */
+void wt_native_reinit(void);
+
+/* SERVICE_HSM submit hook (matches wt_hsm_relay_submit_fn): service one
+ * native wire packet. submit_ctx carries the owning partition id. */
+int wt_native_submit(void* submit_ctx, int32_t client_id, const uint8_t* req,
+ size_t req_len, uint8_t* resp, size_t resp_cap,
+ size_t* resp_len);
+
+/* wolfCrypt key-op backend over vault NVM objects (WT-FFM-0046): private
+ * material is stored SENSITIVE + NONEXPORTABLE and never leaves the
+ * privileged vault domain. */
+struct whNvmContext_t;
+int wt_hsm_keyvault_init(struct whNvmContext_t* nvm);
+extern const wt_vault_key_backend_t wt_hsm_key_backend;
+
+/* Destroy one key object in the caller's namespace. Refuses anything that
+ * is not a key, so the key wire can never delete a storage object. */
+psa_status_t wt_hsm_keyvault_destroy(int32_t owner, int32_t sub, uint64_t uid);
+
+/* Vault-domain DRBG for the RANDOM face and key generation. */
+psa_status_t wt_hsm_keyvault_random(uint8_t* out, size_t len);
+
+/* Invalidate the vault DRBG so it re-seeds on next use (fault recovery). */
+void wt_hsm_keyvault_reset(void);
+
+#endif /* WOLFTRUST_SERVICES_CRYPTO_NATIVE_H */
diff --git a/include/wolftrust/services/hsm.h b/include/wolftrust/services/hsm.h
index d7728b4b..f7c3d1d3 100644
--- a/include/wolftrust/services/hsm.h
+++ b/include/wolftrust/services/hsm.h
@@ -211,6 +211,14 @@ void wt_hsm_vault_make_label(uint8_t* label, int32_t owner, int32_t sub,
uint64_t uid, uint32_t flags);
uint32_t wt_hsm_vault_flags_of(const uint8_t* label);
+/* Reserve pool space for a shared-store object add of len bytes, holding back
+ * the counter-table headroom and compacting reclaimable entries first. A
+ * writer must call this before wh_Nvm_AddObject so a doomed add on a full pool
+ * cannot fail mid-write and poison later adds, and so key churn cannot starve
+ * the seal-counter table or the rollback floor. Returns INSUFFICIENT_STORAGE
+ * when even reclaim cannot make room. */
+psa_status_t wt_hsm_vault_reserve_object(whNvmSize len);
+
/* Vault-domain RNG (WT-FFM-0054): entropy for SERVICE_VAULT's RANDOM face,
* produced by a wolfCrypt DRBG owned by the privileged vault domain. Installed
* via wt_vault_service_set_rng at boot. Only linked into builds that carry
diff --git a/mk/arch-armv8m.mk b/mk/arch-armv8m.mk
index 62fdd494..c87a7e3c 100644
--- a/mk/arch-armv8m.mk
+++ b/mk/arch-armv8m.mk
@@ -64,4 +64,9 @@ define arch_image_checks
echo "FAIL: expected $(NSC_COUNT) FF-M veneers, found $$n (WT-FFM-0057)" >&2; \
exit 1; \
fi
+ @if grep -E ' (malloc|free|calloc|realloc|_sbrk|_malloc_r|_free_r)$$' \
+ $(BUILD_DIR)/nsc-syms.txt; then \
+ echo "FAIL: heap allocator symbol in the zero-heap secure image" >&2; \
+ exit 1; \
+ fi
endef
diff --git a/mk/common.mk b/mk/common.mk
index 0d2e7896..71b42169 100644
--- a/mk/common.mk
+++ b/mk/common.mk
@@ -22,7 +22,38 @@ WT_MAX_GUESTS ?= 2
# the SP_SMALL math switch; PSPLIM_S faults any real overflow, so this floor
# is measured, not guessed.
WT_CO_STACK_SIZE ?= 10240
-WT_ENGINE_HSM ?= 1
+# Secure crypto engine. native (the default) calls wolfCrypt directly; hsm
+# links the wolfHSM server as a key-management add-on. Legacy WT_ENGINE_HSM
+# values map onto the selector.
+WT_ENGINE_LEGACY :=
+ifeq ($(WT_ENGINE_HSM),0)
+WT_ENGINE_LEGACY := native
+endif
+ifeq ($(WT_ENGINE_HSM),1)
+WT_ENGINE_LEGACY := hsm
+endif
+ifneq ($(WT_ENGINE_HSM),)
+ifeq ($(WT_ENGINE_LEGACY),)
+$(error unsupported WT_ENGINE_HSM='$(WT_ENGINE_HSM)' (want 0 or 1))
+endif
+endif
+ifneq ($(WT_ENGINE_LEGACY),)
+ifneq ($(WT_ENGINE),)
+ifneq ($(WT_ENGINE),$(WT_ENGINE_LEGACY))
+$(error conflicting engine selectors: WT_ENGINE=$(WT_ENGINE) but WT_ENGINE_HSM=$(WT_ENGINE_HSM) selects $(WT_ENGINE_LEGACY))
+endif
+endif
+WT_ENGINE := $(WT_ENGINE_LEGACY)
+endif
+WT_ENGINE ?= native
+ifneq ($(words $(WT_ENGINE))/$(filter native hsm,$(WT_ENGINE)),1/$(WT_ENGINE))
+$(error unsupported WT_ENGINE='$(WT_ENGINE)' (want native or hsm))
+endif
+ifeq ($(WT_ENGINE),hsm)
+WT_ENGINE_HSM := 1
+else
+WT_ENGINE_HSM := 0
+endif
WT_ATTEST_COSE ?= 1
WT_FFM_NEGATIVE_PROBE ?= 0
WT_KEYSTORE_NEG_PROBE ?= 0
@@ -52,9 +83,16 @@ WT_VNET_UNKNOWN_UCAST_FLOOD ?= 0
HSM_INCLUDES := -I$(WOLFHSM_DIR) -I$(WOLFSSL_DIR) -I$(BUILD_DIR)
HSM_INCLUDES_SECURE := $(HSM_INCLUDES) -I$(WOLFHAL_DIR) -I$(abspath $(WOLFHSM_RUNNER_DIR))
HSM_DEFS_SECURE := -DWOLFSSL_USER_SETTINGS -DWOLFHSM_CFG \
- -DWOLF_CRYPTO_CB -UNO_CODING \
- -DWC_RESEED_INTERVAL=1000000 -DWT_ENGINE_HSM=$(WT_ENGINE_HSM) \
+ -UNO_CODING \
+ -DWC_RESEED_INTERVAL=1000000 \
$(ARCH_HSM_DEFS)
+ifeq ($(WT_ENGINE),hsm)
+HSM_DEFS_SECURE += -DWOLF_CRYPTO_CB -DWT_ENGINE_HSM=1
+else
+# Native links only the wolfHSM NVM object store; NO_CRYPTO drops the server's
+# wolfCrypt dependency (and its WOLF_CRYPTO_CB requirement).
+HSM_DEFS_SECURE += -DWT_ENGINE_NATIVE=1 -DWOLFHSM_CFG_NO_CRYPTO
+endif
ifeq ($(WT_ATTEST_COSE),1)
SECURE_CFLAGS_COSE := -I$(WOLFCOSE_DIR)/include \
@@ -197,6 +235,13 @@ WOLFHSM_SECURE_SRCS := \
$(WOLFHSM_DIR)/src/wh_crypto.c \
$(WOLFHSM_DIR)/src/wh_keyid.c
+# Native engine keeps only the self-contained NVM object store (vault/ITS/PS/FWU
+# ride it); the wolfHSM server, comm, and message layers are hsm-only.
+ifeq ($(WT_ENGINE),native)
+WOLFHSM_SECURE_SRCS := $(filter %/wh_nvm.c %/wh_nvm_flash.c %/wh_flash_unit.c \
+ %/wh_lock.c %/wh_utils.c %/wh_keyid.c,$(WOLFHSM_SECURE_SRCS))
+endif
+
WOLFCRYPT_SECURE_SRCS := \
$(WOLFSSL_DIR)/wolfcrypt/src/aes.c \
$(WOLFSSL_DIR)/wolfcrypt/src/asn.c \
@@ -226,12 +271,24 @@ WT_SECURE_EXTRA_SRCS := \
$(TARGET_EXTRA_SRCS) \
$(wildcard $(WOLFHSM_RUNNER_DIR)/libc_stubs.c) \
$(wildcard $(ROOT)/src/services/wolfhsm/*.c) \
+ $(ROOT)/src/services/nvm_store.c \
$(ROOT)/src/services/boot_handoff.c \
$(ROOT)/src/services/hsm_relay_service.c \
$(ROOT)/src/services/storage_service.c \
$(ROOT)/src/services/fwu_service.c \
$(ROOT)/src/services/vault_service.c
+# Engine split: wt_hsm.c drives the wolfHSM server (hsm engine only); the
+# native engine dispatches wolfCrypt directly behind the same SERVICE_HSM
+# door and keeps the server-free vault/seal/lock glue over the shared store.
+ifeq ($(WT_ENGINE),native)
+WT_SECURE_EXTRA_SRCS := $(filter-out %/wolfhsm/wt_hsm.c,$(WT_SECURE_EXTRA_SRCS))
+WT_SECURE_EXTRA_SRCS += \
+ $(ROOT)/src/services/native/crypto_native.c \
+ $(ROOT)/src/services/native/native_wire.c \
+ $(ROOT)/src/services/native/keyvault.c
+endif
+
ifeq ($(WT_ATTEST_COSE),1)
WT_SECURE_EXTRA_SRCS += \
$(ROOT)/src/services/attestation_cose.c \
@@ -1300,7 +1357,9 @@ $(BUILD_MODE_STAMP): FORCE | $(BUILD_DIR)
'WT_SHARED_UART=$(WT_SHARED_UART)' \
'WT_TIMESLICE_MS=$(WT_TIMESLICE_MS)' \
'WT_GUEST_CORE_CLOCK_HZ=$(WT_GUEST_CORE_CLOCK_HZ)' \
- 'WT_GUEST_UART_CLOCK_HZ=$(WT_GUEST_UART_CLOCK_HZ)' > "$$tmp"; \
+ 'WT_GUEST_UART_CLOCK_HZ=$(WT_GUEST_UART_CLOCK_HZ)' \
+ 'WT_EXTRA_CFLAGS=$(WT_EXTRA_CFLAGS)' \
+ 'WT_EXTRA_LDFLAGS=$(WT_EXTRA_LDFLAGS)' > "$$tmp"; \
if test -f "$@" && cmp -s "$$tmp" "$@"; then \
rm -f "$$tmp"; \
else \
@@ -1346,6 +1405,9 @@ $(BUILD_DIR)/wt_sec_%.o: $(WOLFHAL_DIR)/src/rng/%.c $(WOLFHSM_CFG_H) $(BUILD_MOD
$(BUILD_DIR)/wt_sec_%.o: $(ROOT)/src/services/wolfhsm/%.c $(WOLFHSM_CFG_H) $(BUILD_MODE_STAMP) | $(BUILD_DIR)
$(CC) $(SECURE_CFLAGS) -c -o $@ $<
+$(BUILD_DIR)/wt_sec_%.o: $(ROOT)/src/services/native/%.c $(WOLFHSM_CFG_H) $(BUILD_MODE_STAMP) | $(BUILD_DIR)
+ $(CC) $(SECURE_CFLAGS) -c -o $@ $<
+
$(BUILD_DIR)/wt_sec_%.o: $(ROOT)/src/vnet/%.c $(WOLFHSM_CFG_H) $(BUILD_MODE_STAMP) | $(BUILD_DIR)
$(CC) $(SECURE_CFLAGS) -c -o $@ $<
diff --git a/src/arch/armv8m/guest_context_armv8m.c b/src/arch/armv8m/guest_context_armv8m.c
index fff85819..89f108d7 100644
--- a/src/arch/armv8m/guest_context_armv8m.c
+++ b/src/arch/armv8m/guest_context_armv8m.c
@@ -116,7 +116,6 @@ void wt_arch_init(void)
WT_SCB_AIRCR_S = WT_SCB_AIRCR_VECTKEY |
(WT_SCB_AIRCR_S & WT_SCB_AIRCR_CFG_MASK) |
WT_SCB_AIRCR_SYSRESETREQS;
-#ifdef WT_ENGINE_HSM
/* PendSV and the secure SysTick must share the lowest priority: SysTick at
* the reset default (0, highest) would preempt PendSV mid-coroutine switch,
* and a nested exception return off the half-saved frame faults INVPC.
@@ -124,7 +123,6 @@ void wt_arch_init(void)
WT_SCB_SHPR3_S |= (0xFFu << WT_SCB_SHPR3_PENDSV_SHIFT) |
(0xFFu << WT_SCB_SHPR3_SYSTICK_SHIFT);
WT_SCB_ICSR_S = WT_SCB_ICSR_PENDSVCLR;
-#endif
g_switch_count = 0u;
g_active_guest = UINT32_MAX;
}
@@ -699,7 +697,6 @@ uint32_t wt_arch_active_guest_id(void)
__attribute__((naked)) void SecureFault_Handler(void)
{
__asm volatile(
-#ifdef WT_ENGINE_HSM
/* EXC_RETURN bit6 = secure frame, bit3 = Thread. A fault from Secure
* Thread with a live tasklet is a Secure Partition/tasklet fault, not
* a guest escalation: blaming the scheduled NS guest would restart an
@@ -718,7 +715,6 @@ __attribute__((naked)) void SecureFault_Handler(void)
"beq 1f \n"
"b wt_armv8m_tasklet_fault_entry \n"
"1: \n"
-#endif
"mov r2, sp \n"
"ldr r1, =g_secure_entry_sp \n"
"str r2, [r1] \n"
diff --git a/src/arch/armv8m/sp_fault_armv8m.c b/src/arch/armv8m/sp_fault_armv8m.c
index 1ca3a996..e7ae92ec 100644
--- a/src/arch/armv8m/sp_fault_armv8m.c
+++ b/src/arch/armv8m/sp_fault_armv8m.c
@@ -32,8 +32,8 @@
#include
-#ifdef WT_ENGINE_HSM
#include "wolftrust/sched/tasklet.h"
+#ifdef WT_ENGINE_HSM
#include "wolftrust/services/hsm.h"
#endif
@@ -48,7 +48,6 @@ static volatile uint32_t g_tasklet_fault_icsr;
static volatile uint32_t g_tasklet_fault_co;
static volatile uint32_t g_tasklet_fault_co_sp;
-#ifdef WT_ENGINE_HSM
/* -----------------------------------------------------------------------
* Secure-side tasklet fault path.
*
@@ -140,10 +139,12 @@ static void wt_secure_tasklet_fault_dispatch(uint32_t *frame,
return;
}
+#ifdef WT_ENGINE_HSM
wt_guest_id_t gid = wt_hsm_guest_for_tasklet(tasklet);
if (gid < WT_MAX_GUESTS) {
(void)wt_hsm_signal_fault(gid);
}
+#endif
wt_tasklet_mark_faulted(tasklet);
}
@@ -202,4 +203,3 @@ __attribute__((naked)) void UsageFault_Handler(void)
{
__asm volatile("b wt_armv8m_tasklet_fault_entry \n");
}
-#endif /* WT_ENGINE_HSM */
diff --git a/src/arch/common/spm_gate_core.c b/src/arch/common/spm_gate_core.c
index 89c2ea7e..7c864b43 100644
--- a/src/arch/common/spm_gate_core.c
+++ b/src/arch/common/spm_gate_core.c
@@ -41,6 +41,7 @@
#include "wolftrust/services/fwu_service.h"
#include "wolftrust/services/hsm.h"
#include "wolftrust/services/hsm_relay.h"
+#include "wolftrust/services/crypto_native.h"
#include "wolftrust/sync/mutex.h"
#include "wolftrust/services/storage_service.h"
#include "wolftrust/services/vault_service.h"
@@ -51,6 +52,8 @@
#include "wolftrust/sp_recovery.h"
#include "wolftrust/spm_gate.h"
+#include
+
/* Generated in every secure build; the ITS entry embeds SERVICE_VAULT_SID as
* a code constant — the unprivileged loop cannot read SPM RAM at runtime. */
#include "psa_manifest/pid.h"
@@ -181,15 +184,21 @@ static void wt_spm_fault_release(void* ctx)
{
wt_spm_fault_ctx_t* c = (wt_spm_fault_ctx_t*)ctx;
-#if defined(WT_ENGINE_HSM)
+ /* The shared NVM lock is engine-independent (nvm_store.c): a faulted
+ * holder must release it in both engines or later acquirers deadlock. */
wt_hsm_release_locks(c->slot->co);
+#if defined(WT_ENGINE_HSM)
if (c->slot->partition_id == g_spm_hsm_partition_id) {
/* The fault may have torn a per-guest server mid-request; rebuild
* them all. Fails closed — a guest whose re-init fails stays down. */
(void)wt_hsm_relay_reinit_servers();
}
-#else
- (void)c;
+#elif defined(WT_ENGINE_NATIVE)
+ /* The native vault DRBG is process-global — the HSM relay, SERVICE_VAULT
+ * (RANDOM and key ops), and attestation signing all draw from the one
+ * g_kv_rng. Any recovered fault may have torn it mid-draw, so invalidate
+ * it unconditionally; the next draw re-seeds and fails closed on error. */
+ wt_native_reinit();
#endif
}
diff --git a/src/boot.c b/src/boot.c
index 6d5576b3..475b44ed 100644
--- a/src/boot.c
+++ b/src/boot.c
@@ -33,9 +33,10 @@
#include "wolftrust/platform.h"
#include "wolftrust/services/hsm.h"
#include "wolftrust/services/initial_attestation.h"
-#ifdef WT_ENGINE_HSM
-#include "wolftrust/sched/tasklet.h"
#include "wolfhsm/wh_error.h"
+#include "wolftrust/sched/tasklet.h"
+#ifndef WT_ENGINE_HSM
+#include "wolftrust/services/crypto_native.h"
#endif
#include
@@ -56,12 +57,8 @@ void wt_boot_run(void)
handoffRet = wt_boot_handoff_consume(&bootHandoff);
wt_boot_handoff_clear();
#endif
-#ifdef WT_ENGINE_HSM
- /* Bring up the secure-side wolfHSM service before dispatching guests:
- * 1. tasklet scheduler (provides the bootstrap context)
- * 2. shared wolfCrypt + NVM + lock
- * 3. one transport + server context + tasklet per guest
- * Any failure here is fatal because guests require this engine. */
+ /* Coroutine runtime first: the SP scheduler and (in the hsm engine) the
+ * per-guest server tasklets both ride it. */
wt_tasklet_init();
#if defined(WT_ATTEST_COSE) && (WT_ATTEST_COSE == 1)
/* Gate vault auto-reformat on the wolfBoot-reported lifecycle before the
@@ -71,7 +68,11 @@ void wt_boot_run(void)
wt_hsm_set_boot_lifecycle(bootHandoff.lifecycle);
}
#endif
+#ifdef WT_ENGINE_HSM
if (wt_hsm_init() != 0) wt_platform_panic();
+#else
+ if (wt_native_init() != 0) wt_platform_panic();
+#endif
/* WT-FFM-0050: the vault NVM is live and no guest has dispatched, so the
* monotonic version floors gate every domain now. A missing handoff
* reports version zero, which fails closed once a floor is armed. */
@@ -81,6 +82,7 @@ void wt_boot_run(void)
#else
(void)wt_hsm_rollback_enforce(0u);
#endif
+#ifdef WT_ENGINE_HSM
/* WT-FFM-0054: every guest server binds the secure relay capture
* transport — packets arrive only through SERVICE_HSM's mediated
* psa_call path, never a shared NS-RAM window. */
@@ -93,6 +95,7 @@ void wt_boot_run(void)
wt_platform_panic();
}
}
+#endif
#if defined(WT_ATTEST_COSE) && (WT_ATTEST_COSE == 1)
if (wt_hsm_attest_bootstrap() != WH_ERROR_OK) {
/* The vault could not be provisioned and auto-reformat was not
@@ -117,12 +120,11 @@ void wt_boot_run(void)
wt_ffm_set_lifecycle(wt_ffm_boot_runtime_mut(), bootHandoff.lifecycle);
}
#endif
- /* P1t: crypto SP becomes a scheduled unprivileged coroutine now that
- * the tasklet scheduler exists. Fail closed — guests depend on it. */
+ /* P1t: the service partitions become scheduled unprivileged coroutines.
+ * Fail closed — guests depend on them. */
if (wt_ffm_boot_start_sched() != WT_FFM_SUCCESS) {
wt_platform_panic();
}
-#endif
#if defined(WT_REMEASURE_PROBE)
wt_platform_remeasure_probe();
#endif
diff --git a/src/client/crypto_native_client.c b/src/client/crypto_native_client.c
new file mode 100644
index 00000000..c34adf0e
--- /dev/null
+++ b/src/client/crypto_native_client.c
@@ -0,0 +1,173 @@
+/* crypto_native_client.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 .
+ */
+
+/* OS-neutral Non-secure client for the native crypto wire: one mediated
+ * psa_call to SERVICE_HSM per request, no shared-RAM window, no raw veneer.
+ * A Secure Partition restart invalidates the cached handle; every call
+ * reconnects on demand so the client heals across recovery windows. */
+
+#include
+#include
+#include
+
+#include "psa/client.h"
+#include "wolftrust/services/hsm_relay.h"
+#include "wolftrust/crypto_native_client.h"
+
+/* SERVICE_HSM SID from the platform manifest, same door the wolfHSM client
+ * glue connects to in the hsm engine. */
+#ifndef WT_CRYPTO_NATIVE_SID
+#define WT_CRYPTO_NATIVE_SID 4102u
+#endif
+#ifndef WT_CRYPTO_NATIVE_SID_VERSION
+#define WT_CRYPTO_NATIVE_SID_VERSION 1u
+#endif
+
+/* A Secure Partition restart window (WT-SYS-0008 graceful recovery) makes
+ * the door refuse connects and calls for a short while; every consumer of
+ * this client — including a wc_InitRng seed fetch mid-keygen — must ride it
+ * out, so the round trip below retries with bounded patience, mirroring the
+ * wolfHSM client glue's heal-on-demand. */
+#ifndef WT_CRYPTO_NATIVE_RETRIES
+#define WT_CRYPTO_NATIVE_RETRIES 64
+#endif
+
+static psa_handle_t g_native_handle;
+
+static int wt_crypto_native_ensure_connected(void)
+{
+ if (g_native_handle > 0) {
+ return 0;
+ }
+ g_native_handle = psa_connect(WT_CRYPTO_NATIVE_SID,
+ WT_CRYPTO_NATIVE_SID_VERSION);
+ return (g_native_handle > 0) ? 0 : -1;
+}
+
+psa_status_t wt_crypto_native_call(const wt_crypto_wire_req_t* hdr,
+ const uint8_t* payload,
+ size_t payload_len, uint8_t* out,
+ size_t out_cap, size_t* out_len)
+{
+ uint8_t req[WT_HSM_RELAY_MSG_MAX];
+ uint8_t resp[WT_HSM_RELAY_MSG_MAX];
+ psa_invec in_vec;
+ psa_outvec out_vec;
+ psa_status_t status;
+ int32_t wire_status;
+ size_t got;
+ int attempt;
+
+ /* Subtraction, not sizeof(*hdr) + payload_len, so a payload_len near
+ * SIZE_MAX cannot wrap the bound and let the memcpy overrun req. The
+ * header always fits, so sizeof(req) - sizeof(*hdr) never underflows. */
+ if (hdr == NULL || (payload == NULL && payload_len != 0U) ||
+ payload_len > sizeof(req) - sizeof(*hdr)) {
+ return PSA_ERROR_INVALID_ARGUMENT;
+ }
+ (void)memcpy(req, hdr, sizeof(*hdr));
+ if (payload_len != 0U) {
+ (void)memcpy(req + sizeof(*hdr), payload, payload_len);
+ }
+
+ status = PSA_ERROR_CONNECTION_REFUSED;
+ for (attempt = 0; attempt < WT_CRYPTO_NATIVE_RETRIES; attempt++) {
+ if (wt_crypto_native_ensure_connected() != 0) {
+ continue;
+ }
+ in_vec.base = req;
+ in_vec.len = sizeof(*hdr) + payload_len;
+ out_vec.base = resp;
+ out_vec.len = sizeof(resp);
+ status = psa_call(g_native_handle, PSA_IPC_CALL, &in_vec, 1U,
+ &out_vec, 1U);
+ if (status == PSA_SUCCESS) {
+ break;
+ }
+ psa_close(g_native_handle);
+ g_native_handle = 0;
+ /* A call that failed after reaching the door is ambiguous: a mutating
+ * op may have committed to NVM before its reply was lost, so replaying
+ * it would corrupt the store or misreport the result. Surface the
+ * failure; only connect setup and idempotent ops are safe to retry. */
+ if (hdr->op == WT_CRYPTO_OP_KEY_GENERATE ||
+ hdr->op == WT_CRYPTO_OP_KEY_IMPORT ||
+ hdr->op == WT_CRYPTO_OP_KEY_DESTROY) {
+ return status;
+ }
+ }
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ if (out_vec.len < sizeof(wire_status)) {
+ return PSA_ERROR_COMMUNICATION_FAILURE;
+ }
+ (void)memcpy(&wire_status, resp, sizeof(wire_status));
+ got = out_vec.len - sizeof(wire_status);
+ if (out != NULL && got != 0U) {
+ if (got > out_cap) {
+ return PSA_ERROR_BUFFER_TOO_SMALL;
+ }
+ (void)memcpy(out, resp + sizeof(wire_status), got);
+ }
+ if (out_len != NULL) {
+ *out_len = got;
+ }
+ return (psa_status_t)wire_status;
+}
+
+psa_status_t wt_crypto_native_random(uint8_t* out, size_t len)
+{
+ wt_crypto_wire_req_t hdr;
+ size_t done = 0U;
+ size_t chunk;
+ size_t got = 0U;
+ psa_status_t status = PSA_SUCCESS;
+
+ if (out == NULL || len == 0U) {
+ return PSA_ERROR_INVALID_ARGUMENT;
+ }
+ while (done < len) {
+ chunk = len - done;
+ if (chunk > WT_CRYPTO_RANDOM_MAX) {
+ chunk = WT_CRYPTO_RANDOM_MAX;
+ }
+ (void)memset(&hdr, 0, sizeof(hdr));
+ hdr.op = WT_CRYPTO_OP_RANDOM;
+ hdr.usage = (uint32_t)chunk;
+ status = wt_crypto_native_call(&hdr, NULL, 0U, out + done, chunk,
+ &got);
+ if (status != PSA_SUCCESS) {
+ break;
+ }
+ if (got != chunk) {
+ status = PSA_ERROR_COMMUNICATION_FAILURE;
+ break;
+ }
+ done += chunk;
+ }
+ return status;
+}
+
+int wolftrust_guest_rng_stub(unsigned char* output, unsigned int sz)
+{
+ return (wt_crypto_native_random(output, (size_t)sz) == PSA_SUCCESS) ?
+ 0 : -1;
+}
diff --git a/src/services/hsm_relay_service.c b/src/services/hsm_relay_service.c
index b3d54edd..25ccfb8f 100644
--- a/src/services/hsm_relay_service.c
+++ b/src/services/hsm_relay_service.c
@@ -108,9 +108,21 @@ static int wt_hsm_relay_write_resp(wt_ffm_runtime_t* runtime,
return WT_FFM_SUCCESS;
}
-static psa_status_t wt_hsm_relay_call(wt_ffm_runtime_t* runtime,
- int32_t partition_id,
- const psa_msg_t* msg)
+/* Scrub a relay copy buffer. volatile so the clear is not optimized away; the
+ * relay unit is port-free and does not link wolfCrypt's ForceZero. */
+static void wt_hsm_relay_zeroize(uint8_t* buf, size_t len)
+{
+ volatile uint8_t* p = buf;
+ size_t i;
+
+ for (i = 0U; i < len; i++) {
+ p[i] = 0U;
+ }
+}
+
+static psa_status_t wt_hsm_relay_call_inner(wt_ffm_runtime_t* runtime,
+ int32_t partition_id,
+ const psa_msg_t* msg)
{
size_t req_len = 0U;
size_t resp_len = 0U;
@@ -157,6 +169,21 @@ static psa_status_t wt_hsm_relay_call(wt_ffm_runtime_t* runtime,
return PSA_SUCCESS;
}
+/* Single cleanup path (key hygiene): the relay copy buffers live in the shared
+ * keystore band and carry imported key material and decrypted plaintext, so
+ * scrub both after every call, on success and on every error path. */
+static psa_status_t wt_hsm_relay_call(wt_ffm_runtime_t* runtime,
+ int32_t partition_id,
+ const psa_msg_t* msg)
+{
+ psa_status_t status;
+
+ status = wt_hsm_relay_call_inner(runtime, partition_id, msg);
+ wt_hsm_relay_zeroize(g_relay_io.req, sizeof(g_relay_io.req));
+ wt_hsm_relay_zeroize(g_relay_io.resp, sizeof(g_relay_io.resp));
+ return status;
+}
+
int wt_hsm_relay_dispatch(void* context, wt_ffm_runtime_t* runtime,
int32_t partition_id)
{
diff --git a/src/services/native/crypto_native.c b/src/services/native/crypto_native.c
new file mode 100644
index 00000000..e70f6689
--- /dev/null
+++ b/src/services/native/crypto_native.c
@@ -0,0 +1,246 @@
+/* crypto_native.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 .
+ */
+
+/*
+ * Native crypto engine (WT_ENGINE=native): direct wolfCrypt dispatch behind
+ * the unchanged SERVICE_HSM door. wt_native_init is the boot bring-up peer
+ * of wt_hsm_init; wt_native_submit services the native wire at the relay
+ * seam; the attestation signer runs wc_ecc against a vault-stored IAK.
+ */
+
+/* wolfCrypt settings must come first. */
+#include "wolfssl/wolfcrypt/settings.h"
+#include "wolfssl/wolfcrypt/types.h"
+#include "wolfssl/wolfcrypt/error-crypt.h"
+
+#include "wolfhsm/wh_error.h"
+
+#include "wolftrust/types.h"
+#include "wolftrust/sync/mutex.h"
+#include "wolftrust/nvm_store.h"
+#include "wolftrust/services/hsm.h"
+#include "wolftrust/services/hsm_relay.h"
+#include "wolftrust/services/vault_service.h"
+#include "wolftrust/services/crypto_native.h"
+
+#include "wolftrust/port_nvm.h"
+#include "psa/lifecycle.h"
+
+#include
+#include
+#include
+
+/* The IAK vault home: owner/sub 0 is reachable by no SPM-stamped caller
+ * (partitions are positive, NS clients negative), so only the boot and
+ * attestation paths below can address it. */
+#define WT_NATIVE_IAK_OWNER 0
+#define WT_NATIVE_IAK_SUB 0
+#define WT_NATIVE_IAK_UID 0xF0u
+
+static bool g_native_attest_ready;
+
+int wt_native_init(void)
+{
+ int rc;
+
+ rc = wolfCrypt_Init();
+ if (rc != 0) {
+ return rc;
+ }
+
+ rc = g_wt_hsm_flash_cb.Init(wt_hsm_flash_context(),
+ wt_hsm_flash_config());
+ if (rc != 0) {
+ return rc;
+ }
+
+ /* Initialise the shared NVM lock once, before wh_Nvm_Init wires it in. */
+ wt_mutex_init(&g_wt_nvm_lock_mutex);
+
+ rc = wt_nvm_store_bind();
+ if (rc != WH_ERROR_OK) {
+ return rc;
+ }
+
+ if (wt_hsm_vault_init(&g_wt_nvm_ctx) == 0) {
+ wt_vault_service_set_backend(&wt_hsm_vault_backend);
+ if (wt_hsm_seal_init(&g_wt_nvm_ctx) == 0) {
+ wt_hsm_vault_set_sealer(&wt_hsm_sealer);
+ }
+ else {
+ wt_hsm_vault_set_sealer(NULL);
+ }
+ if (wt_hsm_keyvault_init(&g_wt_nvm_ctx) == 0) {
+ wt_vault_service_set_key_backend(&wt_hsm_key_backend);
+ }
+ wt_vault_service_set_rng(wt_hsm_keyvault_random);
+ }
+
+ return 0;
+}
+
+/* Native-engine fault-recovery hook (WT-SYS-0008): invalidate the shared vault
+ * DRBG a torn request may have left mid-draw so the restart re-seeds from clean
+ * state. Per-operation wolfCrypt contexts are stack-local and die with the
+ * scrubbed coroutine, so the DRBG is the only mutable crypto state to reset. */
+void wt_native_reinit(void)
+{
+ wt_hsm_keyvault_reset();
+}
+
+/* =========================================================================
+ * Attestation (native): the IAK is a vault key object, provisioned at boot
+ * and exercised through the key backend so private material never leaves
+ * the privileged vault domain. Same wire forms as the hsm engine: 64-byte
+ * r||s signatures, 65-byte X9.63 public point.
+ * ====================================================================== */
+static psa_status_t wt_native_iak_generate(void)
+{
+ return wt_hsm_key_backend.generate(WT_NATIVE_IAK_OWNER,
+ WT_NATIVE_IAK_SUB, WT_NATIVE_IAK_UID,
+ WT_VAULT_KEY_P256,
+ WT_VAULT_KEY_USAGE_SIGN |
+ WT_VAULT_KEY_USAGE_VERIFY);
+}
+
+static psa_status_t wt_native_iak_export(void)
+{
+ uint8_t publicKey[WT_VAULT_KEY_PUB_LEN];
+ size_t publicKeySize = 0U;
+
+ return wt_hsm_key_backend.export_public(WT_NATIVE_IAK_OWNER,
+ WT_NATIVE_IAK_SUB,
+ WT_NATIVE_IAK_UID, publicKey,
+ sizeof(publicKey),
+ &publicKeySize);
+}
+
+#if defined(WT_VAULT_FOREIGN_PROBE)
+/* Negative test, same contract as the hsm engine: make the first provisioning
+ * look blocked so the real recovery path runs exactly once (self-heal when
+ * unlocked, fail closed when WT_VAULT_PROBE_SECURED forces a locked
+ * lifecycle). */
+static int g_native_foreign_probe_fired;
+#endif
+
+int wt_hsm_attest_init(void)
+{
+ psa_status_t status;
+
+#if defined(WT_VAULT_FOREIGN_PROBE) && defined(WT_VAULT_PROBE_SECURED)
+ wt_hsm_set_boot_lifecycle(PSA_LIFECYCLE_SECURED);
+#endif
+ if (g_native_attest_ready) {
+ return WH_ERROR_OK;
+ }
+ status = wt_native_iak_export();
+#if defined(WT_VAULT_FOREIGN_PROBE)
+ if (g_native_foreign_probe_fired == 0) {
+ status = PSA_ERROR_NOT_PERMITTED;
+ }
+#endif
+ if (status != PSA_SUCCESS && wt_nvm_reformat_allowed() != 0) {
+ /* An unreadable IAK (absent, foreign, or corrupt) is (re)provisioned
+ * here, but only while the lifecycle still permits it. A SECURED
+ * device must never silently mint a fresh attestation identity for an
+ * absent IAK (e.g. across an hsm-to-native engine migration): it falls
+ * through and fails closed, so attestation degrades rather than the
+ * device adopting a new key that pinned verifiers would reject. */
+ status = wt_native_iak_generate();
+#if defined(WT_VAULT_FOREIGN_PROBE)
+ if (g_native_foreign_probe_fired == 0) {
+ g_native_foreign_probe_fired = 1;
+ status = PSA_ERROR_NOT_PERMITTED;
+ }
+#endif
+ if (status != PSA_SUCCESS) {
+ if (wt_hsm_flash_format() == 0 && wt_native_init() == 0) {
+ wt_nvm_mark_reformatted();
+ status = wt_native_iak_generate();
+ }
+ }
+ if (status == PSA_SUCCESS) {
+ status = wt_native_iak_export();
+ }
+ }
+ if (status != PSA_SUCCESS) {
+ return WH_ERROR_ABORTED;
+ }
+ g_native_attest_ready = true;
+ return WH_ERROR_OK;
+}
+
+int wt_hsm_attest_bootstrap(void)
+{
+ /* No server tasklet to pump in the native engine: provisioning runs on
+ * the boot stack against the vault directly. */
+ return wt_hsm_attest_init();
+}
+
+int wt_hsm_attest_sign(const uint8_t* digest, size_t digestSize,
+ uint8_t* signature, size_t signatureCapacity,
+ size_t* signatureSize)
+{
+ psa_status_t status;
+
+ /* Same argument contract as the hsm engine: reject NULL/undersized
+ * buffers here so a bad caller cannot fault the key backend. */
+ if (signatureSize == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+ *signatureSize = 0u;
+ if (!g_native_attest_ready) {
+ return WH_ERROR_NOTREADY;
+ }
+ if (digest == NULL || digestSize != 32u || signature == NULL ||
+ signatureCapacity < 64u) {
+ return WH_ERROR_BADARGS;
+ }
+ status = wt_hsm_key_backend.sign(WT_NATIVE_IAK_OWNER, WT_NATIVE_IAK_SUB,
+ WT_NATIVE_IAK_UID, digest, digestSize,
+ signature, signatureCapacity,
+ signatureSize);
+ return (status == PSA_SUCCESS) ? WH_ERROR_OK : WH_ERROR_ABORTED;
+}
+
+int wt_hsm_attest_public_key(uint8_t* publicKey, size_t publicKeyCapacity,
+ size_t* publicKeySize)
+{
+ psa_status_t status;
+
+ /* Report the required size and reject NULL/undersized buffers as the hsm
+ * engine does, before forwarding to the key backend. */
+ if (publicKeySize == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+ *publicKeySize = WT_VAULT_KEY_PUB_LEN;
+ if (!g_native_attest_ready) {
+ return WH_ERROR_NOTREADY;
+ }
+ if (publicKey == NULL || publicKeyCapacity < WT_VAULT_KEY_PUB_LEN) {
+ return WH_ERROR_BADARGS;
+ }
+ status = wt_hsm_key_backend.export_public(WT_NATIVE_IAK_OWNER,
+ WT_NATIVE_IAK_SUB,
+ WT_NATIVE_IAK_UID, publicKey,
+ publicKeyCapacity,
+ publicKeySize);
+ return (status == PSA_SUCCESS) ? WH_ERROR_OK : WH_ERROR_ABORTED;
+}
diff --git a/src/services/native/keyvault.c b/src/services/native/keyvault.c
new file mode 100644
index 00000000..6519e5c7
--- /dev/null
+++ b/src/services/native/keyvault.c
@@ -0,0 +1,582 @@
+/* keyvault.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 .
+ */
+
+/* wolfCrypt key-op backend (WT-FFM-0046): every private-key computation runs
+ * inside the privileged vault domain against material that never leaves it.
+ * Keys are vault NVM objects (shared (owner, sub_owner, uid) directory)
+ * stored SENSITIVE + NONEXPORTABLE, so no *Checked NVM path can return the
+ * bytes, the storage face refuses key-flagged objects, and no wire op
+ * exports private material — three independent layers between a compromised
+ * Secure Partition and raw key bytes, which is the property TF-M's
+ * Crypto-partition-RAM key storage does not have. P-256 objects store
+ * [d 32][X9.63 public 65] with the public point derived once at creation;
+ * AES-256 objects store the raw 32-byte key. */
+
+/* wolfCrypt settings must come first. */
+#include "wolfssl/wolfcrypt/settings.h"
+#include "wolfssl/wolfcrypt/types.h"
+#include "wolfssl/wolfcrypt/ecc.h"
+#include "wolfssl/wolfcrypt/aes.h"
+#include "wolfssl/wolfcrypt/random.h"
+#include "wolfssl/wolfcrypt/error-crypt.h"
+
+#include "wolfhsm/wh_error.h"
+#include "wolfhsm/wh_common.h"
+#include "wolfhsm/wh_nvm.h"
+
+#include "wolftrust/services/vault_service.h"
+#include "wolftrust/services/hsm.h"
+#include "wolftrust/services/crypto_native.h"
+
+#include
+
+#define WT_HSM_KEY_P256_OBJ_LEN \
+ (WT_VAULT_KEY_MATERIAL_LEN + WT_VAULT_KEY_PUB_LEN)
+#define WT_HSM_KEY_AES_OBJ_LEN WT_VAULT_KEY_MATERIAL_LEN
+
+#define WT_HSM_KEY_DER_SIG_MAX 80U
+
+#define WT_HSM_KEY_LABEL(usage, type) \
+ (WT_VAULT_FLAG_KEY | ((usage) & WT_VAULT_KEY_USAGE_MASK) | \
+ (((uint32_t)(type) & 0xFFU) << 24))
+#define WT_HSM_KEY_TYPE_OF(flags) (((flags) >> 24) & 0xFFU)
+
+static whNvmContext* g_kv_nvm;
+static WC_RNG g_kv_rng;
+static int g_kv_rng_ready;
+
+int wt_hsm_keyvault_init(whNvmContext* nvm)
+{
+ if (nvm == NULL) {
+ return -1;
+ }
+ g_kv_nvm = nvm;
+ return 0;
+}
+
+/* Fault-recovery hook (WT-SYS-0008): the vault DRBG is the only mutable crypto
+ * state that outlives a torn native request. Invalidate it so the restarted
+ * service re-seeds on next use; a re-seed failure then fails the op closed
+ * rather than drawing from a half-updated generator. */
+void wt_hsm_keyvault_reset(void)
+{
+ if (g_kv_rng_ready != 0) {
+ (void)wc_FreeRng(&g_kv_rng);
+ g_kv_rng_ready = 0;
+ }
+}
+
+static void wt_hsm_kv_zeroize(uint8_t* buf, size_t len)
+{
+ volatile uint8_t* p = buf;
+ size_t i;
+
+ for (i = 0U; i < len; i++) {
+ p[i] = 0U;
+ }
+}
+
+static psa_status_t wt_hsm_kv_rng(WC_RNG** out)
+{
+ if (g_kv_rng_ready == 0) {
+ if (wc_InitRng_ex(&g_kv_rng, NULL, INVALID_DEVID) != 0) {
+ return PSA_ERROR_GENERIC_ERROR;
+ }
+ g_kv_rng_ready = 1;
+ }
+ *out = &g_kv_rng;
+ return PSA_SUCCESS;
+}
+
+/* Vault-domain randomness (WT-FFM-0054): serves the vault RANDOM face and
+ * the native wire so non-secure DRBG seeds come from the same vault RNG
+ * that generates key material. */
+psa_status_t wt_hsm_keyvault_random(uint8_t* out, size_t len)
+{
+ WC_RNG* rng;
+ psa_status_t status;
+
+ if (out == NULL || len == 0U) {
+ return PSA_ERROR_INVALID_ARGUMENT;
+ }
+ status = wt_hsm_kv_rng(&rng);
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ if (wc_RNG_GenerateBlock(rng, out, (word32)len) != 0) {
+ return PSA_ERROR_GENERIC_ERROR;
+ }
+ return PSA_SUCCESS;
+}
+
+/* Store new key material at a free directory slot; refuses any existing
+ * object (storage or key) at the same (owner, sub, uid) with ALREADY_EXISTS,
+ * matching psa_import_key/psa_generate_key on an occupied persistent id. */
+static psa_status_t wt_hsm_kv_store(int32_t owner, int32_t sub, uint64_t uid,
+ uint32_t type, uint32_t usage,
+ const uint8_t* obj, size_t obj_len)
+{
+ whNvmMetadata meta;
+ whNvmId free_id = WH_NVM_ID_INVALID;
+ psa_status_t status;
+ int rc;
+
+ status = wt_hsm_vault_lookup(owner, sub, uid, NULL, NULL, &free_id);
+ if (status == PSA_SUCCESS) {
+ return PSA_ERROR_ALREADY_EXISTS;
+ }
+ if (status != PSA_ERROR_DOES_NOT_EXIST) {
+ return status;
+ }
+ if (free_id == WH_NVM_ID_INVALID) {
+ return PSA_ERROR_INSUFFICIENT_STORAGE;
+ }
+ /* Reserve and reclaim before the add, exactly like wt_hsm_vault_set: key
+ * generate/destroy churn over the native wire must not fill the shared log
+ * or eat the headroom the seal-counter table and rollback floor need. */
+ status = wt_hsm_vault_reserve_object((whNvmSize)obj_len);
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ (void)memset(&meta, 0, sizeof(meta));
+ meta.id = free_id;
+ meta.access = WH_NVM_ACCESS_ANY;
+ meta.flags = WH_NVM_FLAGS_SENSITIVE | WH_NVM_FLAGS_NONEXPORTABLE;
+ meta.len = (whNvmSize)obj_len;
+ wt_hsm_vault_make_label(meta.label, owner, sub, uid,
+ WT_HSM_KEY_LABEL(usage, type));
+ rc = wh_Nvm_AddObject(g_kv_nvm, &meta, (whNvmSize)obj_len, obj);
+ return (rc == WH_ERROR_OK) ? PSA_SUCCESS : PSA_ERROR_STORAGE_FAILURE;
+}
+
+/* Load key material for one operation: the object must be a key of the
+ * expected type carrying the required usage bit. The plain (non-Checked)
+ * read is the privileged domain's internal path — NONEXPORTABLE blocks
+ * every Checked consumer. Caller zeroizes obj after use. */
+static psa_status_t wt_hsm_kv_load(int32_t owner, int32_t sub, uint64_t uid,
+ uint32_t type, uint32_t usage_needed,
+ uint8_t* obj, size_t obj_len)
+{
+ whNvmMetadata meta;
+ whNvmId id = WH_NVM_ID_INVALID;
+ uint32_t flags;
+ psa_status_t status;
+
+ if (g_kv_nvm == NULL) {
+ return PSA_ERROR_NOT_SUPPORTED;
+ }
+ status = wt_hsm_vault_lookup(owner, sub, uid, &id, &meta, NULL);
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ flags = wt_hsm_vault_flags_of(meta.label);
+ if ((flags & WT_VAULT_FLAG_KEY) == 0U) {
+ return PSA_ERROR_NOT_PERMITTED;
+ }
+ if (WT_HSM_KEY_TYPE_OF(flags) != type ||
+ (flags & usage_needed) != usage_needed) {
+ return PSA_ERROR_NOT_PERMITTED;
+ }
+ if (meta.len != obj_len) {
+ return PSA_ERROR_STORAGE_FAILURE;
+ }
+ if (wh_Nvm_Read(g_kv_nvm, id, 0U, (whNvmSize)obj_len, obj) !=
+ WH_ERROR_OK) {
+ return PSA_ERROR_STORAGE_FAILURE;
+ }
+ return PSA_SUCCESS;
+}
+
+/* Derive [d][X9.63 public] from a P-256 private scalar. */
+static psa_status_t wt_hsm_kv_p256_object(const uint8_t* d, uint8_t* obj)
+{
+ ecc_key key;
+ word32 len;
+ int rc;
+
+ rc = wc_ecc_init_ex(&key, NULL, INVALID_DEVID);
+ if (rc != 0) {
+ return PSA_ERROR_GENERIC_ERROR;
+ }
+ rc = wc_ecc_import_private_key_ex(d, WT_VAULT_KEY_MATERIAL_LEN, NULL, 0U,
+ &key, ECC_SECP256R1);
+ if (rc == 0) {
+ rc = wc_ecc_make_pub(&key, NULL);
+ }
+ if (rc == 0) {
+ (void)memcpy(obj, d, WT_VAULT_KEY_MATERIAL_LEN);
+ len = WT_VAULT_KEY_PUB_LEN;
+ rc = wc_ecc_export_x963(&key, obj + WT_VAULT_KEY_MATERIAL_LEN, &len);
+ if (rc == 0 && len != WT_VAULT_KEY_PUB_LEN) {
+ rc = -1;
+ }
+ }
+ wc_ecc_free(&key);
+ return (rc == 0) ? PSA_SUCCESS : PSA_ERROR_GENERIC_ERROR;
+}
+
+static psa_status_t wt_hsm_kv_generate(int32_t owner, int32_t sub,
+ uint64_t uid, uint32_t type,
+ uint32_t usage)
+{
+ uint8_t obj[WT_HSM_KEY_P256_OBJ_LEN];
+ uint8_t d[WT_VAULT_KEY_MATERIAL_LEN];
+ ecc_key key;
+ WC_RNG* rng;
+ word32 len;
+ psa_status_t status;
+ int rc;
+
+ if (g_kv_nvm == NULL) {
+ return PSA_ERROR_NOT_SUPPORTED;
+ }
+ if ((usage & ~WT_VAULT_KEY_USAGE_MASK) != 0U || usage == 0U) {
+ return PSA_ERROR_INVALID_ARGUMENT;
+ }
+ status = wt_hsm_kv_rng(&rng);
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ if (type == WT_VAULT_KEY_P256) {
+ rc = wc_ecc_init_ex(&key, NULL, INVALID_DEVID);
+ if (rc != 0) {
+ return PSA_ERROR_GENERIC_ERROR;
+ }
+ rc = wc_ecc_make_key_ex(rng, 32, &key, ECC_SECP256R1);
+ if (rc == 0) {
+ len = WT_VAULT_KEY_MATERIAL_LEN;
+ rc = wc_ecc_export_private_only(&key, obj, &len);
+ }
+ if (rc == 0) {
+ len = WT_VAULT_KEY_PUB_LEN;
+ rc = wc_ecc_export_x963(&key, obj + WT_VAULT_KEY_MATERIAL_LEN,
+ &len);
+ }
+ wc_ecc_free(&key);
+ if (rc != 0) {
+ wt_hsm_kv_zeroize(obj, sizeof(obj));
+ return PSA_ERROR_GENERIC_ERROR;
+ }
+ status = wt_hsm_kv_store(owner, sub, uid, type, usage, obj,
+ WT_HSM_KEY_P256_OBJ_LEN);
+ wt_hsm_kv_zeroize(obj, sizeof(obj));
+ return status;
+ }
+ if (type == WT_VAULT_KEY_AES256) {
+ if (wc_RNG_GenerateBlock(rng, d, WT_VAULT_KEY_MATERIAL_LEN) != 0) {
+ return PSA_ERROR_GENERIC_ERROR;
+ }
+ status = wt_hsm_kv_store(owner, sub, uid, type, usage, d,
+ WT_HSM_KEY_AES_OBJ_LEN);
+ wt_hsm_kv_zeroize(d, sizeof(d));
+ return status;
+ }
+ return PSA_ERROR_NOT_SUPPORTED;
+}
+
+static psa_status_t wt_hsm_kv_import(int32_t owner, int32_t sub, uint64_t uid,
+ uint32_t type, uint32_t usage,
+ const uint8_t* data, size_t len)
+{
+ uint8_t obj[WT_HSM_KEY_P256_OBJ_LEN];
+ psa_status_t status;
+
+ if (g_kv_nvm == NULL) {
+ return PSA_ERROR_NOT_SUPPORTED;
+ }
+ if ((usage & ~WT_VAULT_KEY_USAGE_MASK) != 0U || usage == 0U ||
+ len != WT_VAULT_KEY_MATERIAL_LEN) {
+ return PSA_ERROR_INVALID_ARGUMENT;
+ }
+ if (type == WT_VAULT_KEY_P256) {
+ status = wt_hsm_kv_p256_object(data, obj);
+ if (status == PSA_SUCCESS) {
+ status = wt_hsm_kv_store(owner, sub, uid, type, usage, obj,
+ WT_HSM_KEY_P256_OBJ_LEN);
+ }
+ wt_hsm_kv_zeroize(obj, sizeof(obj));
+ return status;
+ }
+ if (type == WT_VAULT_KEY_AES256) {
+ return wt_hsm_kv_store(owner, sub, uid, type, usage, data,
+ WT_HSM_KEY_AES_OBJ_LEN);
+ }
+ return PSA_ERROR_NOT_SUPPORTED;
+}
+
+/* Public halves are exportable by design; usage bits do not gate this. */
+static psa_status_t wt_hsm_kv_export_public(int32_t owner, int32_t sub,
+ uint64_t uid, uint8_t* out,
+ size_t cap, size_t* out_len)
+{
+ uint8_t obj[WT_HSM_KEY_P256_OBJ_LEN];
+ psa_status_t status;
+
+ if (cap < WT_VAULT_KEY_PUB_LEN) {
+ return PSA_ERROR_BUFFER_TOO_SMALL;
+ }
+ status = wt_hsm_kv_load(owner, sub, uid, WT_VAULT_KEY_P256, 0U, obj,
+ WT_HSM_KEY_P256_OBJ_LEN);
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ (void)memcpy(out, obj + WT_VAULT_KEY_MATERIAL_LEN, WT_VAULT_KEY_PUB_LEN);
+ wt_hsm_kv_zeroize(obj, sizeof(obj));
+ *out_len = WT_VAULT_KEY_PUB_LEN;
+ return PSA_SUCCESS;
+}
+
+static psa_status_t wt_hsm_kv_sign(int32_t owner, int32_t sub, uint64_t uid,
+ const uint8_t* digest, size_t digest_len,
+ uint8_t* sig, size_t cap, size_t* out_len)
+{
+ uint8_t obj[WT_HSM_KEY_P256_OBJ_LEN];
+ uint8_t der[WT_HSM_KEY_DER_SIG_MAX];
+ uint8_t r[WT_VAULT_KEY_MATERIAL_LEN];
+ uint8_t s[WT_VAULT_KEY_MATERIAL_LEN];
+ ecc_key key;
+ WC_RNG* rng;
+ word32 der_len = sizeof(der);
+ word32 r_len = sizeof(r);
+ word32 s_len = sizeof(s);
+ psa_status_t status;
+ int rc;
+
+ if (digest_len != WT_VAULT_KEY_DIGEST_LEN ||
+ cap < WT_VAULT_KEY_SIG_LEN) {
+ return PSA_ERROR_INVALID_ARGUMENT;
+ }
+ status = wt_hsm_kv_load(owner, sub, uid, WT_VAULT_KEY_P256,
+ WT_VAULT_KEY_USAGE_SIGN, obj,
+ WT_HSM_KEY_P256_OBJ_LEN);
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ /* Capture the vault DRBG only after the NVM lock is dropped: a
+ * fault-recovery reset during the lock wait must not free the RNG out
+ * from under this in-flight signature. */
+ status = wt_hsm_kv_rng(&rng);
+ if (status != PSA_SUCCESS) {
+ wt_hsm_kv_zeroize(obj, sizeof(obj));
+ return status;
+ }
+ rc = wc_ecc_init_ex(&key, NULL, INVALID_DEVID);
+ if (rc == 0) {
+ rc = wc_ecc_import_private_key_ex(obj, WT_VAULT_KEY_MATERIAL_LEN,
+ NULL, 0U, &key, ECC_SECP256R1);
+ if (rc == 0) {
+ rc = wc_ecc_sign_hash(digest, (word32)digest_len, der, &der_len,
+ rng, &key);
+ }
+ wc_ecc_free(&key);
+ }
+ wt_hsm_kv_zeroize(obj, sizeof(obj));
+ if (rc == 0) {
+ rc = wc_ecc_sig_to_rs(der, der_len, r, &r_len, s, &s_len);
+ }
+ if (rc != 0 || r_len > 32U || s_len > 32U) {
+ return PSA_ERROR_GENERIC_ERROR;
+ }
+ /* Fixed-width raw r||s, left-padded with zeros. */
+ (void)memset(sig, 0, WT_VAULT_KEY_SIG_LEN);
+ (void)memcpy(sig + (32U - r_len), r, r_len);
+ (void)memcpy(sig + 32U + (32U - s_len), s, s_len);
+ *out_len = WT_VAULT_KEY_SIG_LEN;
+ return PSA_SUCCESS;
+}
+
+static psa_status_t wt_hsm_kv_verify(int32_t owner, int32_t sub, uint64_t uid,
+ const uint8_t* digest, size_t digest_len,
+ const uint8_t* sig, size_t sig_len)
+{
+ uint8_t obj[WT_HSM_KEY_P256_OBJ_LEN];
+ uint8_t der[WT_HSM_KEY_DER_SIG_MAX];
+ ecc_key key;
+ word32 der_len = sizeof(der);
+ int res = 0;
+ psa_status_t status;
+ int rc;
+
+ if (digest_len != WT_VAULT_KEY_DIGEST_LEN ||
+ sig_len != WT_VAULT_KEY_SIG_LEN) {
+ return PSA_ERROR_INVALID_ARGUMENT;
+ }
+ status = wt_hsm_kv_load(owner, sub, uid, WT_VAULT_KEY_P256,
+ WT_VAULT_KEY_USAGE_VERIFY, obj,
+ WT_HSM_KEY_P256_OBJ_LEN);
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ rc = wc_ecc_rs_raw_to_sig(sig, 32U, sig + 32U, 32U, der, &der_len);
+ if (rc == 0) {
+ rc = wc_ecc_init_ex(&key, NULL, INVALID_DEVID);
+ }
+ if (rc == 0) {
+ rc = wc_ecc_import_x963_ex(obj + WT_VAULT_KEY_MATERIAL_LEN,
+ WT_VAULT_KEY_PUB_LEN, &key, ECC_SECP256R1);
+ if (rc == 0) {
+ rc = wc_ecc_verify_hash(der, der_len, digest, (word32)digest_len,
+ &res, &key);
+ }
+ wc_ecc_free(&key);
+ }
+ wt_hsm_kv_zeroize(obj, sizeof(obj));
+ if (rc != 0) {
+ return PSA_ERROR_GENERIC_ERROR;
+ }
+ return (res == 1) ? PSA_SUCCESS : PSA_ERROR_INVALID_SIGNATURE;
+}
+
+static psa_status_t wt_hsm_kv_encrypt(int32_t owner, int32_t sub,
+ uint64_t uid, const uint8_t* input,
+ size_t input_len, uint8_t* out,
+ size_t cap, size_t* out_len)
+{
+ uint8_t obj[WT_HSM_KEY_AES_OBJ_LEN];
+ Aes aes;
+ WC_RNG* rng;
+ psa_status_t status;
+ int rc;
+
+ if (cap < input_len + WT_VAULT_KEY_NONCE_LEN + WT_VAULT_KEY_TAG_LEN) {
+ return PSA_ERROR_BUFFER_TOO_SMALL;
+ }
+ status = wt_hsm_kv_load(owner, sub, uid, WT_VAULT_KEY_AES256,
+ WT_VAULT_KEY_USAGE_ENCRYPT, obj,
+ WT_HSM_KEY_AES_OBJ_LEN);
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ /* Capture the vault DRBG only after the NVM lock is dropped (see the
+ * sign path): a fault-recovery reset during the lock wait must not free
+ * the RNG mid-encrypt. */
+ status = wt_hsm_kv_rng(&rng);
+ if (status != PSA_SUCCESS) {
+ wt_hsm_kv_zeroize(obj, sizeof(obj));
+ return status;
+ }
+ rc = wc_RNG_GenerateBlock(rng, out, WT_VAULT_KEY_NONCE_LEN);
+ if (rc == 0) {
+ rc = wc_AesInit(&aes, NULL, INVALID_DEVID);
+ if (rc == 0) {
+ rc = wc_AesGcmSetKey(&aes, obj, WT_VAULT_KEY_MATERIAL_LEN);
+ if (rc == 0) {
+ rc = wc_AesGcmEncrypt(&aes, out + WT_VAULT_KEY_NONCE_LEN,
+ input, (word32)input_len, out,
+ WT_VAULT_KEY_NONCE_LEN,
+ out + WT_VAULT_KEY_NONCE_LEN +
+ input_len,
+ WT_VAULT_KEY_TAG_LEN, NULL, 0U);
+ }
+ wc_AesFree(&aes);
+ }
+ }
+ wt_hsm_kv_zeroize(obj, sizeof(obj));
+ if (rc != 0) {
+ return PSA_ERROR_GENERIC_ERROR;
+ }
+ *out_len = input_len + WT_VAULT_KEY_NONCE_LEN + WT_VAULT_KEY_TAG_LEN;
+ return PSA_SUCCESS;
+}
+
+static psa_status_t wt_hsm_kv_decrypt(int32_t owner, int32_t sub,
+ uint64_t uid, const uint8_t* input,
+ size_t input_len, uint8_t* out,
+ size_t cap, size_t* out_len)
+{
+ uint8_t obj[WT_HSM_KEY_AES_OBJ_LEN];
+ Aes aes;
+ size_t pt_len;
+ psa_status_t status;
+ int rc;
+
+ if (input_len < WT_VAULT_KEY_NONCE_LEN + WT_VAULT_KEY_TAG_LEN) {
+ return PSA_ERROR_INVALID_ARGUMENT;
+ }
+ pt_len = input_len - WT_VAULT_KEY_NONCE_LEN - WT_VAULT_KEY_TAG_LEN;
+ if (cap < pt_len) {
+ return PSA_ERROR_BUFFER_TOO_SMALL;
+ }
+ status = wt_hsm_kv_load(owner, sub, uid, WT_VAULT_KEY_AES256,
+ WT_VAULT_KEY_USAGE_DECRYPT, obj,
+ WT_HSM_KEY_AES_OBJ_LEN);
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ rc = wc_AesInit(&aes, NULL, INVALID_DEVID);
+ if (rc == 0) {
+ rc = wc_AesGcmSetKey(&aes, obj, WT_VAULT_KEY_MATERIAL_LEN);
+ if (rc == 0) {
+ rc = wc_AesGcmDecrypt(&aes, out,
+ input + WT_VAULT_KEY_NONCE_LEN,
+ (word32)pt_len, input,
+ WT_VAULT_KEY_NONCE_LEN,
+ input + input_len - WT_VAULT_KEY_TAG_LEN,
+ WT_VAULT_KEY_TAG_LEN, NULL, 0U);
+ }
+ wc_AesFree(&aes);
+ }
+ wt_hsm_kv_zeroize(obj, sizeof(obj));
+ if (rc != 0) {
+ /* wolfSSL leaves out[] undefined after a non-zero AES-GCM decrypt;
+ * clear the plaintext region so no unauthenticated bytes survive in
+ * the shared response band. */
+ wt_hsm_kv_zeroize(out, pt_len);
+ return (rc == AES_GCM_AUTH_E) ? PSA_ERROR_INVALID_SIGNATURE :
+ PSA_ERROR_GENERIC_ERROR;
+ }
+ *out_len = pt_len;
+ return PSA_SUCCESS;
+}
+
+/* Key destruction is key-typed on purpose: the storage-face remove applies
+ * PSA storage semantics to the label's low bits, where a key object keeps its
+ * usage mask, so it would read a signing key's usage bit as WRITE_ONCE. */
+psa_status_t wt_hsm_keyvault_destroy(int32_t owner, int32_t sub, uint64_t uid)
+{
+ whNvmMetadata meta;
+ whNvmId id = WH_NVM_ID_INVALID;
+ psa_status_t status;
+
+ if (g_kv_nvm == NULL) {
+ return PSA_ERROR_NOT_SUPPORTED;
+ }
+ status = wt_hsm_vault_lookup(owner, sub, uid, &id, &meta, NULL);
+ if (status != PSA_SUCCESS) {
+ return status;
+ }
+ if ((wt_hsm_vault_flags_of(meta.label) & WT_VAULT_FLAG_KEY) == 0U) {
+ return PSA_ERROR_NOT_PERMITTED;
+ }
+ if (wh_Nvm_DestroyObjectsChecked(g_kv_nvm, 1U, &id) != WH_ERROR_OK) {
+ return PSA_ERROR_STORAGE_FAILURE;
+ }
+ return PSA_SUCCESS;
+}
+
+const wt_vault_key_backend_t wt_hsm_key_backend = {
+ wt_hsm_kv_generate,
+ wt_hsm_kv_import,
+ wt_hsm_kv_export_public,
+ wt_hsm_kv_sign,
+ wt_hsm_kv_verify,
+ wt_hsm_kv_encrypt,
+ wt_hsm_kv_decrypt
+};
diff --git a/src/services/native/native_wire.c b/src/services/native/native_wire.c
new file mode 100644
index 00000000..4eafe6ed
--- /dev/null
+++ b/src/services/native/native_wire.c
@@ -0,0 +1,175 @@
+/* native_wire.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 .
+ */
+
+/*
+ * Native crypto wire: SERVICE_HSM's submit backend for WT_ENGINE=native.
+ * Port-free by design (no flash, boot, or attestation dependencies) so the
+ * packet parser and every operation are host-testable against a RAM-backed
+ * NVM store.
+ */
+
+/* wolfCrypt settings must come first. */
+#include "wolfssl/wolfcrypt/settings.h"
+#include "wolfssl/wolfcrypt/types.h"
+#include "wolfssl/wolfcrypt/sha256.h"
+#include "wolfssl/wolfcrypt/error-crypt.h"
+
+#include "wolftrust/services/hsm.h"
+#include "wolftrust/services/vault_service.h"
+#include "wolftrust/services/crypto_native.h"
+
+#include
+#include
+
+/* =========================================================================
+ * wt_native_submit — SERVICE_HSM's native wire backend.
+ *
+ * One request packet ([wt_crypto_wire_req_t][payload]) in, one response
+ * packet ([int32_t psa_status][payload]) out, both bounded by the relay's
+ * copied buffers. Key ops execute in the key backend with the SPM-stamped
+ * client as delegated sub_owner, so a client only reaches its own keys.
+ * ====================================================================== */
+static psa_status_t wt_native_hash(const uint8_t* input, size_t input_len,
+ uint8_t* out, size_t out_cap,
+ size_t* out_len)
+{
+ wc_Sha256 sha;
+ int rc;
+
+ if (out_cap < WC_SHA256_DIGEST_SIZE) {
+ return PSA_ERROR_BUFFER_TOO_SMALL;
+ }
+ rc = wc_InitSha256_ex(&sha, NULL, INVALID_DEVID);
+ if (rc == 0) {
+ rc = wc_Sha256Update(&sha, input, (word32)input_len);
+ }
+ if (rc == 0) {
+ rc = wc_Sha256Final(&sha, out);
+ }
+ if (rc != 0) {
+ return PSA_ERROR_GENERIC_ERROR;
+ }
+ *out_len = WC_SHA256_DIGEST_SIZE;
+ return PSA_SUCCESS;
+}
+
+int wt_native_submit(void* submit_ctx, int32_t client_id, const uint8_t* req,
+ size_t req_len, uint8_t* resp, size_t resp_cap,
+ size_t* resp_len)
+{
+ wt_crypto_wire_req_t hdr;
+ int32_t owner = (int32_t)(intptr_t)submit_ctx;
+ const uint8_t* payload;
+ uint8_t* out;
+ size_t payload_len;
+ size_t out_cap;
+ size_t out_len = 0U;
+ size_t rand_len;
+ int32_t wire_status;
+ psa_status_t status;
+
+ if (req == NULL || resp == NULL || resp_len == NULL ||
+ req_len < sizeof(hdr) || resp_cap < sizeof(wire_status)) {
+ return -1;
+ }
+ (void)memcpy(&hdr, req, sizeof(hdr));
+ payload = req + sizeof(hdr);
+ payload_len = req_len - sizeof(hdr);
+ out = resp + sizeof(wire_status);
+ out_cap = resp_cap - sizeof(wire_status);
+
+ /* Strict parser: reserved must be zero so a future op cannot repurpose
+ * it as a flag a current client left set. Clients are told to zero it. */
+ if (hdr.reserved != 0U) {
+ status = PSA_ERROR_INVALID_ARGUMENT;
+ }
+ else switch (hdr.op) {
+ case WT_CRYPTO_OP_KEY_GENERATE:
+ status = wt_hsm_key_backend.generate(owner, client_id, hdr.uid,
+ hdr.key_type, hdr.usage);
+ break;
+ case WT_CRYPTO_OP_KEY_IMPORT:
+ status = wt_hsm_key_backend.import(owner, client_id, hdr.uid,
+ hdr.key_type, hdr.usage, payload,
+ payload_len);
+ break;
+ case WT_CRYPTO_OP_KEY_EXPORT_PUBLIC:
+ status = wt_hsm_key_backend.export_public(owner, client_id, hdr.uid,
+ out, out_cap, &out_len);
+ break;
+ case WT_CRYPTO_OP_KEY_SIGN:
+ status = wt_hsm_key_backend.sign(owner, client_id, hdr.uid, payload,
+ payload_len, out, out_cap,
+ &out_len);
+ break;
+ case WT_CRYPTO_OP_KEY_VERIFY:
+ /* payload = [digest 32][signature 64]. */
+ if (payload_len != WT_VAULT_KEY_DIGEST_LEN + WT_VAULT_KEY_SIG_LEN) {
+ status = PSA_ERROR_INVALID_ARGUMENT;
+ }
+ else {
+ status = wt_hsm_key_backend.verify(owner, client_id, hdr.uid,
+ payload,
+ WT_VAULT_KEY_DIGEST_LEN,
+ payload +
+ WT_VAULT_KEY_DIGEST_LEN,
+ WT_VAULT_KEY_SIG_LEN);
+ }
+ break;
+ case WT_CRYPTO_OP_KEY_ENCRYPT:
+ status = wt_hsm_key_backend.encrypt(owner, client_id, hdr.uid,
+ payload, payload_len, out,
+ out_cap, &out_len);
+ break;
+ case WT_CRYPTO_OP_KEY_DECRYPT:
+ status = wt_hsm_key_backend.decrypt(owner, client_id, hdr.uid,
+ payload, payload_len, out,
+ out_cap, &out_len);
+ break;
+ case WT_CRYPTO_OP_KEY_DESTROY:
+ status = wt_hsm_keyvault_destroy(owner, client_id, hdr.uid);
+ break;
+ case WT_CRYPTO_OP_RANDOM:
+ rand_len = hdr.usage;
+ if (rand_len == 0U || rand_len > WT_CRYPTO_RANDOM_MAX ||
+ rand_len > out_cap) {
+ status = PSA_ERROR_INVALID_ARGUMENT;
+ }
+ else {
+ status = wt_hsm_keyvault_random(out, rand_len);
+ if (status == PSA_SUCCESS) {
+ out_len = rand_len;
+ }
+ }
+ break;
+ case WT_CRYPTO_OP_HASH:
+ status = wt_native_hash(payload, payload_len, out, out_cap,
+ &out_len);
+ break;
+ default:
+ status = PSA_ERROR_NOT_SUPPORTED;
+ break;
+ }
+
+ wire_status = (int32_t)status;
+ (void)memcpy(resp, &wire_status, sizeof(wire_status));
+ *resp_len = sizeof(wire_status) + out_len;
+ return 0;
+}
diff --git a/src/services/nvm_store.c b/src/services/nvm_store.c
new file mode 100644
index 00000000..58f185d3
--- /dev/null
+++ b/src/services/nvm_store.c
@@ -0,0 +1,291 @@
+/* nvm_store.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 .
+ */
+
+/*
+ * Engine-independent secure NVM store: the shared flash-backed object store,
+ * its serialisation lock, the boot lifecycle latch, and the WT-FFM-0050
+ * anti-rollback floors. Linked in both crypto engines — the wolfHSM server
+ * (hsm engine) and the native dispatch both ride this store.
+ */
+
+/* wolfCrypt settings must come first. */
+#include "wolfssl/wolfcrypt/settings.h"
+
+#include "wolfhsm/wh_error.h"
+#include "wolfhsm/wh_nvm.h"
+#include "wolfhsm/wh_nvm_flash.h"
+#include "wolfhsm/wh_lock.h"
+
+#include "wolftrust/types.h"
+#include "wolftrust/guest_verify.h"
+#include "wolftrust/monitor.h"
+#include "wolftrust/platform.h"
+#include "wolftrust/rollback.h"
+#include "wolftrust/sync/mutex.h"
+#include "wolftrust/nvm_store.h"
+#include "wolftrust/services/hsm.h"
+
+#include "wolftrust/port_nvm.h"
+#include "psa/lifecycle.h"
+
+#include
+#include
+
+#ifndef WOLFHSM_CFG_THREADSAFE
+#error "wolfTrust requires WOLFHSM_CFG_THREADSAFE for the shared NVM store"
+#endif
+
+/* -------------------------------------------------------------------------
+ * Shared NVM state (one instance, serialised by g_wt_nvm_lock_mutex).
+ * ---------------------------------------------------------------------- */
+whNvmContext g_wt_nvm_ctx;
+static whNvmFlashContext g_nvm_flash_ctx;
+
+static const whNvmCb g_nvm_flash_cb[1] = {WH_NVM_FLASH_CB};
+
+/* -------------------------------------------------------------------------
+ * Shared NVM lock.
+ *
+ * g_wt_hsm_lock_cb is defined in the parallel Wave 3B file wt_hsm_lock.c.
+ * Its callbacks dispatch acquire/release to g_wt_nvm_lock_mutex.
+ * ---------------------------------------------------------------------- */
+wt_mutex_t g_wt_nvm_lock_mutex;
+static whLockConfig g_nvm_lock_cfg;
+extern const whLockCb g_wt_hsm_lock_cb; /* defined in wt_hsm_lock.c */
+
+/* -------------------------------------------------------------------------
+ * Store recovery policy. A pool written by an older firmware generation (or
+ * a corrupt one) can block boot provisioning; recovery reformats it, but
+ * only in an unlocked development lifecycle — a SECURED device must never
+ * auto-wipe WRITE_ONCE storage or the sealed device key, so an unset or
+ * unknown lifecycle stays locked.
+ * ---------------------------------------------------------------------- */
+static uint32_t g_boot_lifecycle; /* PSA lifecycle from wolfBoot handoff */
+static int g_vault_reformatted; /* observability: reformatted this boot */
+
+void wt_hsm_set_boot_lifecycle(uint32_t lifecycle)
+{
+ g_boot_lifecycle = lifecycle;
+}
+
+int wt_hsm_vault_was_reformatted(void)
+{
+ return g_vault_reformatted;
+}
+
+void wt_nvm_mark_reformatted(void)
+{
+ g_vault_reformatted = 1;
+}
+
+int wt_nvm_reformat_allowed(void)
+{
+ return (g_boot_lifecycle == PSA_LIFECYCLE_ASSEMBLY_AND_TEST) ||
+ (g_boot_lifecycle == PSA_LIFECYCLE_PSA_ROT_PROVISIONING);
+}
+
+int wt_nvm_store_bind(void)
+{
+ whNvmFlashConfig nvm_flash_cfg;
+ whNvmConfig nvm_cfg;
+
+ (void)memset(&g_nvm_flash_ctx, 0, sizeof(g_nvm_flash_ctx));
+ (void)memset(&nvm_flash_cfg, 0, sizeof(nvm_flash_cfg));
+ nvm_flash_cfg.cb = &g_wt_hsm_flash_cb;
+ nvm_flash_cfg.context = wt_hsm_flash_context();
+ nvm_flash_cfg.config = wt_hsm_flash_config();
+
+ g_nvm_lock_cfg.cb = &g_wt_hsm_lock_cb;
+ g_nvm_lock_cfg.context = &g_wt_nvm_lock_mutex;
+ g_nvm_lock_cfg.config = NULL;
+
+ (void)memset(&g_wt_nvm_ctx, 0, sizeof(g_wt_nvm_ctx));
+ (void)memset(&nvm_cfg, 0, sizeof(nvm_cfg));
+ nvm_cfg.cb = (whNvmCb *)g_nvm_flash_cb;
+ nvm_cfg.context = &g_nvm_flash_ctx;
+ nvm_cfg.config = &nvm_flash_cfg;
+ nvm_cfg.lockConfig = &g_nvm_lock_cfg;
+
+ return wh_Nvm_Init(&g_wt_nvm_ctx, &nvm_cfg);
+}
+
+void wt_hsm_release_locks(struct wt_co *co)
+{
+ /* Drop every secure-side store lock the faulted coroutine still held, and
+ * unlink it if it died parked as a waiter, so no later acquirer deadlocks
+ * behind a dead holder or a dead queued waiter. The NVM lock is the only
+ * such mutex today; add any future ones here. Recovery runs this before
+ * the partition is restarted, so the waiter is gone before it can
+ * re-enqueue. */
+ if (co != NULL) {
+ wt_mutex_release_if_holder(&g_wt_nvm_lock_mutex, co);
+ wt_mutex_remove_waiter(&g_wt_nvm_lock_mutex, co);
+ }
+}
+
+wt_mutex_t *wt_hsm_nvm_lock_mutex(void)
+{
+ return &g_wt_nvm_lock_mutex;
+}
+
+/* -------------------------------------------------------------------------
+ * WT-FFM-0050 firmware anti-rollback: monotonic version floors in a plain
+ * NVM object (WT_HSM_ROLLBACK_TABLE_ID), same access idiom as the vault
+ * counter table. Runs on the boot stack after the store binds and before
+ * the first dispatch.
+ * ---------------------------------------------------------------------- */
+static int wt_nvm_rollback_load(wt_rollback_table_t* table)
+{
+ whNvmMetadata meta;
+ int rc;
+
+ rc = wh_Nvm_GetMetadata(&g_wt_nvm_ctx, WT_HSM_ROLLBACK_TABLE_ID, &meta);
+ if (rc == WH_ERROR_NOTFOUND) {
+ wt_rollback_table_init(table);
+ return 0;
+ }
+ if (rc != WH_ERROR_OK || meta.len != sizeof(*table)) {
+ return -1;
+ }
+ rc = wh_Nvm_Read(&g_wt_nvm_ctx, WT_HSM_ROLLBACK_TABLE_ID, 0U,
+ (whNvmSize)sizeof(*table), (uint8_t*)table);
+ if (rc != WH_ERROR_OK || !wt_rollback_table_valid(table)) {
+ return -1;
+ }
+ return 0;
+}
+
+static int wt_nvm_rollback_store(const wt_rollback_table_t* table)
+{
+ whNvmMetadata meta;
+ int rc;
+
+ (void)memset(&meta, 0, sizeof(meta));
+ meta.id = WT_HSM_ROLLBACK_TABLE_ID;
+ meta.access = WH_NVM_ACCESS_ANY;
+ meta.flags = 0U;
+ meta.len = (whNvmSize)sizeof(*table);
+ rc = wh_Nvm_AddObject(&g_wt_nvm_ctx, &meta, (whNvmSize)sizeof(*table),
+ (const uint8_t*)table);
+ return (rc == WH_ERROR_OK) ? 0 : -1;
+}
+
+static uint32_t g_active_image_version;
+
+uint32_t wt_hsm_active_image_version(void)
+{
+ return g_active_image_version;
+}
+
+int wt_hsm_rollback_enforce(uint32_t image_version)
+{
+ wt_rollback_table_t table;
+ const wt_guest_measurement_t* records;
+ size_t record_count = 0U;
+ size_t guest_count;
+ size_t i;
+ int refused_platform = 0;
+ int changed = 0;
+
+ g_active_image_version = image_version;
+ guest_count = wt_monitor_state()->guest_count;
+
+ if (wt_nvm_rollback_load(&table) != 0) {
+ /* An unreadable floor cannot prove anything: fail closed. */
+ refused_platform = 1;
+ }
+
+#if defined(WT_ROLLBACK_PROBE)
+ /* Negative test: force the locked lifecycle (the emulator chain boots in
+ * assembly-and-test, which rightly bypasses enforcement), then on the
+ * first pass arm the image floor one above the running version and
+ * reboot, so the second pass exercises the real downgrade refusal
+ * against a floor that survived SYSRESETREQ. A failed arming store is a
+ * broken test, not a refusal: trap loudly. */
+ g_boot_lifecycle = PSA_LIFECYCLE_SECURED;
+ if (!refused_platform && table.image_floor <= image_version) {
+ table.image_floor = image_version + 1U;
+ if (wt_nvm_rollback_store(&table) != 0) {
+ wt_platform_panic();
+ }
+ wt_platform_system_reset();
+ }
+#endif
+
+ if (!refused_platform &&
+ wt_rollback_check(g_boot_lifecycle, image_version,
+ table.image_floor) != WT_ROLLBACK_OK) {
+ refused_platform = 1;
+ }
+
+ records = wt_platform_guest_measurements(&record_count);
+
+ if (refused_platform) {
+ for (i = 0U; i < guest_count; i++) {
+ wt_monitor_quarantine_guest((wt_guest_id_t)i);
+ }
+ return WT_ROLLBACK_REFUSED;
+ }
+
+ changed = wt_rollback_advance(image_version, &table.image_floor);
+ for (i = 0U; records != NULL && i < record_count; i++) {
+ uint32_t guest = records[i].guest_id;
+
+ if (guest >= WT_GUEST_MEAS_MAX_RECORDS) {
+ continue;
+ }
+ if (wt_rollback_check(g_boot_lifecycle, records[i].version,
+ table.guest_floor[guest]) != WT_ROLLBACK_OK) {
+ wt_monitor_quarantine_guest((wt_guest_id_t)guest);
+ }
+ else if (wt_rollback_advance(records[i].version,
+ &table.guest_floor[guest]) != 0) {
+ changed = 1;
+ }
+ }
+
+ if (changed && wt_nvm_rollback_store(&table) != 0) {
+ /* An unpersisted floor must not launch guests: the next reset would
+ * accept the previous floor again. Quarantine fail-closed; secure
+ * services stay up so the wedge is observable and recoverable. */
+ for (i = 0U; i < guest_count; i++) {
+ wt_monitor_quarantine_guest((wt_guest_id_t)i);
+ }
+ return WT_ROLLBACK_REFUSED;
+ }
+
+ return WT_ROLLBACK_OK;
+}
+
+int wt_hsm_rollback_image_floor(uint32_t* floor)
+{
+ wt_rollback_table_t table;
+
+ if (floor == NULL) {
+ return -1;
+ }
+ if (wt_nvm_rollback_load(&table) != 0) {
+ return -1;
+ }
+ /* The unlocked provisioning lifecycles bypass refusal at boot; the
+ * staging floor mirrors that so development flows are never bricked. */
+ *floor = wt_nvm_reformat_allowed() ? 0U : table.image_floor;
+ return 0;
+}
diff --git a/src/services/wolfhsm/runner/secure.ld b/src/services/wolfhsm/runner/secure.ld
index d0d02774..d0ab5cf5 100644
--- a/src/services/wolfhsm/runner/secure.ld
+++ b/src/services/wolfhsm/runner/secure.ld
@@ -159,6 +159,9 @@ SECTIONS {
. = ALIGN(32);
_s_keystore = .;
*wt_hsm.o(.data .data.*)
+ *nvm_store.o(.data .data.*)
+ *crypto_native.o(.data .data.*)
+ *keyvault.o(.data .data.*)
*wt_hsm_lock.o(.data .data.*)
*wt_hsm_seal.o(.data .data.*)
*wt_hsm_vault.o(.data .data.*)
@@ -180,6 +183,9 @@ SECTIONS {
.keystore_bss (NOLOAD) : {
_s_keystore_bss = .;
*wt_hsm.o(.bss .bss.* COMMON)
+ *nvm_store.o(.bss .bss.* COMMON)
+ *crypto_native.o(.bss .bss.* COMMON)
+ *keyvault.o(.bss .bss.* COMMON)
*wt_hsm_lock.o(.bss .bss.* COMMON)
*wt_hsm_seal.o(.bss .bss.* COMMON)
*wt_hsm_vault.o(.bss .bss.* COMMON)
diff --git a/src/services/wolfhsm/wt_hsm.c b/src/services/wolfhsm/wt_hsm.c
index 3808a294..90965fdb 100644
--- a/src/services/wolfhsm/wt_hsm.c
+++ b/src/services/wolfhsm/wt_hsm.c
@@ -67,6 +67,7 @@
#include "wolftrust/rollback.h"
#include "wolftrust/sched/tasklet.h"
#include "wolftrust/sync/mutex.h"
+#include "wolftrust/nvm_store.h"
#include "wolftrust/services/hsm.h"
#include "wolftrust/services/hsm_relay.h"
#include "wolftrust/services/vault_service.h"
@@ -132,34 +133,8 @@ static int g_attest_init_status = WH_ERROR_NOTREADY;
static bool g_attest_init_attempted;
static bool g_attest_ready;
-/* -------------------------------------------------------------------------
- * Shared NVM state (one instance, serialised by g_nvm_lock_mutex).
- * ---------------------------------------------------------------------- */
-static whNvmContext g_nvm_ctx;
-static whNvmFlashContext g_nvm_flash_ctx;
-
-static const whNvmCb g_nvm_flash_cb[1] = {WH_NVM_FLASH_CB};
-
-/* -------------------------------------------------------------------------
- * Shared NVM lock.
- *
- * g_wt_hsm_lock_cb is defined in the parallel Wave 3B file wt_hsm_lock.c.
- * Its callbacks dispatch acquire/release to g_nvm_lock_mutex.
- * ---------------------------------------------------------------------- */
-static wt_mutex_t g_nvm_lock_mutex;
-static whLockConfig g_nvm_lock_cfg;
-extern const whLockCb g_wt_hsm_lock_cb; /* defined in wt_hsm_lock.c */
-
-/* -------------------------------------------------------------------------
- * Vault recovery policy. A vault pool written by an older firmware generation
- * (or a corrupt one) can block boot provisioning: the IAK slot is held by a
- * NONMODIFIABLE object, so a fresh keygen commit returns WH_ERROR_ACCESS.
- * The recovery reformats and re-provisions, but only in an unlocked
- * development lifecycle -- a SECURED device must never auto-wipe WRITE_ONCE
- * storage or the sealed device key, so an unset/unknown lifecycle stays locked.
- * ---------------------------------------------------------------------- */
-static uint32_t g_boot_lifecycle; /* PSA lifecycle from wolfBoot handoff */
-static int g_vault_reformatted; /* observability: reformatted this boot */
+/* The shared NVM store, lock, lifecycle latch, and rollback floors moved to
+ * the engine-independent src/services/nvm_store.c (wolftrust/nvm_store.h). */
#if defined(WT_VAULT_FOREIGN_PROBE)
/* Negative test: make the first provisioning look blocked, as if a
@@ -169,167 +144,6 @@ static int g_vault_reformatted; /* observability: reformatted this boot
static int g_foreign_probe_fired;
#endif
-void wt_hsm_set_boot_lifecycle(uint32_t lifecycle)
-{
- g_boot_lifecycle = lifecycle;
-}
-
-int wt_hsm_vault_was_reformatted(void)
-{
- return g_vault_reformatted;
-}
-
-static int wt_hsm_reformat_allowed(void)
-{
- return (g_boot_lifecycle == PSA_LIFECYCLE_ASSEMBLY_AND_TEST) ||
- (g_boot_lifecycle == PSA_LIFECYCLE_PSA_ROT_PROVISIONING);
-}
-
-/* -------------------------------------------------------------------------
- * WT-FFM-0050 firmware anti-rollback: monotonic version floors in a plain
- * NVM object (WT_HSM_ROLLBACK_TABLE_ID), same access idiom as the vault
- * counter table. Runs on the boot stack after wt_hsm_init and before the
- * first dispatch.
- * ---------------------------------------------------------------------- */
-static int wt_hsm_rollback_load(wt_rollback_table_t* table)
-{
- whNvmMetadata meta;
- int rc;
-
- rc = wh_Nvm_GetMetadata(&g_nvm_ctx, WT_HSM_ROLLBACK_TABLE_ID, &meta);
- if (rc == WH_ERROR_NOTFOUND) {
- wt_rollback_table_init(table);
- return 0;
- }
- if (rc != WH_ERROR_OK || meta.len != sizeof(*table)) {
- return -1;
- }
- rc = wh_Nvm_Read(&g_nvm_ctx, WT_HSM_ROLLBACK_TABLE_ID, 0U,
- (whNvmSize)sizeof(*table), (uint8_t*)table);
- if (rc != WH_ERROR_OK || !wt_rollback_table_valid(table)) {
- return -1;
- }
- return 0;
-}
-
-static int wt_hsm_rollback_store(const wt_rollback_table_t* table)
-{
- whNvmMetadata meta;
- int rc;
-
- (void)memset(&meta, 0, sizeof(meta));
- meta.id = WT_HSM_ROLLBACK_TABLE_ID;
- meta.access = WH_NVM_ACCESS_ANY;
- meta.flags = 0U;
- meta.len = (whNvmSize)sizeof(*table);
- rc = wh_Nvm_AddObject(&g_nvm_ctx, &meta, (whNvmSize)sizeof(*table),
- (const uint8_t*)table);
- return (rc == WH_ERROR_OK) ? 0 : -1;
-}
-
-static uint32_t g_active_image_version;
-
-uint32_t wt_hsm_active_image_version(void)
-{
- return g_active_image_version;
-}
-
-int wt_hsm_rollback_enforce(uint32_t image_version)
-{
- wt_rollback_table_t table;
- const wt_guest_measurement_t* records;
- size_t record_count = 0U;
- size_t guest_count;
- size_t i;
- int refused_platform = 0;
- int changed = 0;
-
- g_active_image_version = image_version;
- guest_count = wt_monitor_state()->guest_count;
-
- if (wt_hsm_rollback_load(&table) != 0) {
- /* An unreadable floor cannot prove anything: fail closed. */
- refused_platform = 1;
- }
-
-#if defined(WT_ROLLBACK_PROBE)
- /* Negative test: force the locked lifecycle (the emulator chain boots in
- * assembly-and-test, which rightly bypasses enforcement), then on the
- * first pass arm the image floor one above the running version and
- * reboot, so the second pass exercises the real downgrade refusal
- * against a floor that survived SYSRESETREQ. A failed arming store is a
- * broken test, not a refusal: trap loudly. */
- g_boot_lifecycle = PSA_LIFECYCLE_SECURED;
- if (!refused_platform && table.image_floor <= image_version) {
- table.image_floor = image_version + 1U;
- if (wt_hsm_rollback_store(&table) != 0) {
- wt_platform_panic();
- }
- wt_platform_system_reset();
- }
-#endif
-
- if (!refused_platform &&
- wt_rollback_check(g_boot_lifecycle, image_version,
- table.image_floor) != WT_ROLLBACK_OK) {
- refused_platform = 1;
- }
-
- records = wt_platform_guest_measurements(&record_count);
-
- if (refused_platform) {
- for (i = 0U; i < guest_count; i++) {
- wt_monitor_quarantine_guest((wt_guest_id_t)i);
- }
- return WT_ROLLBACK_REFUSED;
- }
-
- changed = wt_rollback_advance(image_version, &table.image_floor);
- for (i = 0U; records != NULL && i < record_count; i++) {
- uint32_t guest = records[i].guest_id;
-
- if (guest >= WT_GUEST_MEAS_MAX_RECORDS) {
- continue;
- }
- if (wt_rollback_check(g_boot_lifecycle, records[i].version,
- table.guest_floor[guest]) != WT_ROLLBACK_OK) {
- wt_monitor_quarantine_guest((wt_guest_id_t)guest);
- }
- else if (wt_rollback_advance(records[i].version,
- &table.guest_floor[guest]) != 0) {
- changed = 1;
- }
- }
-
- if (changed && wt_hsm_rollback_store(&table) != 0) {
- /* An unpersisted floor must not launch guests: the next reset would
- * accept the previous floor again. Quarantine fail-closed; secure
- * services stay up so the wedge is observable and recoverable. */
- for (i = 0U; i < guest_count; i++) {
- wt_monitor_quarantine_guest((wt_guest_id_t)i);
- }
- return WT_ROLLBACK_REFUSED;
- }
-
- return WT_ROLLBACK_OK;
-}
-
-int wt_hsm_rollback_image_floor(uint32_t* floor)
-{
- wt_rollback_table_t table;
-
- if (floor == NULL) {
- return -1;
- }
- if (wt_hsm_rollback_load(&table) != 0) {
- return -1;
- }
- /* The unlocked provisioning lifecycles bypass refusal at boot; the
- * staging floor mirrors that so development flows are never bricked. */
- *floor = wt_hsm_reformat_allowed() ? 0U : table.image_floor;
- return 0;
-}
-
/* -------------------------------------------------------------------------
* Forward declaration — tasklet body defined below.
* ---------------------------------------------------------------------- */
@@ -344,35 +158,16 @@ static void wt_hsm_tasklet_main(void *arg);
* per-guest tasklets share one wolfHSM NVM context. */
static int wt_hsm_bind_store(void)
{
- whNvmFlashConfig nvm_flash_cfg;
- whNvmConfig nvm_cfg;
- int rc;
-
- (void)memset(&g_nvm_flash_ctx, 0, sizeof(g_nvm_flash_ctx));
- (void)memset(&nvm_flash_cfg, 0, sizeof(nvm_flash_cfg));
- nvm_flash_cfg.cb = &g_wt_hsm_flash_cb;
- nvm_flash_cfg.context = wt_hsm_flash_context();
- nvm_flash_cfg.config = wt_hsm_flash_config();
-
- g_nvm_lock_cfg.cb = &g_wt_hsm_lock_cb;
- g_nvm_lock_cfg.context = &g_nvm_lock_mutex;
- g_nvm_lock_cfg.config = NULL;
-
- (void)memset(&g_nvm_ctx, 0, sizeof(g_nvm_ctx));
- (void)memset(&nvm_cfg, 0, sizeof(nvm_cfg));
- nvm_cfg.cb = (whNvmCb *)g_nvm_flash_cb;
- nvm_cfg.context = &g_nvm_flash_ctx;
- nvm_cfg.config = &nvm_flash_cfg;
- nvm_cfg.lockConfig = &g_nvm_lock_cfg;
-
- rc = wh_Nvm_Init(&g_nvm_ctx, &nvm_cfg);
+ int rc;
+
+ rc = wt_nvm_store_bind();
if (rc != WH_ERROR_OK) {
return rc;
}
- if (wt_hsm_vault_init(&g_nvm_ctx) == 0) {
+ if (wt_hsm_vault_init(&g_wt_nvm_ctx) == 0) {
wt_vault_service_set_backend(&wt_hsm_vault_backend);
- if (wt_hsm_seal_init(&g_nvm_ctx) == 0) {
+ if (wt_hsm_seal_init(&g_wt_nvm_ctx) == 0) {
wt_hsm_vault_set_sealer(&wt_hsm_sealer);
}
else {
@@ -391,7 +186,7 @@ static int wt_hsm_bind_store(void)
}
/* Erase the whole vault region and rebuild a blank store. Caller must have
- * checked wt_hsm_reformat_allowed() -- this destroys every object, including
+ * checked wt_nvm_reformat_allowed() -- this destroys every object, including
* WRITE_ONCE storage and the sealed device key. */
static int wt_hsm_vault_format(void)
{
@@ -402,7 +197,7 @@ static int wt_hsm_vault_format(void)
rc = wt_hsm_bind_store();
}
if (rc == 0) {
- g_vault_reformatted = 1;
+ wt_nvm_mark_reformatted();
}
return rc;
}
@@ -447,7 +242,7 @@ int wt_hsm_init(void)
}
/* Initialise the shared NVM lock once, before wh_Nvm_Init wires it in. */
- wt_mutex_init(&g_nvm_lock_mutex);
+ wt_mutex_init(&g_wt_nvm_lock_mutex);
return wt_hsm_bind_store();
}
@@ -550,7 +345,7 @@ int wt_hsm_guest_init(wt_guest_id_t guest_id,
* 5. Build server config.
* ---------------------------------------------------------------- */
g->server_cfg.comm_config = &g->comm_cfg;
- g->server_cfg.nvm = &g_nvm_ctx;
+ g->server_cfg.nvm = &g_wt_nvm_ctx;
g->server_cfg.crypto = &g->crypto;
#if defined(WOLF_CRYPTO_CB)
g->server_cfg.devId = INVALID_DEVID;
@@ -864,24 +659,6 @@ void wt_hsm_set_fault_notify(wt_hsm_fault_notify_fn fn)
g_hsm_fault_notify = (fn != NULL) ? fn : wt_hsm_fault_notify_noop;
}
-void wt_hsm_release_locks(struct wt_co *co)
-{
- /* Drop every secure-side wolfHSM lock the faulted coroutine still held, and
- * unlink it if it died parked as a waiter, so no later acquirer deadlocks
- * behind a dead holder or a dead queued waiter. The NVM lock is the only
- * such mutex today; add any future ones here. Recovery runs this before the
- * partition is restarted, so the waiter is gone before it can re-enqueue. */
- if (co != NULL) {
- wt_mutex_release_if_holder(&g_nvm_lock_mutex, co);
- wt_mutex_remove_waiter(&g_nvm_lock_mutex, co);
- }
-}
-
-wt_mutex_t *wt_hsm_nvm_lock_mutex(void)
-{
- return &g_nvm_lock_mutex;
-}
-
int wt_hsm_relay_reinit_servers(void)
{
int rc = WH_ERROR_OK;
@@ -1141,7 +918,7 @@ int wt_hsm_attest_init(void)
int ret;
#if defined(WT_VAULT_FOREIGN_PROBE) && defined(WT_VAULT_PROBE_SECURED)
- g_boot_lifecycle = PSA_LIFECYCLE_SECURED;
+ wt_hsm_set_boot_lifecycle(PSA_LIFECYCLE_SECURED);
#endif
if (g_attest_ready) {
return WH_ERROR_OK;
@@ -1164,7 +941,7 @@ int wt_hsm_attest_init(void)
(void)memset(&g_attest_server_cfg, 0, sizeof(g_attest_server_cfg));
g_attest_server_cfg.comm_config = &g_attest_comm_cfg;
- g_attest_server_cfg.nvm = &g_nvm_ctx;
+ g_attest_server_cfg.nvm = &g_wt_nvm_ctx;
g_attest_server_cfg.crypto = &g_attest_crypto;
#if defined(WOLF_CRYPTO_CB)
g_attest_server_cfg.devId = INVALID_DEVID;
@@ -1198,9 +975,9 @@ int wt_hsm_attest_init(void)
* NONMODIFIABLE/NONDESTROYABLE flags reject the fresh commit. In an
* unlocked lifecycle, reformat the vault once and re-provision; a
* SECURED device never reaches here, so its data is never wiped. */
- if (ret != WH_ERROR_OK && wt_hsm_reformat_allowed()) {
+ if (ret != WH_ERROR_OK && wt_nvm_reformat_allowed()) {
if (wt_hsm_vault_format() == 0) {
- /* vault_format re-inited g_nvm_ctx under the attest server;
+ /* vault_format re-inited the store under the attest server;
* rebind the server to the fresh store before re-provisioning
* so its keystore view is not stale. */
ret = wh_Server_Init(&g_attest_server, &g_attest_server_cfg);
diff --git a/src/services/wolfhsm/wt_hsm_vault.c b/src/services/wolfhsm/wt_hsm_vault.c
index d8d7d648..9a27b672 100644
--- a/src/services/wolfhsm/wt_hsm_vault.c
+++ b/src/services/wolfhsm/wt_hsm_vault.c
@@ -164,6 +164,17 @@ static psa_status_t wt_hsm_vault_reserve(uint32_t need_size,
return PSA_SUCCESS;
}
+/* Object-add reservation for the native key backend: reserve the object's
+ * bytes plus the counter-table headroom, and two directory entries (the add
+ * plus one kept free) so a later sealed write's counter-table create always
+ * has a slot (WT-FFM-0048). Exported for keyvault.c. */
+psa_status_t wt_hsm_vault_reserve_object(whNvmSize len)
+{
+ return wt_hsm_vault_reserve(
+ wt_hsm_vault_storage_size(len) +
+ wt_hsm_vault_storage_size(sizeof(wt_hsm_vault_table_t)), 2U);
+}
+
static psa_status_t wt_hsm_vault_table_store(const wt_hsm_vault_table_t* table)
{
whNvmMetadata meta;
@@ -717,8 +728,13 @@ static psa_status_t wt_hsm_vault_remove(int32_t owner, int32_t sub,
if (status != PSA_SUCCESS) {
return status;
}
- if ((wt_hsm_vault_flags_of(meta.label) &
- WT_VAULT_FLAG_WRITE_ONCE) != 0U) {
+ /* Key objects hold WT_VAULT_KEY_USAGE_* bits in the low label bits that
+ * alias WT_VAULT_FLAG_WRITE_ONCE (USAGE_SIGN == WRITE_ONCE == 0x1); they
+ * are never write-once stores, so apply the storage gate to non-key
+ * objects only and let REMOVE destroy a key as psa_destroy_key promises. */
+ if ((wt_hsm_vault_flags_of(meta.label) & WT_VAULT_FLAG_KEY) == 0U &&
+ (wt_hsm_vault_flags_of(meta.label) &
+ WT_VAULT_FLAG_WRITE_ONCE) != 0U) {
return PSA_ERROR_NOT_PERMITTED;
}
if ((wt_hsm_vault_flags_of(meta.label) & WT_VAULT_FLAG_SEALED) != 0U) {
diff --git a/src/spm_partitions.c b/src/spm_partitions.c
index a01f10c7..c6acfb99 100644
--- a/src/spm_partitions.c
+++ b/src/spm_partitions.c
@@ -39,6 +39,9 @@
#include "wolftrust/services/fwu_service.h"
#include "wolftrust/services/hsm.h"
#include "wolftrust/services/hsm_relay.h"
+#ifndef WT_ENGINE_HSM
+#include "wolftrust/services/crypto_native.h"
+#endif
#include "wolftrust/sync/mutex.h"
#include "wolftrust/services/storage_service.h"
#include "wolftrust/services/vault_service.h"
@@ -49,6 +52,8 @@
#include "wolftrust/sp_recovery.h"
#include "wolftrust/spm_gate.h"
+#include
+
/* Generated in every secure build; the ITS entry embeds SERVICE_VAULT_SID as
* a code constant — the unprivileged loop cannot read SPM RAM at runtime. */
#include "psa_manifest/sid.h"
@@ -92,6 +97,10 @@ int wt_spm_hsm_start(wt_ffm_runtime_t* runtime, int32_t partition_id)
wt_hsm_relay_set_transport(wt_spm_svc_transport);
#if defined(WT_ENGINE_HSM)
wt_hsm_relay_set_submit(wt_hsm_relay_submit, NULL);
+#else
+ /* Native engine: SERVICE_HSM stays the single mediated door; its packets
+ * carry the native wire and dispatch straight into wolfCrypt. */
+ wt_hsm_relay_set_submit(wt_native_submit, (void*)(intptr_t)partition_id);
#endif
wt_spm_set_hsm_partition(partition_id);
return wt_spm_sched_add(runtime, partition_id, wt_spm_hsm_entry,
diff --git a/tests/firmware/zephyr-stm32h5/apps/freertos_guest1/main.c b/tests/firmware/zephyr-stm32h5/apps/freertos_guest1/main.c
index 0ca23c9e..aa1a6393 100644
--- a/tests/firmware/zephyr-stm32h5/apps/freertos_guest1/main.c
+++ b/tests/firmware/zephyr-stm32h5/apps/freertos_guest1/main.c
@@ -26,8 +26,12 @@
#include "wolfssl/wolfcrypt/settings.h"
#include "wolfssl/wolfcrypt/random.h"
+#if defined(WT_ENGINE_HSM)
+#include "wolfssl/wolfcrypt/cryptocb.h"
#include "wolfhsm/wh_error.h"
#include "wolfhsm/wh_client.h"
+#include "wolfhsm/wh_client_cryptocb.h"
+#endif
#include
#include "wolfpsa/psa_engine.h"
@@ -35,12 +39,14 @@
#include "psa/client.h"
/* SERVICE_HSM (port/stm32h563/manifest.json): the single mediated door to
- * the wolfHSM server — the same path guest0 uses. */
+ * the secure crypto engine — the same path guest0 uses. */
#define WT_SERVICE_HSM_SID 4102u
+#if defined(WT_ENGINE_HSM)
/* wolfHSM client glue (module/wolfhsm-client/src/wolfhsm_client_glue.c). */
int wolfhsm_guest_init(void);
whClientContext *wolfhsm_guest_client(void);
+#endif
int wolftrust_guest_rng_stub(unsigned char *output, unsigned int sz);
extern uint32_t _sidata;
@@ -380,8 +386,10 @@ static int guest_crypto_init(void)
{
int rc;
- /* Boot can race a Secure Partition restart window. The shared glue
- * installs a retry callback when the first connection is refused. */
+#if defined(WT_ENGINE_HSM)
+ /* Boot can race a Secure Partition restart window; the glue cryptocb
+ * heals by retrying the connect on demand, so a failed init here is a
+ * warning, not a terminal error. */
rc = wolfhsm_guest_init();
if (rc != WH_ERROR_OK) {
uart_puts("freertos_guest1: hsm client init FAILED rc=");
@@ -390,6 +398,7 @@ static int guest_crypto_init(void)
return -1;
}
(void)wolfPSA_SetDefaultDevID(WH_DEV_ID);
+#endif
/* PSA requires psa_crypto_init before any other psa_* call; guest0 gets
* this from wolfPSA's Zephyr SYS_INIT, the bare FreeRTOS guest does it
* here so the first mediated psa_hash_compute is not BAD_STATE. */
diff --git a/tests/firmware/zephyr-stm32h5/apps/guest0_psa/src/main.c b/tests/firmware/zephyr-stm32h5/apps/guest0_psa/src/main.c
index 0440d2ad..2ef564fc 100644
--- a/tests/firmware/zephyr-stm32h5/apps/guest0_psa/src/main.c
+++ b/tests/firmware/zephyr-stm32h5/apps/guest0_psa/src/main.c
@@ -52,6 +52,9 @@
#include "attestation_verify.h"
+/* The hsmattackneg probe drives the raw wolfHSM client; only that probe
+ * build (hsm engine) links the client, so the headers gate with it. */
+#if defined(WT_HSM_ATTACK_PROBE)
#include "wolfssl/wolfcrypt/ecc.h"
#include "wolfhsm/wh_error.h"
#include "wolfhsm/wh_comm.h"
@@ -61,6 +64,7 @@
#include "wolfhsm/wh_message.h"
#include "wolfhsm/wh_message_nvm.h"
#include "wolftrust/services/hsm.h"
+#endif
LOG_MODULE_REGISTER(guest0_psa, LOG_LEVEL_INF);
diff --git a/tests/firmware/zephyr-stm32h5/module/wolfhsm-client/CMakeLists.txt b/tests/firmware/zephyr-stm32h5/module/wolfhsm-client/CMakeLists.txt
index 785654a9..ed8ca5e0 100644
--- a/tests/firmware/zephyr-stm32h5/module/wolfhsm-client/CMakeLists.txt
+++ b/tests/firmware/zephyr-stm32h5/module/wolfhsm-client/CMakeLists.txt
@@ -160,3 +160,90 @@ if(CONFIG_WOLFTRUST_WOLFHSM_CLIENT)
)
endif()
+
+if(CONFIG_WOLFTRUST_NATIVE_CLIENT)
+
+ set(WT_REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../../..)
+ set(WT_WOLFSSL_DIR ${WT_REPO_ROOT}/lib/wolfSSL)
+ set(WT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
+
+ zephyr_include_directories(
+ ${WT_MODULE_DIR}/include
+ ${WT_MODULE_DIR}/include/wolftrust/wolfhsm
+ ${WT_WOLFSSL_DIR}
+ ${WT_REPO_ROOT}/include
+ )
+
+ # Same wolfCrypt shape as the hsm client, minus the crypto_cb routing and
+ # every wolfHSM define: primitives run Non-secure, seeds ride the native
+ # wire (crypto_native_client.c) to the secure vault RNG.
+ zephyr_compile_definitions(
+ WOLFSSL_USER_SETTINGS
+ WC_RESEED_INTERVAL=1000000
+ WT_ENGINE_NATIVE=1
+ WOLFSSL_SP_ASM
+ WOLFSSL_SP_ARM_CORTEX_M_ASM
+ WOLFSSL_ARM_ARCH=8
+ WOLFSSL_ECDSA_DETERMINISTIC_K
+ WOLFSSL_ARMASM
+ WOLFSSL_ARMASM_NO_HW_CRYPTO
+ WOLFSSL_ARMASM_INLINE
+ WOLFSSL_ARMASM_NO_NEON
+ WOLFSSL_ARMASM_THUMB2
+ NO_ERROR_STRINGS
+ HAVE_ANONYMOUS_INLINE_AGGREGATES=1
+ WOLFSSL_ZEPHYR
+ )
+
+ zephyr_library_named(wolftrust_native_client)
+
+ zephyr_library_sources(
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/aes.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/ecc.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/random.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/sha256.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/asn.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/coding.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/error.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/hash.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/hmac.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/memory.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/pwdbased.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/sha.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/sp_cortexm.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/sp_int.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/wolfmath.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/wc_port.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/port/arm/thumb2-aes-asm_c.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/port/arm/thumb2-sha256-asm_c.c
+ )
+
+ zephyr_library_sources(
+ ${WT_REPO_ROOT}/src/client/crypto_native_client.c
+ ${WT_REPO_ROOT}/src/client/psa_ffm_client.c
+ )
+
+ set_source_files_properties(
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/aes.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/ecc.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/random.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/sha256.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/asn.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/coding.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/error.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/hash.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/hmac.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/memory.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/pwdbased.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/sha.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/sp_cortexm.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/sp_int.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/wolfmath.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/wc_port.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/port/arm/thumb2-aes-asm_c.c
+ ${WT_WOLFSSL_DIR}/wolfcrypt/src/port/arm/thumb2-sha256-asm_c.c
+ PROPERTIES COMPILE_OPTIONS
+ "-Wno-unused-function;-Wno-unused-variable;-Wno-unused-parameter;-Wno-type-limits"
+ )
+
+endif()
diff --git a/tests/firmware/zephyr-stm32h5/module/wolfhsm-client/Kconfig b/tests/firmware/zephyr-stm32h5/module/wolfhsm-client/Kconfig
index 86a2c47d..53be928d 100644
--- a/tests/firmware/zephyr-stm32h5/module/wolfhsm-client/Kconfig
+++ b/tests/firmware/zephyr-stm32h5/module/wolfhsm-client/Kconfig
@@ -17,3 +17,13 @@ if WOLFTRUST_WOLFHSM_CLIENT
# in the module's SYS_INIT hook. Nothing to configure here yet.
endif # WOLFTRUST_WOLFHSM_CLIENT
+
+config WOLFTRUST_NATIVE_CLIENT
+ bool "wolfTrust non-secure native crypto client (WT_ENGINE=native)"
+ depends on ARM
+ depends on !WOLFTRUST_WOLFHSM_CLIENT
+ help
+ Link the native-engine crypto client into the Zephyr image: wolfCrypt
+ runs Non-secure with no crypto_cb routing, and DRBG seed material
+ comes from the secure vault RNG over the SERVICE_HSM native wire
+ (one psa_call per request through the same 5-veneer FF-M gateway).
diff --git a/tests/firmware/zephyr-stm32h5/module/wolfpsa/Kconfig b/tests/firmware/zephyr-stm32h5/module/wolfpsa/Kconfig
index bbcae718..abc1c44f 100644
--- a/tests/firmware/zephyr-stm32h5/module/wolfpsa/Kconfig
+++ b/tests/firmware/zephyr-stm32h5/module/wolfpsa/Kconfig
@@ -1,13 +1,14 @@
menuconfig WOLFPSA
- bool "wolfPSA Crypto engine (PSA API over wolfCrypt + wolfHSM)"
- depends on WOLFTRUST_WOLFHSM_CLIENT
+ bool "wolfPSA Crypto engine (PSA API over wolfCrypt)"
+ depends on WOLFTRUST_WOLFHSM_CLIENT || WOLFTRUST_NATIVE_CLIENT
depends on !MBEDTLS
depends on !BUILD_WITH_TFM
help
Link the lib/wolfPSA submodule into the Zephyr image and expose
- the PSA Crypto API on top of wolfCrypt. The wolfPSA glue calls
- wolfPSA_SetDefaultDevID(WH_DEV_ID) in its SYS_INIT hook so every
- psa_*() call routes through the wolfHSM client (registered by
- the wolfhsm-client module) and across the CMSE boundary into
- the secure side. Mutually exclusive with Zephyr's bundled
- mbedTLS PSA front-end and TF-M.
+ the PSA Crypto API on top of wolfCrypt. With the wolfhsm-client
+ module (hsm engine) the glue calls wolfPSA_SetDefaultDevID(WH_DEV_ID)
+ in its SYS_INIT hook so every psa_*() call routes through the wolfHSM
+ client across the CMSE boundary; with the native client, primitives
+ run Non-secure and only DRBG seeds cross to the secure vault RNG.
+ Mutually exclusive with Zephyr's bundled mbedTLS PSA front-end
+ and TF-M.
diff --git a/tests/firmware/zephyr-stm32h5/module/wolfpsa/src/wolfpsa_zephyr_init.c b/tests/firmware/zephyr-stm32h5/module/wolfpsa/src/wolfpsa_zephyr_init.c
index 31db2754..021207f4 100644
--- a/tests/firmware/zephyr-stm32h5/module/wolfpsa/src/wolfpsa_zephyr_init.c
+++ b/tests/firmware/zephyr-stm32h5/module/wolfpsa/src/wolfpsa_zephyr_init.c
@@ -35,7 +35,9 @@
#include
#include
+#ifdef CONFIG_WOLFTRUST_WOLFHSM_CLIENT
#include "wolfhsm/wh_client.h"
+#endif
LOG_MODULE_REGISTER(wolfpsa_zephyr, LOG_LEVEL_INF);
@@ -43,7 +45,9 @@ static int wolfpsa_zephyr_sys_init(void)
{
psa_status_t st;
+#ifdef CONFIG_WOLFTRUST_WOLFHSM_CLIENT
(void)wolfPSA_SetDefaultDevID(WH_DEV_ID);
+#endif
st = psa_crypto_init();
if (st != PSA_SUCCESS) {
@@ -51,7 +55,11 @@ static int wolfpsa_zephyr_sys_init(void)
return -EIO;
}
+#ifdef CONFIG_WOLFTRUST_WOLFHSM_CLIENT
LOG_INF("wolfPSA up; default devId=0x%08x", (unsigned)WH_DEV_ID);
+#else
+ LOG_INF("wolfPSA up; native engine (Non-secure wolfCrypt)");
+#endif
return 0;
}
diff --git a/tests/firmware/zephyr-stm32h5/scripts/build_freertos_guest.sh b/tests/firmware/zephyr-stm32h5/scripts/build_freertos_guest.sh
index 9b872dc5..f2b169fd 100755
--- a/tests/firmware/zephyr-stm32h5/scripts/build_freertos_guest.sh
+++ b/tests/firmware/zephyr-stm32h5/scripts/build_freertos_guest.sh
@@ -32,6 +32,7 @@ WOLFPSA_DIR="${ROOT}/lib/wolfPSA"
WOLFHSM_DIR="${ROOT}/lib/wolfHSM"
BAREMETAL_NS_DIR="${ROOT}/tests/firmware/stm32h563/nonsecure"
WOLFHSM_MODULE_DIR="${SUBTREE_DIR}/module/wolfhsm-client"
+. "${ROOT}/tests/target/lib/engine.sh"
if [ ! -d "${FREERTOS_KERNEL}" ]; then
echo "missing FreeRTOS workspace: ${FREERTOS_KERNEL}" >&2
@@ -106,8 +107,18 @@ ${WOLFHSM_DIR}/src/wh_message_counter.c \
${WOLFHSM_DIR}/src/wh_utils.c"
# OS-neutral wolfTrust NS client core: psa_connect/call/close over the
-# WolfTrust_FFM_* veneers, plus the wolfHSM-over-psa_call transport and the
-# guest glue that registers the wolfHSM crypto-callback device.
+# WolfTrust_FFM_* veneers, plus the engine-specific client: the
+# wolfHSM-over-psa_call transport + cryptocb glue (hsm), or the native wire
+# client whose only secure crossing is the vault RNG seed (native).
+if [ "${WT_ENGINE}" = "native" ]; then
+WOLFHSM_CLIENT_SRCS=""
+WT_CLIENT_SRCS="\
+${ROOT}/src/client/psa_ffm_client.c \
+${ROOT}/src/client/crypto_native_client.c"
+
+GUEST_GLUE_SRCS="\
+${BAREMETAL_NS_DIR}/libc_stubs_guest.c"
+else
WT_CLIENT_SRCS="\
${ROOT}/src/client/psa_ffm_client.c \
${ROOT}/src/client/hsm_psa_transport.c"
@@ -115,6 +126,7 @@ ${ROOT}/src/client/hsm_psa_transport.c"
GUEST_GLUE_SRCS="\
${BAREMETAL_NS_DIR}/libc_stubs_guest.c \
${WOLFHSM_MODULE_DIR}/src/wolfhsm_client_glue.c"
+fi
# wh_settings.h picks up config via `#ifdef WOLFHSM_CFG / #include
# "wolfhsm_cfg.h"`; generate the trampolines pointing at the shared guest
@@ -128,6 +140,12 @@ printf '#include "%s"\n' "${BAREMETAL_NS_DIR}/wh_settings_guest.h" \
APP_SRCS="${APP_DIR}/main.c"
+if [ "${WT_ENGINE}" = "native" ]; then
+WT_ENGINE_DEFS="-DWT_ENGINE_NATIVE=1"
+else
+WT_ENGINE_DEFS="-DWOLFHSM_CFG -DWOLF_CRYPTO_CB -DWT_ENGINE_HSM=1 -DWT_WOLFHSM_CLIENT_ID=2"
+fi
+
CFLAGS="\
-mcpu=cortex-m33 -mthumb -mgeneral-regs-only \
-ffreestanding -fno-builtin -nostdlib -Os -g \
@@ -149,10 +167,7 @@ CFLAGS="\
-DWOLFSSL_USER_SETTINGS \
-DWOLFSSL_PSA_ENGINE \
-DWOLFPSA_NO_TRACE \
--DWOLFHSM_CFG \
--DWOLF_CRYPTO_CB \
--DWT_ENGINE_HSM=1 \
--DWT_WOLFHSM_CLIENT_ID=2 \
+${WT_ENGINE_DEFS} \
-DWC_RESEED_INTERVAL=1000000 \
-include ${SUBTREE_DIR}/module/wolfpsa/wolfpsa_no_trace.h \
-DWOLFSSL_SP_ASM -DWOLFSSL_SP_ARM_CORTEX_M_ASM -DWOLFSSL_ARM_ARCH=8 \
@@ -191,7 +206,16 @@ NM_OUT=$(arm-none-eabi-nm "${BUILD_DIR}/freertos_guest1.elf") || {
echo "FAIL: nm on the guest1 image failed" >&2
exit 1
}
-if ! printf '%s\n' "${NM_OUT}" | grep -q "wt_hsm_psa_transport_cb"; then
+if [ "${WT_ENGINE}" = "native" ]; then
+ if ! printf '%s\n' "${NM_OUT}" | grep -q "wt_crypto_native_call"; then
+ echo "guest1 is not wired to the native crypto wire client" >&2
+ exit 1
+ fi
+ if printf '%s\n' "${NM_OUT}" | grep -q "wh_Client"; then
+ echo "FAIL: wolfHSM client linked into a native-engine guest1" >&2
+ exit 1
+ fi
+elif ! printf '%s\n' "${NM_OUT}" | grep -q "wt_hsm_psa_transport_cb"; then
echo "guest1 is not wired to the SPM-mediated wolfHSM transport" >&2
exit 1
fi
diff --git a/tests/firmware/zephyr-stm32h5/scripts/build_guest.sh b/tests/firmware/zephyr-stm32h5/scripts/build_guest.sh
index 80beafda..f784ef3b 100755
--- a/tests/firmware/zephyr-stm32h5/scripts/build_guest.sh
+++ b/tests/firmware/zephyr-stm32h5/scripts/build_guest.sh
@@ -30,6 +30,7 @@ WT_FWU_PROBE="${WT_FWU_PROBE:-0}"
WT_WRITE_ONCE_RESET_PROBE="${WT_WRITE_ONCE_RESET_PROBE:-0}"
WT_HSM_ATTACK_PROBE="${WT_HSM_ATTACK_PROBE:-0}"
WT_MPU_BYPASS_PROBE="${WT_MPU_BYPASS_PROBE:-0}"
+. "$ROOT/tests/target/lib/engine.sh"
if [ ! -d "$APP_DIR" ]; then
echo "unknown guest app: $APP_NAME" >&2
@@ -65,6 +66,16 @@ set -- \
"-DWT_HSM_ATTACK_PROBE=$WT_HSM_ATTACK_PROBE" \
"-DWT_MPU_BYPASS_PROBE=$WT_MPU_BYPASS_PROBE"
+if [ "$WT_ENGINE" = "native" ]; then
+ set -- "$@" \
+ "-DCONFIG_WOLFTRUST_WOLFHSM_CLIENT=n" \
+ "-DCONFIG_WOLFTRUST_NATIVE_CLIENT=y"
+else
+ set -- "$@" \
+ "-DCONFIG_WOLFTRUST_WOLFHSM_CLIENT=y" \
+ "-DCONFIG_WOLFTRUST_NATIVE_CLIENT=n"
+fi
+
if [ -n "${ZEPHYR_TOOLCHAIN_VARIANT:-}" ]; then
set -- "$@" "-DZEPHYR_TOOLCHAIN_VARIANT=$ZEPHYR_TOOLCHAIN_VARIANT"
fi
@@ -115,7 +126,16 @@ if printf '%s\n' "$NM_OUT" | \
echo "FAIL: retired direct veneers linked into $APP_NAME" >&2
exit 1
fi
-if ! printf '%s\n' "$NM_OUT" | grep -q 'wt_hsm_psa_transport_cb'; then
+if [ "$WT_ENGINE" = "native" ]; then
+ if ! printf '%s\n' "$NM_OUT" | grep -q 'wt_crypto_native_call'; then
+ echo "FAIL: $APP_NAME is not wired to the native crypto wire client" >&2
+ exit 1
+ fi
+ if printf '%s\n' "$NM_OUT" | grep -q 'wh_Client'; then
+ echo "FAIL: wolfHSM client linked into a native-engine $APP_NAME" >&2
+ exit 1
+ fi
+elif ! printf '%s\n' "$NM_OUT" | grep -q 'wt_hsm_psa_transport_cb'; then
echo "FAIL: $APP_NAME is not wired to the SPM-mediated wolfHSM transport" >&2
exit 1
fi
diff --git a/tests/host/Makefile b/tests/host/Makefile
index 49afe1ab..0b7d178c 100644
--- a/tests/host/Makefile
+++ b/tests/host/Makefile
@@ -32,7 +32,8 @@ endif
UNIT_SUITES := domain manifest lifecycle guest_verify rollback sp_recovery \
ipc ffm spm \
- vnet wolfhsm_loopback wolfhsm_relay keystore_isolation \
+ vnet wolfhsm_loopback wolfhsm_relay native_wire native_wire_client \
+ keystore_isolation \
attestation_service attestation attestation_token \
attestation_golden attestation_negatives attestation_replay \
attestation_iak ffm_domain sp_layout port_binding boot_handoff spm_gate \
diff --git a/tests/host/native_wire/Makefile b/tests/host/native_wire/Makefile
new file mode 100644
index 00000000..bdb37b23
--- /dev/null
+++ b/tests/host/native_wire/Makefile
@@ -0,0 +1,128 @@
+# Makefile
+#
+# Copyright (C) 2026 wolfSSL Inc.
+#
+# This file is part of wolfTrust.
+#
+# wolfTrust is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# wolfTrust is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
+
+ROOT := ../../..
+WOLFHSM := $(ROOT)/lib/wolfHSM
+WOLFSSL := $(ROOT)/lib/wolfSSL
+
+CC ?= cc
+BUILD_DIR ?= build
+
+CFLAGS := \
+ -DWOLFSSL_USER_SETTINGS \
+ -UHAVE_CONFIG_H \
+ -DWOLFHSM_CFG_NO_CRYPTO \
+ -DWOLFHSM_CFG_NO_SYS_TIME \
+ -I. \
+ -I$(ROOT)/include \
+ -I$(WOLFHSM) \
+ -I$(WOLFSSL) \
+ -std=c11 -O0 -g -Wall -Wextra -Werror -pedantic \
+ -Wno-unused-function -Wno-unused-parameter
+CFLAGS += $(EXTRA_CFLAGS)
+
+# wolfCrypt's ECC/ASN sources need GNU libc declarations that strict C11
+# hides; wolfTrust and test sources keep the strict flags above.
+WC_CFLAGS := $(filter-out -std=c11 -Werror -pedantic -Wextra,$(CFLAGS))
+
+# The native crypto wire and its key backend over a RAM-backed NVM store:
+# native_wire.c is port-free, so the packet parser and every operation run
+# here exactly as SERVICE_HSM's submit hook drives them on target.
+_WT_SRCS := \
+ $(ROOT)/src/services/native/native_wire.c \
+ $(ROOT)/src/services/native/keyvault.c \
+ $(ROOT)/src/services/wolfhsm/wt_hsm_vault.c
+
+_WH_SRCS := \
+ $(WOLFHSM)/src/wh_nvm.c \
+ $(WOLFHSM)/src/wh_nvm_flash.c \
+ $(WOLFHSM)/src/wh_flash_ramsim.c \
+ $(WOLFHSM)/src/wh_flash_unit.c \
+ $(WOLFHSM)/src/wh_utils.c
+
+_WC_SRCS := \
+ $(WOLFSSL)/wolfcrypt/src/aes.c \
+ $(WOLFSSL)/wolfcrypt/src/asn.c \
+ $(WOLFSSL)/wolfcrypt/src/coding.c \
+ $(WOLFSSL)/wolfcrypt/src/ecc.c \
+ $(WOLFSSL)/wolfcrypt/src/sp_int.c \
+ $(WOLFSSL)/wolfcrypt/src/wolfmath.c \
+ $(WOLFSSL)/wolfcrypt/src/sha256.c \
+ $(WOLFSSL)/wolfcrypt/src/random.c \
+ $(WOLFSSL)/wolfcrypt/src/memory.c \
+ $(WOLFSSL)/wolfcrypt/src/error.c \
+ $(WOLFSSL)/wolfcrypt/src/logging.c \
+ $(WOLFSSL)/wolfcrypt/src/hash.c \
+ $(WOLFSSL)/wolfcrypt/src/wc_port.c \
+ $(WOLFSSL)/wolfcrypt/src/misc.c
+
+_TEST_SRCS := main.c
+
+_WT_OBJS := $(patsubst %.c,wt_%.o,$(notdir $(_WT_SRCS)))
+_WH_OBJS := $(patsubst %.c,wh_%.o,$(notdir $(_WH_SRCS)))
+_WC_OBJS := $(patsubst %.c,wc_%.o,$(notdir $(_WC_SRCS)))
+_TEST_OBJS := $(patsubst %.c,%.o,$(notdir $(_TEST_SRCS)))
+
+ALL_OBJS := $(addprefix $(BUILD_DIR)/,$(_WT_OBJS) $(_WH_OBJS) $(_WC_OBJS) $(_TEST_OBJS))
+TEST_BIN := $(BUILD_DIR)/test_native_wire
+
+.PHONY: all run compilers sanitize valgrind clean
+
+all: $(TEST_BIN)
+
+$(BUILD_DIR):
+ mkdir -p $@
+
+$(BUILD_DIR)/wt_%.o: $(ROOT)/src/services/native/%.c | $(BUILD_DIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(BUILD_DIR)/wt_%.o: $(ROOT)/src/services/wolfhsm/%.c | $(BUILD_DIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(BUILD_DIR)/wh_%.o: $(WOLFHSM)/src/%.c | $(BUILD_DIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(BUILD_DIR)/wc_%.o: $(WOLFSSL)/wolfcrypt/src/%.c | $(BUILD_DIR)
+ $(CC) $(WC_CFLAGS) -c -o $@ $<
+
+$(BUILD_DIR)/%.o: %.c | $(BUILD_DIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(TEST_BIN): $(ALL_OBJS) | $(BUILD_DIR)
+ $(CC) $(CFLAGS) $(EXTRA_LDFLAGS) -o $@ $(ALL_OBJS)
+
+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: $(TEST_BIN)
+ valgrind --error-exitcode=1 --leak-check=full $(TEST_BIN)
+
+clean:
+ rm -rf $(BUILD_DIR)
diff --git a/tests/host/native_wire/main.c b/tests/host/native_wire/main.c
new file mode 100644
index 00000000..6a1d8cda
--- /dev/null
+++ b/tests/host/native_wire/main.c
@@ -0,0 +1,460 @@
+/* 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 .
+ */
+
+/* Native crypto wire suite: drives wt_native_submit with real request
+ * packets over a RAM-backed NVM store, exactly as SERVICE_HSM's relay hands
+ * them over on target — every operation, the per-client key namespace, usage
+ * and type enforcement, malformed packets, and AES-GCM tamper rejection. */
+
+#include "wolfssl/wolfcrypt/settings.h"
+#include "wolfssl/wolfcrypt/types.h"
+
+#include "wolfhsm/wh_error.h"
+#include "wolfhsm/wh_nvm.h"
+#include "wolfhsm/wh_nvm_flash.h"
+#include "wolfhsm/wh_flash_ramsim.h"
+
+#include "wolftrust/services/hsm.h"
+#include "wolftrust/services/hsm_relay.h"
+#include "wolftrust/services/vault_service.h"
+#include "wolftrust/services/crypto_native.h"
+
+#include
+#include
+#include
+
+#define TEST_OWNER 6
+#define TEST_NS_GUEST0 (-1)
+#define TEST_NS_GUEST1 (-2)
+
+#define RAMSIM_SIZE (64 * 1024)
+#define RAMSIM_SECTOR 4096
+#define RAMSIM_PAGE 8
+
+static uint8_t g_flash_memory[RAMSIM_SIZE];
+static whFlashRamsimCfg g_ramsim_cfg;
+static whFlashRamsimCtx g_ramsim_ctx;
+static const whFlashCb g_ramsim_cb[1] = {WH_FLASH_RAMSIM_CB};
+static whNvmFlashConfig g_nvm_flash_cfg;
+static whNvmFlashContext g_nvm_flash_ctx;
+static const whNvmCb g_nvm_cb[1] = {WH_NVM_FLASH_CB};
+static whNvmConfig g_nvm_cfg;
+static whNvmContext g_nvm_ctx;
+
+static int g_failures;
+
+static void check(int ok, const char* what)
+{
+ if (ok) {
+ (void)printf("PASS: %s\n", what);
+ }
+ else {
+ (void)printf("FAIL: %s\n", what);
+ g_failures++;
+ }
+}
+
+static int test_store_up(void)
+{
+ (void)memset(&g_ramsim_cfg, 0, sizeof(g_ramsim_cfg));
+ g_ramsim_cfg.memory = g_flash_memory;
+ g_ramsim_cfg.size = RAMSIM_SIZE;
+ g_ramsim_cfg.sectorSize = RAMSIM_SECTOR;
+ g_ramsim_cfg.pageSize = RAMSIM_PAGE;
+ g_ramsim_cfg.erasedByte = 0xFF;
+ (void)memset(&g_ramsim_ctx, 0, sizeof(g_ramsim_ctx));
+ (void)memset(&g_nvm_flash_cfg, 0, sizeof(g_nvm_flash_cfg));
+ g_nvm_flash_cfg.cb = g_ramsim_cb;
+ g_nvm_flash_cfg.context = &g_ramsim_ctx;
+ g_nvm_flash_cfg.config = &g_ramsim_cfg;
+ (void)memset(&g_nvm_flash_ctx, 0, sizeof(g_nvm_flash_ctx));
+ (void)memset(&g_nvm_cfg, 0, sizeof(g_nvm_cfg));
+ g_nvm_cfg.cb = (whNvmCb*)g_nvm_cb;
+ g_nvm_cfg.context = &g_nvm_flash_ctx;
+ g_nvm_cfg.config = &g_nvm_flash_cfg;
+ (void)memset(&g_nvm_ctx, 0, sizeof(g_nvm_ctx));
+ if (wh_Nvm_Init(&g_nvm_ctx, &g_nvm_cfg) != WH_ERROR_OK) {
+ return -1;
+ }
+ if (wt_hsm_vault_init(&g_nvm_ctx) != 0) {
+ return -1;
+ }
+ if (wt_hsm_keyvault_init(&g_nvm_ctx) != 0) {
+ return -1;
+ }
+ return 0;
+}
+
+/* One wire round trip as the relay performs it: [header][payload] in,
+ * [status][payload] out, both bounded by the relay's copied buffers. */
+static int wire(int32_t client, uint32_t op, uint64_t uid, uint32_t usage,
+ uint32_t key_type, const uint8_t* payload,
+ size_t payload_len, uint8_t* out, size_t out_cap,
+ size_t* out_len, psa_status_t* status)
+{
+ uint8_t req[WT_HSM_RELAY_MSG_MAX];
+ uint8_t resp[WT_HSM_RELAY_MSG_MAX];
+ wt_crypto_wire_req_t hdr;
+ size_t resp_len = 0U;
+ size_t got;
+ int32_t wire_status = 0;
+ int rc;
+
+ if (sizeof(hdr) + payload_len > sizeof(req)) {
+ return -2;
+ }
+ (void)memset(&hdr, 0, sizeof(hdr));
+ hdr.uid = uid;
+ hdr.op = op;
+ hdr.usage = usage;
+ hdr.key_type = key_type;
+ (void)memcpy(req, &hdr, sizeof(hdr));
+ if (payload_len != 0U) {
+ (void)memcpy(req + sizeof(hdr), payload, payload_len);
+ }
+ rc = wt_native_submit((void*)(intptr_t)TEST_OWNER, client, req,
+ sizeof(hdr) + payload_len, resp, sizeof(resp),
+ &resp_len);
+ if (rc != 0) {
+ return rc;
+ }
+ if (resp_len < sizeof(wire_status)) {
+ return -3;
+ }
+ (void)memcpy(&wire_status, resp, sizeof(wire_status));
+ got = resp_len - sizeof(wire_status);
+ if (out != NULL) {
+ if (got > out_cap) {
+ return -4;
+ }
+ (void)memcpy(out, resp + sizeof(wire_status), got);
+ }
+ if (out_len != NULL) {
+ *out_len = got;
+ }
+ *status = (psa_status_t)wire_status;
+ return 0;
+}
+
+static void test_random_and_hash(void)
+{
+ /* SHA-256("abc") */
+ static const uint8_t abc_digest[32] = {
+ 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,
+ 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
+ 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,
+ 0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad
+ };
+ uint8_t out[WT_HSM_RELAY_MSG_MAX];
+ uint8_t zero[64];
+ size_t got = 0U;
+ psa_status_t status = PSA_ERROR_GENERIC_ERROR;
+ int rc;
+
+ (void)memset(zero, 0, sizeof(zero));
+ (void)memset(out, 0, sizeof(out));
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_RANDOM, 0U, 64U, 0U, NULL, 0U,
+ out, sizeof(out), &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS && got == 64U &&
+ memcmp(out, zero, sizeof(zero)) != 0,
+ "RANDOM returns exactly the requested nonzero bytes");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_RANDOM, 0U, 0U, 0U, NULL, 0U,
+ out, sizeof(out), &got, &status);
+ check(rc == 0 && status == PSA_ERROR_INVALID_ARGUMENT && got == 0U,
+ "RANDOM refuses a zero-length request");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_RANDOM, 0U,
+ WT_CRYPTO_RANDOM_MAX + 1U, 0U, NULL, 0U, out, sizeof(out),
+ &got, &status);
+ check(rc == 0 && status == PSA_ERROR_INVALID_ARGUMENT && got == 0U,
+ "RANDOM refuses a request above the per-call cap");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_HASH, 0U, 0U, 0U,
+ (const uint8_t*)"abc", 3U, out, sizeof(out), &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS && got == sizeof(abc_digest) &&
+ memcmp(out, abc_digest, sizeof(abc_digest)) == 0,
+ "HASH returns the SHA-256 known answer");
+}
+
+static void test_p256_lifecycle(void)
+{
+ static const uint8_t digest[32] = {
+ 0x57, 0x54, 0x4e, 0x57, 0x01, 0x02, 0x03, 0x04,
+ 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
+ 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14,
+ 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c
+ };
+ uint8_t out[WT_HSM_RELAY_MSG_MAX];
+ uint8_t sig[WT_VAULT_KEY_SIG_LEN];
+ uint8_t verify_in[WT_VAULT_KEY_DIGEST_LEN + WT_VAULT_KEY_SIG_LEN];
+ size_t got = 0U;
+ psa_status_t status = PSA_ERROR_GENERIC_ERROR;
+ int rc;
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_GENERATE, 0x1001ULL,
+ WT_VAULT_KEY_USAGE_SIGN | WT_VAULT_KEY_USAGE_VERIFY,
+ WT_VAULT_KEY_P256, NULL, 0U, NULL, 0U, &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS, "P-256 key generates");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_GENERATE, 0x1001ULL,
+ WT_VAULT_KEY_USAGE_SIGN, WT_VAULT_KEY_P256, NULL, 0U, NULL,
+ 0U, &got, &status);
+ check(rc == 0 && status == PSA_ERROR_ALREADY_EXISTS,
+ "generate refuses an occupied UID");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_EXPORT_PUBLIC, 0x1001ULL, 0U,
+ 0U, NULL, 0U, out, sizeof(out), &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS && got == WT_VAULT_KEY_PUB_LEN &&
+ out[0] == 0x04U,
+ "public export returns the 65-byte X9.63 point");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_SIGN, 0x1001ULL, 0U, 0U,
+ digest, sizeof(digest), sig, sizeof(sig), &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS && got == sizeof(sig),
+ "sign returns a 64-byte r||s signature");
+
+ (void)memcpy(verify_in, digest, sizeof(digest));
+ (void)memcpy(verify_in + sizeof(digest), sig, sizeof(sig));
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_VERIFY, 0x1001ULL, 0U, 0U,
+ verify_in, sizeof(verify_in), NULL, 0U, &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS, "verify accepts the signature");
+
+ verify_in[0] ^= 0x01U;
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_VERIFY, 0x1001ULL, 0U, 0U,
+ verify_in, sizeof(verify_in), NULL, 0U, &got, &status);
+ check(rc == 0 && status == PSA_ERROR_INVALID_SIGNATURE,
+ "verify rejects a tampered digest");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_VERIFY, 0x1001ULL, 0U, 0U,
+ verify_in, sizeof(verify_in) - 1U, NULL, 0U, &got, &status);
+ check(rc == 0 && status == PSA_ERROR_INVALID_ARGUMENT,
+ "verify refuses a malformed digest+signature payload");
+
+ /* The key namespace is (owner, SPM-stamped client, uid): another client
+ * using the same UID reaches nothing. */
+ rc = wire(TEST_NS_GUEST1, WT_CRYPTO_OP_KEY_SIGN, 0x1001ULL, 0U, 0U,
+ digest, sizeof(digest), sig, sizeof(sig), &got, &status);
+ check(rc == 0 && status == PSA_ERROR_DOES_NOT_EXIST && got == 0U,
+ "another client cannot sign with this client's key");
+ rc = wire(TEST_NS_GUEST1, WT_CRYPTO_OP_KEY_EXPORT_PUBLIC, 0x1001ULL, 0U,
+ 0U, NULL, 0U, out, sizeof(out), &got, &status);
+ check(rc == 0 && status == PSA_ERROR_DOES_NOT_EXIST,
+ "another client cannot export this client's public key");
+ rc = wire(TEST_NS_GUEST1, WT_CRYPTO_OP_KEY_DESTROY, 0x1001ULL, 0U, 0U,
+ NULL, 0U, NULL, 0U, &got, &status);
+ check(rc == 0 && status == PSA_ERROR_DOES_NOT_EXIST,
+ "another client cannot destroy this client's key");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_DESTROY, 0x1001ULL, 0U, 0U,
+ NULL, 0U, NULL, 0U, &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS, "owner destroys the key");
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_EXPORT_PUBLIC, 0x1001ULL, 0U,
+ 0U, NULL, 0U, out, sizeof(out), &got, &status);
+ check(rc == 0 && status == PSA_ERROR_DOES_NOT_EXIST,
+ "a destroyed key is gone");
+}
+
+static void test_usage_and_type(void)
+{
+ static const uint8_t digest[32] = { 0x01 };
+ uint8_t out[WT_HSM_RELAY_MSG_MAX];
+ size_t got = 0U;
+ psa_status_t status = PSA_ERROR_GENERIC_ERROR;
+ int rc;
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_GENERATE, 0x2001ULL,
+ WT_VAULT_KEY_USAGE_VERIFY, WT_VAULT_KEY_P256, NULL, 0U, NULL,
+ 0U, &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS, "verify-only P-256 key generates");
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_SIGN, 0x2001ULL, 0U, 0U,
+ digest, sizeof(digest), out, sizeof(out), &got, &status);
+ check(rc == 0 && status == PSA_ERROR_NOT_PERMITTED && got == 0U,
+ "sign is refused without the SIGN usage bit");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_GENERATE, 0x2002ULL, 0U,
+ WT_VAULT_KEY_P256, NULL, 0U, NULL, 0U, &got, &status);
+ check(rc == 0 && status == PSA_ERROR_INVALID_ARGUMENT,
+ "generate refuses an empty usage mask");
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_GENERATE, 0x2003ULL,
+ WT_VAULT_KEY_USAGE_SIGN, 0x7fU, NULL, 0U, NULL, 0U, &got,
+ &status);
+ check(rc == 0 && status == PSA_ERROR_NOT_SUPPORTED,
+ "generate refuses an unknown key type");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_GENERATE, 0x2004ULL,
+ WT_VAULT_KEY_USAGE_ENCRYPT | WT_VAULT_KEY_USAGE_DECRYPT,
+ WT_VAULT_KEY_AES256, NULL, 0U, NULL, 0U, &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS, "AES-256 key generates");
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_SIGN, 0x2004ULL, 0U, 0U,
+ digest, sizeof(digest), out, sizeof(out), &got, &status);
+ check(rc == 0 && status == PSA_ERROR_NOT_PERMITTED,
+ "an AES key cannot be used to sign");
+}
+
+static void test_aes_gcm(void)
+{
+ static const uint8_t plaintext[] = "native wire AES-GCM round trip";
+ uint8_t ct[WT_HSM_RELAY_MSG_MAX];
+ uint8_t pt[WT_HSM_RELAY_MSG_MAX];
+ size_t ct_len = 0U;
+ size_t pt_len = 0U;
+ psa_status_t status = PSA_ERROR_GENERIC_ERROR;
+ int rc;
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_ENCRYPT, 0x2004ULL, 0U, 0U,
+ plaintext, sizeof(plaintext), ct, sizeof(ct), &ct_len,
+ &status);
+ check(rc == 0 && status == PSA_SUCCESS &&
+ ct_len == sizeof(plaintext) + WT_VAULT_KEY_NONCE_LEN +
+ WT_VAULT_KEY_TAG_LEN,
+ "encrypt frames nonce || ciphertext || tag");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_DECRYPT, 0x2004ULL, 0U, 0U,
+ ct, ct_len, pt, sizeof(pt), &pt_len, &status);
+ check(rc == 0 && status == PSA_SUCCESS && pt_len == sizeof(plaintext) &&
+ memcmp(pt, plaintext, sizeof(plaintext)) == 0,
+ "decrypt recovers the plaintext");
+
+ ct[ct_len - 1U] ^= 0x01U;
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_DECRYPT, 0x2004ULL, 0U, 0U,
+ ct, ct_len, pt, sizeof(pt), &pt_len, &status);
+ check(rc == 0 && status == PSA_ERROR_INVALID_SIGNATURE && pt_len == 0U,
+ "decrypt rejects a tampered tag and returns no plaintext");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_DECRYPT, 0x2004ULL, 0U, 0U,
+ ct, WT_VAULT_KEY_NONCE_LEN, pt, sizeof(pt), &pt_len, &status);
+ check(rc == 0 && status == PSA_ERROR_INVALID_ARGUMENT,
+ "decrypt refuses input shorter than nonce + tag");
+}
+
+static void test_import(void)
+{
+ static const uint8_t scalar[WT_VAULT_KEY_MATERIAL_LEN] = {
+ 0xc9, 0xaf, 0xa9, 0xd8, 0x45, 0xba, 0x75, 0x16,
+ 0x6b, 0x5c, 0x21, 0x57, 0x67, 0xb1, 0xd6, 0x93,
+ 0x4e, 0x50, 0xc3, 0xdb, 0x36, 0xe8, 0x9b, 0x12,
+ 0x7b, 0x8a, 0x62, 0x2b, 0x12, 0x0f, 0x67, 0x21
+ };
+ /* RFC 6979 A.2.5 public point for the scalar above. */
+ static const uint8_t expect_x[8] = {
+ 0x60, 0xfe, 0xd4, 0xba, 0x25, 0x5a, 0x9d, 0x31
+ };
+ uint8_t out[WT_HSM_RELAY_MSG_MAX];
+ size_t got = 0U;
+ psa_status_t status = PSA_ERROR_GENERIC_ERROR;
+ int rc;
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_IMPORT, 0x3001ULL,
+ WT_VAULT_KEY_USAGE_SIGN, WT_VAULT_KEY_P256, scalar,
+ sizeof(scalar), NULL, 0U, &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS, "P-256 scalar imports");
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_EXPORT_PUBLIC, 0x3001ULL, 0U,
+ 0U, NULL, 0U, out, sizeof(out), &got, &status);
+ check(rc == 0 && status == PSA_SUCCESS && got == WT_VAULT_KEY_PUB_LEN &&
+ out[0] == 0x04U && memcmp(out + 1, expect_x, sizeof(expect_x)) == 0,
+ "imported key derives the RFC 6979 public point");
+
+ rc = wire(TEST_NS_GUEST0, WT_CRYPTO_OP_KEY_IMPORT, 0x3002ULL,
+ WT_VAULT_KEY_USAGE_SIGN, WT_VAULT_KEY_P256, scalar,
+ sizeof(scalar) - 1U, NULL, 0U, &got, &status);
+ check(rc == 0 && status == PSA_ERROR_INVALID_ARGUMENT,
+ "import refuses a short scalar");
+}
+
+static void test_malformed(void)
+{
+ uint8_t req[WT_HSM_RELAY_MSG_MAX];
+ uint8_t resp[WT_HSM_RELAY_MSG_MAX];
+ uint8_t out[WT_HSM_RELAY_MSG_MAX];
+ wt_crypto_wire_req_t hdr;
+ size_t resp_len = 0U;
+ size_t got = 0U;
+ psa_status_t status = PSA_ERROR_GENERIC_ERROR;
+ int32_t wire_status = 0;
+ int rc;
+
+ (void)memset(req, 0, sizeof(req));
+ rc = wt_native_submit((void*)(intptr_t)TEST_OWNER, TEST_NS_GUEST0, req,
+ sizeof(hdr) - 1U, resp, sizeof(resp), &resp_len);
+ check(rc != 0, "a packet shorter than the header is refused");
+
+ rc = wt_native_submit((void*)(intptr_t)TEST_OWNER, TEST_NS_GUEST0, req,
+ sizeof(hdr), resp, sizeof(wire_status) - 1U,
+ &resp_len);
+ check(rc != 0, "a response buffer smaller than the status is refused");
+
+ rc = wt_native_submit((void*)(intptr_t)TEST_OWNER, TEST_NS_GUEST0, NULL,
+ sizeof(hdr), resp, sizeof(resp), &resp_len);
+ check(rc != 0, "a NULL request is refused");
+
+ rc = wire(TEST_NS_GUEST0, 0x7fffU, 0U, 0U, 0U, NULL, 0U, out,
+ sizeof(out), &got, &status);
+ check(rc == 0 && status == PSA_ERROR_NOT_SUPPORTED && got == 0U,
+ "an unknown operation is refused");
+
+ (void)memset(&hdr, 0, sizeof(hdr));
+ hdr.op = WT_CRYPTO_OP_RANDOM;
+ hdr.usage = 16U;
+ hdr.reserved = 0xdeadbeefU;
+ (void)memcpy(req, &hdr, sizeof(hdr));
+ resp_len = 0U;
+ rc = wt_native_submit((void*)(intptr_t)TEST_OWNER, TEST_NS_GUEST0, req,
+ sizeof(hdr), resp, sizeof(resp), &resp_len);
+ (void)memcpy(&wire_status, resp, sizeof(wire_status));
+ check(rc == 0 && resp_len == sizeof(wire_status) &&
+ wire_status == (int32_t)PSA_ERROR_INVALID_ARGUMENT,
+ "a nonzero reserved field is refused");
+
+ /* A response buffer too small for the public point: status only. */
+ (void)memset(&hdr, 0, sizeof(hdr));
+ hdr.uid = 0x3001ULL;
+ hdr.op = WT_CRYPTO_OP_KEY_EXPORT_PUBLIC;
+ (void)memcpy(req, &hdr, sizeof(hdr));
+ rc = wt_native_submit((void*)(intptr_t)TEST_OWNER, TEST_NS_GUEST0, req,
+ sizeof(hdr), resp,
+ sizeof(wire_status) + WT_VAULT_KEY_PUB_LEN - 1U,
+ &resp_len);
+ (void)memcpy(&wire_status, resp, sizeof(wire_status));
+ check(rc == 0 && resp_len == sizeof(wire_status) &&
+ wire_status == (int32_t)PSA_ERROR_BUFFER_TOO_SMALL,
+ "an undersized output buffer yields BUFFER_TOO_SMALL and no data");
+}
+
+int main(void)
+{
+ if (test_store_up() != 0) {
+ (void)fprintf(stderr, "NVM/keyvault bring-up failed\n");
+ return 1;
+ }
+
+ test_random_and_hash();
+ test_p256_lifecycle();
+ test_usage_and_type();
+ test_aes_gcm();
+ test_import();
+ test_malformed();
+
+ if (g_failures != 0) {
+ return 1;
+ }
+ (void)printf("PASS: native crypto wire over the vault key backend\n");
+ return 0;
+}
diff --git a/tests/host/native_wire/user_settings.h b/tests/host/native_wire/user_settings.h
new file mode 100644
index 00000000..2bb04ff8
--- /dev/null
+++ b/tests/host/native_wire/user_settings.h
@@ -0,0 +1,65 @@
+/* user_settings.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 .
+ */
+
+/* Host wolfCrypt profile for the native crypto wire suite: the same
+ * algorithm scope as the Secure image (P-256, AES-GCM, SHA-256, HashDRBG)
+ * on portable C math. */
+
+#ifndef USER_SETTINGS_H
+#define USER_SETTINGS_H
+
+#define WOLFCRYPT_ONLY
+#define WOLFSSL_USER_IO
+#define NO_TLS
+
+#define HAVE_ECC
+#define ECC_USER_CURVES
+#define ECC_TIMING_RESISTANT
+#define WOLFSSL_SP_MATH_ALL
+
+#define HAVE_AESGCM
+#define WOLFSSL_SHA256
+#define HAVE_HASHDRBG
+
+#define NO_SHA
+#define NO_RSA
+#define NO_DH
+#define NO_DSA
+#define NO_DES3
+#define NO_RC4
+#define NO_MD4
+#define NO_MD5
+#define NO_PWDBASED
+#define NO_PKCS7
+#define NO_ASN_TIME
+
+#define WOLFSSL_USE_ALIGN
+#define WOLFSSL_IGNORE_FILE_WARN
+#define NO_MAIN_DRIVER
+#define NO_OLD_RNGNAME
+#define NO_OLD_WC_NAMES
+#define NO_OLD_SSL_NAMES
+#define NO_OLD_SHA_NAMES
+#define NO_OLD_MD5_NAME
+#define NO_ERROR_STRINGS
+#define NO_ERROR_QUEUE
+#define NO_INLINE
+
+#endif /* USER_SETTINGS_H */
diff --git a/tests/host/native_wire_client/Makefile b/tests/host/native_wire_client/Makefile
new file mode 100644
index 00000000..c1811e7f
--- /dev/null
+++ b/tests/host/native_wire_client/Makefile
@@ -0,0 +1,82 @@
+# 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
+WOLFSSL := $(ROOT)/lib/wolfSSL
+
+CC ?= cc
+BUILD_DIR ?= build
+
+CFLAGS := \
+ -DWOLFSSL_USER_SETTINGS \
+ -UHAVE_CONFIG_H \
+ -DWOLFHSM_CFG_NO_CRYPTO \
+ -DWOLFHSM_CFG_NO_SYS_TIME \
+ -I. \
+ -I$(ROOT)/include \
+ -I$(WOLFHSM) \
+ -I$(WOLFSSL) \
+ -std=c11 -O0 -g -Wall -Wextra -Werror -pedantic \
+ -Wno-unused-function -Wno-unused-parameter
+CFLAGS += $(EXTRA_CFLAGS)
+
+# The client is marshalling only: no wolfCrypt, no wolfHSM store. A scripted
+# PSA transport in main.c stands in for the SERVICE_HSM door.
+_SRCS := \
+ $(ROOT)/src/client/crypto_native_client.c \
+ main.c
+
+_OBJS := $(addprefix $(BUILD_DIR)/,crypto_native_client.o main.o)
+TEST_BIN := $(BUILD_DIR)/test_native_wire_client
+
+.PHONY: all run compilers sanitize valgrind clean
+
+all: $(TEST_BIN)
+
+$(BUILD_DIR):
+ mkdir -p $@
+
+$(BUILD_DIR)/crypto_native_client.o: $(ROOT)/src/client/crypto_native_client.c | $(BUILD_DIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(BUILD_DIR)/%.o: %.c | $(BUILD_DIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(TEST_BIN): $(_OBJS) | $(BUILD_DIR)
+ $(CC) $(CFLAGS) $(EXTRA_LDFLAGS) -o $@ $(_OBJS)
+
+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: $(TEST_BIN)
+ valgrind --error-exitcode=1 --leak-check=full $(TEST_BIN)
+
+clean:
+ rm -rf $(BUILD_DIR)
diff --git a/tests/host/native_wire_client/main.c b/tests/host/native_wire_client/main.c
new file mode 100644
index 00000000..62e74335
--- /dev/null
+++ b/tests/host/native_wire_client/main.c
@@ -0,0 +1,358 @@
+/* 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 .
+ */
+
+/* Native wire client suite: a scripted PSA transport stands in for the
+ * SERVICE_HSM door so the client's connect caching, restart heal, retry
+ * exhaustion, chunked randomness, response framing, and output-length
+ * handling are all exercised without a Secure image. */
+
+#include "psa/client.h"
+#include "psa/error.h"
+#include "wolftrust/services/hsm_relay.h"
+#include "wolftrust/services/crypto_native.h"
+#include "wolftrust/crypto_native_client.h"
+
+#include
+#include
+#include
+
+static int g_failures;
+
+static void check(int ok, const char* what)
+{
+ if (ok) {
+ (void)printf("PASS: %s\n", what);
+ }
+ else {
+ (void)printf("FAIL: %s\n", what);
+ g_failures++;
+ }
+}
+
+/* ---- Scripted PSA transport ------------------------------------------- */
+static int g_connect_refuse; /* refuse this many connects, then grant */
+static int g_call_fail; /* fail this many calls, then serve */
+static int32_t g_resp_status; /* wire status the served call reports */
+static size_t g_resp_payload; /* served payload byte count */
+static int g_resp_len_override; /* forced outvec len, or -1 for real */
+static int g_connects;
+static int g_calls;
+static int g_closes;
+static uint32_t g_last_random_usage;
+
+static void mock_reset(void)
+{
+ g_connect_refuse = 0;
+ g_call_fail = 0;
+ g_resp_status = (int32_t)PSA_SUCCESS;
+ g_resp_payload = 0U;
+ g_resp_len_override = -1;
+ g_connects = 0;
+ g_calls = 0;
+ g_closes = 0;
+ g_last_random_usage = 0U;
+}
+
+uint32_t psa_framework_version(void) { return 0x0100u; }
+uint32_t psa_version(uint32_t sid) { (void)sid; return 1u; }
+
+psa_handle_t psa_connect(uint32_t sid, uint32_t version)
+{
+ (void)sid;
+ (void)version;
+ g_connects++;
+ if (g_connect_refuse > 0) {
+ g_connect_refuse--;
+ return (psa_handle_t)PSA_ERROR_CONNECTION_REFUSED;
+ }
+ return (psa_handle_t)42;
+}
+
+void psa_close(psa_handle_t handle)
+{
+ (void)handle;
+ g_closes++;
+}
+
+psa_status_t psa_call(psa_handle_t handle, int32_t type,
+ const psa_invec* in_vec, size_t in_len,
+ psa_outvec* out_vec, size_t out_len)
+{
+ wt_crypto_wire_req_t hdr;
+ uint8_t* out;
+ int32_t wire_status;
+ size_t payload;
+ size_t i;
+
+ (void)type;
+ g_calls++;
+ if (handle <= 0 || in_vec == NULL || in_len != 1U || out_vec == NULL ||
+ out_len != 1U) {
+ return PSA_ERROR_PROGRAMMER_ERROR;
+ }
+ if (g_call_fail > 0) {
+ g_call_fail--;
+ return PSA_ERROR_COMMUNICATION_FAILURE;
+ }
+
+ /* Serve a framed [status][payload] response. RANDOM echoes exactly the
+ * requested byte count so the client's chunk reassembly is exercised. */
+ (void)memset(&hdr, 0, sizeof(hdr));
+ if (in_vec[0].len >= sizeof(hdr)) {
+ (void)memcpy(&hdr, in_vec[0].base, sizeof(hdr));
+ }
+ payload = g_resp_payload;
+ if (hdr.op == WT_CRYPTO_OP_RANDOM) {
+ g_last_random_usage = hdr.usage;
+ payload = hdr.usage;
+ }
+ out = (uint8_t*)out_vec[0].base;
+ wire_status = g_resp_status;
+ (void)memcpy(out, &wire_status, sizeof(wire_status));
+ for (i = 0U; i < payload && sizeof(wire_status) + i < out_vec[0].len;
+ i++) {
+ out[sizeof(wire_status) + i] = (uint8_t)(0xA0U + (i & 0x0FU));
+ }
+ if (g_resp_len_override >= 0) {
+ out_vec[0].len = (size_t)g_resp_len_override;
+ }
+ else {
+ out_vec[0].len = sizeof(wire_status) + payload;
+ }
+ return PSA_SUCCESS;
+}
+
+/* The client caches its connection handle across calls. These drive it to a
+ * known warm (handle cached) or cold (handle dropped) state before a test
+ * asserts connect/call counts. Call mock_reset() afterwards to zero counters;
+ * neither helper's handle state is disturbed by mock_reset. */
+static void client_go_warm(void)
+{
+ uint8_t b[4];
+
+ g_connect_refuse = 0;
+ g_call_fail = 0;
+ g_resp_status = (int32_t)PSA_SUCCESS;
+ g_resp_payload = 0U;
+ g_resp_len_override = -1;
+ (void)wt_crypto_native_random(b, 4U);
+}
+
+static void client_go_cold(void)
+{
+ uint8_t b[4];
+
+ g_connect_refuse = 1000000;
+ g_call_fail = 1000000;
+ (void)wt_crypto_native_random(b, 4U);
+}
+
+/* ---- Tests ------------------------------------------------------------- */
+static void test_happy_and_status(void)
+{
+ uint8_t buf[64];
+ psa_status_t st;
+
+ mock_reset();
+ g_resp_payload = 32U;
+ st = wt_crypto_native_random(buf, 32U);
+ check(st == PSA_SUCCESS && g_connects == 1 && g_calls == 1,
+ "first request connects once and calls once");
+
+ mock_reset();
+ g_resp_status = (int32_t)PSA_ERROR_NOT_PERMITTED;
+ st = wt_crypto_native_random(buf, 16U);
+ check(st == PSA_ERROR_NOT_PERMITTED,
+ "the secure-side status passes through unchanged");
+}
+
+static void test_heal_after_restart(void)
+{
+ uint8_t buf[32];
+ psa_status_t st;
+
+ /* One in-flight call fails as a partition restarts; the client closes the
+ * torn handle, reconnects, and the retry succeeds. Start warm so the
+ * first call uses the cached handle, as it would on a live guest. */
+ client_go_warm();
+ mock_reset();
+ g_call_fail = 1;
+ st = wt_crypto_native_random(buf, 16U);
+ check(st == PSA_SUCCESS && g_closes >= 1 && g_calls == 2,
+ "a torn call heals by reconnecting and retrying");
+}
+
+static void test_mutating_no_replay(void)
+{
+ wt_crypto_wire_req_t hdr;
+ uint8_t out[16];
+ size_t got = 0U;
+ psa_status_t st;
+ uint32_t muta[3];
+ size_t i;
+
+ /* A mutating op whose call fails after reaching the door is ambiguous: the
+ * store may already be changed, so the client must NOT replay it. Unlike
+ * idempotent RANDOM (test_heal_after_restart), it surfaces the failure
+ * after exactly one call rather than retrying. */
+ muta[0] = WT_CRYPTO_OP_KEY_GENERATE;
+ muta[1] = WT_CRYPTO_OP_KEY_IMPORT;
+ muta[2] = WT_CRYPTO_OP_KEY_DESTROY;
+ for (i = 0U; i < 3U; i++) {
+ client_go_warm();
+ mock_reset();
+ g_call_fail = 1;
+ (void)memset(&hdr, 0, sizeof(hdr));
+ hdr.op = muta[i];
+ hdr.uid = 0x3001U;
+ st = wt_crypto_native_call(&hdr, NULL, 0U, out, sizeof(out), &got);
+ check(st == PSA_ERROR_COMMUNICATION_FAILURE && g_calls == 1 &&
+ g_closes >= 1,
+ "a failed mutating call surfaces the failure without replay");
+ }
+}
+
+static void test_connect_refused(void)
+{
+ uint8_t buf[16];
+ psa_status_t st;
+
+ /* The door refuses every connect: no call is ever issued and the client
+ * reports the refusal rather than spinning forever. */
+ client_go_cold();
+ mock_reset();
+ g_connect_refuse = 1000000;
+ st = wt_crypto_native_random(buf, 16U);
+ check(st == PSA_ERROR_CONNECTION_REFUSED && g_calls == 0,
+ "a permanently refused connection fails closed with no call");
+}
+
+static void test_retry_exhaustion(void)
+{
+ uint8_t buf[16];
+ psa_status_t st;
+
+ /* Every call fails: the client exhausts its bounded retries and returns
+ * the transport failure instead of looping without end. */
+ mock_reset();
+ g_call_fail = 1000000;
+ st = wt_crypto_native_random(buf, 16U);
+ check(st == PSA_ERROR_COMMUNICATION_FAILURE && g_calls > 1 &&
+ g_calls <= 64,
+ "exhausted retries return the transport failure");
+}
+
+static void test_response_framing(void)
+{
+ wt_crypto_wire_req_t hdr;
+ uint8_t out[128];
+ size_t got = 999U;
+ psa_status_t st;
+
+ /* A response shorter than the status word is a framing failure. */
+ mock_reset();
+ g_resp_len_override = (int)sizeof(int32_t) - 1;
+ (void)memset(&hdr, 0, sizeof(hdr));
+ hdr.op = WT_CRYPTO_OP_HASH;
+ st = wt_crypto_native_call(&hdr, (const uint8_t*)"abc", 3U, out,
+ sizeof(out), &got);
+ check(st == PSA_ERROR_COMMUNICATION_FAILURE,
+ "a response shorter than the status word is refused");
+
+ /* A payload larger than the caller's buffer yields BUFFER_TOO_SMALL. */
+ mock_reset();
+ g_resp_payload = 40U;
+ got = 999U;
+ (void)memset(&hdr, 0, sizeof(hdr));
+ hdr.op = WT_CRYPTO_OP_KEY_EXPORT_PUBLIC;
+ hdr.uid = 7U;
+ st = wt_crypto_native_call(&hdr, NULL, 0U, out, 16U, &got);
+ check(st == PSA_ERROR_BUFFER_TOO_SMALL,
+ "a payload larger than the output buffer is refused");
+
+ /* A status-only success (no payload) reports zero output length. */
+ mock_reset();
+ got = 999U;
+ (void)memset(&hdr, 0, sizeof(hdr));
+ hdr.op = WT_CRYPTO_OP_KEY_DESTROY;
+ hdr.uid = 7U;
+ st = wt_crypto_native_call(&hdr, NULL, 0U, NULL, 0U, &got);
+ check(st == PSA_SUCCESS && got == 0U,
+ "a status-only response reports no payload");
+}
+
+static void test_chunked_random(void)
+{
+ uint8_t buf[600];
+ size_t i;
+ int nonzero = 0;
+ psa_status_t st;
+
+ /* 600 bytes exceeds the 256-byte per-call cap, so the client must issue
+ * three calls and stitch the pieces together. */
+ mock_reset();
+ (void)memset(buf, 0, sizeof(buf));
+ st = wt_crypto_native_random(buf, sizeof(buf));
+ for (i = 0U; i < sizeof(buf); i++) {
+ if (buf[i] != 0U) {
+ nonzero = 1;
+ }
+ }
+ check(st == PSA_SUCCESS && g_calls == 3 && nonzero != 0,
+ "a request above the per-call cap is split across calls");
+
+ mock_reset();
+ check(wt_crypto_native_random(NULL, 16U) == PSA_ERROR_INVALID_ARGUMENT &&
+ wt_crypto_native_random(buf, 0U) == PSA_ERROR_INVALID_ARGUMENT,
+ "random refuses a NULL buffer or a zero length");
+}
+
+static void test_rng_stub(void)
+{
+ uint8_t buf[8];
+
+ mock_reset();
+ check(wolftrust_guest_rng_stub(buf, sizeof(buf)) == 0,
+ "the wolfCrypt RNG hook returns success when the wire serves");
+
+ client_go_cold();
+ mock_reset();
+ g_connect_refuse = 1000000;
+ check(wolftrust_guest_rng_stub(buf, sizeof(buf)) != 0,
+ "the wolfCrypt RNG hook returns failure when the wire is down");
+}
+
+int main(void)
+{
+ test_happy_and_status();
+ test_heal_after_restart();
+ test_mutating_no_replay();
+ test_connect_refused();
+ test_retry_exhaustion();
+ test_response_framing();
+ test_chunked_random();
+ test_rng_stub();
+
+ if (g_failures != 0) {
+ return 1;
+ }
+ (void)printf("PASS: native crypto wire client recovery and framing\n");
+ return 0;
+}
diff --git a/tests/host/native_wire_client/user_settings.h b/tests/host/native_wire_client/user_settings.h
new file mode 100644
index 00000000..2043f075
--- /dev/null
+++ b/tests/host/native_wire_client/user_settings.h
@@ -0,0 +1,31 @@
+/* user_settings.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 .
+ */
+
+/* The native wire client is marshalling only; it pulls in no wolfCrypt. This
+ * settings header exists so the shared wolfTrust headers compile on host. */
+
+#ifndef USER_SETTINGS_H
+#define USER_SETTINGS_H
+
+#define WOLFCRYPT_ONLY
+#define WOLFSSL_USER_IO
+#define NO_TLS
+
+#endif /* USER_SETTINGS_H */
diff --git a/tests/target/lib/engine.sh b/tests/target/lib/engine.sh
new file mode 100644
index 00000000..c9d1df12
--- /dev/null
+++ b/tests/target/lib/engine.sh
@@ -0,0 +1,52 @@
+# engine.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 .
+
+# Resolve the crypto engine exactly as mk/common.mk does, so the Secure image
+# and every guest always agree. POSIX sh; source it, do not execute it.
+# Legacy WT_ENGINE_HSM=0/1 maps onto WT_ENGINE; a conflict or an unsupported
+# value stops the build instead of producing a hybrid image.
+
+wt_engine_legacy=""
+case "${WT_ENGINE_HSM:-}" in
+ 1) wt_engine_legacy="hsm" ;;
+ 0) wt_engine_legacy="native" ;;
+ "") ;;
+ *)
+ echo "unsupported WT_ENGINE_HSM='${WT_ENGINE_HSM}' (want 0 or 1)" >&2
+ exit 2
+ ;;
+esac
+
+if [ -n "${WT_ENGINE:-}" ] && [ -n "$wt_engine_legacy" ] && \
+ [ "$WT_ENGINE" != "$wt_engine_legacy" ]; then
+ echo "conflicting engine selectors: WT_ENGINE=$WT_ENGINE but" \
+ "WT_ENGINE_HSM=$WT_ENGINE_HSM selects $wt_engine_legacy" >&2
+ exit 2
+fi
+
+WT_ENGINE="${WT_ENGINE:-${wt_engine_legacy:-native}}"
+case "$WT_ENGINE" in
+ native|hsm) ;;
+ *)
+ echo "unsupported WT_ENGINE='$WT_ENGINE' (want native or hsm)" >&2
+ exit 2
+ ;;
+esac
+export WT_ENGINE
+unset wt_engine_legacy
diff --git a/tests/target/run_h5_suite.sh b/tests/target/run_h5_suite.sh
index ea1ea7eb..e7885807 100755
--- a/tests/target/run_h5_suite.sh
+++ b/tests/target/run_h5_suite.sh
@@ -16,6 +16,7 @@ repo="$(cd "$(dirname "$0")/../.." && pwd)"
runner="$repo/tests/target/run_h5_hardware.sh"
img="${WT_H5_DOCKER_IMAGE:-}"
scenarios="${WT_H5_SCENARIOS:-positive restart crossdomain confboot}"
+. "$repo/tests/target/lib/engine.sh"
if ! "$repo/tests/target/detect_h5.sh" >/dev/null 2>&1; then
echo "SKIP: H5 hardware suite ($("$repo/tests/target/detect_h5.sh" 2>&1))"
@@ -24,7 +25,8 @@ fi
build_one() {
if [ -n "$img" ]; then
- docker run --rm -v "$repo":/workspace -w /workspace "$img" \
+ docker run --rm -e WT_ENGINE="$WT_ENGINE" \
+ -v "$repo":/work -w /work "$img" \
bash tests/target/run_h5_hardware.sh build "$1"
else
"$runner" build "$1"
diff --git a/tests/target/run_m33mu_scenario.sh b/tests/target/run_m33mu_scenario.sh
index 2b7ca1d9..40b76c36 100755
--- a/tests/target/run_m33mu_scenario.sh
+++ b/tests/target/run_m33mu_scenario.sh
@@ -67,6 +67,9 @@ git config --global --add safe.directory '*'
git config --global --add safe.directory "$repo"
# --- Workflow env (wolfboot-wolftrust-m33mu job). Keep in sync with the yml. ---
+# Crypto engine under test: hsm (wolfHSM server) or native (direct wolfCrypt).
+# Flows into the secure image build and both guest builds.
+. "$repo/tests/target/lib/engine.sh"
export CROSS_COMPILE=/usr/local/bin/arm-none-eabi-
export ZEPHYR_TOOLCHAIN_VARIANT=cross-compile
export WT_SECURE_FLASH_BASE=0x0C060000
@@ -162,6 +165,11 @@ elif [ "$scenario" = "remeasureneg" ]; then
secure_flags="WT_REMEASURE_PROBE=1"
elif [ "$scenario" = "bootupdate" ]; then
secure_flags="WT_BOOTUPDATE_PROBE=1"
+elif [ "$scenario" = "spbudgetneg" ]; then
+ # Must land in the FIRST secure build: the pre-patch stash taken right
+ # after it is what gets signed and flashed, so a probe assigned in the
+ # guest chain below never reaches the image.
+ secure_flags="WT_SP_FAULT_ALWAYS_PROBE=1"
elif [ "$scenario" = "vnet" ]; then
# Mediated virtual network (WT-FFM-0058): the production chain with the
# SERVICE_VNET partition compiled in; guests are the bare-metal wolfIP pair.
@@ -211,8 +219,6 @@ elif [ "$scenario" = "fwustage" ]; then
guest_flags="WT_FWU_PROBE=1"
elif [ "$scenario" = "gtzcneg" ]; then
guest_flags="WT_MPU_BYPASS_PROBE=1"
-elif [ "$scenario" = "spbudgetneg" ]; then
- secure_flags="WT_SP_FAULT_ALWAYS_PROBE=1"
fi
# Guest images per scenario: the vnet scenario swaps the Zephyr/FreeRTOS pair