diff --git a/.github/workflows/build-and-test-clientonly.yml b/.github/workflows/build-and-test-clientonly.yml index 22ee55ca9..d1b6dc153 100644 --- a/.github/workflows/build-and-test-clientonly.yml +++ b/.github/workflows/build-and-test-clientonly.yml @@ -49,14 +49,18 @@ jobs: config-hash: ${{ matrix.transport }} read-only: ${{ github.event_name == 'pull_request' }} - # Build example server + # Build example server. The SHE client tests use the test-only + # preprogram/destroy key-management messages, so the CI server must be + # built with WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT (never enable this in + # production builds). - name: Build POSIX server run: | cd examples/posix/wh_posix_server + SHE_TEST_CFLAGS="-Wextra -DWOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT" if [ "${{ matrix.transport }}" = "tcp" ]; then - make -j SHE=1 WOLFSSL_DIR=../../../wolfssl + make -j SHE=1 WOLFSSL_DIR=../../../wolfssl CFLAGS_EXTRA="$SHE_TEST_CFLAGS" else - make -j TLS=1 SHE=1 WOLFSSL_DIR=../../../wolfssl + make -j TLS=1 SHE=1 WOLFSSL_DIR=../../../wolfssl CFLAGS_EXTRA="$SHE_TEST_CFLAGS" fi # Start the server in the background @@ -123,16 +127,18 @@ jobs: make -j CLIENT_ONLY=1 TLS=1 SHE=1 AUTH=1 WOLFSSL_DIR=../wolfssl && make run fi - # Rebuild the server with AUTH support and restart + # Rebuild the server with AUTH support and restart. Keep the SHE test + # key-management define so the SHE client tests still work. - name: Rebuild and restart POSIX server for AUTH run: | kill $SERVER_PID || true cd examples/posix/wh_posix_server make clean + SHE_TEST_CFLAGS="-Wextra -DWOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT" if [ "${{ matrix.transport }}" = "tcp" ]; then - make -j SHE=1 AUTH=1 WOLFSSL_DIR=../../../wolfssl + make -j SHE=1 AUTH=1 WOLFSSL_DIR=../../../wolfssl CFLAGS_EXTRA="$SHE_TEST_CFLAGS" else - make -j TLS=1 SHE=1 AUTH=1 WOLFSSL_DIR=../../../wolfssl + make -j TLS=1 SHE=1 AUTH=1 WOLFSSL_DIR=../../../wolfssl CFLAGS_EXTRA="$SHE_TEST_CFLAGS" fi rm -f *.bin || true ./Build/wh_posix_server.elf --type ${{ matrix.transport }} & diff --git a/.github/workflows/build-and-test-refactor.yml b/.github/workflows/build-and-test-refactor.yml index 05126b073..ca3a0d3f6 100644 --- a/.github/workflows/build-and-test-refactor.yml +++ b/.github/workflows/build-and-test-refactor.yml @@ -140,6 +140,19 @@ jobs: if: matrix.group == 'wolfcrypt' run: cd test-refactor/posix && make clean && make -j SHE_GLOBAL=1 ASAN=1 WOLFSSL_DIR=../../wolfssl && make run + # Build and test with the global keys feature disabled (exercises the NVM + # id translation paths, including the DMA add path, with + # WOLFHSM_CFG_GLOBAL_KEYS undefined) + - name: Build and test refactor ASAN NOGLOBALKEYS DMA + if: matrix.group == 'wolfcrypt' + run: cd test-refactor/posix && make clean && make -j NOGLOBALKEYS=1 DMA=1 ASAN=1 WOLFSSL_DIR=../../wolfssl && make run + + # Build and test with the legacy flat client NVM id space + # (WOLFHSM_CFG_LEGACY_CLIENT_NVM) + - name: Build and test refactor ASAN LEGACYNVM DMA + if: matrix.group == 'wolfcrypt' + run: cd test-refactor/posix && make clean && make -j LEGACYNVM=1 DMA=1 ASAN=1 WOLFSSL_DIR=../../wolfssl && make run + # Build and test with DEBUG=1 - name: Build and test refactor with DEBUG if: matrix.group == 'base' diff --git a/.github/workflows/build-and-test-stress.yml b/.github/workflows/build-and-test-stress.yml index e4891b44e..845358a0d 100644 --- a/.github/workflows/build-and-test-stress.yml +++ b/.github/workflows/build-and-test-stress.yml @@ -22,7 +22,9 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} runs-on: ubuntu-latest - timeout-minutes: 10 + # The stress suite runs every phase in three namespace variants + # (global/local/mixed), so give it headroom beyond the old 10 minutes + timeout-minutes: 20 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index eead9b48d..38991c3c4 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -128,6 +128,19 @@ jobs: if: matrix.group == 'wolfcrypt' run: cd test && make clean && make -j SHE_GLOBAL=1 ASAN=1 WOLFSSL_DIR=../wolfssl && make run + # Build and test with the global keys feature disabled (exercises the NVM + # id translation paths, including the DMA add path, with + # WOLFHSM_CFG_GLOBAL_KEYS undefined) + - name: Build and test ASAN NOGLOBALKEYS DMA + if: matrix.group == 'wolfcrypt' + run: cd test && make clean && make -j NOGLOBALKEYS=1 DMA=1 ASAN=1 WOLFSSL_DIR=../wolfssl && make run + + # Build and test with the legacy flat client NVM id space + # (WOLFHSM_CFG_LEGACY_CLIENT_NVM) + - name: Build and test ASAN LEGACYNVM DMA + if: matrix.group == 'wolfcrypt' + run: cd test && make clean && make -j LEGACYNVM=1 DMA=1 ASAN=1 WOLFSSL_DIR=../wolfssl && make run + # Build and test with per-client crypto affinity enabled (runs the crypto # affinity unit test, gated behind WOLFHSM_CFG_CRYPTO_AFFINITY) - name: Build and test CRYPTO_AFFINITY ASAN diff --git a/docs/draft/auth.md b/docs/draft/auth.md index d6866300a..fcb67f13f 100644 --- a/docs/draft/auth.md +++ b/docs/draft/auth.md @@ -272,7 +272,7 @@ cfg.nvm = my_nvm_context; /* whNvmContext* -> persistent; NULL -> RAM-only */ Split object layout: sensitive credential material is deliberately kept out of the shared user index so that a single object read cannot leak PIN hashes or cert material. -- User index: one object at `WH_NVM_ID_AUTH_USER_INDEX` holding the serialized `whAuthUser` records (identity and permissions, with `is_active` cleared). It carries no credential material. `method` and `credentials_len` are not stored here; they live in the per-user credential object metadata and are rebuilt at load. Marked `SENSITIVE | NONEXPORTABLE | NONMODIFIABLE` so clients cannot read, overwrite, or destroy it through the NVM message group. Written with the unchecked NVM add so the backend itself is not blocked by those policy flags. +- User index: one object at `WH_NVM_ID_AUTH_USER_INDEX` holding the serialized `whAuthUser` records (identity and permissions, with `is_active` cleared). It carries no credential material. `method` and `credentials_len` are not stored here; they live in the per-user credential object metadata and are rebuilt at load. Marked `SENSITIVE | NONEXPORTABLE | NONMODIFIABLE` so clients cannot read, overwrite, or destroy it through the NVM message group. On default builds the reserved range is additionally outside the client NVM API's reachable id space entirely: client-supplied ids are translated into the per-client `WH_KEYTYPE_NVM` namespace, which cannot name these ids at all. Only with `WOLFHSM_CFG_LEGACY_CLIENT_NVM` (flat id space) do the policy flags become the sole protection. Written with the unchecked NVM add so the backend itself is not blocked by those policy flags. - Per-user credentials: each user's credential blob is its own object at `WH_NVM_ID_AUTH_CRED_BASE + (user_id - 1)`. These are read into a static, force-zeroed scratch buffer only for the duration of an authentication or credential change, never held resident. - The reserved id range therefore spans `WH_NVM_ID_AUTH_USER_INDEX` through `WH_NVM_ID_AUTH_CRED_BASE + (WH_AUTH_BASE_MAX_USERS - 1)`. diff --git a/docs/src/3-Quickstart.md b/docs/src/3-Quickstart.md index 018b4fd94..aa77a082a 100644 --- a/docs/src/3-Quickstart.md +++ b/docs/src/3-Quickstart.md @@ -244,7 +244,7 @@ Passing `INVALID_DEVID` makes the server perform crypto in software. To offload `whServerConfig` aggregates the three pieces configured above — the comm config (`.comm_config`), the initialized NVM context (`.nvm`), and the crypto context (`.crypto`) — into a single configuration. `wh_Server_Init()` wires them into the `whServerContext`, which from then on serves as the handle for all server operations. -After initialization, the server must be told when the underlying transport is actually ready for communication by calling `wh_Server_SetConnected(server, WH_COMM_CONNECTED)`. Until the server is connected, `wh_Server_HandleRequestMessage()` returns `WH_ERROR_NOTREADY`. On the client side, the corresponding steps are simply `wh_Client_Init()` followed by `wh_Client_CommInit()`. +After initialization, the server must be told when the underlying transport is actually ready for communication by calling `wh_Server_SetConnected(server, WH_COMM_CONNECTED)`. Until the server is connected, `wh_Server_HandleRequestMessage()` returns `WH_ERROR_NOTREADY`. On the client side, the corresponding steps are simply `wh_Client_Init()` followed by `wh_Client_CommInit()`. COMM INIT binds the client id that the server uses to namespace keys and objects, so the server refuses every other request until it has completed. ### Processing Requests diff --git a/docs/src/5-Features.md b/docs/src/5-Features.md index 65961ee0b..a67d928a8 100644 --- a/docs/src/5-Features.md +++ b/docs/src/5-Features.md @@ -14,6 +14,7 @@ This chapter provides a detailed overview of the high level features that wolfHS - [Non-Volatile Memory (NVM)](#non-volatile-memory-nvm) - [High Level NVM Interface](#high-level-nvm-interface) - [Object Metadata and Access Attributes](#object-metadata-and-access-attributes) + - [Client NVM Access and Per-Client Namespaces](#client-nvm-access-and-per-client-namespaces) - [NVM Backends](#nvm-backends) - [Flash Abstraction](#flash-abstraction) - [Optional NVM Backing](#optional-nvm-backing) @@ -178,7 +179,7 @@ The NVM library presents non-volatile storage as a collection of opaque, variabl - A fixed-size **metadata** record describing the object (see [Object Metadata and Access Attributes](#object-metadata-and-access-attributes)) - A variable-length **payload** of arbitrary bytes -Applications and higher-level subsystems do not address NVM by byte offset; they create, read, enumerate, and destroy whole objects through the `wh_Nvm_*` API. This object orientation is what allows the keystore, certificate manager, and counter subsystems to share a single backing store without colliding: each subsystem owns a range of identifiers and a set of metadata flags, and the underlying NVM layer is unaware of what the objects mean. +Applications and higher-level subsystems do not address NVM by byte offset; they create, read, enumerate, and destroy whole objects through the `wh_Nvm_*` API. This object orientation is what allows the keystore, certificate manager, and counter subsystems to share a single backing store without colliding: each subsystem owns a range of identifiers and a set of metadata flags, and the underlying NVM layer is unaware of what the objects mean. Remote clients get the same object model through the `wh_Client_Nvm*` request API, with one additional layer on top: the ids they supply are namespaced per client, as described in [Client NVM Access and Per-Client Namespaces](#client-nvm-access-and-per-client-namespaces). The core operations exposed by the interface are: @@ -220,6 +221,30 @@ The NVM library exposes both a raw and a policy-checked variant of the mutating The access field is used to express coarser-grained permissions (owner / other / user buckets, with read/write/exec/special bits) that higher layers may consult, and is the primary filter used by `wh_Nvm_List()` when enumerating objects. +### Client NVM Access and Per-Client Namespaces + +Everything above describes the server's view of the object store. Clients reach it remotely through the `wh_Client_Nvm*` request API — `wh_Client_NvmAddObject`, `wh_Client_NvmRead`, `wh_Client_NvmList`, `wh_Client_NvmGetMetadata`, `wh_Client_NvmDestroyObjects`, `wh_Client_NvmGetAvailable`, and the [DMA variants](#dma-support) of add and read — which the server executes against the same `wh_Nvm_*` interface. The id a client supplies, however, is not the raw internal `whNvmId`: exactly as with [key ids](#key-cache-key-ids-and-nvm-backing-store), the server translates every client-supplied NVM id at the request boundary, giving each client a private namespace of objects. + +Every such translation depends on the client id that COMM INIT binds to the connection, so the server refuses every request outside the COMM group until that handshake has completed: `wh_Client_CommInit()` must precede any NVM, key, counter, certificate, or SHE request. The refusal carries `WH_ERROR_ACCESS` in the response layout of the refused action. + +A client-facing NVM id uses the same encoding as a client-facing `whKeyId`: + +- **Bits 0–7**: the numeric object id, `1` through `255`. Zero is reserved as the erased sentinel and is rejected by `wh_Client_NvmAddObject` — unlike a key cache request, an NVM add never auto-assigns an id. +- **Bit 8** (`WH_KEYID_CLIENT_GLOBAL_FLAG`): selects the **shared global namespace** instead of the calling client's private one. This is the same flag-and-translation mechanism used by [global keys](#global-keys), and like global keys it is honored when `WOLFHSM_CFG_GLOBAL_KEYS` is defined. Without that define there is no global namespace: `wh_Client_NvmAddObject` rejects ids carrying the flag, and the remaining verbs (including `wh_Client_NvmList`) ignore it and operate on the caller's own namespace. +- **Bits 9–10** (the wrapped and hardware-only key flags): not meaningful for NVM objects; every NVM API rejects ids carrying either flag. Bits above bit 10 are rejected as well, since translation would otherwise drop them and silently address a different object. + +On each request the server expands the client's id into the full internal form with TYPE = `WH_KEYTYPE_NVM` and USER = the connection's client id (or `0` for the global namespace), and collapses it back in responses. The isolation consequences mirror the keystore's: + +- Every client sees the same `[1, 255]` id range, but two clients that both write "object 5" create two distinct objects; neither can read, overwrite, enumerate, or destroy the other's. +- Because the translation always stamps TYPE = `WH_KEYTYPE_NVM` and the caller's own USER, the client NVM API cannot name keys, counters, SHE slots, another client's objects, or server-internal NVM-resident stores (such as the authentication manager's user database) at all. +- When `WOLFHSM_CFG_GLOBAL_KEYS` is defined, objects in the global namespace are visible to every client that sets the GLOBAL flag. Factory images produced by the [NVM provisioning tool](6-Utilities.md#nvm-provisioning-tool) place plain `obj` entries with ids up to 255 in exactly this namespace, so provisioned objects are reachable by all clients out of the box. On builds without the define, these USER=0 objects are not reachable through the client NVM API at all. + +When `WOLFHSM_CFG_GLOBAL_KEYS` is defined, `wh_Client_NvmList` treats the GLOBAL flag on its `startId` argument as a namespace selector: pass `0` to enumerate the calling client's own objects from the beginning, or `WH_KEYID_CLIENT_GLOBAL_FLAG` to enumerate the global namespace. The returned count covers only the selected namespace, and each returned id carries the appropriate flag, so it can be passed back unchanged as the next `startId` to continue the walk. Without the define, the flag on `startId` is ignored and the walk always covers the caller's own namespace. + +Access- and flags-based filtering and the policy-checked NVM variants apply unchanged after translation, and server-local code with direct access to the `wh_Nvm_*` API is unaffected — it continues to address objects by full internal id. + +For integrations that depend on the historical behavior, defining `WOLFHSM_CFG_LEGACY_CLIENT_NVM` disables the translation for the NVM message group only, restoring the flat 16-bit id space shared by all clients for the `wh_Client_Nvm*` requests — along with the cross-client reachability that comes with it. Key, counter, and certificate ids are translated regardless of the define, and COMM INIT remains a prerequisite for every request. See [Configuration](9-Configuration.md#nvm-storage). + ### NVM Backends The `wh_Nvm_*` API is implemented against a backend callback table (`whNvmCb`) that abstracts the details of how objects are actually laid out on storage. The core library does not depend on any particular backend — selecting a backend is part of server configuration, and ports or applications can supply their own implementations against the same interface. wolfHSM ships with two reference backends, both built on top of the [flash abstraction](#flash-abstraction): @@ -301,7 +326,7 @@ Keys are named by a 16-bit identifier (`whKeyId`), which has two forms — a sim The server-side `whKeyId` packs three fields into its 16 bits: -- **TYPE** (top 4 bits): the kind of object — `WH_KEYTYPE_CRYPTO` for ordinary crypto keys, `WH_KEYTYPE_SHE` for AUTOSAR SHE keys, `WH_KEYTYPE_COUNTER` for monotonic counters, `WH_KEYTYPE_WRAPPED` for wrapped-key metadata, and `WH_KEYTYPE_NVM` for non-key NVM objects that share the same id space. +- **TYPE** (top 4 bits): the kind of object — `WH_KEYTYPE_CRYPTO` for ordinary crypto keys, `WH_KEYTYPE_SHE` for AUTOSAR SHE keys, `WH_KEYTYPE_COUNTER` for monotonic counters, `WH_KEYTYPE_WRAPPED` for wrapped-key metadata, and `WH_KEYTYPE_NVM` for non-key NVM objects that share the same id space (see [Client NVM Access and Per-Client Namespaces](#client-nvm-access-and-per-client-namespaces)). - **USER** (middle 4 bits): the owning client. Value `0` is reserved for the global-key namespace when `WOLFHSM_CFG_GLOBAL_KEYS` is enabled. - **ID** (low 8 bits): the number the client chose. @@ -479,7 +504,11 @@ Under the hood, chain verification is delegated to wolfSSL's `WOLFSSL_CERT_MANAG ### Trusted Root Storage -Trusted root certificates are stored as ordinary NVM objects (see [Non-Volatile Memory](#non-volatile-memory-nvm)). Each root is a DER-encoded X.509 certificate written into NVM under a caller-chosen `whNvmId` with full `whNvmMetadata` — access bits, flags, and label — so that the same access-control machinery that applies to keys also applies to roots. +Trusted root certificates are stored as NVM objects (see [Non-Volatile Memory](#non-volatile-memory-nvm)) of TYPE = `WH_KEYTYPE_CERT`. Each root is a DER-encoded X.509 certificate written into NVM with full `whNvmMetadata` — access bits, flags, and label — so that the same access-control machinery that applies to keys also applies to roots. + +The id a client supplies follows the same per-client scheme as [keys](#key-cache-key-ids-and-nvm-backing-store), [NVM objects](#client-nvm-access-and-per-client-namespaces), and counters: a plain id `1`–`255` names a root in the calling client's own trust store, and `WH_KEYID_CLIENT_GLOBAL_FLAG` selects the shared global trust store when `WOLFHSM_CFG_GLOBAL_KEYS` is defined. The server expands the id to TYPE = `WH_KEYTYPE_CERT`, USER = the connection's client id (or `0` for the global store), and ID = the supplied value, on every certificate API including the verify root ids. Ids with bits outside those fields, or carrying the wrapped or hardware key flags, are rejected with `WH_ERROR_BADARGS`, and adding a root at id `0` is rejected as well. `WOLFHSM_CFG_LEGACY_CLIENT_NVM` does not affect certificate ids. + +Server-internal callers, such as the [image manager](#image-manager)'s `sigNvmId`, address roots by full internal id, so a root shared between a client and the image manager must be referenced internally as `WH_MAKE_KEYID(WH_KEYTYPE_CERT, user, id)`. A root provisioned at build time must likewise be written into the image with that internal id; see the [NVM provisioning tool](6-Utilities.md#nvm-provisioning-tool). The lifecycle operations exposed to clients are: @@ -676,10 +705,13 @@ The SHE client API is declared in `wolfhsm/wh_client_she.h` and maps one-to-one - **Status**: `wh_Client_SheGetStatus` (`CMD_GET_STATUS`) — reads the SHE status register (SREG) - **Module identity**: `wh_Client_SheGetId` (`CMD_GET_ID`) — returns the ECU UID, the status register, and a CMAC over the caller's challenge, UID, and status register computed under the `MASTER_ECU_KEY`, letting a party that holds that key verify the module's identity. If the `MASTER_ECU_KEY` slot is empty the MAC is computed with an all-zero key. -In addition to the spec commands, wolfHSM exposes two non-standard helpers that fill gaps left by the spec's assumption of dedicated hardware: +In addition to the spec commands, wolfHSM exposes non-standard helpers that fill gaps left by the spec's assumption of dedicated hardware: - `wh_Client_SheSetUid`: explicitly programs the 15-byte ECU UID that the key update protocol binds against. The AUTOSAR spec assumes this value is hardware-fused; wolfHSM needs a software path to install it, and rejects most SHE operations until it has been set. Where the UID really does live in hardware or in NVM, the server can be pointed at it instead with [UID storage callbacks](#she-uid-storage), in which case `CMD_SET_UID` returns `WH_SHE_ERC_WRITE_PROTECTED` on a read-only store. -- `wh_Client_ShePreProgramKey`: writes a key directly into a SHE NVM slot, bypassing the encrypted M1–M5 protocol. This exists to support initial provisioning on a blank device — once a `MASTER_ECU_KEY` exists, all subsequent updates can go through the spec-compliant protocol. +- `wh_Client_ShePreProgramKey`: writes a key, its protection flags, and its initial key counter directly into a SHE NVM slot over a dedicated SHE message, bypassing the encrypted M1–M5 protocol. This exists to support initial provisioning on a blank device — once a `MASTER_ECU_KEY` exists, all subsequent updates can go through the spec-compliant protocol. +- `wh_Client_SheDestroyKey`: removes a SHE key slot from the calling client's NVM namespace, a capability the spec omits because it treats key slots as fixed hardware. + +Because `wh_Client_ShePreProgramKey` and `wh_Client_SheDestroyKey` bypass the SHE key-update and authorization protocols, both are compiled only when `WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT` is defined (see [Configuration](9-Configuration.md#cryptography-features)) and are intended for provisioning and test builds, not production firmware. All SHE commands return one of the spec's `WH_SHE_ERC_*` error codes (`SEQUENCE_ERROR`, `KEY_NOT_AVAILABLE`, `WRITE_PROTECTED`, `KEY_UPDATE_ERROR`, etc.) alongside the wolfHSM transport return code, so applications can distinguish protocol-level failures from communication failures. @@ -796,7 +828,7 @@ The SHE extension is built on top of the same infrastructure as every other wolf - **Wrapped keys**: SHE keys interoperate with the [wrapped keys](#wrapped-keys) feature by explicit type rather than by flag (the SHE keyId namespace does not interpret the wrapped flag): the client passes `WH_KEYTYPE_SHE` to *wrap-export* to receive a slot's key wrapped under a [trusted KEK](#trusted-keks), and presents the blob to *unwrap-and-cache* to prime a slot directly in the key cache — the provisioning path for servers with [no NVM](#optional-nvm-backing), guarded by the slot's counter rollback check. *Unwrap-and-export* refuses SHE blobs, so a wrapped SHE key can re-enter the keystore but its plaintext is never returned to a client. - **Global keys**: off by default — every SHE keyId carries the connection's client ID in the USER field, giving each client its own set of slots. Defining `WOLFHSM_CFG_SHE_GLOBAL_KEYS` instead places all SHE slots in the [global](#global-keys) (`WH_KEYUSER_GLOBAL`) namespace so every client shares one SHE device view; see [Global SHE Keys](#global-she-keys). Without that option, applications that need to share a key across clients must provision it into each client's SHE namespace separately. -A typical automotive deployment uses the SHE extension end-to-end: the bootloader and `BOOT_MAC` are programmed into NVM at production using `wh_Client_ShePreProgramKey`, the device's UID is set on first boot with `wh_Client_SheSetUid`, secure boot is run on every reset via `wh_Client_SheSecureBoot`, in-field key updates flow through the encrypted `CMD_LOAD_KEY` protocol, and CAN message authentication uses `wh_Client_SheGenerateMac` / `wh_Client_SheVerifyMac` against pre-provisioned user-slot keys. +A typical automotive deployment uses the SHE extension end-to-end: the bootloader and `BOOT_MAC` are programmed into NVM at production via whnvmtool or via `wh_Client_ShePreProgramKey`, the device's UID is set on first boot with `wh_Client_SheSetUid`, secure boot is run on every reset via `wh_Client_SheSecureBoot`, in-field key updates flow through the encrypted `CMD_LOAD_KEY` protocol, and CAN message authentication uses `wh_Client_SheGenerateMac` / `wh_Client_SheVerifyMac` against pre-provisioned user-slot keys. ## Non-Volatile Monotonic Counters @@ -817,7 +849,9 @@ Every mutating operation is committed by the NVM layer before the response is re ### Counter Identifiers and Storage -A counter is referenced by a 16-bit `whNvmId` supplied by the caller, with `WH_KEYID_ERASED` (0) reserved as invalid. Internally the server encodes it as a `whKeyId` with TYPE = `WH_KEYTYPE_COUNTER`, USER = the connection's client id, and ID = the supplied value. This means counters inherit the keystore's [per-client isolation](#key-cache-key-ids-and-nvm-backing-store) — each client has its own counter namespace — and that counter id 5 and key id 5 are distinct objects in the same NVM store. +A counter is referenced by a client-facing id supplied by the caller: bits 0–7 hold the counter number `1`–`255` (`WH_KEYID_ERASED`, 0, is invalid), and bit 8 (`WH_KEYID_CLIENT_GLOBAL_FLAG`) selects the shared global namespace when `WOLFHSM_CFG_GLOBAL_KEYS` is defined. Internally the server encodes it as a `whKeyId` with TYPE = `WH_KEYTYPE_COUNTER`, USER = the connection's client id (or `0` for a global counter), and ID = the supplied value. This means counters inherit the keystore's [per-client isolation](#key-cache-key-ids-and-nvm-backing-store) — each client has its own counter namespace, plus the shared global one — and that counter id 5 and key id 5 are distinct objects in the same NVM store. Ids with bits outside the id and flag fields, or carrying the wrapped or hardware key flags, are rejected with `WH_ERROR_BADARGS`; without `WOLFHSM_CFG_GLOBAL_KEYS` an init with the GLOBAL flag is rejected too. + +A global counter has no owner: any client that names it can initialize it (which rewinds it), increment, read, or destroy it, exactly as any client can use or erase a global key. Hand out global counter ids only to clients that are trusted with the counter's monotonicity. The 32-bit value is stored in the **`label` field of the object's `whNvmMetadata`** with a zero-length payload. A counter therefore lives entirely in the metadata that the NVM layer already reads on every directory operation, so an increment is a single metadata write and a read is satisfied by `wh_Nvm_GetMetadata` alone. The remainder of the label and the access/flags fields are unused by the counter subsystem. Counters share the `WOLFHSM_CFG_NVM_OBJECT_COUNT` object budget with keys and other NVM objects. diff --git a/docs/src/6-Utilities.md b/docs/src/6-Utilities.md index 21e6c8823..242e49244 100644 --- a/docs/src/6-Utilities.md +++ b/docs/src/6-Utilities.md @@ -20,6 +20,8 @@ The NVM provisioning tool (`tools/whnvmtool/`) is a host-side utility that build Because the on-flash layout depends on build-time configuration, the tool must be compiled against the same wolfHSM version as the target server and with a matching `WOLFHSM_CFG_NVM_OBJECT_COUNT` and `WOLFHSM_CFG_NVM_FLASH_CRC16` setting, and the `--size` argument must match the server's `whNvmFlash` partition size. For the full configuration file schema, command-line options, hex conversion recipe, and test workflow, see [`tools/whnvmtool/README.md`](https://github.com/wolfSSL/wolfHSM/blob/main/tools/whnvmtool/README.md). +The ids in the configuration file are **server-internal** ids, written into the image verbatim. This matters when the provisioned objects are meant to be reached through the client NVM API, which namespaces ids per client (see [Client NVM Access and Per-Client Namespaces](5-Features.md#client-nvm-access-and-per-client-namespaces)): a plain `obj` entry with an id of `255` or below lands in the shared global namespace, which clients address by setting `WH_KEYID_CLIENT_GLOBAL_FLAG` on the request id. To pre-provision an object into one specific client's private namespace, encode the owning client id into bits 8–11 of the entry's id — the same encoding the tool's `key` entries construct from their separate `clientId` field. The typed object spaces work the same way with the TYPE nibble in bits 12–15: a trusted root reachable through the client certificate API must be written with TYPE = `WH_KEYTYPE_CERT` (`0x6`), and a counter with TYPE = `WH_KEYTYPE_COUNTER` (`0x3`), so `obj 0x6005` provisions global root certificate 5 and `obj 0x6105` root 5 of client 1 (see [Trusted Root Storage](5-Features.md#trusted-root-storage)). Servers built with `WOLFHSM_CFG_LEGACY_CLIENT_NVM` skip client-id translation for the NVM group only, and their clients address plain provisioned objects by verbatim id instead. + ## Benchmark Suite ### Benchmark Suite Overview diff --git a/docs/src/8-Integration.md b/docs/src/8-Integration.md index 417fb307c..3ffba7ee5 100644 --- a/docs/src/8-Integration.md +++ b/docs/src/8-Integration.md @@ -49,7 +49,7 @@ Independently of which mode wolfBoot itself is running in, a wolfHSM **server ap - `WH_IMG_MGR_IMG_TYPE_WOLFBOOT` — verifies the image against a key resident in the server's keystore, matching wolfBoot's standard signing model. - `WH_IMG_MGR_IMG_TYPE_WOLFBOOT_CERT` — verifies a certificate chain embedded in the wolfBoot header against a trusted root in NVM and then uses the leaf public key to verify the image, matching wolfBoot's cert-chain signing mode. -The full mechanism — header parsing, signature TLV extraction, public-key-hint validation, and the DMA-aware payload reads that make verifying multi-megabyte images practical — is documented in [wolfBoot Image Support](5-Features.md#wolfboot-image-support). The practical upshot is that a wolfBoot client and a wolfHSM-equipped system can share a single image format and a single trust anchor: the same `.bin` that wolfBoot would verify locally can be verified by a wolfHSM server through the image manager, and the same root CA provisioned on the HSM works for both flows. +The full mechanism — header parsing, signature TLV extraction, public-key-hint validation, and the DMA-aware payload reads that make verifying multi-megabyte images practical — is documented in [wolfBoot Image Support](5-Features.md#wolfboot-image-support). The practical upshot is that a wolfBoot client and a wolfHSM-equipped system can share a single image format and a single trust anchor: the same `.bin` that wolfBoot would verify locally can be verified by a wolfHSM server through the image manager, and the same root CA provisioned on the HSM works for both flows, provided it lives in the certificate namespace (TYPE = `WH_KEYTYPE_CERT`) that both the client certificate API and the image manager's `sigNvmId` reference; see [Trusted Root Storage](5-Features.md#trusted-root-storage). ## wolfIP diff --git a/docs/src/9-Configuration.md b/docs/src/9-Configuration.md index 01760f2ac..dd9c0b1f8 100644 --- a/docs/src/9-Configuration.md +++ b/docs/src/9-Configuration.md @@ -72,6 +72,7 @@ These macros enable or tune optional cryptographic subsystems built on top of wo | Macro | Default | Description | |---|---|---| | `WOLFHSM_CFG_SHE_EXTENSION` | Undefined | If defined, compile the AUTOSAR SHE subsystem (SHE message types, SHE key slots, M1-M5 update protocol, SHE-specific RNG and SREG handling). Requires wolfCrypt built with AES, `WOLFSSL_CMAC`, `WOLFSSL_AES_DIRECT`, and `HAVE_AES_ECB`. | +| `WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT` | Undefined | If defined, compile the SHE provisioning/test key-management helpers `wh_Client_ShePreProgramKey` and `wh_Client_SheDestroyKey` and their server handlers, which write and remove SHE key slots directly, bypassing the M1-M5 encrypted update protocol. Intended for provisioning and test builds only; leave undefined in production. Only meaningful when `WOLFHSM_CFG_SHE_EXTENSION` is defined. | | `WOLFHSM_CFG_KEYWRAP` | Undefined | If defined, compile the key-wrap subsystem (`wh_Client_KeyWrap*` / server counterparts). Uses AES-GCM internally and therefore requires wolfCrypt built with AES and `HAVE_AESGCM`. Incompatible with `WOLFHSM_CFG_NO_CRYPTO`. | | `WOLFHSM_CFG_KEYWRAP_MAX_KEY_SIZE` | `2000`, or `WOLFHSM_CFG_COMM_DATA_LEN` minus the request overhead when that is smaller | Maximum size, in bytes, of a key that can be wrapped or unwrapped in a single operation. Only consulted when `WOLFHSM_CFG_KEYWRAP` is defined. The key plus its request header and metadata must fit within `WOLFHSM_CFG_COMM_DATA_LEN`; the default is derived so that it always does, and an explicit value that does not fit fails to build. | | `WOLFHSM_CFG_KEYWRAP_MAX_DATA_SIZE` | `2000`, or `WOLFHSM_CFG_COMM_DATA_LEN` minus the request overhead when that is smaller | Maximum size, in bytes, of the plaintext or wrapped payload carried by a single key-wrap request. Only consulted when `WOLFHSM_CFG_KEYWRAP` is defined. The payload plus its request header must fit within `WOLFHSM_CFG_COMM_DATA_LEN`; the default is derived so that it always does, and an explicit value that does not fit fails to build. | @@ -100,6 +101,7 @@ These macros size the server-side key cache. The cache is split into "regular" s | `WOLFHSM_CFG_NVM_OBJECT_COUNT` | `32` | Maximum number of objects the NVM directory can hold simultaneously (RAM directory cache *and* the on-disk directory it mirrors). Determines the upper bound on the number of keys, certificates, counters, and user objects that can coexist in NVM at one time. | | `WOLFHSM_CFG_NVM_FLASH_CRC16` | Undefined | If defined, the `nvm_flash` backend stores a CRC16 of each object's metadata and data in the on-flash object state and verifies them: metadata when the directory is loaded (failing objects become invisible and reclaimable), data on full-object reads and reclaim copies (returning `WH_ERROR_NOTVERIFIED` on mismatch). Partial reads are not verified. Changes the on-flash format: images written with and without this option are mutually incompatible, and `whnvmtool` must be built with the same setting as the server. | | `WOLFHSM_CFG_SERVER_NVM_FLASH_LOG` | Undefined | If defined, compile the log-structured NVM flash backend (`wh_nvm_flash_log`). When enabled it can be selected at runtime as an alternative to the regular flash backend; useful for flash parts that tolerate fewer erases or that prefer append-only update patterns. | +| `WOLFHSM_CFG_LEGACY_CLIENT_NVM` | Undefined | If defined, the server's client-facing NVM handlers pass object ids to the NVM layer verbatim, restoring the legacy flat 16-bit id space shared by all clients for the `wh_Client_Nvm*` API only; key, counter, and certificate ids are translated regardless, and COMM INIT remains a prerequisite for every request. When left undefined (the default), every client-supplied NVM id is translated into the server-internal TYPE/USER/ID encoding, giving each client a private `1`–`255` object namespace (plus, when `WOLFHSM_CFG_GLOBAL_KEYS` is defined, a shared global namespace selected with `WH_KEYID_CLIENT_GLOBAL_FLAG`), and preventing the client NVM API from reaching keys, counters, or other clients' objects. See [Client NVM Access and Per-Client Namespaces](5-Features.md#client-nvm-access-and-per-client-namespaces). | ## Certificate Manager diff --git a/examples/posix/wh_posix_server/wolfhsm_cfg.h b/examples/posix/wh_posix_server/wolfhsm_cfg.h index 4bb3fec11..f29422b2e 100644 --- a/examples/posix/wh_posix_server/wolfhsm_cfg.h +++ b/examples/posix/wh_posix_server/wolfhsm_cfg.h @@ -37,7 +37,7 @@ /* Must match client WOLFHSM_CFG_COMM_DATA_LEN */ #define WOLFHSM_CFG_COMM_DATA_LEN (1024 * 8) -#define WOLFHSM_CFG_NVM_OBJECT_COUNT 30 +#define WOLFHSM_CFG_NVM_OBJECT_COUNT 64 #define WOLFHSM_CFG_SERVER_KEYCACHE_COUNT 9 #define WOLFHSM_CFG_SERVER_KEYCACHE_SIZE 1024 diff --git a/src/wh_client_she.c b/src/wh_client_she.c index 9daaad7a8..f40e1a2a7 100644 --- a/src/wh_client_she.c +++ b/src/wh_client_she.c @@ -47,29 +47,111 @@ #include "wolfhsm/wh_client_she.h" +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT int wh_Client_ShePreProgramKey(whClientContext* c, whNvmId keyId, - whNvmFlags flags, uint8_t* key, whNvmSize keySz) + uint32_t count, whNvmFlags flags, uint8_t* key, + whNvmSize keySz) { - int ret; - int32_t outRc; - uint8_t label[WH_NVM_LABEL_LEN] = { 0 }; + int ret; + uint16_t group; + uint16_t action; + uint16_t dataSz; + whMessageShe_PreProgramKeyRequest* req; + whMessageShe_PreProgramKeyResponse* resp; + uint8_t* reqBuf; + uint8_t* key_data; + + if (c == NULL || key == NULL || keySz == 0) { + return WH_ERROR_BADARGS; + } + if (sizeof(*req) + keySz > WOLFHSM_CFG_COMM_DATA_LEN) { + return WH_ERROR_BADARGS; + } + + reqBuf = (uint8_t*)wh_CommClient_GetDataPtr(c->comm); + req = (whMessageShe_PreProgramKeyRequest*)reqBuf; + key_data = reqBuf + sizeof(*req); + + req->keyId = keyId; + req->count = count; + req->flags = flags; + req->keySz = keySz; + memcpy(key_data, key, keySz); + + ret = wh_Client_SendRequest(c, WH_MESSAGE_GROUP_SHE, WH_SHE_PRE_PROGRAM_KEY, + sizeof(*req) + keySz, reqBuf); + if (ret == 0) { + /* Receive into the COMM_DATA_LEN-sized comm buffer (as every other + * handler here does) so the copy is bounded; a small stack struct + * could be overrun by an oversized response. */ + resp = (whMessageShe_PreProgramKeyResponse*)wh_CommClient_GetDataPtr( + c->comm); + do { + ret = wh_Client_RecvResponse(c, &group, &action, &dataSz, + WOLFHSM_CFG_COMM_DATA_LEN, + (uint8_t*)resp); + } while (ret == WH_ERROR_NOTREADY); + } + if (ret == 0) { + /* Validate the response. A server built without + * WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT sends an empty response for + * this action; the size check rejects it so a stale buffer is never + * read as a successful rc. */ + if ((group != WH_MESSAGE_GROUP_SHE) || + (action != WH_SHE_PRE_PROGRAM_KEY) || (dataSz != sizeof(*resp))) { + ret = WH_ERROR_ABORTED; + } + else { + ret = resp->rc; + } + } + return ret; +} + +int wh_Client_SheDestroyKey(whClientContext* c, whNvmId keyId) +{ + int ret; + uint16_t group; + uint16_t action; + uint16_t dataSz; + whMessageShe_DestroyKeyRequest* req; + whMessageShe_DestroyKeyResponse* resp; - /* SHE slots hold exactly one AES-128 key. The server appends a 16 byte - * constant after the slot contents when deriving key update keys, so a - * longer object would overrun its kdf input buffer. */ - if ((c == NULL) || (key == NULL) || (keySz != WH_SHE_KEY_SZ)) { + if (c == NULL) { return WH_ERROR_BADARGS; } - /* Create a key with 0 counter */ - wh_She_Meta2Label(0, flags, label); - ret = wh_Client_NvmAddObject( - c, WH_SHE_MAKE_KEYID(c->comm->client_id, keyId), 0, 0, sizeof(label), - label, keySz, key, (int32_t*)&outRc); - if (ret == 0) - ret = outRc; + req = (whMessageShe_DestroyKeyRequest*)wh_CommClient_GetDataPtr(c->comm); + /* Zero the struct so the pad field is not sent as stale buffer bytes */ + memset(req, 0, sizeof(*req)); + req->keyId = keyId; + + ret = wh_Client_SendRequest(c, WH_MESSAGE_GROUP_SHE, WH_SHE_DESTROY_KEY, + sizeof(*req), (uint8_t*)req); + if (ret == 0) { + /* Receive into the comm buffer, not a stack struct (see + * wh_Client_ShePreProgramKey). */ + resp = + (whMessageShe_DestroyKeyResponse*)wh_CommClient_GetDataPtr(c->comm); + do { + ret = wh_Client_RecvResponse(c, &group, &action, &dataSz, + WOLFHSM_CFG_COMM_DATA_LEN, + (uint8_t*)resp); + } while (ret == WH_ERROR_NOTREADY); + } + if (ret == 0) { + /* Validate the response (see wh_Client_ShePreProgramKey) */ + if ((group != WH_MESSAGE_GROUP_SHE) || (action != WH_SHE_DESTROY_KEY) || + (dataSz != sizeof(*resp))) { + ret = WH_ERROR_ABORTED; + } + else { + ret = resp->rc; + } + } return ret; } +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ int wh_Client_SheSetUidRequest(whClientContext* c, uint8_t* uid, uint32_t uidSz) { diff --git a/src/wh_keyid.c b/src/wh_keyid.c index 185ce8ec4..128c498d6 100644 --- a/src/wh_keyid.c +++ b/src/wh_keyid.c @@ -23,6 +23,7 @@ */ #include "wolfhsm/wh_keyid.h" +#include "wolfhsm/wh_error.h" whKeyId wh_KeyId_TranslateFromClient(uint16_t type, uint16_t clientId, whKeyId reqId) @@ -55,6 +56,53 @@ whKeyId wh_KeyId_TranslateFromClient(uint16_t type, uint16_t clientId, return WH_MAKE_KEYID(type, user, id); } +whKeyId wh_KeyId_TranslateObjectFromClient(uint16_t type, uint16_t clientId, + whKeyId reqId) +{ + /* Strip the wrapped and hardware flags so they cannot override the fixed + * type; the GLOBAL flag is preserved and resolved by the plain translator. + * This keeps the object in its own type namespace, preventing a client + * from reaching a wrapped-key or hardware object through a fixed-type API + * (NVM, counter, cert). */ + reqId &= + (whKeyId) ~(WH_KEYID_CLIENT_WRAPPED_FLAG | WH_KEYID_CLIENT_HW_FLAG); + return wh_KeyId_TranslateFromClient(type, clientId, reqId); +} + +int wh_KeyId_CheckClientObjectId(whKeyId reqId) +{ + /* Bits above the id and client-flag fields would be dropped by + * translation, remapping the request onto a different object, so a + * legacy-style 16-bit id must fail loudly instead */ + if ((reqId & (whKeyId) ~(WH_KEYID_MASK | WH_CLIENT_KEYID_FLAGS_MASK)) != + 0) { + return WH_ERROR_BADARGS; + } + /* The wrapped and hardware flags select key sub-types; a fixed-type + * object has no such sub-type */ + if ((reqId & (WH_KEYID_CLIENT_WRAPPED_FLAG | WH_KEYID_CLIENT_HW_FLAG)) != + 0) { + return WH_ERROR_BADARGS; + } + return WH_ERROR_OK; +} + +int wh_KeyId_CheckClientObjectIdForCreate(whKeyId reqId) +{ + /* id 0 is the erased sentinel; a create never auto-assigns an id */ + if (WH_KEYID_ISERASED(reqId)) { + return WH_ERROR_BADARGS; + } +#ifndef WOLFHSM_CFG_GLOBAL_KEYS + /* No global namespace in this build: fail loudly instead of silently + * creating the object in the caller's own namespace */ + if ((reqId & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0) { + return WH_ERROR_BADARGS; + } +#endif + return wh_KeyId_CheckClientObjectId(reqId); +} + whKeyId wh_KeyId_TranslateToClient(whKeyId serverId) { whKeyId clientId = WH_KEYID_ID(serverId); diff --git a/src/wh_message_she.c b/src/wh_message_she.c index 9aa82d376..fb080b9f3 100644 --- a/src/wh_message_she.c +++ b/src/wh_message_she.c @@ -461,4 +461,53 @@ int wh_MessageShe_TranslateGetIdResponse( return 0; } -#endif /* WOLFHSM_CFG_SHE_EXTENSION */ \ No newline at end of file +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT +int wh_MessageShe_TranslatePreProgramKeyRequest( + uint16_t magic, const whMessageShe_PreProgramKeyRequest* src, + whMessageShe_PreProgramKeyRequest* dest) +{ + if ((src == NULL) || (dest == NULL)) { + return WH_ERROR_BADARGS; + } + WH_T32(magic, dest, src, keyId); + WH_T32(magic, dest, src, count); + WH_T32(magic, dest, src, flags); + WH_T32(magic, dest, src, keySz); + return 0; +} + +int wh_MessageShe_TranslatePreProgramKeyResponse( + uint16_t magic, const whMessageShe_PreProgramKeyResponse* src, + whMessageShe_PreProgramKeyResponse* dest) +{ + if ((src == NULL) || (dest == NULL)) { + return WH_ERROR_BADARGS; + } + WH_T32(magic, dest, src, rc); + return 0; +} + +int wh_MessageShe_TranslateDestroyKeyRequest( + uint16_t magic, const whMessageShe_DestroyKeyRequest* src, + whMessageShe_DestroyKeyRequest* dest) +{ + if ((src == NULL) || (dest == NULL)) { + return WH_ERROR_BADARGS; + } + WH_T32(magic, dest, src, keyId); + return 0; +} + +int wh_MessageShe_TranslateDestroyKeyResponse( + uint16_t magic, const whMessageShe_DestroyKeyResponse* src, + whMessageShe_DestroyKeyResponse* dest) +{ + if ((src == NULL) || (dest == NULL)) { + return WH_ERROR_BADARGS; + } + WH_T32(magic, dest, src, rc); + return 0; +} +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ + +#endif /* WOLFHSM_CFG_SHE_EXTENSION */ diff --git a/src/wh_server.c b/src/wh_server.c index 09a868c19..241e21f23 100644 --- a/src/wh_server.c +++ b/src/wh_server.c @@ -42,6 +42,8 @@ #include "wolfhsm/wh_message.h" #include "wolfhsm/wh_message_comm.h" #include "wolfhsm/wh_message_nvm.h" +#include "wolfhsm/wh_message_counter.h" +#include "wolfhsm/wh_message_keystore.h" #ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION #include "wolfhsm/wh_message_auth.h" #endif /* WOLFHSM_CFG_ENABLE_AUTHENTICATION */ @@ -232,12 +234,20 @@ int wh_Server_SetConnected(whServerContext *server, whCommConnected connected) if (rc != WH_ERROR_OK) { WH_LOG(&server->log, WH_LOG_LEVEL_SECEVENT, "Failed to clear auth session on disconnect"); - server->connected = connected; + server->comm->client_id = 0; + server->connected = connected; return rc; } } #endif /* WOLFHSM_CFG_ENABLE_AUTHENTICATION */ + /* The client id bound by COMM INIT belongs to the session that just + * ended. Clear it so the next peer must complete its own COMM INIT before + * any request that resolves ids through the client id is served. */ + if (connected == WH_COMM_DISCONNECTED) { + server->comm->client_id = 0; + } + server->connected = connected; return WH_ERROR_OK; } @@ -279,17 +289,16 @@ static int _wh_Server_HandleCommRequest(whServerContext* server, wh_MessageComm_TranslateInitRequest(magic, (whMessageCommInitRequest*)req_packet, &req); - if (req.client_id > WH_CLIENT_ID_MAX) { + /* client_id 0 (USER=0) names the shared/global and factory-provisioned + * namespace, not a client, so it is never a valid connection id. The + * client library already refuses it; enforce it here too so a raw + * client cannot bind id 0 and reach that namespace through the + * per-client key and NVM APIs. */ + if ((req.client_id == WH_KEYUSER_GLOBAL) || + (req.client_id > WH_CLIENT_ID_MAX)) { *out_resp_size = 0; return WH_ERROR_BADARGS; } -#ifdef WOLFHSM_CFG_GLOBAL_KEYS - /* USER=0 is reserved for global keys, client_id must be non-zero */ - if (req.client_id == WH_KEYUSER_GLOBAL) { - *out_resp_size = 0; - return WH_ERROR_BADARGS; - } -#endif /* Process the init action */ server->comm->client_id = req.client_id; @@ -394,13 +403,30 @@ static int _wh_Server_HandlePkcs11Request(whServerContext* server, return rc; } -#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION -/* Helper to format an authorization error response for any group/action. - * All response structures have int32_t rc as the first field. - * Returns the response size to send. */ -static uint16_t _FormatAuthErrorResponse(uint16_t magic, uint16_t group, - uint16_t action, int32_t error_code, - void* resp_packet) +/* Zero a response of the given size and store the translated error code in + * its leading rc field. Only for replies that start with a 32-bit rc; the + * keystore DMA replies lead with a DMA address status and must be built + * through their structs instead. */ +static uint16_t _FormatRcOnlyResponse(uint16_t magic, int32_t error_code, + uint16_t size, void* resp_packet) +{ + int32_t translated_rc = + (int32_t)wh_Translate32(magic, (uint32_t)error_code); + + memset(resp_packet, 0, size); + memcpy(resp_packet, &translated_rc, sizeof(translated_rc)); + return size; +} + +/* Format an error response for a request that is refused before its handler + * runs (an authorization failure, or a request before COMM INIT). The response + * is shaped per group/action wherever the client parser checks the size, so + * the client sees the error code rather than a malformed reply. Nearly all + * response structures have int32_t rc as the first field; the exceptions are + * shaped through their structs below. Returns the response size. */ +static uint16_t _FormatErrorResponse(uint16_t magic, uint16_t group, + uint16_t action, int32_t error_code, + void* resp_packet) { uint16_t resp_size = sizeof(int32_t); /* Minimum: just the rc field */ @@ -408,8 +434,9 @@ static uint16_t _FormatAuthErrorResponse(uint16_t magic, uint16_t group, return 0; } - /* Write error code to first int32_t (rc field) - all responses start with - * this. Use memcpy since resp_packet may be only byte-aligned. */ + /* Write error code to the first int32_t (rc field) - most responses start + * with this; the cases below overwrite it where they don't. Use memcpy + * since resp_packet may be only byte-aligned. */ { int32_t translated_rc = (int32_t)wh_Translate32(magic, (uint32_t)error_code); @@ -551,6 +578,118 @@ static uint16_t _FormatAuthErrorResponse(uint16_t magic, uint16_t group, break; #endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */ + case WH_MESSAGE_GROUP_COUNTER: + /* Every counter response is rc plus one 32-bit word */ + resp_size = _FormatRcOnlyResponse( + magic, error_code, sizeof(whMessageCounter_ReadResponse), + resp_packet); + break; + +#ifndef WOLFHSM_CFG_NO_CRYPTO + case WH_MESSAGE_GROUP_KEY: { + /* Most keystore error responses differ only in size, so pick the + * struct for the action. The DMA replies lead with the DMA + * address status rather than rc, so those are built through + * their structs to put rc where the client reads it. */ + uint16_t size = 0; + switch (action) { + case WH_KEY_CACHE: + case WH_KEY_CACHE_RANDOM: + size = sizeof(whMessageKeystore_CacheResponse); + break; + case WH_KEY_EVICT: + size = sizeof(whMessageKeystore_EvictResponse); + break; + case WH_KEY_COMMIT: + size = sizeof(whMessageKeystore_CommitResponse); + break; + case WH_KEY_ERASE: + size = sizeof(whMessageKeystore_EraseResponse); + break; + case WH_KEY_REVOKE: + size = sizeof(whMessageKeystore_RevokeResponse); + break; + case WH_KEY_EXPORT: + size = sizeof(whMessageKeystore_ExportResponse); + break; + case WH_KEY_EXPORT_PUBLIC: + size = sizeof(whMessageKeystore_ExportPublicResponse); + break; +#ifdef WOLFHSM_CFG_DMA + /* badAddr stays zero: the refusal is not a DMA fault */ + case WH_KEY_CACHE_DMA: { + whMessageKeystore_CacheDmaResponse resp = {0}; + resp.rc = error_code; + memset(resp_packet, 0, sizeof(resp)); + (void)wh_MessageKeystore_TranslateCacheDmaResponse( + magic, &resp, + (whMessageKeystore_CacheDmaResponse*)resp_packet); + resp_size = sizeof(resp); + } break; + case WH_KEY_EXPORT_DMA: { + whMessageKeystore_ExportDmaResponse resp = {0}; + resp.rc = error_code; + memset(resp_packet, 0, sizeof(resp)); + (void)wh_MessageKeystore_TranslateExportDmaResponse( + magic, &resp, + (whMessageKeystore_ExportDmaResponse*)resp_packet); + resp_size = sizeof(resp); + } break; + case WH_KEY_EXPORT_PUBLIC_DMA: { + whMessageKeystore_ExportPublicDmaResponse resp = {0}; + resp.rc = error_code; + memset(resp_packet, 0, sizeof(resp)); + (void)wh_MessageKeystore_TranslateExportPublicDmaResponse( + magic, &resp, + (whMessageKeystore_ExportPublicDmaResponse*) + resp_packet); + resp_size = sizeof(resp); + } break; +#endif /* WOLFHSM_CFG_DMA */ +#ifdef WOLFHSM_CFG_KEYWRAP + case WH_KEY_KEYWRAP: + size = sizeof(whMessageKeystore_KeyWrapResponse); + break; + case WH_KEY_KEYWRAPEXPORT: + size = sizeof(whMessageKeystore_KeyWrapExportResponse); + break; + case WH_KEY_KEYUNWRAPEXPORT: + size = sizeof(whMessageKeystore_KeyUnwrapAndExportResponse); + break; + case WH_KEY_KEYUNWRAPCACHE: + size = sizeof(whMessageKeystore_KeyUnwrapAndCacheResponse); + break; + case WH_KEY_DATAWRAP: + size = sizeof(whMessageKeystore_DataWrapResponse); + break; + case WH_KEY_DATAUNWRAP: + size = sizeof(whMessageKeystore_DataUnwrapResponse); + break; +#endif /* WOLFHSM_CFG_KEYWRAP */ + default: + size = sizeof(int32_t); + break; + } + if (size != 0) { + resp_size = + _FormatRcOnlyResponse(magic, error_code, size, resp_packet); + } + } break; +#endif /* !WOLFHSM_CFG_NO_CRYPTO */ + +#ifdef WOLFHSM_CFG_SHE_EXTENSION + case WH_MESSAGE_GROUP_SHE: + /* SHE replies carry SHE error codes in action-specific layouts */ + resp_size = wh_Server_SheFormatErrorResponse(magic, action, + error_code, + resp_packet); + if (resp_size == 0) { + /* Unknown action: keep the bare rc written above */ + resp_size = sizeof(int32_t); + } + break; +#endif /* WOLFHSM_CFG_SHE_EXTENSION */ + default: /* For other groups, use minimum size (just rc field). * Most response structures have int32_t rc as first field, so @@ -563,7 +702,6 @@ static uint16_t _FormatAuthErrorResponse(uint16_t magic, uint16_t group, return resp_size; } -#endif /* WOLFHSM_CFG_ENABLE_AUTHENTICATION */ int wh_Server_HandleRequestMessage(whServerContext* server) @@ -597,6 +735,28 @@ int wh_Server_HandleRequestMessage(whServerContext* server) group = WH_MESSAGE_GROUP(kind); action = WH_MESSAGE_ACTION(kind); + /* Every group except COMM resolves ids through the client id that + * COMM INIT binds to this connection. Until then that id is 0, which + * names the shared/global and factory-provisioned USER=0 namespace, + * so refuse the request with a properly shaped error response. */ + if ((group != WH_MESSAGE_GROUP_COMM) && + (server->comm->client_id == WH_KEYUSER_GLOBAL)) { + uint16_t resp_size = _FormatErrorResponse( + magic, group, action, WH_ERROR_ACCESS, data); + + do { + rc = wh_CommServer_SendResponse(server->comm, magic, kind, seq, + resp_size, data); + } while (rc == WH_ERROR_NOTREADY); + + WH_LOG_ON_ERROR_F(&server->log, WH_LOG_LEVEL_ERROR, + WH_ERROR_ACCESS, + "Request before COMM INIT refused (group=%d, " + "action=%d, seq=%d)", + group, action, seq); + return rc; + } + #ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION /* General authentication check for if user has permissions for the * group and action requested. When dealing with key ID's there should @@ -608,7 +768,7 @@ int wh_Server_HandleRequestMessage(whServerContext* server) /* Authorization failed - format and send error response to * client */ int32_t error_code = (int32_t)WH_AUTH_PERMISSION_ERROR; - uint16_t resp_size = _FormatAuthErrorResponse( + uint16_t resp_size = _FormatErrorResponse( magic, group, action, error_code, data); /* Send error response to client */ diff --git a/src/wh_server_cert.c b/src/wh_server_cert.c index 53995fadb..b00017290 100644 --- a/src/wh_server_cert.c +++ b/src/wh_server_cert.c @@ -878,6 +878,30 @@ int wh_Server_CertVerifyAcert(whServerContext* server, const uint8_t* cert, } #endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT */ +/* Check a client-supplied certificate id and translate it into the + * server-internal cert namespace, following the same scheme as keys, NVM + * objects, and counters: TYPE=CERT, USER = the connection's client id by + * default, or WH_KEYUSER_GLOBAL when the client sets the GLOBAL flag (each + * client has its own trust store; shared roots live in the global namespace). + * The check refuses ids that translation would silently alias (bits above the + * id and flag fields) and the wrapped/hardware flags; with create set it also + * refuses id 0 and, without global keys, the GLOBAL flag. This confines every + * client cert verb to certificate objects, so a client cannot reach a key, + * counter, or server-internal object (e.g. the auth user database) through a + * cert verb. Server-internal callers of the wh_Server_Cert* API pass full + * internal ids directly and are unaffected. */ +static int _CertTranslateFromClient(whServerContext* server, whNvmId reqId, + int create, whNvmId* out_id) +{ + int rc = create ? wh_KeyId_CheckClientObjectIdForCreate(reqId) + : wh_KeyId_CheckClientObjectId(reqId); + if (rc == WH_ERROR_OK) { + *out_id = wh_KeyId_TranslateObjectFromClient( + WH_KEYTYPE_CERT, server->comm->client_id, reqId); + } + return rc; +} + /* Handle a certificate request and generate a response */ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, uint16_t action, uint16_t seq, @@ -925,8 +949,11 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, wh_MessageCert_TranslateAddTrustedRequest( magic, (whMessageCert_AddTrustedRequest*)req_packet, &req); - /* Validate certificate data fits within request */ - if (req.cert_len > req_size - sizeof(req)) { + /* Validate the id and that the certificate data fits within the + * request */ + rc = _CertTranslateFromClient(server, req.id, 1, &req.id); + if ((rc != WH_ERROR_OK) || + (req.cert_len > req_size - sizeof(req))) { resp.rc = WH_ERROR_BADARGS; wh_MessageCert_TranslateSimpleResponse( magic, &resp, (whMessageCert_SimpleResponse*)resp_packet); @@ -961,9 +988,12 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, /* Convert request struct */ wh_MessageCert_TranslateEraseTrustedRequest( magic, (whMessageCert_EraseTrustedRequest*)req_packet, &req); + rc = _CertTranslateFromClient(server, req.id, 0, &req.id); /* Process the delete trusted action */ - rc = WH_SERVER_NVM_LOCK(server); + if (rc == WH_ERROR_OK) { + rc = WH_SERVER_NVM_LOCK(server); + } if (rc == WH_ERROR_OK) { rc = wh_Server_CertEraseTrusted(server, req.id); @@ -990,6 +1020,7 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, /* Convert request struct */ wh_MessageCert_TranslateReadTrustedRequest( magic, (whMessageCert_ReadTrustedRequest*)req_packet, &req); + rc = _CertTranslateFromClient(server, req.id, 0, &req.id); /* Get pointer to certificate data buffer */ cert_data = (uint8_t*)resp_packet + sizeof(resp); @@ -997,11 +1028,14 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, ? max_transport_cert_len : WOLFHSM_CFG_MAX_CERT_SIZE; - /* Deny reading non-exportable or server-only (trusted KEK) - * objects. Keys and certs share the NVM id space, so a client - * could pass a protected key's id here. This is the only gate: - * wh_Server_CertReadTrusted() does an unchecked NVM read. */ - rc = WH_SERVER_NVM_LOCK(server); + /* Deny reading a non-exportable or server-only cert object. The id + * is confined to the cert namespace above, so this no longer + * guards against reaching a key; it enforces per-object export + * policy within the cert store. wh_Server_CertReadTrusted() does an + * unchecked NVM read, so this is the only gate. */ + if (rc == WH_ERROR_OK) { + rc = WH_SERVER_NVM_LOCK(server); + } if (rc == WH_ERROR_OK) { rc = wh_Nvm_GetMetadata(server->nvm, req.id, &meta); if (rc == WH_ERROR_OK) { @@ -1051,8 +1085,12 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, wh_MessageCert_TranslateVerifyRequest( magic, (whMessageCert_VerifyRequest*)req_packet, &req); - /* Validate certificate data fits within request */ - if (req.cert_len > req_size - sizeof(req)) { + /* Validate the root id and that the certificate data fits + * within the request */ + rc = _CertTranslateFromClient(server, req.trustedRootNvmId, 0, + &req.trustedRootNvmId); + if ((rc != WH_ERROR_OK) || + (req.cert_len > req_size - sizeof(req))) { resp.rc = WH_ERROR_BADARGS; wh_MessageCert_TranslateVerifyResponse( magic, &resp, @@ -1138,8 +1176,19 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, /* Locate and translate the inline root id array */ payload = (const uint8_t*)req_packet + sizeof(req); root_ids_wire = (const whNvmId*)payload; - for (i = 0; i < req.numRoots; i++) { - root_ids[i] = wh_Translate16(magic, root_ids_wire[i]); + rc = WH_ERROR_OK; + for (i = 0; (rc == WH_ERROR_OK) && (i < req.numRoots); i++) { + rc = _CertTranslateFromClient( + server, wh_Translate16(magic, root_ids_wire[i]), 0, + &root_ids[i]); + } + if (rc != WH_ERROR_OK) { + resp.rc = rc; + wh_MessageCert_TranslateVerifyResponse( + magic, &resp, + (whMessageCert_VerifyResponse*)resp_packet); + *out_resp_size = sizeof(resp); + break; } /* Certificate data follows the root id array */ @@ -1238,7 +1287,9 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, wh_MessageCert_TranslateAddTrustedDmaRequest( magic, (whMessageCert_AddTrustedDmaRequest*)req_packet, &req); - + resp.rc = _CertTranslateFromClient(server, req.id, 1, &req.id); + } + if (resp.rc == WH_ERROR_OK) { /* Process client address */ resp.rc = wh_Server_DmaProcessClientAddress( server, req.cert_addr, &cert_data, req.cert_len, @@ -1289,7 +1340,9 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, wh_MessageCert_TranslateReadTrustedDmaRequest( magic, (whMessageCert_ReadTrustedDmaRequest*)req_packet, &req); - + resp.rc = _CertTranslateFromClient(server, req.id, 0, &req.id); + } + if (resp.rc == WH_ERROR_OK) { /* Process client address */ resp.rc = wh_Server_DmaProcessClientAddress( server, req.cert_addr, &cert_data, req.cert_len, @@ -1351,11 +1404,14 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, /* Convert request struct */ wh_MessageCert_TranslateVerifyDmaRequest( magic, (whMessageCert_VerifyDmaRequest*)req_packet, &req); + resp.rc = _CertTranslateFromClient( + server, req.trustedRootNvmId, 0, &req.trustedRootNvmId); /* Map client keyId to server keyId space */ keyId = wh_KeyId_TranslateFromClient( WH_KEYTYPE_CRYPTO, server->comm->client_id, req.keyId); - + } + if (resp.rc == WH_ERROR_OK) { /* Process client address */ resp.rc = wh_Server_DmaProcessClientAddress( server, req.cert_addr, &cert_data, req.cert_len, @@ -1399,6 +1455,7 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, void* cert_data = NULL; whKeyId keyId = WH_KEYID_ERASED; int cert_dma_pre_ok = 0; + uint16_t ri; if (req_size != sizeof(req)) { /* Request is malformed */ @@ -1409,11 +1466,30 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, wh_MessageCert_TranslateVerifyMultiRootDmaRequest( magic, (whMessageCert_VerifyMultiRootDmaRequest*)req_packet, &req); + /* Validate numRoots, then check and confine each used root + * id to the cert namespace. The array is fixed-size, so clear + * the unused tail rather than trust the client-supplied + * numRoots to keep the callee away from it. */ + if ((req.numRoots == 0) || + (req.numRoots > WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS)) { + resp.rc = WH_ERROR_BADARGS; + } + for (ri = 0; (resp.rc == WH_ERROR_OK) && (ri < req.numRoots); + ri++) { + resp.rc = _CertTranslateFromClient( + server, req.trustedRootNvmIds[ri], 0, + &req.trustedRootNvmIds[ri]); + } + for (ri = req.numRoots; ri < WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS; + ri++) { + req.trustedRootNvmIds[ri] = WH_KEYID_ERASED; + } /* Map client keyId to server keyId space */ keyId = wh_KeyId_TranslateFromClient( WH_KEYTYPE_CRYPTO, server->comm->client_id, req.keyId); - + } + if (resp.rc == WH_ERROR_OK) { /* Process client address */ resp.rc = wh_Server_DmaProcessClientAddress( server, req.cert_addr, &cert_data, req.cert_len, @@ -1467,8 +1543,12 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, wh_MessageCert_TranslateVerifyAcertRequest( magic, (whMessageCert_VerifyAcertRequest*)req_packet, &req); - /* Validate certificate data fits within request */ - if (req.cert_len > req_size - sizeof(req)) { + /* Validate the root id and that the certificate data fits within + * the request */ + rc = _CertTranslateFromClient(server, req.trustedRootNvmId, 0, + &req.trustedRootNvmId); + if ((rc != WH_ERROR_OK) || + (req.cert_len > req_size - sizeof(req))) { resp.rc = WH_ERROR_BADARGS; wh_MessageCert_TranslateSimpleResponse( magic, &resp, (whMessageCert_SimpleResponse*)resp_packet); @@ -1520,7 +1600,10 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, /* Convert request struct */ wh_MessageCert_TranslateVerifyDmaRequest( magic, (whMessageCert_VerifyDmaRequest*)req_packet, &req); - + rc = _CertTranslateFromClient(server, req.trustedRootNvmId, 0, + &req.trustedRootNvmId); + } + if (rc == WH_ERROR_OK) { /* Process client address */ rc = wh_Server_DmaProcessClientAddress( server, req.cert_addr, &cert_data, req.cert_len, diff --git a/src/wh_server_counter.c b/src/wh_server_counter.c index a681c894a..012b77f66 100644 --- a/src/wh_server_counter.c +++ b/src/wh_server_counter.c @@ -68,22 +68,29 @@ int wh_Server_HandleCounter(whServerContext* server, uint16_t magic, (void)wh_MessageCounter_TranslateInitRequest( magic, (whMessageCounter_InitRequest*)req_packet, &req); - /* write 0 to nvm with the supplied id and user_id */ - meta->id = WH_MAKE_KEYID(WH_KEYTYPE_COUNTER, - (uint16_t)server->comm->client_id, - (uint16_t)req.counterId); - /* use the label buffer to hold the counter value */ - *counter = req.counter; - - ret = WH_SERVER_NVM_LOCK(server); + /* Refuse an id that translation would silently alias before + * creating anything */ + ret = wh_KeyId_CheckClientObjectIdForCreate(req.counterId); if (ret == WH_ERROR_OK) { - ret = wh_Nvm_AddObjectWithReclaim(server->nvm, meta, 0, NULL); + /* write the initial value to nvm with the supplied id and + * user_id */ + meta->id = wh_KeyId_TranslateObjectFromClient( + WH_KEYTYPE_COUNTER, server->comm->client_id, + req.counterId); + /* use the label buffer to hold the counter value */ + *counter = req.counter; + + ret = WH_SERVER_NVM_LOCK(server); if (ret == WH_ERROR_OK) { - resp.counter = *counter; - } + ret = wh_Nvm_AddObjectWithReclaim(server->nvm, meta, 0, + NULL); + if (ret == WH_ERROR_OK) { + resp.counter = *counter; + } - (void)WH_SERVER_NVM_UNLOCK(server); - } /* WH_SERVER_NVM_LOCK() */ + (void)WH_SERVER_NVM_UNLOCK(server); + } /* WH_SERVER_NVM_LOCK() */ + } resp.rc = ret; (void)wh_MessageCounter_TranslateInitResponse( @@ -109,14 +116,17 @@ int wh_Server_HandleCounter(whServerContext* server, uint16_t magic, (void)wh_MessageCounter_TranslateIncrementRequest( magic, (whMessageCounter_IncrementRequest*)req_packet, &req); - ret = WH_SERVER_NVM_LOCK(server); + ret = wh_KeyId_CheckClientObjectId(req.counterId); + if (ret == WH_ERROR_OK) { + ret = WH_SERVER_NVM_LOCK(server); + } if (ret == WH_ERROR_OK) { /* read the counter, stored in the metadata label */ ret = wh_Nvm_GetMetadata( server->nvm, - WH_MAKE_KEYID(WH_KEYTYPE_COUNTER, - (uint16_t)server->comm->client_id, - (uint16_t)req.counterId), + wh_KeyId_TranslateObjectFromClient(WH_KEYTYPE_COUNTER, + server->comm->client_id, + req.counterId), meta); /* increment and write the counter back */ @@ -164,14 +174,17 @@ int wh_Server_HandleCounter(whServerContext* server, uint16_t magic, (void)wh_MessageCounter_TranslateReadRequest( magic, (whMessageCounter_ReadRequest*)req_packet, &req); - ret = WH_SERVER_NVM_LOCK(server); + ret = wh_KeyId_CheckClientObjectId(req.counterId); + if (ret == WH_ERROR_OK) { + ret = WH_SERVER_NVM_LOCK(server); + } if (ret == WH_ERROR_OK) { /* read the counter, stored in the metadata label */ ret = wh_Nvm_GetMetadata( server->nvm, - WH_MAKE_KEYID(WH_KEYTYPE_COUNTER, - (uint16_t)server->comm->client_id, - (uint16_t)req.counterId), + wh_KeyId_TranslateObjectFromClient(WH_KEYTYPE_COUNTER, + server->comm->client_id, + req.counterId), meta); /* return counter to the caller */ @@ -206,11 +219,13 @@ int wh_Server_HandleCounter(whServerContext* server, uint16_t magic, (void)wh_MessageCounter_TranslateDestroyRequest( magic, (whMessageCounter_DestroyRequest*)req_packet, &req); - counterId = WH_MAKE_KEYID(WH_KEYTYPE_COUNTER, - (uint16_t)server->comm->client_id, - (uint16_t)req.counterId); + counterId = wh_KeyId_TranslateObjectFromClient( + WH_KEYTYPE_COUNTER, server->comm->client_id, req.counterId); - ret = WH_SERVER_NVM_LOCK(server); + ret = wh_KeyId_CheckClientObjectId(req.counterId); + if (ret == WH_ERROR_OK) { + ret = WH_SERVER_NVM_LOCK(server); + } if (ret == WH_ERROR_OK) { ret = wh_Nvm_DestroyObjects(server->nvm, 1, &counterId); diff --git a/src/wh_server_nvm.c b/src/wh_server_nvm.c index b73a767e1..85874b3ef 100644 --- a/src/wh_server_nvm.c +++ b/src/wh_server_nvm.c @@ -36,6 +36,7 @@ #include "wolfhsm/wh_comm.h" #include "wolfhsm/wh_nvm.h" +#include "wolfhsm/wh_keyid.h" #include "wolfhsm/wh_message.h" #include "wolfhsm/wh_message_nvm.h" @@ -48,6 +49,37 @@ #include "wolfhsm/wh_crypto.h" #endif +/* Translate a client-supplied NVM id to the server-internal TYPE/USER/ID + * encoding. When WOLFHSM_CFG_LEGACY_CLIENT_NVM is defined, the id is passed + * through verbatim (legacy global-flat behavior). */ +static whNvmId _NvmTranslateFromClient(whServerContext* server, + whNvmId clientId) +{ +#ifdef WOLFHSM_CFG_LEGACY_CLIENT_NVM + (void)server; + return clientId; +#else + /* Same fixed-type translation as counters and certificates: stamps + * TYPE=NVM with the connection's client id (or the global namespace when + * the GLOBAL flag is set), and strips the wrapped/hardware flags so a + * client cannot reach a wrapped-key or hardware object through the NVM API. + * Every request path already rejects those flags (see + * wh_KeyId_CheckClientObjectId); the helper's stripping is a backstop that + * keeps any future path TYPE=NVM. */ + return wh_KeyId_TranslateObjectFromClient( + WH_KEYTYPE_NVM, server->comm->client_id, clientId); +#endif +} + +static whNvmId _NvmTranslateToClient(whNvmId serverId) +{ +#ifdef WOLFHSM_CFG_LEGACY_CLIENT_NVM + return serverId; +#else + return wh_KeyId_TranslateToClient(serverId); +#endif +} + /* Handle NVM read, do access checking and clamping */ static int _HandleNvmRead(whServerContext* server, uint8_t* out_data, whNvmSize offset, whNvmSize len, whNvmSize* out_len, @@ -158,11 +190,82 @@ int wh_Server_HandleNvmRequest(whServerContext* server, wh_MessageNvm_TranslateListRequest(magic, (whMessageNvm_ListRequest*)req_packet, &req); +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + rc = wh_KeyId_CheckClientObjectId(req.startId); + if (rc == WH_ERROR_OK) { + rc = WH_SERVER_NVM_LOCK(server); + } +#else rc = WH_SERVER_NVM_LOCK(server); +#endif if (rc == WH_ERROR_OK) { +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM +#ifdef WOLFHSM_CFG_GLOBAL_KEYS + /* The GLOBAL flag on startId selects which namespace to + * iterate: set => global (USER=0), clear => this client's own + * objects (USER=client_id). The flag rides through both + * translation helpers, so iterating with the previously + * returned id stays in the same namespace. */ + const uint16_t target_user = + ((req.startId & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0) + ? WH_KEYUSER_GLOBAL + : server->comm->client_id; +#else + /* Without global keys the GLOBAL flag is inert, here and in + * the translation helpers: only the caller's own namespace + * can be listed. */ + const uint16_t target_user = server->comm->client_id; +#endif + /* startId id-portion of 0 is the start-from-beginning + * sentinel; pass it through unchanged. Otherwise translate to + * the server-internal id so wh_Nvm_List resumes after it. */ + whNvmId cur = + ((req.startId & WH_KEYID_MASK) == 0) + ? 0 + : _NvmTranslateFromClient(server, req.startId); + whNvmId hit_id = 0; + whNvmId total = 0; + int iter = 0; + + for (;;) { + whNvmId next_id = 0; + whNvmId remaining = 0; + /* The directory holds at most + * WOLFHSM_CFG_NVM_OBJECT_COUNT entries, so a backend that + * fails to advance past cur must not hang the server + * while it holds the NVM lock. */ + if (iter++ >= WOLFHSM_CFG_NVM_OBJECT_COUNT) { + rc = WH_ERROR_ABORTED; + break; + } + rc = wh_Nvm_List(server->nvm, req.access, req.flags, cur, + &remaining, &next_id); + if (rc != WH_ERROR_OK || remaining == 0) { + break; + } + + if (WH_KEYID_TYPE(next_id) == WH_KEYTYPE_NVM && + WH_KEYID_USER(next_id) == target_user) { + if (hit_id == 0) { + hit_id = next_id; + } + total++; + } + cur = next_id; + if (remaining == 1) { + break; + } + } + + if (rc == WH_ERROR_OK) { + resp.id = (hit_id != 0) ? _NvmTranslateToClient(hit_id) : 0; + resp.count = total; + } +#else /* Process the list action */ rc = wh_Nvm_List(server->nvm, req.access, req.flags, req.startId, &resp.count, &resp.id); +#endif (void)WH_SERVER_NVM_UNLOCK(server); } /* WH_SERVER_NVM_LOCK() */ @@ -214,16 +317,25 @@ int wh_Server_HandleNvmRequest(whServerContext* server, wh_MessageNvm_TranslateGetMetadataRequest(magic, (whMessageNvm_GetMetadataRequest*)req_packet, &req); +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + rc = wh_KeyId_CheckClientObjectId(req.id); + if (rc == WH_ERROR_OK) { + rc = WH_SERVER_NVM_LOCK(server); + } +#else rc = WH_SERVER_NVM_LOCK(server); +#endif if (rc == WH_ERROR_OK) { /* Process the getmetadata action */ - rc = wh_Nvm_GetMetadata(server->nvm, req.id, &meta); + rc = wh_Nvm_GetMetadata(server->nvm, + _NvmTranslateFromClient(server, req.id), + &meta); (void)WH_SERVER_NVM_UNLOCK(server); } /* WH_SERVER_NVM_LOCK() */ if (rc == WH_ERROR_OK) { - resp.id = meta.id; + resp.id = _NvmTranslateToClient(meta.id); resp.access = meta.access; resp.flags = meta.flags; resp.len = meta.len; @@ -253,32 +365,48 @@ int wh_Server_HandleNvmRequest(whServerContext* server, wh_MessageNvm_TranslateAddObjectRequest(magic, (whMessageNvm_AddObjectRequest*)req_packet, &req); if(req_size == (hdr_len + req.len)) { - /* Process the AddObject action */ - meta.id = req.id; - meta.access = req.access; - meta.flags = req.flags; - meta.len = req.len; - memcpy(meta.label, req.label, sizeof(meta.label)); +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + int validate_rc = + wh_KeyId_CheckClientObjectIdForCreate(req.id); + if (validate_rc != WH_ERROR_OK) { + resp.rc = validate_rc; + } + else +#endif + { + /* Process the AddObject action */ + meta.id = _NvmTranslateFromClient(server, req.id); + meta.access = req.access; + meta.flags = req.flags; + meta.len = req.len; + memcpy(meta.label, req.label, sizeof(meta.label)); - rc = WH_ERROR_OK; + rc = WH_ERROR_OK; #if !defined(WOLFHSM_CFG_NO_CRYPTO) && \ (defined(WOLFSSL_HAVE_LMS) || defined(WOLFSSL_HAVE_XMSS)) - /* Block direct NVM import of stateful (LMS/XMSS) private key - * state; only on-HSM keygen may create such objects. */ - if (wh_Crypto_IsStatefulSigPrivBlob(data, (uint16_t)req.len)) { - rc = WH_ERROR_ACCESS; - } + /* Block direct NVM import of stateful (LMS/XMSS) private + * key state; only on-HSM keygen may create such objects. */ + if (wh_Crypto_IsStatefulSigPrivBlob(data, + (uint16_t)req.len)) { + rc = WH_ERROR_ACCESS; + } #endif - if (rc == WH_ERROR_OK) { - rc = WH_SERVER_NVM_LOCK(server); if (rc == WH_ERROR_OK) { - rc = wh_Nvm_AddObjectChecked(server->nvm, &meta, - req.len, data); + rc = WH_SERVER_NVM_LOCK(server); + if (rc == WH_ERROR_OK) { + rc = wh_Nvm_AddObjectChecked(server->nvm, &meta, + req.len, data); - (void)WH_SERVER_NVM_UNLOCK(server); - } /* WH_SERVER_NVM_LOCK() */ + (void)WH_SERVER_NVM_UNLOCK(server); + } /* WH_SERVER_NVM_LOCK() */ + } + resp.rc = rc; } - resp.rc = rc; + } + else { + /* Payload length disagrees with the packet: nothing was + * written, so do not report success */ + resp.rc = WH_ERROR_ABORTED; } } /* Convert the response struct */ @@ -301,11 +429,30 @@ int wh_Server_HandleNvmRequest(whServerContext* server, (whMessageNvm_DestroyObjectsRequest*)req_packet, &req); if (req.list_count <= WH_MESSAGE_NVM_MAX_DESTROY_OBJECTS_COUNT) { - rc = WH_SERVER_NVM_LOCK(server); + whNvmId + translated_ids[WH_MESSAGE_NVM_MAX_DESTROY_OBJECTS_COUNT]; + whNvmId i; + rc = WH_ERROR_OK; + for (i = 0; i < req.list_count; i++) { +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + /* One malformed id fails the whole request, so no entry + * can silently target a different object */ + rc = wh_KeyId_CheckClientObjectId(req.list[i]); + if (rc != WH_ERROR_OK) { + break; + } +#endif + translated_ids[i] = + _NvmTranslateFromClient(server, req.list[i]); + } + + if (rc == WH_ERROR_OK) { + rc = WH_SERVER_NVM_LOCK(server); + } if (rc == WH_ERROR_OK) { /* Process the DestroyObjects action */ - rc = wh_Nvm_DestroyObjectsChecked(server->nvm, - req.list_count, req.list); + rc = wh_Nvm_DestroyObjectsChecked( + server->nvm, req.list_count, translated_ids); (void)WH_SERVER_NVM_UNLOCK(server); } /* WH_SERVER_NVM_LOCK() */ @@ -339,10 +486,18 @@ int wh_Server_HandleNvmRequest(whServerContext* server, wh_MessageNvm_TranslateReadRequest( magic, (whMessageNvm_ReadRequest*)req_packet, &req); +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + rc = wh_KeyId_CheckClientObjectId(req.id); + if (rc == WH_ERROR_OK) { + rc = WH_SERVER_NVM_LOCK(server); + } +#else rc = WH_SERVER_NVM_LOCK(server); +#endif if (rc == WH_ERROR_OK) { rc = _HandleNvmRead(server, data, req.offset, req.data_len, - &req.data_len, req.id); + &req.data_len, + _NvmTranslateFromClient(server, req.id)); if (rc == WH_ERROR_OK) { data_len = req.data_len; } @@ -401,23 +556,31 @@ int wh_Server_HandleNvmRequest(whServerContext* server, } } if (resp.rc == 0) { + /* Take a local copy of the metadata so we can rewrite the id + * field without touching host memory. */ + whNvmMetadata local_meta = *(const whNvmMetadata*)metadata; +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + resp.rc = wh_KeyId_CheckClientObjectIdForCreate(local_meta.id); +#endif #if !defined(WOLFHSM_CFG_NO_CRYPTO) && \ (defined(WOLFSSL_HAVE_LMS) || defined(WOLFSSL_HAVE_XMSS)) /* Block direct NVM import of stateful (LMS/XMSS) private key state; * only on-HSM keygen may create such objects. */ - if (wh_Crypto_IsStatefulSigPrivBlob((const uint8_t*)data, - (uint16_t)req.data_len)) { + if ((resp.rc == WH_ERROR_OK) && + wh_Crypto_IsStatefulSigPrivBlob((const uint8_t*)data, + (uint16_t)req.data_len)) { resp.rc = WH_ERROR_ACCESS; } - else #endif - { + if (resp.rc == WH_ERROR_OK) { + local_meta.id = _NvmTranslateFromClient(server, local_meta.id); + rc = WH_SERVER_NVM_LOCK(server); if (rc == WH_ERROR_OK) { /* Process the AddObject action */ - rc = wh_Nvm_AddObjectChecked( - server->nvm, (whNvmMetadata*)metadata, req.data_len, - (const uint8_t*)data); + rc = wh_Nvm_AddObjectChecked(server->nvm, &local_meta, + req.data_len, + (const uint8_t*)data); (void)WH_SERVER_NVM_UNLOCK(server); } /* WH_SERVER_NVM_LOCK() */ @@ -459,10 +622,15 @@ int wh_Server_HandleNvmRequest(whServerContext* server, /* Convert request struct */ wh_MessageNvm_TranslateReadDmaRequest(magic, (whMessageNvm_ReadDmaRequest*)req_packet, &req); - +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + resp.rc = wh_KeyId_CheckClientObjectId(req.id); +#endif + } + if (resp.rc == 0) { rc = WH_SERVER_NVM_LOCK(server); if (rc == WH_ERROR_OK) { - rc = wh_Nvm_GetMetadata(server->nvm, req.id, &meta); + whNvmId server_id = _NvmTranslateFromClient(server, req.id); + rc = wh_Nvm_GetMetadata(server->nvm, server_id, &meta); if (rc == 0) { if (req.offset >= meta.len) { @@ -492,7 +660,7 @@ int wh_Server_HandleNvmRequest(whServerContext* server, } if (rc == 0) { /* Process the Read action */ - rc = wh_Nvm_ReadChecked(server->nvm, req.id, req.offset, + rc = wh_Nvm_ReadChecked(server->nvm, server_id, req.offset, read_len, (uint8_t*)data); } /* Always call POST for successful PRE, regardless of read diff --git a/src/wh_server_she.c b/src/wh_server_she.c index bff937d9d..2a6cd83b2 100644 --- a/src/wh_server_she.c +++ b/src/wh_server_she.c @@ -129,6 +129,14 @@ static int _GetId(whServerContext* server, uint16_t magic, uint16_t req_size, const void* req_packet, uint16_t* out_resp_size, void* resp_packet); static uint8_t _BuildSreg(whServerContext* server); +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT +static int _PreProgramKey(whServerContext* server, uint16_t magic, + uint16_t req_size, const void* req_packet, + uint16_t* out_resp_size, void* resp_packet); +static int _DestroyKey(whServerContext* server, uint16_t magic, + uint16_t req_size, const void* req_packet, + uint16_t* out_resp_size, void* resp_packet); +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ static int _TranslateSheReturnCode(int ret); static int _GetUid(whServerContext* server, uint8_t* outUid); static int _StoreUid(whServerContext* server, const uint8_t* uid); @@ -1813,6 +1821,298 @@ static int _GetId(whServerContext* server, uint16_t magic, uint16_t req_size, return ret; } +/* Pre-program a SHE-typed NVM entry under the calling client's USER namespace. + * Replaces the historical client-side use of wh_Client_NvmAddObject with a + * hand-constructed SHE-typed id, which is incompatible with the new client-id + * translation on the NVM message path. */ +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT +static int _PreProgramKey(whServerContext* server, uint16_t magic, + uint16_t req_size, const void* req_packet, + uint16_t* out_resp_size, void* resp_packet) +{ + int ret = 0; + whMessageShe_PreProgramKeyRequest req = {0}; + whMessageShe_PreProgramKeyResponse resp = {0}; + whNvmMetadata meta = {0}; + const uint8_t* key_data; + const uint16_t hdr_len = sizeof(req); + + if (req_size < hdr_len) { + ret = WH_ERROR_BUFFER_SIZE; + } + if (ret == 0) { + ret = wh_MessageShe_TranslatePreProgramKeyRequest(magic, req_packet, + &req); + } + /* SHE slots hold exactly one AES-128 key. The server appends a 16 byte + * constant after the slot contents when deriving key update keys, so any + * other size would overrun its kdf input buffer. */ + if (ret == 0 && + (req.keySz != WH_SHE_KEY_SZ || req_size != hdr_len + req.keySz)) { + ret = WH_ERROR_BADARGS; + } + /* SHE slots are 0..WH_SHE_PRNG_SEED_ID. The id macro keeps only 8 bits, + * so a larger value would silently land on another slot. */ + if (ret == 0 && req.keyId > WH_SHE_PRNG_SEED_ID) { + ret = WH_ERROR_BADARGS; + } + if (ret == 0) { + key_data = (const uint8_t*)req_packet + hdr_len; + + meta.id = + WH_SHE_MAKE_KEYID(server->comm->client_id, (whNvmId)req.keyId); + meta.access = 0; + meta.flags = 0; + meta.len = (whNvmSize)req.keySz; + wh_She_Meta2Label(req.count, req.flags, meta.label); + + ret = WH_SERVER_NVM_LOCK(server); + if (ret == WH_ERROR_OK) { + /* Key reads are cache-first and an earlier SHE command may have + * cached this slot, so drop that copy or it would keep serving + * the old key and label after this write */ + ret = wh_Server_KeystoreEvictKey(server, meta.id); + if (ret == WH_ERROR_NOTFOUND) { + ret = WH_ERROR_OK; + } + if (ret == WH_ERROR_OK) { + ret = wh_Nvm_AddObjectChecked(server->nvm, &meta, + (whNvmSize)req.keySz, key_data); + } + (void)WH_SERVER_NVM_UNLOCK(server); + } + } + + resp.rc = ret; + (void)wh_MessageShe_TranslatePreProgramKeyResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + return ret; +} + +static int _DestroyKey(whServerContext* server, uint16_t magic, + uint16_t req_size, const void* req_packet, + uint16_t* out_resp_size, void* resp_packet) +{ + int ret = 0; + whMessageShe_DestroyKeyRequest req = {0}; + whMessageShe_DestroyKeyResponse resp = {0}; + whNvmId id; + + if (req_size < sizeof(req)) { + ret = WH_ERROR_BUFFER_SIZE; + } + if (ret == 0) { + ret = wh_MessageShe_TranslateDestroyKeyRequest(magic, req_packet, &req); + } + /* Same slot bound as _PreProgramKey */ + if (ret == 0 && req.keyId > WH_SHE_PRNG_SEED_ID) { + ret = WH_ERROR_BADARGS; + } + if (ret == 0) { + id = WH_SHE_MAKE_KEYID(server->comm->client_id, (whNvmId)req.keyId); + ret = WH_SERVER_NVM_LOCK(server); + if (ret == WH_ERROR_OK) { + /* Drop any cached copy so the slot stops serving the destroyed + * key */ + ret = wh_Server_KeystoreEvictKey(server, id); + if (ret == WH_ERROR_NOTFOUND) { + ret = WH_ERROR_OK; + } + if (ret == WH_ERROR_OK) { + ret = wh_Nvm_DestroyObjectsChecked(server->nvm, 1, &id); + } + (void)WH_SERVER_NVM_UNLOCK(server); + } + } + + resp.rc = ret; + (void)wh_MessageShe_TranslateDestroyKeyResponse(magic, &resp, resp_packet); + *out_resp_size = sizeof(resp); + return ret; +} +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ + + +/* Build the action-specific response that carries an error for a SHE request + * that will not run. rc is a wolfHSM error code and is reported as the + * matching SHE error code, except for the test key-management actions, which + * report wolfHSM codes unchanged. Leaves *out_resp_size at 0 for an unknown + * action. */ +static void _FormatSheErrorResponse(uint16_t magic, uint16_t action, int rc, + uint16_t* out_resp_size, void* resp_packet) +{ + int32_t sheRc = _TranslateSheReturnCode(rc); + + switch (action) { + case WH_SHE_SET_UID: { + whMessageShe_SetUidResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateSetUidResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_SECURE_BOOT_INIT: { + whMessageShe_SecureBootInitResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateSecureBootInitResponse( + magic, &resp, resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_SECURE_BOOT_UPDATE: { + whMessageShe_SecureBootUpdateResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateSecureBootUpdateResponse( + magic, &resp, resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_SECURE_BOOT_FINISH: { + whMessageShe_SecureBootFinishResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateSecureBootFinishResponse( + magic, &resp, resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_GET_STATUS: { + whMessageShe_GetStatusResponse resp = {0}; + resp.rc = sheRc; + resp.sreg = 0; + (void)wh_MessageShe_TranslateGetStatusResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_LOAD_KEY: { + whMessageShe_LoadKeyResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateLoadKeyResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_LOAD_PLAIN_KEY: { + whMessageShe_LoadPlainKeyResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateLoadPlainKeyResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_EXPORT_RAM_KEY: { + whMessageShe_ExportRamKeyResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateExportRamKeyResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_INIT_RND: { + whMessageShe_InitRngResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateInitRngResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_RND: { + whMessageShe_RndResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateRndResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_EXTEND_SEED: { + whMessageShe_ExtendSeedResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateExtendSeedResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_ENC_ECB: { + whMessageShe_EncEcbResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateEncEcbResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_ENC_CBC: { + whMessageShe_EncCbcResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateEncCbcResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_DEC_ECB: { + whMessageShe_DecEcbResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateDecEcbResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_DEC_CBC: { + whMessageShe_DecCbcResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateDecCbcResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_GEN_MAC: { + whMessageShe_GenMacResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateGenMacResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_VERIFY_MAC: { + whMessageShe_VerifyMacResponse resp = {0}; + resp.rc = sheRc; + resp.status = 1; /* Verification failed */ + (void)wh_MessageShe_TranslateVerifyMacResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_GET_ID: { + whMessageShe_GetIdResponse resp = {0}; + resp.rc = sheRc; + (void)wh_MessageShe_TranslateGetIdResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT + /* The test key-management actions report wolfHSM error codes + * unchanged */ + case WH_SHE_PRE_PROGRAM_KEY: { + whMessageShe_PreProgramKeyResponse resp = {0}; + resp.rc = rc; + (void)wh_MessageShe_TranslatePreProgramKeyResponse( + magic, &resp, resp_packet); + *out_resp_size = sizeof(resp); + break; + } + case WH_SHE_DESTROY_KEY: { + whMessageShe_DestroyKeyResponse resp = {0}; + resp.rc = rc; + (void)wh_MessageShe_TranslateDestroyKeyResponse(magic, &resp, + resp_packet); + *out_resp_size = sizeof(resp); + break; + } +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ + } +} /* TODO: This is terrible, but without implementing a SHE sub-protocol like we * do for crypto layer, there is no way to return non-request specific error @@ -1831,6 +2131,10 @@ static int _ReportInvalidSheState(whServerContext* server, uint16_t magic, * or UID setup. The UID store is deliberately not consulted so a * failing backend still leaves status readable. */ } + else if (action == WH_SHE_PRE_PROGRAM_KEY || action == WH_SHE_DESTROY_KEY) { + /* Key pre-programming and destruction are provisioning-time + * operations: allowed before UID setup and secure boot. */ + } else { int provisioned = _UidIsProvisioned(server); @@ -1868,159 +2172,25 @@ static int _ReportInvalidSheState(whServerContext* server, uint16_t magic, if (ret != 0) { /* State is invalid, create an error response based on the action */ - switch (action) { - case WH_SHE_SET_UID: { - whMessageShe_SetUidResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateSetUidResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_SECURE_BOOT_INIT: { - whMessageShe_SecureBootInitResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateSecureBootInitResponse( - magic, &resp, resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_SECURE_BOOT_UPDATE: { - whMessageShe_SecureBootUpdateResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateSecureBootUpdateResponse( - magic, &resp, resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_SECURE_BOOT_FINISH: { - whMessageShe_SecureBootFinishResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateSecureBootFinishResponse( - magic, &resp, resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_GET_STATUS: { - whMessageShe_GetStatusResponse resp; - resp.rc = WH_SHE_ERC_SEQUENCE_ERROR; - resp.sreg = 0; - (void)wh_MessageShe_TranslateGetStatusResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_LOAD_KEY: { - whMessageShe_LoadKeyResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateLoadKeyResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_LOAD_PLAIN_KEY: { - whMessageShe_LoadPlainKeyResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateLoadPlainKeyResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_EXPORT_RAM_KEY: { - whMessageShe_ExportRamKeyResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateExportRamKeyResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_INIT_RND: { - whMessageShe_InitRngResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateInitRngResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_RND: { - whMessageShe_RndResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateRndResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_EXTEND_SEED: { - whMessageShe_ExtendSeedResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateExtendSeedResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_ENC_ECB: { - whMessageShe_EncEcbResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateEncEcbResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_ENC_CBC: { - whMessageShe_EncCbcResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateEncCbcResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_DEC_ECB: { - whMessageShe_DecEcbResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateDecEcbResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_DEC_CBC: { - whMessageShe_DecCbcResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateDecCbcResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_GEN_MAC: { - whMessageShe_GenMacResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateGenMacResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_VERIFY_MAC: { - whMessageShe_VerifyMacResponse resp; - resp.rc = _TranslateSheReturnCode(ret); - resp.status = 1; /* Verification failed */ - (void)wh_MessageShe_TranslateVerifyMacResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - case WH_SHE_GET_ID: { - whMessageShe_GetIdResponse resp = {0}; - resp.rc = _TranslateSheReturnCode(ret); - (void)wh_MessageShe_TranslateGetIdResponse(magic, &resp, - resp_packet); - *out_resp_size = sizeof(resp); - break; - } - } + _FormatSheErrorResponse(magic, action, ret, out_resp_size, + resp_packet); } return ret; } +uint16_t wh_Server_SheFormatErrorResponse(uint16_t magic, uint16_t action, + int rc, void* resp_packet) +{ + uint16_t resp_size = 0; + + if (resp_packet == NULL) { + return 0; + } + _FormatSheErrorResponse(magic, action, rc, &resp_size, resp_packet); + return resp_size; +} + int wh_Server_HandleSheRequest(whServerContext* server, uint16_t magic, uint16_t action, uint16_t req_size, const void* req_packet, uint16_t* out_resp_size, @@ -2173,6 +2343,16 @@ int wh_Server_HandleSheRequest(whServerContext* server, uint16_t magic, (void)WH_SERVER_NVM_UNLOCK(server); } /* WH_SERVER_NVM_LOCK() */ break; +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT + case WH_SHE_PRE_PROGRAM_KEY: + ret = _PreProgramKey(server, magic, req_size, req_packet, + out_resp_size, resp_packet); + break; + case WH_SHE_DESTROY_KEY: + ret = _DestroyKey(server, magic, req_size, req_packet, + out_resp_size, resp_packet); + break; +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ default: ret = WH_ERROR_BADARGS; break; diff --git a/test-refactor/client-server/wh_test_crypto_lms.c b/test-refactor/client-server/wh_test_crypto_lms.c index 79d3ca7f0..65938276a 100644 --- a/test-refactor/client-server/wh_test_crypto_lms.c +++ b/test-refactor/client-server/wh_test_crypto_lms.c @@ -431,7 +431,7 @@ static int _whTest_CryptoLmsCryptoCb(whClientContext* ctx, int devId, uint32_t lmsMagic = 0x4C4D5301u; /* 'LMS\1', see wh_crypto.c */ int32_t addRc = 0; int addRet; - whNvmId addId = 0x1042; /* An arbitrary ID in the NVM range */ + whNvmId addId = 0x42; /* arbitrary valid client NVM id (1-255) */ memset(fakeBlob, 0, sizeof(fakeBlob)); memcpy(fakeBlob, &lmsMagic, sizeof(lmsMagic)); fakeBlob[6] = 1; /* privLen field nonzero: a private-bearing blob */ diff --git a/test-refactor/client-server/wh_test_crypto_xmss.c b/test-refactor/client-server/wh_test_crypto_xmss.c index 44b98ccd6..cf6350f7a 100644 --- a/test-refactor/client-server/wh_test_crypto_xmss.c +++ b/test-refactor/client-server/wh_test_crypto_xmss.c @@ -417,7 +417,7 @@ static int _whTest_CryptoXmssCryptoCb(whClientContext* ctx, int devId, uint32_t xmssMagic = 0x584D5301u; /* 'XMS\1', see wh_crypto.c */ int32_t addRc = 0; int addRet; - whNvmId addId = 0x1042; /* An arbitrary ID in the NVM range */ + whNvmId addId = 0x42; /* arbitrary valid client NVM id (1-255) */ memset(fakeBlob, 0, sizeof(fakeBlob)); memcpy(fakeBlob, &xmssMagic, sizeof(xmssMagic)); fakeBlob[6] = 1; /* privLen field nonzero: a private-bearing blob */ diff --git a/test-refactor/client-server/wh_test_keywrap.c b/test-refactor/client-server/wh_test_keywrap.c index c66a3f689..4229b1382 100644 --- a/test-refactor/client-server/wh_test_keywrap.c +++ b/test-refactor/client-server/wh_test_keywrap.c @@ -187,23 +187,31 @@ static int _whTest_KeywrapTrustedKekPolicy(whClientContext* client) } /* (b) A client that provisions an NVM object carrying - * WH_NVM_FLAGS_TRUSTED at a crypto-key id (keys and NVM objects share - * the id space) must not - * obtain a trusted KEK either: the checked NVM add path strips the flag */ + * WH_NVM_FLAGS_TRUSTED at a crypto-key id must not obtain a trusted KEK + * either. The forged id carries the crypto TYPE bits above the client id + * and flag fields, so the checked add rejects it and no KEK may appear + * at the crypto-key id. */ { whNvmId nvmObjId = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, client->comm->client_id, WH_TEST_KW_NVM_FORGE_ID); - int32_t nvmRc = 0; + int32_t nvmRc = 0; + int32_t expectedAddRc = 0; + int expectedRc; + +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + expectedAddRc = WH_ERROR_BADARGS; +#endif ret = wh_Client_NvmAddObject( client, nvmObjId, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_TRUSTED | WH_NVM_FLAGS_USAGE_WRAP, sizeof(label), label, sizeof(srcKey), srcKey, &nvmRc); - if (ret != 0 || nvmRc != 0) { - WH_ERROR_PRINT("trusted-kek: NvmAddObject failed ret=%d rc=%d\n", - ret, (int)nvmRc); - ret = (ret != 0) ? ret : (int)nvmRc; + if (ret != 0 || nvmRc != expectedAddRc) { + WH_ERROR_PRINT("trusted-kek: NvmAddObject expected rc=%d, got " + "ret=%d rc=%d\n", + (int)expectedAddRc, ret, (int)nvmRc); + ret = (ret != 0) ? ret : WH_ERROR_ABORTED; goto cleanup; } wrappedKeySz = sizeof(wrappedKey); @@ -214,10 +222,20 @@ static int _whTest_KeywrapTrustedKekPolicy(whClientContext* client) int32_t destroyRc = 0; (void)wh_Client_NvmDestroyObjects(client, 1, &nvmObjId, &destroyRc); } - if (ret != WH_ERROR_ACCESS) { +#ifdef WOLFHSM_CFG_LEGACY_CLIENT_NVM + /* Legacy flat id space: the forged object lands at the crypto-key + * id; the checked NVM add stripped the TRUSTED flag, so KEK use is + * refused. */ + expectedRc = WH_ERROR_ACCESS; +#else + /* The rejected forge stored nothing, so no KEK exists at the + * crypto-key id. */ + expectedRc = WH_ERROR_NOTFOUND; +#endif + if (ret != expectedRc) { WH_ERROR_PRINT("trusted-kek: wrap-export with NVM-forged KEK " - "expected ACCESS, got %d\n", - ret); + "expected %d, got %d\n", + expectedRc, ret); ret = WH_ERROR_ABORTED; goto cleanup; } diff --git a/test-refactor/client-server/wh_test_she.c b/test-refactor/client-server/wh_test_she.c index 93ad3111b..4c3edcc3b 100644 --- a/test-refactor/client-server/wh_test_she.c +++ b/test-refactor/client-server/wh_test_she.c @@ -63,16 +63,7 @@ * SHE keys are supposed to be fixed hardware keys. */ static int _destroySheKey(whClientContext* client, whNvmId clientSheKeyId) { - int rc = 0; - int32_t serverRc = 0; - whNvmId id = WH_SHE_MAKE_KEYID(client->comm->client_id, clientSheKeyId); - - rc = wh_Client_NvmDestroyObjects(client, 1, &id, &serverRc); - if (rc == WH_ERROR_OK) { - rc = serverRc; - } - - return rc; + return wh_Client_SheDestroyKey(client, clientSheKeyId); } @@ -157,8 +148,6 @@ int whTest_She(whClientContext* client) const uint32_t SHE_TEST_VECTOR_KEY_ID = 4; const uint32_t SHE_WP_KEY_ID = 6; const uint32_t SHE_SIZE_CHECK_KEY_ID = 7; - const uint32_t SHE_OVERSIZE_AUTH_ID = 8; - const uint32_t SHE_OVERSIZE_TARGET_ID = 9; if (client == NULL) { return WH_ERROR_BADARGS; @@ -232,12 +221,12 @@ int whTest_She(whClientContext* client) goto exit; } /* store the boot MAC key and digest */ - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, key, sizeof(key))) != 0) { WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); goto exit; } - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, 0, bootMacDigest, sizeof(bootMacDigest))) != 0) { WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); @@ -270,12 +259,12 @@ int whTest_She(whClientContext* client) /* === Loadable keys and test vectors === */ /* load the secret key and prng seed using pre program */ - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_SECRET_KEY_ID, 0, + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_SECRET_KEY_ID, 0, 0, secretKey, sizeof(secretKey))) != 0) { WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); goto exit; } - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_PRNG_SEED_ID, 0, + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_PRNG_SEED_ID, 0, 0, prngSeed, sizeof(prngSeed))) != 0) { WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); goto exit; @@ -419,9 +408,9 @@ int whTest_She(whClientContext* client) * to the M2 layout overlap between flags and count). Then * re-load the slot with an all-zero UID; the server must * accept it because the stored flags contain WILDCARD. */ - if ((ret = wh_Client_ShePreProgramKey(client, SHE_WILDCARD_KEY_ID, - WH_SHE_FLAG_WILDCARD, vectorRawKey, sizeof(vectorRawKey))) - != 0) { + if ((ret = wh_Client_ShePreProgramKey( + client, SHE_WILDCARD_KEY_ID, 0, WH_SHE_FLAG_WILDCARD, + vectorRawKey, sizeof(vectorRawKey))) != 0) { WH_ERROR_PRINT("Failed to preload wildcard key %d\n", ret); goto exit; } @@ -535,10 +524,9 @@ int whTest_She(whClientContext* client) * overwritten via SHE LoadKey; the server must return * WH_SHE_ERC_WRITE_PROTECTED. Reuses the secret key (auth) and the * secure boot established above; uses a clean slot of its own. */ - if ((ret = wh_Client_ShePreProgramKey(client, SHE_WP_KEY_ID, - WH_SHE_FLAG_WRITE_PROTECT, - vectorRawKey, - sizeof(vectorRawKey))) != 0) { + if ((ret = wh_Client_ShePreProgramKey( + client, SHE_WP_KEY_ID, 0, WH_SHE_FLAG_WRITE_PROTECT, vectorRawKey, + sizeof(vectorRawKey))) != 0) { WH_ERROR_PRINT("Failed to pre-program write-protected key %d\n", ret); goto exit; } @@ -563,7 +551,7 @@ int whTest_She(whClientContext* client) /* A SHE slot holds exactly one AES-128 key. The server appends a 16 * byte constant after the slot contents when deriving key update keys, * so an oversized slot would overrun its kdf input buffer. */ - ret = wh_Client_ShePreProgramKey(client, SHE_SIZE_CHECK_KEY_ID, 0, + ret = wh_Client_ShePreProgramKey(client, SHE_SIZE_CHECK_KEY_ID, 0, 0, oversizeKey, sizeof(oversizeKey)); if (ret != WH_ERROR_BADARGS) { WH_ERROR_PRINT("Oversized SHE pre-program: expected WH_ERROR_BADARGS, " @@ -571,7 +559,7 @@ int whTest_She(whClientContext* client) ret = WH_ERROR_ABORTED; goto exit; } - ret = wh_Client_ShePreProgramKey(client, SHE_SIZE_CHECK_KEY_ID, 0, + ret = wh_Client_ShePreProgramKey(client, SHE_SIZE_CHECK_KEY_ID, 0, 0, oversizeKey, WH_SHE_KEY_SZ / 2); if (ret != WH_ERROR_BADARGS) { WH_ERROR_PRINT("Short SHE pre-program: expected WH_ERROR_BADARGS, " @@ -579,7 +567,10 @@ int whTest_She(whClientContext* client) ret = WH_ERROR_ABORTED; goto exit; } - /* neither attempt may leave anything behind in the slot */ +#ifdef WOLFHSM_CFG_LEGACY_CLIENT_NVM + /* neither attempt may leave anything behind in the slot. Only the legacy + * flat id space can probe a SHE slot through the raw NVM API. */ + (void)oversizeLabel; if ((ret = wh_Client_NvmGetMetadata( client, WH_SHE_MAKE_KEYID(client->comm->client_id, SHE_SIZE_CHECK_KEY_ID), @@ -593,89 +584,39 @@ int whTest_She(whClientContext* client) ret = WH_ERROR_ABORTED; goto exit; } +#endif /* WOLFHSM_CFG_LEGACY_CLIENT_NVM */ WH_TEST_PRINT("SHE pre-program key size SUCCESS\n"); - /* === Oversized auth key slot === */ - - /* The SHE and NVM id spaces overlap, so a client can plant an oversized - * object straight into a SHE slot. LoadKey must reject such a slot as an - * auth key instead of reading it into its fixed kdf input buffer. */ - wh_She_Meta2Label(0, 0, oversizeLabel); - if ((ret = wh_Client_NvmAddObject( - client, - WH_SHE_MAKE_KEYID(client->comm->client_id, SHE_OVERSIZE_AUTH_ID), - 0, 0, sizeof(oversizeLabel), oversizeLabel, sizeof(oversizeKey), - oversizeKey, &sheMetaRc)) != 0) { - WH_ERROR_PRINT("Failed to wh_Client_NvmAddObject %d\n", ret); - goto exit; - } - if (sheMetaRc != 0) { - WH_ERROR_PRINT("Failed to plant oversized SHE slot, got %d\n", - (int)sheMetaRc); - ret = WH_ERROR_ABORTED; - goto exit; - } - if ((ret = wh_She_GenerateLoadableKey( - SHE_SIZE_CHECK_KEY_ID, SHE_OVERSIZE_AUTH_ID, 1, 0, sheUid, - vectorRawKey, vectorRawKey, messageOne, messageTwo, messageThree, - messageFour, messageFive)) != 0) { - WH_ERROR_PRINT("Failed to generate loadable key %d\n", ret); - goto exit; - } - ret = wh_Client_SheLoadKey(client, messageOne, messageTwo, messageThree, - messageFour, messageFive); - if (ret != WH_SHE_ERC_KEY_INVALID) { - WH_ERROR_PRINT("Oversized SHE auth key: expected " - "WH_SHE_ERC_KEY_INVALID, got %d\n", ret); - ret = WH_ERROR_ABORTED; - goto exit; - } - if ((ret = _destroySheKey(client, SHE_OVERSIZE_AUTH_ID)) != 0) { - WH_ERROR_PRINT("Failed to _destroySheKey, ret=%d\n", ret); - goto exit; - } - WH_TEST_PRINT("SHE oversized auth key SUCCESS\n"); - - /* === Oversized target key === */ +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + /* === SHE slots unreachable via raw NVM ids === */ - /* Same overlap, but with the oversized object planted at the target - * slot. Reading it leaves the metadata unset, so an unchecked read sees - * a zeroed label and overwrites a write-protected slot. */ - wh_She_Meta2Label(0, WH_SHE_FLAG_WRITE_PROTECT, oversizeLabel); - if ((ret = wh_Client_NvmAddObject( - client, - WH_SHE_MAKE_KEYID(client->comm->client_id, SHE_OVERSIZE_TARGET_ID), - 0, 0, sizeof(oversizeLabel), oversizeLabel, sizeof(oversizeKey), - oversizeKey, &sheMetaRc)) != 0) { - WH_ERROR_PRINT("Failed to wh_Client_NvmAddObject %d\n", ret); - goto exit; - } - if (sheMetaRc != 0) { - WH_ERROR_PRINT("Failed to plant oversized SHE target slot, got %d\n", - (int)sheMetaRc); - ret = WH_ERROR_ABORTED; - goto exit; - } - if ((ret = wh_She_GenerateLoadableKey( - SHE_OVERSIZE_TARGET_ID, WH_SHE_SECRET_KEY_ID, 1, 0, sheUid, - vectorRawKey, secretKey, messageOne, messageTwo, messageThree, - messageFour, messageFive)) != 0) { - WH_ERROR_PRINT("Failed to generate loadable key %d\n", ret); - goto exit; - } - ret = wh_Client_SheLoadKey(client, messageOne, messageTwo, messageThree, - messageFour, messageFive); - if (ret != WH_SHE_ERC_KEY_INVALID) { - WH_ERROR_PRINT("Oversized SHE target key: expected " - "WH_SHE_ERC_KEY_INVALID, got %d\n", ret); - ret = WH_ERROR_ABORTED; - goto exit; - } - if ((ret = _destroySheKey(client, SHE_OVERSIZE_TARGET_ID)) != 0) { - WH_ERROR_PRINT("Failed to _destroySheKey, ret=%d\n", ret); - goto exit; + /* With per-client NVM id translation the SHE and client NVM id spaces do + * not overlap, so a client cannot plant into a SHE slot: the add itself + * must be rejected. (LoadKey's refusal of an oversized slot is covered + * server-side by the server group's whTest_SheLoadKeyOversizedSlot.) */ + { + const uint32_t SHE_OVERSIZE_AUTH_ID = 8; + + wh_She_Meta2Label(0, 0, oversizeLabel); + if ((ret = wh_Client_NvmAddObject( + client, + WH_SHE_MAKE_KEYID(client->comm->client_id, + SHE_OVERSIZE_AUTH_ID), + 0, 0, sizeof(oversizeLabel), oversizeLabel, + sizeof(oversizeKey), oversizeKey, &sheMetaRc)) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_NvmAddObject %d\n", ret); + goto exit; + } + if (sheMetaRc != WH_ERROR_BADARGS) { + WH_ERROR_PRINT("Planting into a SHE slot: expected " + "WH_ERROR_BADARGS, got %d\n", + (int)sheMetaRc); + ret = WH_ERROR_ABORTED; + goto exit; + } + WH_TEST_PRINT("SHE slot unreachable via raw NVM id SUCCESS\n"); } - WH_TEST_PRINT("SHE oversized target key SUCCESS\n"); +#endif /* !WOLFHSM_CFG_LEGACY_CLIENT_NVM */ /* === Cleanup: destroy provisioned keys so we don't leak NVM === */ diff --git a/test-refactor/config/wolfhsm_cfg.h b/test-refactor/config/wolfhsm_cfg.h index ac428acfe..dda03dd50 100644 --- a/test-refactor/config/wolfhsm_cfg.h +++ b/test-refactor/config/wolfhsm_cfg.h @@ -32,11 +32,14 @@ #define WOLFHSM_CFG_COMM_DATA_LEN (1024 * 8) +/* NOGLOBALKEYS=1 builds leave global keys disabled to cover that config */ +#ifndef WOLFHSM_CFG_TEST_NO_GLOBAL_KEYS #define WOLFHSM_CFG_GLOBAL_KEYS +#endif #define WOLFHSM_CFG_LOGGING -#define WOLFHSM_CFG_NVM_OBJECT_COUNT 30 +#define WOLFHSM_CFG_NVM_OBJECT_COUNT 64 #define WOLFHSM_CFG_SERVER_KEYCACHE_COUNT 9 #define WOLFHSM_CFG_SERVER_KEYCACHE_BUFSIZE 300 #define WOLFHSM_CFG_DMAADDR_COUNT 8 @@ -52,6 +55,11 @@ #define WOLFHSM_CFG_HWKEYSTORE #endif +/* Enable SHE preprogram/destroy test-only key-management APIs. These bypass + * the authenticated SHE key-update protocol and are not part of the SHE + * specification, so they must never be enabled in production builds. */ +#define WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT + /* Test log-based NVM flash backend */ #define WOLFHSM_CFG_SERVER_NVM_FLASH_LOG diff --git a/test-refactor/misc/wh_test_multiclient.c b/test-refactor/misc/wh_test_multiclient.c index bdc021051..6f4949c7a 100644 --- a/test-refactor/misc/wh_test_multiclient.c +++ b/test-refactor/misc/wh_test_multiclient.c @@ -177,6 +177,82 @@ static int _testGlobalKeyBasic(whClientContext* client1, return 0; } +/* + * Test: Global counter shared across clients + * - Client 1 creates and increments a counter in the global namespace + * - Client 2 reads the same global counter id and sees the shared value + * - A plain (per-client) id of the same number is a different counter that + * does not exist for client 2 + * - A global counter has no owner: client 2 can increment, re-initialize and + * destroy it, and client 1 sees each change + */ +static int _testGlobalCounter(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId gCtr = WH_CLIENT_KEYID_MAKE_GLOBAL(7); + uint32_t val = 0; + int ret; + + WH_TEST_PRINT("Test: Global counter shared across clients\n"); + + /* Client 1 creates a global counter starting at 41 */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterInitRequest(client1, gCtr, 41)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterInitResponse(client1, &val)); + WH_TEST_ASSERT_RETURN(val == 41); + + /* Client 1 increments it -> 42 */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterIncrementRequest(client1, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterIncrementResponse(client1, &val)); + WH_TEST_ASSERT_RETURN(val == 42); + + /* Client 2 reads the SAME global counter and sees the shared value */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadRequest(client2, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadResponse(client2, &val)); + WH_TEST_ASSERT_RETURN(val == 42); + + /* Isolation: client 2's own plain counter 7 is a different object that was + * never created */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadRequest(client2, 7)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + ret = wh_Client_CounterReadResponse(client2, &val); + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_NOTFOUND); + + /* A global counter has no owner, exactly like a global key: client 2 + * increments it, resets it to 5, and client 1 sees both changes. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterIncrementRequest(client2, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterIncrementResponse(client2, &val)); + WH_TEST_ASSERT_RETURN(val == 43); + + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterInitRequest(client2, gCtr, 5)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterInitResponse(client2, &val)); + WH_TEST_ASSERT_RETURN(val == 5); + + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadRequest(client1, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadResponse(client1, &val)); + WH_TEST_ASSERT_RETURN(val == 5); + + /* Client 2 destroys it, and it is gone for client 1 too */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterDestroyRequest(client2, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterDestroyResponse(client2)); + + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadRequest(client1, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + ret = wh_Client_CounterReadResponse(client1, &val); + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_NOTFOUND); + + WH_TEST_PRINT(" PASS: Global counter shared across clients\n"); + return 0; +} + /* * Test 2: Local key isolation * - Both clients cache local keys with the same ID but different data @@ -1294,6 +1370,9 @@ static int _runGlobalKeysTests(whClientContext* client1, WH_TEST_RETURN_ON_FAIL( _testGlobalKeyBasic(client1, server1, client2, server2)); + WH_TEST_RETURN_ON_FAIL( + _testGlobalCounter(client1, server1, client2, server2)); + WH_TEST_RETURN_ON_FAIL( _testLocalKeyIsolation(client1, server1, client2, server2)); @@ -1349,6 +1428,946 @@ static int _runGlobalKeysTests(whClientContext* client1, #endif /* WOLFHSM_CFG_GLOBAL_KEYS */ +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM +/* ============================================================================ + * CLIENT NVM ID-TRANSLATION TEST SUITE + * + * These tests assert the per-client NVM id namespace: each client sees its own + * 1..255 id range plus a shared 1..255 global range. Cross-client raw access + * via the NVM api is impossible. + * + * Only meaningful when client NVM id translation is enabled (default). + * ========================================================================== */ + +static const uint8_t NVM_ISOLATION_PAYLOAD_A[] = "client-A-secret-NVM-payload"; +static const uint8_t NVM_ISOLATION_PAYLOAD_B[] = "client-B-different-payload"; + +/* + * Helper: add an NVM object via the explicit Request/Handle/Response + * pattern so that the matching server can be driven manually (multiclient + * sequential setup has no automatic dispatch). + */ +static int _nvmAddViaServer(whClientContext* client, whServerContext* server, + whNvmId id, whNvmSize len, const uint8_t* data, + int32_t* out_rc) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectRequest( + client, id, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, 0, NULL, len, data)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectResponse(client, out_rc)); + return WH_ERROR_OK; +} + +static int _nvmReadViaServer(whClientContext* client, whServerContext* server, + whNvmId id, whNvmSize len, int32_t* out_rc, + whNvmSize* out_len, uint8_t* buf) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmReadRequest(client, id, 0, len)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmReadResponse(client, out_rc, out_len, buf)); + return WH_ERROR_OK; +} + +static int _nvmDestroyViaServer(whClientContext* client, + whServerContext* server, whNvmId id, + int32_t* out_rc) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjectsRequest(client, 1, &id)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjectsResponse(client, out_rc)); + return WH_ERROR_OK; +} + +static int _nvmListViaServer(whClientContext* client, whServerContext* server, + whNvmId startId, int32_t* out_rc, + whNvmId* out_count, whNvmId* out_id) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmListRequest( + client, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, startId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmListResponse(client, out_rc, out_count, out_id)); + return WH_ERROR_OK; +} + +static int _nvmGetMetadataViaServer(whClientContext* client, + whServerContext* server, whNvmId id, + int32_t* out_rc) +{ + whNvmId got_id = 0; + whNvmAccess access = 0; + whNvmFlags flags = 0; + whNvmSize len = 0; + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadataRequest(client, id)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadataResponse( + client, out_rc, &got_id, &access, &flags, &len, 0, NULL)); + return WH_ERROR_OK; +} + +/* + * Client A adds NVM id 5 with secret payload. Client B reading id 5 must NOT + * see A's bytes (either NOTFOUND, or B's own value if B has one). + */ +static int _testNvmClientIsolation(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId shared_id = 5; + int32_t out_rc = 0; + uint8_t buf[64] = {0}; + whNvmSize out_len = 0; + + WH_TEST_PRINT("Testing NVM client isolation...\n"); + + /* Client A adds a secret object at id=5 */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, shared_id, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Client A confirms it can read its own bytes back */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) == 0); + + /* Client B reads id=5: must NOT find A's bytes */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client2, server2, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + /* B has no object 5 yet, so the read must miss and return no bytes */ + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_NOTFOUND); + WH_TEST_ASSERT_RETURN(out_len == 0); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) != 0); + + /* Client B adds its own object at the same client-facing id=5 */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client2, server2, shared_id, + sizeof(NVM_ISOLATION_PAYLOAD_B), + NVM_ISOLATION_PAYLOAD_B, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Client B reads id=5: gets ITS OWN value */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client2, server2, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_B)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_B, + sizeof(NVM_ISOLATION_PAYLOAD_B)) == 0); + + /* Client A still sees ITS OWN value, not B's */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) == 0); + + /* Cleanup: each client destroys its own */ + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, shared_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client2, server2, shared_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM client isolation: PASS\n"); + return WH_ERROR_OK; +} + +/* + * A connection that has not completed COMM INIT has client_id 0, which would + * translate every NVM request into the USER=0 namespace (shared/global and + * factory-provisioned objects). The server must refuse NVM requests from such + * an unbound client. Force the server connection's client_id to 0 and confirm + * a planted USER=0 object is never read, enumerated, added over, or destroyed. + */ +static int _testNvmUnboundClientRejected(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId planted_id = 8; + whNvmId planted_nvm_id; + whNvmMetadata meta = {0}; + uint8_t saved_id; + int32_t out_rc = 0; + int prc; + int leaked = 0; + whNvmId count = 0; + whNvmId list_id = 0; + whNvmSize out_len = 0; + uint8_t buf[64] = {0}; + + (void)client2; + (void)server2; + + WH_TEST_PRINT( + "Testing NVM reject of unbound (client_id 0) connection...\n"); + + /* Plant a USER=0 object directly, as provisioning would. */ + planted_nvm_id = + WH_MAKE_KEYID(WH_KEYTYPE_NVM, WH_KEYUSER_GLOBAL, planted_id); + meta.id = planted_nvm_id; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = sizeof(NVM_ISOLATION_PAYLOAD_B); + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, sizeof(NVM_ISOLATION_PAYLOAD_B), + NVM_ISOLATION_PAYLOAD_B) == WH_ERROR_OK); + + /* Force the server connection into the unbound (pre-COMM-INIT) state. */ + saved_id = server1->comm->client_id; + server1->comm->client_id = WH_KEYUSER_GLOBAL; + + /* Every verb must be refused with a properly shaped reply carrying + * WH_ERROR_ACCESS; a parse failure would be a malformed reply, not a + * refusal. Accumulate a "leaked" flag across all verbs, then restore + * client_id before asserting so a failure can't corrupt later tests + * sharing this server. */ + + /* Read: refused, and never returns the planted bytes. */ + prc = wh_Client_NvmReadRequest(client1, planted_id, 0, sizeof(buf)); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmReadResponse(client1, &out_rc, &out_len, buf); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + if (memcmp(buf, NVM_ISOLATION_PAYLOAD_B, sizeof(NVM_ISOLATION_PAYLOAD_B)) == + 0) { + leaked = 1; + } + + /* GetMetadata: refused. */ + out_rc = 0; + prc = wh_Client_NvmGetMetadataRequest(client1, planted_id); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + whNvmId got_id = 0; + whNvmAccess access = 0; + whNvmFlags flags = 0; + whNvmSize len = 0; + prc = wh_Client_NvmGetMetadataResponse(client1, &out_rc, &got_id, + &access, &flags, &len, 0, NULL); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + + /* List: refused (cannot enumerate the USER=0 namespace). */ + out_rc = 0; + prc = wh_Client_NvmListRequest(client1, WH_NVM_ACCESS_ANY, + WH_NVM_FLAGS_NONE, 0); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmListResponse(client1, &out_rc, &count, &list_id); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + + /* Destroy: refused. */ + out_rc = 0; + { + whNvmId destroy_id = planted_id; + prc = wh_Client_NvmDestroyObjectsRequest(client1, 1, &destroy_id); + } + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmDestroyObjectsResponse(client1, &out_rc); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + + /* Add: refused. */ + out_rc = 0; + prc = wh_Client_NvmAddObjectRequest( + client1, 5, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, 0, NULL, + sizeof(NVM_ISOLATION_PAYLOAD_A), NVM_ISOLATION_PAYLOAD_A); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmAddObjectResponse(client1, &out_rc); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + +#ifdef WOLFHSM_CFG_DMA + /* DMA add: refused (shares the same handler entry gate). */ + out_rc = 0; + { + whNvmMetadata dma_meta = {0}; + dma_meta.id = 5; + dma_meta.access = WH_NVM_ACCESS_ANY; + dma_meta.flags = WH_NVM_FLAGS_NONE; + dma_meta.len = sizeof(NVM_ISOLATION_PAYLOAD_A); + prc = wh_Client_NvmAddObjectDmaRequest(client1, &dma_meta, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmAddObjectDmaResponse(client1, &out_rc); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + } + + /* DMA read: refused, and never returns the planted bytes. */ + out_rc = 0; + memset(buf, 0, sizeof(buf)); + prc = wh_Client_NvmReadDmaRequest(client1, planted_id, 0, + sizeof(NVM_ISOLATION_PAYLOAD_B), buf); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmReadDmaResponse(client1, &out_rc); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + if (memcmp(buf, NVM_ISOLATION_PAYLOAD_B, sizeof(NVM_ISOLATION_PAYLOAD_B)) == + 0) { + leaked = 1; + } +#endif /* WOLFHSM_CFG_DMA */ + + /* Restore the bound client id before asserting. */ + server1->comm->client_id = saved_id; + + WH_TEST_ASSERT_RETURN(leaked == 0); + + /* The refused adds must not have created USER=0 object 5 either. */ + { + whNvmId added_id = + WH_MAKE_KEYID(WH_KEYTYPE_NVM, WH_KEYUSER_GLOBAL, 5); + whNvmMetadata check = {0}; + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server1->nvm, added_id, + &check) == WH_ERROR_NOTFOUND); + } + + /* The planted USER=0 object must still exist, unchanged. */ + memset(buf, 0, sizeof(buf)); + WH_TEST_ASSERT_RETURN(wh_Nvm_Read(server1->nvm, planted_nvm_id, 0, + sizeof(NVM_ISOLATION_PAYLOAD_B), + buf) == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_B, + sizeof(NVM_ISOLATION_PAYLOAD_B)) == 0); + + /* Cleanup */ + WH_TEST_ASSERT_RETURN( + wh_Nvm_DestroyObjects(server1->nvm, 1, &planted_nvm_id) == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM unbound-client reject: PASS\n"); + return WH_ERROR_OK; +} + +#ifdef WOLFHSM_CFG_GLOBAL_KEYS +/* + * List has two namespaces: + * - own : startId without GLOBAL flag + * - global : startId with WH_KEYID_CLIENT_GLOBAL_FLAG set + * + * Client A populates 2 own ids and 2 global ids. Each scan must visit only + * the corresponding namespace. + */ +static int _testNvmGlobalNamespaceList(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int32_t out_rc = 0; + whNvmId count = 0; + whNvmId cur = 0; + int seen_own[256] = {0}; + int seen_global[256] = {0}; + int i; + int iters; + const whNvmId own_ids[2] = {3, 7}; + const whNvmId global_ids[2] = {2, 4}; + (void)server2; + (void)client2; + + WH_TEST_PRINT("Testing NVM list with global namespace...\n"); + + /* Populate own ids */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, own_ids[i], sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + /* Populate global ids */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, global_ids[i] | WH_KEYID_CLIENT_GLOBAL_FLAG, + sizeof(NVM_ISOLATION_PAYLOAD_B), NVM_ISOLATION_PAYLOAD_B, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + /* Iterate own namespace (no flag on startId). Expect ids in own_ids. + * Cap iterations to defend against an unintentional infinite loop. */ + cur = 0; + for (iters = 0; iters < 16; iters++) { + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + if (count == 0) { + break; + } + /* Must not carry the GLOBAL flag */ + WH_TEST_ASSERT_RETURN((cur & WH_KEYID_CLIENT_GLOBAL_FLAG) == 0); + seen_own[cur & WH_KEYID_MASK] = 1; + if (count == 1) { + break; + } + } + WH_TEST_ASSERT_RETURN(iters < 16); + + /* Iterate global namespace (GLOBAL flag on startId). Expect ids in + * global_ids, all returned with GLOBAL flag set. */ + cur = WH_KEYID_CLIENT_GLOBAL_FLAG; + for (iters = 0; iters < 16; iters++) { + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + if (count == 0) { + break; + } + WH_TEST_ASSERT_RETURN((cur & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0); + seen_global[cur & WH_KEYID_MASK] = 1; + if (count == 1) { + break; + } + } + WH_TEST_ASSERT_RETURN(iters < 16); + + for (i = 0; i < 2; i++) { + WH_TEST_ASSERT_RETURN(seen_own[own_ids[i]] == 1); + WH_TEST_ASSERT_RETURN(seen_global[global_ids[i]] == 1); + } + /* Crosscheck: globals never appear in the own scan and vice-versa */ + WH_TEST_ASSERT_RETURN(seen_own[global_ids[0]] == 0); + WH_TEST_ASSERT_RETURN(seen_own[global_ids[1]] == 0); + WH_TEST_ASSERT_RETURN(seen_global[own_ids[0]] == 0); + WH_TEST_ASSERT_RETURN(seen_global[own_ids[1]] == 0); + + /* Cleanup */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, own_ids[i], &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL(_nvmDestroyViaServer( + client1, server1, global_ids[i] | WH_KEYID_CLIENT_GLOBAL_FLAG, + &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + WH_TEST_PRINT(" NVM global namespace list: PASS\n"); + return WH_ERROR_OK; +} + +#else /* !WOLFHSM_CFG_GLOBAL_KEYS */ + +/* + * Without global keys there is no global namespace: AddObject must reject + * the GLOBAL flag and the other verbs must ignore it, resolving to the + * caller's own namespace. USER=0 objects (e.g. provisioned by whnvmtool) + * must stay unreachable, including via List. + */ +static int _testNvmGlobalFlagDisabled(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId own_id = 3; + const whNvmId planted_id = 6; + whNvmId planted_nvm_id; + whNvmMetadata meta = {0}; + int32_t out_rc = 0; + whNvmId count = 0; + whNvmId cur = 0; + whNvmSize out_len = 0; + uint8_t buf[64] = {0}; + + (void)client2; + (void)server2; + + WH_TEST_PRINT("Testing NVM GLOBAL flag with global keys disabled...\n"); + + /* Plant a USER=0 object directly, as provisioning would */ + planted_nvm_id = + WH_MAKE_KEYID(WH_KEYTYPE_NVM, WH_KEYUSER_GLOBAL, planted_id); + meta.id = planted_nvm_id; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = sizeof(NVM_ISOLATION_PAYLOAD_B); + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, sizeof(NVM_ISOLATION_PAYLOAD_B), + NVM_ISOLATION_PAYLOAD_B) == WH_ERROR_OK); + + /* AddObject with the GLOBAL flag fails loudly */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, 5 | WH_KEYID_CLIENT_GLOBAL_FLAG, + sizeof(NVM_ISOLATION_PAYLOAD_A), NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + +#ifdef WOLFHSM_CFG_DMA + /* The DMA add path shares the same id validation */ + { + whNvmMetadata dma_meta = {0}; + dma_meta.id = 5 | WH_KEYID_CLIENT_GLOBAL_FLAG; + dma_meta.access = WH_NVM_ACCESS_ANY; + dma_meta.flags = WH_NVM_FLAGS_NONE; + dma_meta.len = sizeof(NVM_ISOLATION_PAYLOAD_A); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectDmaRequest( + client1, &dma_meta, sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmAddObjectDmaResponse(client1, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + } +#endif + + /* Add an own object without the flag */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, own_id, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* A GLOBAL-flagged List ignores the flag: it walks the caller's own + * namespace and never surfaces the planted USER=0 object */ + cur = WH_KEYID_CLIENT_GLOBAL_FLAG; + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(count == 1); + WH_TEST_ASSERT_RETURN(cur == own_id); + + /* Read ignores the flag the same way: it resolves to the own object */ + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, own_id | WH_KEYID_CLIENT_GLOBAL_FLAG, sizeof(buf), + &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, out_len) == 0); + + /* The planted USER=0 object is not reachable via the flag */ + WH_TEST_RETURN_ON_FAIL(_nvmGetMetadataViaServer( + client1, server1, planted_id | WH_KEYID_CLIENT_GLOBAL_FLAG, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* Cleanup */ + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, own_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN( + wh_Nvm_DestroyObjects(server1->nvm, 1, &planted_nvm_id) == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM GLOBAL flag disabled semantics: PASS\n"); + return WH_ERROR_OK; +} +#endif /* WOLFHSM_CFG_GLOBAL_KEYS */ + +/* + * Reject malformed AddObject requests: + * - id portion == 0 (erased sentinel) + * - wrapped flag set + */ +static int _testNvmAddObjectRejections(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int32_t out_rc = 0; + (void)server2; + (void)client2; + + WH_TEST_PRINT("Testing NVM AddObject bad-id rejections...\n"); + + /* id=0 with own scope */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, 0, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* id=0 with global scope */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, + WH_KEYID_CLIENT_GLOBAL_FLAG, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* wrapped flag set */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, + 5 | WH_KEYID_CLIENT_WRAPPED_FLAG, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* bits above the id and client-flag fields (a legacy-style internal id) + * must fail, not silently truncate to id 0x42 */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, 0x1042, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + WH_TEST_PRINT(" NVM AddObject rejections: PASS\n"); + return WH_ERROR_OK; +} + +/* + * Non-add verbs must reject ids with bits above the id and client-flag + * fields instead of silently truncating them onto another object. Add object + * 0x42, then confirm the legacy-style id 0x1042 cannot read, stat, list-from, + * or destroy it. + */ +static int _testNvmNonAddVerbRejections(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId goodId = 0x42; + const whNvmId aliasId = 0x1042; /* truncates to 0x42 if not rejected */ + int32_t out_rc = 0; + whNvmSize out_len = 0; + whNvmId list_id = 0; + whNvmId count = 0; + uint8_t buf[64] = {0}; + + (void)client2; + (void)server2; + + WH_TEST_PRINT("Testing NVM bad-id rejection on non-add verbs...\n"); + + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, goodId, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + WH_TEST_RETURN_ON_FAIL( + _nvmGetMetadataViaServer(client1, server1, aliasId, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, aliasId, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmListViaServer(client1, server1, aliasId, &out_rc, + &count, &list_id)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + + /* The destructive verb is the dangerous one: without the per-verb check, + * destroying 0x1042 silently destroyed 0x42 */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, aliasId, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + +#ifdef WOLFHSM_CFG_DMA + /* The DMA read path shares the check */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmReadDmaRequest(client1, aliasId, 0, sizeof(buf), buf)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmReadDmaResponse(client1, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); +#endif + + /* The object the malformed id would have truncated onto is untouched */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, goodId, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, out_len) == 0); + + /* Cleanup */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, goodId, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM non-add verb rejections: PASS\n"); + return WH_ERROR_OK; +} + +/* + * The WRAPPED and HW client flags must not let the NVM API reach a + * differently-typed object. Plant a WRAPPED-typed and an HW-typed object in + * client1's own namespace at numeric id 9, then confirm the client NVM + * read/getmetadata/destroy verbs cannot touch them via those flags. + */ +static int _testNvmWrappedHwFlagIsolation(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId planted_id = 9; + const uint8_t secret[] = "planted-non-nvm-secret"; + const whNvmSize secretSz = (whNvmSize)sizeof(secret); + const whKeyId clientFlags[2] = {WH_KEYID_CLIENT_WRAPPED_FLAG, + WH_KEYID_CLIENT_HW_FLAG}; + int32_t out_rc = 0; + whNvmSize out_len = 0; + uint8_t buf[64] = {0}; + whNvmMetadata meta = {0}; + whKeyId wrappedId; + whKeyId hwId; + int i; + + (void)client2; + (void)server2; + + WH_TEST_PRINT("Testing NVM WRAPPED/HW flag type isolation...\n"); + + wrappedId = + WH_MAKE_KEYID(WH_KEYTYPE_WRAPPED, server1->comm->client_id, planted_id); + hwId = WH_MAKE_KEYID(WH_KEYTYPE_HW, server1->comm->client_id, planted_id); + + /* Plant the two forbidden objects directly in the shared store. */ + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = secretSz; + meta.id = wrappedId; + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, secretSz, secret) == WH_ERROR_OK); + meta.id = hwId; + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, secretSz, secret) == WH_ERROR_OK); + + for (i = 0; i < 2; i++) { + whNvmId flagged = (whNvmId)(planted_id | clientFlags[i]); + + /* Read: must not return the planted bytes. The flagged id is rejected + * up front; the translation-time strip is only a backstop. */ + memset(buf, 0, sizeof(buf)); + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, flagged, secretSz, &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(memcmp(buf, secret, secretSz) != 0); + + /* GetMetadata: must not surface the planted object. */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmGetMetadataViaServer(client1, server1, flagged, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* Destroy: must not reach the planted object. */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, flagged, &out_rc)); + } + + /* Both planted objects must still exist after the flagged destroys. */ + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server1->nvm, wrappedId, &meta) == + WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server1->nvm, hwId, &meta) == + WH_ERROR_OK); + + /* Clean up the planted objects. */ + (void)wh_Nvm_DestroyObjects(server1->nvm, 1, &wrappedId); + (void)wh_Nvm_DestroyObjects(server1->nvm, 1, &hwId); + + WH_TEST_PRINT(" NVM WRAPPED/HW flag isolation: PASS\n"); + return WH_ERROR_OK; +} + +static int _runNvmIdTranslationTests(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + WH_TEST_PRINT("=== NVM Id Translation Tests Begin ===\n"); + WH_TEST_RETURN_ON_FAIL( + _testNvmClientIsolation(client1, server1, client2, server2)); + WH_TEST_RETURN_ON_FAIL( + _testNvmUnboundClientRejected(client1, server1, client2, server2)); +#ifdef WOLFHSM_CFG_GLOBAL_KEYS + WH_TEST_RETURN_ON_FAIL( + _testNvmGlobalNamespaceList(client1, server1, client2, server2)); +#else + WH_TEST_RETURN_ON_FAIL( + _testNvmGlobalFlagDisabled(client1, server1, client2, server2)); +#endif + WH_TEST_RETURN_ON_FAIL( + _testNvmAddObjectRejections(client1, server1, client2, server2)); + WH_TEST_RETURN_ON_FAIL( + _testNvmNonAddVerbRejections(client1, server1, client2, server2)); + WH_TEST_RETURN_ON_FAIL( + _testNvmWrappedHwFlagIsolation(client1, server1, client2, server2)); + WH_TEST_PRINT("All NVM Id Translation Tests PASSED ===\n"); + return WH_ERROR_OK; +} + +#endif /* !WOLFHSM_CFG_LEGACY_CLIENT_NVM */ + +/* + * The keystore group shares the pre-COMM-INIT gate, and its DMA replies carry + * rc behind a DMA address status rather than first. Force the unbound state + * and confirm every keystore verb surfaces WH_ERROR_ACCESS through a properly + * shaped reply, so a refused DMA cache or export is never reported as success. + */ +static int _testKeystoreUnboundClientRejected(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + /* Id no other test caches, so the absence checks below are meaningful */ + const whKeyId keyId = 0x3A; + const whKeyId globalId = + WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, WH_KEYUSER_GLOBAL, keyId); + uint8_t keyData[32] = "UnboundKeystoreRejectKey12!"; + uint8_t outBuf[32] = {0}; + uint8_t label[WH_NVM_LABEL_LEN] = {0}; + uint32_t readSz = sizeof(outBuf); + uint16_t outSz = 0; + uint16_t gotId = 0; + uint8_t saved_id; + int prc; + int leaked = 0; + + (void)client2; + (void)server2; + + WH_TEST_PRINT( + "Testing keystore reject of unbound (client_id 0) connection...\n"); + + /* Precondition: no USER=0 key under this id yet. */ + WH_TEST_ASSERT_RETURN( + wh_Server_KeystoreReadKey(server1, globalId, NULL, outBuf, &readSz) == + WH_ERROR_NOTFOUND); + + /* Force the server connection into the unbound (pre-COMM-INIT) state. */ + saved_id = server1->comm->client_id; + server1->comm->client_id = WH_KEYUSER_GLOBAL; + + /* Each verb must return WH_ERROR_ACCESS from a shaped reply: OK would be + * a false success, ABORTED a malformed reply. Accumulate a "leaked" flag, + * then restore client_id before asserting so a failure can't corrupt + * later tests sharing this server. */ + + /* Cache: refused. */ + prc = wh_Client_KeyCacheRequest_ex(client1, 0, (uint8_t*)"Unbound", + sizeof("Unbound"), keyData, + sizeof(keyData), keyId); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_KeyCacheResponse(client1, &gotId); + } + if (prc != WH_ERROR_ACCESS) { + leaked = 1; + } + + /* Export: refused. */ + outSz = sizeof(outBuf); + prc = wh_Client_KeyExportRequest(client1, keyId); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_KeyExportResponse(client1, label, sizeof(label), outBuf, + &outSz); + } + if (prc != WH_ERROR_ACCESS) { + leaked = 1; + } + +#ifdef WOLFHSM_CFG_DMA + /* DMA cache: rc sits behind the DMA address status in the reply. */ + prc = wh_Client_KeyCacheDmaRequest(client1, 0, (uint8_t*)"Unbound", + sizeof("Unbound"), keyData, + sizeof(keyData), keyId); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_KeyCacheDmaResponse(client1, &gotId); + } + if (prc != WH_ERROR_ACCESS) { + leaked = 1; + } + + /* DMA export: refused. */ + outSz = sizeof(outBuf); + prc = wh_Client_KeyExportDmaRequest(client1, keyId, outBuf, sizeof(outBuf)); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_KeyExportDmaResponse(client1, label, sizeof(label), + &outSz); + } + if (prc != WH_ERROR_ACCESS) { + leaked = 1; + } + + /* DMA public export: refused. */ + outSz = sizeof(outBuf); + prc = wh_Client_KeyExportPublicDmaRequest(client1, keyId, WH_KEY_ALGO_ECC, + outBuf, sizeof(outBuf)); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_KeyExportPublicDmaResponse(client1, label, + sizeof(label), &outSz); + } + if (prc != WH_ERROR_ACCESS) { + leaked = 1; + } +#endif /* WOLFHSM_CFG_DMA */ + + /* Restore the bound client id before asserting. */ + server1->comm->client_id = saved_id; + + WH_TEST_ASSERT_RETURN(leaked == 0); + + /* The refused caches must not have left a USER=0 key behind. */ + readSz = sizeof(outBuf); + WH_TEST_ASSERT_RETURN( + wh_Server_KeystoreReadKey(server1, globalId, NULL, outBuf, &readSz) == + WH_ERROR_NOTFOUND); + + WH_TEST_PRINT(" Keystore unbound client rejection: PASS\n"); + return WH_ERROR_OK; +} + /* ============================================================================ * GLOBAL SHE KEYS TEST SUITE * @@ -1366,28 +2385,91 @@ static int _runGlobalKeysTests(whClientContext* client1, #define SHE_MC_PRIME_SLOT 8 #define SHE_MC_CTR_SLOT 9 -/* Provision a SHE slot in the shared NVM, the way ShePreProgramKey does but - * with the split API. Counter and SHE flags go in the object label. */ -static int _sheGlobalAddNvmKey(whClientContext* client, whServerContext* server, - uint8_t sheSlot, uint32_t counter, - uint32_t sheFlags, const uint8_t* key) +/* Provision a SHE slot through the dedicated pre-program message with the + * split API. Counter and SHE flags go in the object label. Raw NVM adds can + * no longer reach SHE slots since client NVM ids live in their own id space. */ +static int _sheGlobalPreProgramKey(whClientContext* client, + whServerContext* server, uint8_t sheSlot, + uint32_t counter, uint32_t sheFlags, + const uint8_t* key) { - int ret; - int32_t rc = 0; - uint8_t label[WH_NVM_LABEL_LEN] = {0}; - - wh_She_Meta2Label(counter, sheFlags, label); - ret = wh_Client_NvmAddObjectRequest( - client, WH_SHE_MAKE_KEYID(client->comm->client_id, sheSlot), 0, 0, - sizeof(label), label, WH_SHE_KEY_SZ, key); + int ret; + uint16_t group = 0; + uint16_t action = 0; + uint16_t dataSz = 0; + whMessageShe_PreProgramKeyRequest* req; + whMessageShe_PreProgramKeyResponse* resp; + uint8_t* reqBuf; + + reqBuf = (uint8_t*)wh_CommClient_GetDataPtr(client->comm); + req = (whMessageShe_PreProgramKeyRequest*)reqBuf; + req->keyId = sheSlot; + req->count = counter; + req->flags = sheFlags; + req->keySz = WH_SHE_KEY_SZ; + memcpy(reqBuf + sizeof(*req), key, WH_SHE_KEY_SZ); + + ret = wh_Client_SendRequest( + client, WH_MESSAGE_GROUP_SHE, WH_SHE_PRE_PROGRAM_KEY, + (uint16_t)(sizeof(*req) + WH_SHE_KEY_SZ), reqBuf); if (ret == 0) { ret = wh_Server_HandleRequestMessage(server); } if (ret == 0) { - ret = wh_Client_NvmAddObjectResponse(client, &rc); + resp = (whMessageShe_PreProgramKeyResponse*)wh_CommClient_GetDataPtr( + client->comm); + ret = wh_Client_RecvResponse(client, &group, &action, &dataSz, + WOLFHSM_CFG_COMM_DATA_LEN, (uint8_t*)resp); + if (ret == 0) { + if ((group != WH_MESSAGE_GROUP_SHE) || + (action != WH_SHE_PRE_PROGRAM_KEY) || + (dataSz != sizeof(*resp))) { + ret = WH_ERROR_ABORTED; + } + else { + ret = (int)resp->rc; + } + } + } + return ret; +} + +/* Remove a SHE slot through the dedicated destroy message with the split API */ +static int _sheGlobalDestroyKey(whClientContext* client, + whServerContext* server, uint8_t sheSlot) +{ + int ret; + uint16_t group = 0; + uint16_t action = 0; + uint16_t dataSz = 0; + whMessageShe_DestroyKeyRequest* req; + whMessageShe_DestroyKeyResponse* resp; + + req = + (whMessageShe_DestroyKeyRequest*)wh_CommClient_GetDataPtr(client->comm); + memset(req, 0, sizeof(*req)); + req->keyId = sheSlot; + + ret = + wh_Client_SendRequest(client, WH_MESSAGE_GROUP_SHE, WH_SHE_DESTROY_KEY, + sizeof(*req), (uint8_t*)req); + if (ret == 0) { + ret = wh_Server_HandleRequestMessage(server); } if (ret == 0) { - ret = (int)rc; + resp = (whMessageShe_DestroyKeyResponse*)wh_CommClient_GetDataPtr( + client->comm); + ret = wh_Client_RecvResponse(client, &group, &action, &dataSz, + WOLFHSM_CFG_COMM_DATA_LEN, (uint8_t*)resp); + if (ret == 0) { + if ((group != WH_MESSAGE_GROUP_SHE) || + (action != WH_SHE_DESTROY_KEY) || (dataSz != sizeof(*resp))) { + ret = WH_ERROR_ABORTED; + } + else { + ret = (int)resp->rc; + } + } } return ret; } @@ -1633,16 +2715,16 @@ static int _runSheGlobalTests(whClientContext* client1, * and the expected bootloader digest; same UID on both servers */ WH_TEST_RETURN_ON_FAIL(_sheGlobalComputeBootMac( bootloader, sizeof(bootloader), bootMacKey, bootDigest)); - WH_TEST_RETURN_ON_FAIL(_sheGlobalAddNvmKey( + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( client1, server1, WH_SHE_SECRET_KEY_ID, 0, 0, secretKey)); - WH_TEST_RETURN_ON_FAIL(_sheGlobalAddNvmKey( + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( client1, server1, WH_SHE_MASTER_ECU_KEY_ID, 0, 0, masterKey)); - WH_TEST_RETURN_ON_FAIL(_sheGlobalAddNvmKey( + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( client1, server1, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, bootMacKey)); - WH_TEST_RETURN_ON_FAIL(_sheGlobalAddNvmKey( + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( client1, server1, WH_SHE_BOOT_MAC, 0, 0, bootDigest)); - WH_TEST_RETURN_ON_FAIL( - _sheGlobalAddNvmKey(client1, server1, SHE_MC_USER_SLOT, 0, 0, userKey)); + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( + client1, server1, SHE_MC_USER_SLOT, 0, 0, userKey)); WH_TEST_RETURN_ON_FAIL( _sheGlobalSetUid(client1, server1, sheUid, sizeof(sheUid))); WH_TEST_RETURN_ON_FAIL( @@ -1730,7 +2812,7 @@ static int _runSheGlobalTests(whClientContext* client1, WH_TEST_PRINT(" PASS: Cross-client unwrap-and-cache prime\n"); /* Counter guard runs against the globally committed slot */ - WH_TEST_RETURN_ON_FAIL(_sheGlobalAddNvmKey( + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( client1, server1, SHE_MC_CTR_SLOT, 5, 0, ctrKey)); blobSz = sizeof(blob); WH_TEST_RETURN_ON_FAIL(whTest_BuildSheKeyBlob( @@ -1788,20 +2870,18 @@ static int _runSheGlobalTests(whClientContext* client1, SHE_MC_USER_SLOT, SHE_MC_LOAD_SLOT, SHE_MC_PRIME_SLOT, SHE_MC_CTR_SLOT, WH_SHE_RAM_KEY_ID, }; - /* All SHE ids are global here, so the client id argument is moot */ - whNvmId destroyList[] = { - WH_SHE_MAKE_KEYID(0, WH_SHE_SECRET_KEY_ID), - WH_SHE_MAKE_KEYID(0, WH_SHE_MASTER_ECU_KEY_ID), - WH_SHE_MAKE_KEYID(0, WH_SHE_BOOT_MAC_KEY_ID), - WH_SHE_MAKE_KEYID(0, WH_SHE_BOOT_MAC), - WH_SHE_MAKE_KEYID(0, SHE_MC_USER_SLOT), - WH_SHE_MAKE_KEYID(0, SHE_MC_LOAD_SLOT), + static const uint8_t destroySlots[] = { + WH_SHE_SECRET_KEY_ID, + WH_SHE_MASTER_ECU_KEY_ID, + WH_SHE_BOOT_MAC_KEY_ID, + WH_SHE_BOOT_MAC, + SHE_MC_USER_SLOT, + SHE_MC_LOAD_SLOT, #if defined(WOLFHSM_CFG_KEYWRAP) && defined(HAVE_AESGCM) /* Only created by the keywrap sub-tests above */ - WH_SHE_MAKE_KEYID(0, SHE_MC_CTR_SLOT), + SHE_MC_CTR_SLOT, #endif }; - int32_t rc = 0; for (i = 0; i < (int)sizeof(evictSlots); i++) { ret = wh_Server_KeystoreEvictKey( @@ -1811,13 +2891,10 @@ static int _runSheGlobalTests(whClientContext* client1, return ret; } } - WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjectsRequest( - client1, (whNvmId)(sizeof(destroyList) / sizeof(destroyList[0])), - destroyList)); - WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); - WH_TEST_RETURN_ON_FAIL( - wh_Client_NvmDestroyObjectsResponse(client1, &rc)); - WH_TEST_ASSERT_RETURN(rc == 0); + for (i = 0; i < (int)sizeof(destroySlots); i++) { + WH_TEST_RETURN_ON_FAIL( + _sheGlobalDestroyKey(client1, server1, destroySlots[i])); + } } WH_TEST_PRINT("All Global SHE Keys Tests PASSED ===\n"); @@ -2071,6 +3148,15 @@ static int _whTest_MultiClient(void) _runSheGlobalTests(client1, server1, client2, server2)); #endif +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + WH_TEST_RETURN_ON_FAIL( + _runNvmIdTranslationTests(client1, server1, client2, server2)); +#endif + + /* The COMM INIT gate is unconditional, so this runs in every build */ + WH_TEST_RETURN_ON_FAIL( + _testKeystoreUnboundClientRejected(client1, server1, client2, server2)); + /* Future test suites here */ /* Cleanup */ diff --git a/test-refactor/misc/wh_test_she_keywrap.c b/test-refactor/misc/wh_test_she_keywrap.c index a443530d2..f6c56f72a 100644 --- a/test-refactor/misc/wh_test_she_keywrap.c +++ b/test-refactor/misc/wh_test_she_keywrap.c @@ -176,6 +176,11 @@ static int _ProvisionNvmKek(whNvmContext* nvm) return wh_Nvm_AddObject(nvm, &meta, meta.len, whTest_KeywrapKek); } +static int _ShePreProgramKey(TestCtx* t, whNvmId keyId, uint32_t count, + uint32_t sheFlags, const uint8_t* key, + whNvmSize keySz); +static int _SheDestroyKey(TestCtx* t, whNvmId keyId); + /* Fresh server + fresh (empty) NVM every call: the reboot interop relies on * this modeling a real power cycle between sessions. */ static int _SetupClientServer(TestCtx* t) @@ -251,6 +256,14 @@ static int _SetupClientServer(TestCtx* t) wh_Server_SetConnected(t->server, WH_COMM_CONNECTED)); WH_TEST_RETURN_ON_FAIL(wh_Client_Init(t->client, t->c_conf)); + /* Until COMM INIT binds a client id, SHE key management must be refused + * since an unbound request would target the USER=0 factory namespace. */ + WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == + _ShePreProgramKey(t, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, + s_bootMacKey, WH_SHE_KEY_SZ)); + WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == + _SheDestroyKey(t, WH_SHE_BOOT_MAC_KEY_ID)); + /* Comm init so the server learns the client id */ WH_TEST_RETURN_ON_FAIL(wh_Client_CommInitRequest(t->client)); WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); @@ -300,30 +313,75 @@ static int _ComputeBootMac(const uint8_t* bootloader, uint32_t bootloaderSz, /* Sequential wrappers: send the request, pump the server once, then collect * the response */ -static int _NvmAddObject(TestCtx* t, whNvmId id, whNvmAccess access, - whNvmFlags flags, whNvmSize labelLen, uint8_t* label, - whNvmSize len, const uint8_t* data) +/* Pre-program a SHE key via the dedicated SHE provisioning message: the + * split equivalent of wh_Client_ShePreProgramKey(), pumping the server + * between the request and the response */ +static int _ShePreProgramKey(TestCtx* t, whNvmId keyId, uint32_t count, + uint32_t sheFlags, const uint8_t* key, + whNvmSize keySz) { - int32_t rc = 0; + whMessageShe_PreProgramKeyRequest* req; + whMessageShe_PreProgramKeyResponse* resp; + uint8_t* reqBuf = NULL; + uint16_t group = 0; + uint16_t action = 0; + uint16_t dataSz = 0; + + reqBuf = (uint8_t*)wh_CommClient_GetDataPtr(t->client->comm); + req = (whMessageShe_PreProgramKeyRequest*)reqBuf; + req->keyId = keyId; + req->count = count; + req->flags = sheFlags; + req->keySz = keySz; + memcpy(reqBuf + sizeof(*req), key, keySz); - WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectRequest( - t->client, id, access, flags, labelLen, label, len, data)); + WH_TEST_RETURN_ON_FAIL(wh_Client_SendRequest( + t->client, WH_MESSAGE_GROUP_SHE, WH_SHE_PRE_PROGRAM_KEY, + (uint16_t)(sizeof(*req) + keySz), reqBuf)); WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); - WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectResponse(t->client, &rc)); - return (int)rc; + /* Receive into the comm buffer so the copy is bounded. An oversized + * response could overrun a small stack struct (see + * wh_Client_ShePreProgramKey). */ + resp = (whMessageShe_PreProgramKeyResponse*)wh_CommClient_GetDataPtr( + t->client->comm); + WH_TEST_RETURN_ON_FAIL( + wh_Client_RecvResponse(t->client, &group, &action, &dataSz, + WOLFHSM_CFG_COMM_DATA_LEN, (uint8_t*)resp)); + if ((group != WH_MESSAGE_GROUP_SHE) || (action != WH_SHE_PRE_PROGRAM_KEY) || + (dataSz != sizeof(*resp))) { + return WH_ERROR_ABORTED; + } + return (int)resp->rc; } -/* Pre-program a SHE key: an NVM object at the SHE key id with a zero-counter - * SHE label (split-API equivalent of wh_Client_ShePreProgramKey) */ -static int _ShePreProgramKey(TestCtx* t, whNvmId keyId, uint32_t sheFlags, - const uint8_t* key, whNvmSize keySz) +/* Split equivalent of wh_Client_SheDestroyKey() */ +static int _SheDestroyKey(TestCtx* t, whNvmId keyId) { - uint8_t label[WH_NVM_LABEL_LEN] = {0}; + whMessageShe_DestroyKeyRequest* req; + whMessageShe_DestroyKeyResponse* resp; + uint16_t group = 0; + uint16_t action = 0; + uint16_t dataSz = 0; + + req = (whMessageShe_DestroyKeyRequest*)wh_CommClient_GetDataPtr( + t->client->comm); + memset(req, 0, sizeof(*req)); + req->keyId = keyId; - wh_She_Meta2Label(0, sheFlags, label); - return _NvmAddObject(t, - WH_SHE_MAKE_KEYID(t->client->comm->client_id, keyId), - 0, 0, sizeof(label), label, keySz, key); + WH_TEST_RETURN_ON_FAIL( + wh_Client_SendRequest(t->client, WH_MESSAGE_GROUP_SHE, + WH_SHE_DESTROY_KEY, sizeof(*req), (uint8_t*)req)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + resp = (whMessageShe_DestroyKeyResponse*)wh_CommClient_GetDataPtr( + t->client->comm); + WH_TEST_RETURN_ON_FAIL( + wh_Client_RecvResponse(t->client, &group, &action, &dataSz, + WOLFHSM_CFG_COMM_DATA_LEN, (uint8_t*)resp)); + if ((group != WH_MESSAGE_GROUP_SHE) || (action != WH_SHE_DESTROY_KEY) || + (dataSz != sizeof(*resp))) { + return WH_ERROR_ABORTED; + } + return (int)resp->rc; } static int _KeyWrapExport(TestCtx* t, whKeyId keyId, uint16_t keyType, @@ -399,6 +457,39 @@ static int _SheGetStatus(TestCtx* t, uint8_t* sreg) return wh_Client_SheGetStatusResponse(t->client, sreg); } +static int _SheGetId(TestCtx* t, uint8_t* challenge, uint32_t challengeSz, + uint8_t* uid, uint8_t* sreg, uint8_t* mac) +{ + WH_TEST_RETURN_ON_FAIL( + wh_Client_SheGetIdRequest(t->client, challenge, challengeSz)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_SheGetIdResponse(t->client, uid, sreg, mac); +} + +/* GET_ID embeds the client id in its MASTER_ECU_KEY lookup, so the server + * refuses it while no client id is bound (same gate as the SHE key-management + * handlers). Force the unbound state on the live server to exercise the gate, + * then confirm a bound client still succeeds. */ +static int _SheGetIdUnboundRejected(TestCtx* t) +{ + int ret; + uint8_t savedId = t->server->comm->client_id; + uint8_t challenge[WH_SHE_KEY_SZ] = {0}; + uint8_t uid[WH_SHE_UID_SZ] = {0}; + uint8_t sreg = 0; + uint8_t mac[WH_SHE_KEY_SZ] = {0}; + + t->server->comm->client_id = 0; + ret = _SheGetId(t, challenge, sizeof(challenge), uid, &sreg, mac); + t->server->comm->client_id = savedId; + if (ret != WH_SHE_ERC_GENERAL_ERROR) { + WH_ERROR_PRINT("SHE GetId unbound: expected GENERAL_ERROR, got %d\n", + ret); + return WH_ERROR_ABORTED; + } + return _SheGetId(t, challenge, sizeof(challenge), uid, &sreg, mac); +} + static int _SheLoadKey(TestCtx* t, uint8_t* m1, uint8_t* m2, uint8_t* m3, uint8_t* m4, uint8_t* m5) { @@ -527,12 +618,12 @@ static int _SheEstablishSecureBoot(TestCtx* t) return ret; } - ret = _ShePreProgramKey(t, WH_SHE_BOOT_MAC_KEY_ID, 0, s_bootMacKey, + ret = _ShePreProgramKey(t, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, s_bootMacKey, WH_SHE_KEY_SZ); if (ret != 0) { return ret; } - ret = _ShePreProgramKey(t, WH_SHE_BOOT_MAC, 0, digest, sizeof(digest)); + ret = _ShePreProgramKey(t, WH_SHE_BOOT_MAC, 0, 0, digest, sizeof(digest)); if (ret != 0) { return ret; } @@ -576,7 +667,6 @@ static int _SheKeywrapInterop(TestCtx* t) uint8_t ecbOut[WH_SHE_KEY_SZ]; uint8_t ecbBack[WH_SHE_KEY_SZ]; uint16_t outId = 0; - uint8_t ctrLabel[WH_NVM_LABEL_LEN]; uint8_t m1[WH_SHE_M1_SZ]; uint8_t m2[WH_SHE_M2_SZ]; uint8_t m3[WH_SHE_M3_SZ]; @@ -591,15 +681,21 @@ static int _SheKeywrapInterop(TestCtx* t) return ret; } + ret = _SheGetIdUnboundRejected(t); + if (ret != 0) { + WH_ERROR_PRINT("SHE interop: unbound GetId gate failed %d\n", ret); + return ret; + } + /* Pre-program the keys the interop uses: SECRET_KEY (slot 0), the master * ECU key to authorize the LoadKey update, and the RAM key (slot 14). */ - ret = _ShePreProgramKey(t, WH_SHE_SECRET_KEY_ID, 0, s_secretKey, + ret = _ShePreProgramKey(t, WH_SHE_SECRET_KEY_ID, 0, 0, s_secretKey, WH_SHE_KEY_SZ); if (ret != 0) { WH_ERROR_PRINT("SHE interop: pre-program SECRET_KEY failed %d\n", ret); return ret; } - ret = _ShePreProgramKey(t, WH_SHE_MASTER_ECU_KEY_ID, 0, s_masterEcuKey, + ret = _ShePreProgramKey(t, WH_SHE_MASTER_ECU_KEY_ID, 0, 0, s_masterEcuKey, WH_SHE_KEY_SZ); if (ret != 0) { WH_ERROR_PRINT("SHE interop: pre-program MASTER_ECU failed %d\n", ret); @@ -739,10 +835,7 @@ static int _SheKeywrapInterop(TestCtx* t) /* Counter guard on the SHE unwrap-and-cache path: seed an NVM SHE slot * with counter=5, then check a lower-counter prime is rejected and an * equal-counter prime is accepted. */ - wh_She_Meta2Label(5, 0, ctrLabel); - ret = _NvmAddObject( - t, WH_SHE_MAKE_KEYID(t->client->comm->client_id, SHE_CTR_SLOT), 0, 0, - sizeof(ctrLabel), ctrLabel, sizeof(sheKey), sheKey); + ret = _ShePreProgramKey(t, SHE_CTR_SLOT, 5, 0, sheKey, sizeof(sheKey)); if (ret != 0) { WH_ERROR_PRINT("SHE interop: seed counter slot failed %d\n", ret); return ret; @@ -938,7 +1031,7 @@ static int _SheInteropProvision(TestCtx* t) /* Provision the secret key, then load the master ECU key (auth=secret) * and the target key (auth=master ECU) using offline-generated M1/M2/M3. */ - ret = _ShePreProgramKey(t, WH_SHE_SECRET_KEY_ID, 0, s_secretKey, + ret = _ShePreProgramKey(t, WH_SHE_SECRET_KEY_ID, 0, 0, s_secretKey, WH_SHE_KEY_SZ); if (ret != 0) { return ret; diff --git a/test-refactor/misc/wh_test_she_uid_cb.c b/test-refactor/misc/wh_test_she_uid_cb.c index 8100d4401..5970ee216 100644 --- a/test-refactor/misc/wh_test_she_uid_cb.c +++ b/test-refactor/misc/wh_test_she_uid_cb.c @@ -190,6 +190,11 @@ static int _SetupServer(TestCtx* t, int useConfig, int readOnly) WH_TEST_RETURN_ON_FAIL(wc_InitRng_ex(t->crypto->rng, NULL, INVALID_DEVID)); WH_TEST_RETURN_ON_FAIL(wh_Server_Init(t->server, t->s_conf)); + /* Bind a client id the way COMM INIT would; GET_ID's key lookup embeds + * it. (The dispatcher refuses requests from an unbound connection, but + * these tests call the handler directly.) */ + t->server->comm->client_id = 1; + return WH_ERROR_OK; } diff --git a/test-refactor/posix/Makefile b/test-refactor/posix/Makefile index 4852c9c57..d7c288119 100644 --- a/test-refactor/posix/Makefile +++ b/test-refactor/posix/Makefile @@ -147,6 +147,16 @@ ifeq ($(SHE_GLOBAL),1) DEF += -DWOLFHSM_CFG_SHE_GLOBAL_KEYS endif +# Build with the global keys feature disabled +ifeq ($(NOGLOBALKEYS),1) + DEF += -DWOLFHSM_CFG_TEST_NO_GLOBAL_KEYS +endif + +# Build with the legacy flat client NVM id space +ifeq ($(LEGACYNVM),1) + DEF += -DWOLFHSM_CFG_LEGACY_CLIENT_NVM +endif + # Support an authentication-capable build ifeq ($(AUTH),1) DEF += -DWOLFHSM_CFG_ENABLE_AUTHENTICATION diff --git a/test-refactor/posix/wh_test_keygen_unique_id.c b/test-refactor/posix/wh_test_keygen_unique_id.c index 9c88032bf..936c8eba0 100644 --- a/test-refactor/posix/wh_test_keygen_unique_id.c +++ b/test-refactor/posix/wh_test_keygen_unique_id.c @@ -615,6 +615,18 @@ int whTest_KeygenUniqueIdConcurrent(void* ctx_arg) goto stop_servers; } + /* Bind each pair's client id: the server refuses every request other + * than COMM until COMM INIT has completed. The keys under test are + * global, so all pairs still contend in the one shared namespace. */ + for (i = 0; i < KU_NUM_CLIENTS; i++) { + rc = wh_Client_CommInit(&ctx->pairs[i].client, NULL, NULL); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("client %d comm init failed: %d\n", i, rc); + result = WH_ERROR_ABORTED; + goto stop_servers; + } + } + /* Run each algorithm: spawn client threads, run all rounds, join. */ for (a = 0; a < KU_NUM_ALGOS; a++) { /* Snapshot so the per-algorithm line reports deltas; the counters diff --git a/test-refactor/posix/wh_test_keyread_race.c b/test-refactor/posix/wh_test_keyread_race.c index 88fd57dcf..eeaa91053 100644 --- a/test-refactor/posix/wh_test_keyread_race.c +++ b/test-refactor/posix/wh_test_keyread_race.c @@ -568,6 +568,18 @@ int whTest_KeyReadRace(void* ctx_arg) goto stop_servers; } + /* Bind each pair's client id: the server refuses every request other + * than COMM until COMM INIT has completed. The keys under test are + * global, so all pairs still contend in the one shared namespace. */ + for (i = 0; i < KR_NUM_CLIENTS; i++) { + rc = wh_Client_CommInit(&ctx->pairs[i].client, NULL, NULL); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("client %d comm init failed: %d\n", i, rc); + result = WH_ERROR_ABORTED; + goto stop_servers; + } + } + for (i = 0; i < KR_NUM_CLIENTS; i++) { rc = pthread_create(&ctx->pairs[i].clientThread, NULL, krClientThread, &ctx->pairs[i]); diff --git a/test-refactor/server/wh_test_cert.c b/test-refactor/server/wh_test_cert.c index 1b46ba5d4..01e9b90c2 100644 --- a/test-refactor/server/wh_test_cert.c +++ b/test-refactor/server/wh_test_cert.c @@ -183,12 +183,14 @@ int whTest_CertNvmPolicy(whServerContext* ctx) } /* - * Keys and certs share the NVM id space, so a client that passes a - * trusted KEK's id to a cert read handler must be refused. The KEK - * flag alone (no NONEXPORTABLE) must be enough: the dispatcher is - * the only gate, since wh_Server_CertReadTrusted() does an unchecked - * NVM read. Driven through wh_Server_HandleCertRequest() because the - * check lives in the dispatcher, not the server cert API. + * The cert read handler checks and translates every client id into the cert + * namespace (TYPE=CERT), so a client that passes a trusted KEK's CRYPTO-typed + * id cannot name it at all: the full internal id is rejected outright, and the + * bare 8-bit id lands on a different (nonexistent) cert object and returns + * NOTFOUND, never the KEK bytes. Within the cert namespace a SERVER_ONLY + * object is refused by the flag check. Driven through + * wh_Server_HandleCertRequest() because the checks live in the handler, not + * the server cert API. */ int whTest_CertReadRejectsServerOnly(whServerContext* ctx) { @@ -205,9 +207,9 @@ int whTest_CertReadRejectsServerOnly(whServerContext* ctx) WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server)); - /* Provision a trusted KEK the way whnvmtool would, deliberately - * WITHOUT NONEXPORTABLE, to prove the trusted flag alone gates the - * read. */ + /* Provision a trusted KEK at a CRYPTO-typed id the way whnvmtool would. + * The cert read path translates client ids into the cert namespace, so + * this crypto-typed KEK is not nameable through it. */ meta.id = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, 0, 0x5A); meta.access = WH_NVM_ACCESS_ANY; meta.flags = WH_NVM_FLAGS_TRUSTED | WH_NVM_FLAGS_USAGE_WRAP; @@ -215,9 +217,10 @@ int whTest_CertReadRejectsServerOnly(whServerContext* ctx) WH_TEST_RETURN_ON_FAIL( wh_Nvm_AddObject(server->nvm, &meta, sizeof(kek), kek)); - /* READTRUSTED must refuse the KEK id and return no cert bytes. - * The handler formats resp.rc and also returns it; resp.rc is the - * client-visible signal, so assert on that. */ + /* READTRUSTED with the KEK's full internal id is rejected as malformed, + * and with its bare id misses; neither returns cert bytes. The handler + * formats resp.rc and also returns it; resp.rc is the client-visible + * signal, so assert on that. */ { whMessageCert_ReadTrustedRequest req = {0}; whMessageCert_ReadTrustedResponse resp = {0}; @@ -225,21 +228,60 @@ int whTest_CertReadRejectsServerOnly(whServerContext* ctx) req.id = meta.id; wh_MessageCert_TranslateReadTrustedRequest( magic, &req, (whMessageCert_ReadTrustedRequest*)req_packet); - (void)wh_Server_HandleCertRequest( server, magic, WH_MESSAGE_CERT_ACTION_READTRUSTED, 0, sizeof(req), req_packet, &resp_size, resp_packet); + wh_MessageCert_TranslateReadTrustedResponse( + magic, (whMessageCert_ReadTrustedResponse*)resp_packet, &resp); + WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_BADARGS); + WH_TEST_ASSERT_RETURN(resp.cert_len == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(resp)); + memset(&resp, 0, sizeof(resp)); + req.id = WH_KEYID_ID(meta.id); + wh_MessageCert_TranslateReadTrustedRequest( + magic, &req, (whMessageCert_ReadTrustedRequest*)req_packet); + (void)wh_Server_HandleCertRequest( + server, magic, WH_MESSAGE_CERT_ACTION_READTRUSTED, 0, sizeof(req), + req_packet, &resp_size, resp_packet); wh_MessageCert_TranslateReadTrustedResponse( magic, (whMessageCert_ReadTrustedResponse*)resp_packet, &resp); + WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_NOTFOUND); + WH_TEST_ASSERT_RETURN(resp.cert_len == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(resp)); + } + + /* A SERVER_ONLY object inside the cert namespace is nameable but must + * be refused by the flag check. */ + { + whMessageCert_ReadTrustedRequest req = {0}; + whMessageCert_ReadTrustedResponse resp = {0}; + whNvmMetadata srv = {0}; + srv.id = WH_MAKE_KEYID(WH_KEYTYPE_CERT, server->comm->client_id, 0x5B); + srv.access = WH_NVM_ACCESS_ANY; + srv.flags = WH_NVM_FLAGS_SERVER_ONLY; + srv.len = sizeof(kek); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &srv, sizeof(kek), kek)); + + req.id = 0x5B; + wh_MessageCert_TranslateReadTrustedRequest( + magic, &req, (whMessageCert_ReadTrustedRequest*)req_packet); + (void)wh_Server_HandleCertRequest( + server, magic, WH_MESSAGE_CERT_ACTION_READTRUSTED, 0, sizeof(req), + req_packet, &resp_size, resp_packet); + wh_MessageCert_TranslateReadTrustedResponse( + magic, (whMessageCert_ReadTrustedResponse*)resp_packet, &resp); WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_ACCESS); WH_TEST_ASSERT_RETURN(resp.cert_len == 0); WH_TEST_ASSERT_RETURN(resp_size == sizeof(resp)); + + WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &srv.id)); } #ifdef WOLFHSM_CFG_DMA - /* READTRUSTED_DMA must refuse it too and write nothing. */ + /* READTRUSTED_DMA with the bare id must miss too and write nothing. */ { whMessageCert_ReadTrustedDmaRequest req = {0}; whMessageCert_SimpleResponse resp = {0}; @@ -247,7 +289,7 @@ int whTest_CertReadRejectsServerOnly(whServerContext* ctx) size_t i; memset(out_buf, 0, sizeof(out_buf)); - req.id = meta.id; + req.id = WH_KEYID_ID(meta.id); req.cert_addr = (uint64_t)(uintptr_t)out_buf; req.cert_len = sizeof(out_buf); wh_MessageCert_TranslateReadTrustedDmaRequest( @@ -261,7 +303,7 @@ int whTest_CertReadRejectsServerOnly(whServerContext* ctx) wh_MessageCert_TranslateSimpleResponse( magic, (whMessageCert_SimpleResponse*)resp_packet, &resp); - WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_ACCESS); + WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_NOTFOUND); for (i = 0; i < sizeof(out_buf); i++) { WH_TEST_ASSERT_RETURN(out_buf[i] == 0); } @@ -274,4 +316,157 @@ int whTest_CertReadRejectsServerOnly(whServerContext* ctx) return 0; } +/* + * A client must not be able to destroy a non-certificate object through the + * cert erase path. EraseTrusted used to take a raw NVM id and refuse only + * SERVER_ONLY / NONDESTROYABLE objects, so a client could erase the auth user + * database (only NONMODIFIABLE). Now the id is checked and translated into the + * cert namespace: an id with TYPE bits set is rejected as malformed, and the + * real object is untouched. + */ +int whTest_CertEraseCannotReachNonCert(whServerContext* ctx) +{ + whServerContext* server = (whServerContext*)ctx; + whNvmMetadata meta = {0}; + whNvmMetadata check = {0}; + const uint8_t secret[8] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04}; + const uint16_t magic = WH_COMM_MAGIC_NATIVE; + uint8_t req_packet[WOLFHSM_CFG_COMM_DATA_LEN] = {0}; + uint8_t resp_packet[WOLFHSM_CFG_COMM_DATA_LEN] = {0}; + uint16_t resp_size = 0; + /* The auth backend's user-index id (WH_NVM_ID_AUTH_USER_INDEX == 0xFE00): + * a TYPE nibble no client translation can produce, carrying only + * NONMODIFIABLE. */ + const whNvmId protectedId = WH_MAKE_KEYID(0xF, 0xE, 0x00); + + WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server)); + + meta.id = protectedId; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONMODIFIABLE; + meta.len = sizeof(secret); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, sizeof(secret), secret)); + + /* Client erase naming the protected id must be rejected: its TYPE bits + * lie outside the client id fields, so it cannot be translated. */ + { + whMessageCert_EraseTrustedRequest req = {0}; + whMessageCert_SimpleResponse resp = {0}; + + req.id = protectedId; + wh_MessageCert_TranslateEraseTrustedRequest( + magic, &req, (whMessageCert_EraseTrustedRequest*)req_packet); + + (void)wh_Server_HandleCertRequest( + server, magic, WH_MESSAGE_CERT_ACTION_ERASETRUSTED, 0, sizeof(req), + req_packet, &resp_size, resp_packet); + + wh_MessageCert_TranslateSimpleResponse( + magic, (whMessageCert_SimpleResponse*)resp_packet, &resp); + WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_BADARGS); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(resp)); + } + + /* The protected object must still be present and intact. */ + WH_TEST_ASSERT_RETURN( + wh_Nvm_GetMetadata(server->nvm, protectedId, &check) == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(check.len == sizeof(secret)); + + /* Clean up with the server-internal API. */ + WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &protectedId)); + + return 0; +} + +/* Drive a client READTRUSTED for `id` at the server's current client_id and + * return the client-visible rc. */ +static int32_t _certReadRc(whServerContext* server, uint16_t magic, whNvmId id, + uint8_t* req_packet, uint8_t* resp_packet) +{ + whMessageCert_ReadTrustedRequest req = {0}; + whMessageCert_ReadTrustedResponse resp = {0}; + uint16_t resp_size = 0; + + req.id = id; + wh_MessageCert_TranslateReadTrustedRequest( + magic, &req, (whMessageCert_ReadTrustedRequest*)req_packet); + (void)wh_Server_HandleCertRequest( + server, magic, WH_MESSAGE_CERT_ACTION_READTRUSTED, 0, sizeof(req), + req_packet, &resp_size, resp_packet); + wh_MessageCert_TranslateReadTrustedResponse( + magic, (whMessageCert_ReadTrustedResponse*)resp_packet, &resp); + return resp.rc; +} + +/* + * Certificate ids follow the same per-client scheme as keys and NVM objects: a + * plain id is private to the calling client, and the GLOBAL flag selects the + * shared namespace. Two clients naming "cert 5" get distinct objects; neither + * sees the other's, and a global cert is visible to both. + */ +int whTest_CertPerClientIsolation(whServerContext* ctx) +{ + whServerContext* server = (whServerContext*)ctx; + whNvmMetadata meta = {0}; + const uint16_t magic = WH_COMM_MAGIC_NATIVE; + uint8_t req_packet[WOLFHSM_CFG_COMM_DATA_LEN] = {0}; + uint8_t resp_packet[WOLFHSM_CFG_COMM_DATA_LEN] = {0}; + const whNvmId client1Cert = WH_MAKE_KEYID(WH_KEYTYPE_CERT, 1, 5); + + WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server)); + + /* Plant a cert in client 1's private namespace (server-direct, full id). */ + meta.id = client1Cert; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = ROOT_A_CERT_len; + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, ROOT_A_CERT_len, ROOT_A_CERT)); + + /* Client 2 asking for cert 5 lands in its own namespace: not found. */ + server->comm->client_id = 2; + WH_TEST_ASSERT_RETURN(_certReadRc(server, magic, 5, req_packet, + resp_packet) == WH_ERROR_NOTFOUND); + + /* Client 1 sees its own cert 5. */ + server->comm->client_id = 1; + WH_TEST_ASSERT_RETURN( + _certReadRc(server, magic, 5, req_packet, resp_packet) == WH_ERROR_OK); + +#ifdef WOLFHSM_CFG_GLOBAL_KEYS + /* A cert in the global namespace is reachable by any client that sets the + * GLOBAL flag, and not by a plain id. */ + memset(&meta, 0, sizeof(meta)); + meta.id = WH_MAKE_KEYID(WH_KEYTYPE_CERT, WH_KEYUSER_GLOBAL, 6); + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = ROOT_B_CERT_len; + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, ROOT_B_CERT_len, ROOT_B_CERT)); + + server->comm->client_id = 2; + WH_TEST_ASSERT_RETURN(_certReadRc(server, magic, + 6 | WH_KEYID_CLIENT_GLOBAL_FLAG, + req_packet, resp_packet) == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(_certReadRc(server, magic, 6, req_packet, + resp_packet) == WH_ERROR_NOTFOUND); + + { + whNvmId g = WH_MAKE_KEYID(WH_KEYTYPE_CERT, WH_KEYUSER_GLOBAL, 6); + WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &g)); + } +#endif + + /* The server context is shared across server-group tests: clean up the + * planted cert and restore the unbound client id. */ + { + whNvmId c1 = client1Cert; + WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &c1)); + } + server->comm->client_id = 0; + + return 0; +} + #endif diff --git a/test-refactor/server/wh_test_cert_readtrusted.c b/test-refactor/server/wh_test_cert_readtrusted.c index 490887c82..ac61e6c28 100644 --- a/test-refactor/server/wh_test_cert_readtrusted.c +++ b/test-refactor/server/wh_test_cert_readtrusted.c @@ -65,7 +65,12 @@ static int _whTest_CertReadTrustedOversized(whServerContext* server) /* Static filler: an automatic copy would double this stack frame on the * embedded targets these suites also run on */ static uint8_t oversized_cert[WH_TEST_CERT_STAGED_LEN + 1]; - const whNvmId certId = 20; + /* The READTRUSTED handler translates the client id into the CERT + * namespace of the bound client, so plant the cert there with the full + * internal id and address it through the handler with the bare id. */ + const whNvmId reqId = 20; + const whNvmId certId = + WH_MAKE_KEYID(WH_KEYTYPE_CERT, server->comm->client_id, reqId); const uint32_t oversized_len = (uint32_t)WH_TEST_CERT_STAGED_LEN + 1; uint16_t resp_size = 0; int handler_rc; @@ -79,7 +84,7 @@ static int _whTest_CertReadTrustedOversized(whServerContext* server) /* Poison makes bytes the handler never staged detectable */ memset(respPkt.bytes, 0xA5, sizeof(respPkt.bytes)); - req->id = certId; + req->id = reqId; /* The server transmits regardless of this return, so out_resp_size is * what actually reaches the client */ @@ -105,7 +110,7 @@ static int _whTest_CertReadTrustedOversized(whServerContext* server) oversized_cert, oversized_len - 1)); memset(respPkt.bytes, 0xA5, sizeof(respPkt.bytes)); - req->id = certId; + req->id = reqId; WH_TEST_RETURN_ON_FAIL(wh_Server_HandleCertRequest( server, WH_COMM_MAGIC_NATIVE, WH_MESSAGE_CERT_ACTION_READTRUSTED, 0, @@ -131,16 +136,19 @@ static int _whTest_CertReadTrustedDenied(whServerContext* server) whMessageCert_ReadTrustedResponse resp; uint8_t bytes[WOLFHSM_CFG_COMM_DATA_LEN]; } respPkt; - const whNvmId certId = 21; - uint16_t resp_size = 0; - int handler_rc; + /* Cert namespace of the bound client, as in the oversized case */ + const whNvmId reqId = 21; + const whNvmId certId = + WH_MAKE_KEYID(WH_KEYTYPE_CERT, server->comm->client_id, reqId); + uint16_t resp_size = 0; + int handler_rc; WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted( server, certId, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONEXPORTABLE, NULL, 0, ROOT_A_CERT, ROOT_A_CERT_len)); memset(respPkt.bytes, 0xA5, sizeof(respPkt.bytes)); - req->id = certId; + req->id = reqId; handler_rc = wh_Server_HandleCertRequest( server, WH_COMM_MAGIC_NATIVE, WH_MESSAGE_CERT_ACTION_READTRUSTED, 0, @@ -158,12 +166,21 @@ static int _whTest_CertReadTrustedDenied(whServerContext* server) int whTest_CertReadTrusted(whServerContext* ctx) { whServerContext* server = (whServerContext*)ctx; + int rc; WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server)); - WH_TEST_RETURN_ON_FAIL(_whTest_CertReadTrustedOversized(server)); - WH_TEST_RETURN_ON_FAIL(_whTest_CertReadTrustedDenied(server)); - return 0; + /* Bind a client id the way COMM INIT would; the handler stamps it into + * every cert id. Restore the unbound id after, as the server context is + * shared across server-group tests. */ + server->comm->client_id = 1; + rc = _whTest_CertReadTrustedOversized(server); + if (rc == WH_ERROR_OK) { + rc = _whTest_CertReadTrustedDenied(server); + } + server->comm->client_id = 0; + + return rc; } diff --git a/test-refactor/server/wh_test_she_server.c b/test-refactor/server/wh_test_she_server.c index 58277f7a7..32ba834ac 100644 --- a/test-refactor/server/wh_test_she_server.c +++ b/test-refactor/server/wh_test_she_server.c @@ -39,6 +39,7 @@ #include "wolfhsm/wh_server_keystore.h" #include "wolfhsm/wh_server_she.h" #include "wolfhsm/wh_she_common.h" +#include "wolfhsm/wh_she_crypto.h" #include "wolfhsm/wh_message.h" #include "wolfhsm/wh_message_she.h" #include "wolfhsm/wh_comm.h" @@ -110,6 +111,10 @@ int whTest_SheReqSizeChecking(whServerContext* server) return WH_ERROR_BADARGS; } + /* Bind a client id the way COMM INIT would; the handlers are called + * directly here, so they see the id a real connection would carry. */ + server->comm->client_id = 1; + /* * Set SHE state so _ReportInvalidSheState allows requests through. * WH_SHE_SET_UID always passes the state gate, but most other handlers @@ -491,15 +496,135 @@ int whTest_SheReqSizeChecking(whServerContext* server) WH_TEST_ASSERT_RETURN(getIdResp->rc != WH_SHE_ERC_NO_ERROR); } - /* Restore a clean SHE context so the poked uidSet/sbState don't - * leak into the live request loop the server enters next. */ + /* Restore a clean SHE context and unbound client id so the poked state + * doesn't leak into the live request loop the server enters next. */ memset(server->she, 0, sizeof(*server->she)); + server->comm->client_id = 0; WH_TEST_PRINT("SHE req_size checking test SUCCESS\n"); return 0; } +/* + * LoadKey must refuse a slot whose NVM object is not exactly one SHE key, + * whether the slot is named as the auth key or as the target: reading it into + * the fixed key buffer fails, and an unchecked read would leave the metadata + * unset, so a zeroed label would clear the target's write protection. The + * slots are planted server-side, as a provisioning image would, because + * client NVM ids cannot reach SHE slots. + */ +int whTest_SheLoadKeyOversizedSlot(whServerContext* server) +{ + int ret = 0; + uint16_t resp_size = 0; + uint8_t req_packet[WOLFHSM_CFG_COMM_DATA_LEN]; + uint8_t resp_packet[WOLFHSM_CFG_COMM_DATA_LEN]; + + const uint8_t SLOT_AUTH = 8; /* oversized, named as the auth key */ + const uint8_t SLOT_TARGET = 9; /* oversized, named as the target */ + const uint8_t SLOT_NEW = 7; /* empty target of the auth-key case */ + uint8_t uid[WH_SHE_UID_SZ] = {0}; + uint8_t secretKey[WH_SHE_KEY_SZ] = { + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, + 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c}; + uint8_t rawKey[WH_SHE_KEY_SZ] = { + 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, + 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00}; + uint8_t oversize[WH_SHE_KEY_SZ * 2]; + uint8_t m4[WH_SHE_M4_SZ]; + uint8_t m5[WH_SHE_M5_SZ]; + whNvmMetadata meta = {0}; + whNvmMetadata check = {0}; + uint32_t count = 0; + uint32_t flags = 0; + whNvmId ids[3]; + whNvmId newId; + whMessageShe_LoadKeyRequest* req = + (whMessageShe_LoadKeyRequest*)req_packet; + whMessageShe_LoadKeyResponse* resp = + (whMessageShe_LoadKeyResponse*)resp_packet; + + if (server == NULL) { + return WH_ERROR_BADARGS; + } + + memset(oversize, 0x5A, sizeof(oversize)); + + /* Bind a client id the way COMM INIT would and open the state gate */ + server->comm->client_id = 1; + server->she->uidSet = 1; + server->she->sbState = TEST_SHE_SB_STATE_SUCCESS; + + ids[0] = WH_SHE_MAKE_KEYID(server->comm->client_id, WH_SHE_SECRET_KEY_ID); + ids[1] = WH_SHE_MAKE_KEYID(server->comm->client_id, SLOT_AUTH); + ids[2] = WH_SHE_MAKE_KEYID(server->comm->client_id, SLOT_TARGET); + newId = WH_SHE_MAKE_KEYID(server->comm->client_id, SLOT_NEW); + + /* SECRET_KEY authorizes the target-slot case */ + meta.id = ids[0]; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = WH_SHE_KEY_SZ; + wh_She_Meta2Label(0, 0, meta.label); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, WH_SHE_KEY_SZ, secretKey)); + + /* An oversized object in a slot that will be named as the auth key */ + meta.id = ids[1]; + meta.len = sizeof(oversize); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, sizeof(oversize), oversize)); + + /* An oversized, write-protected object in a slot named as the target */ + meta.id = ids[2]; + wh_She_Meta2Label(0, WH_SHE_FLAG_WRITE_PROTECT, meta.label); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, sizeof(oversize), oversize)); + + /* === Oversized auth key slot === */ + WH_TEST_RETURN_ON_FAIL(wh_She_GenerateLoadableKey( + SLOT_NEW, SLOT_AUTH, 1, 0, uid, rawKey, oversize, req->messageOne, + req->messageTwo, req->messageThree, m4, m5)); + memset(resp, 0, sizeof(*resp)); + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_LOAD_KEY, sizeof(*req), req_packet, + &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*resp)); + WH_TEST_ASSERT_RETURN(resp->rc == WH_SHE_ERC_KEY_INVALID); + /* Nothing was written to the target */ + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server->nvm, newId, &check) == + WH_ERROR_NOTFOUND); + + /* === Oversized target key slot === */ + WH_TEST_RETURN_ON_FAIL(wh_She_GenerateLoadableKey( + SLOT_TARGET, WH_SHE_SECRET_KEY_ID, 1, 0, uid, rawKey, secretKey, + req->messageOne, req->messageTwo, req->messageThree, m4, m5)); + memset(resp, 0, sizeof(*resp)); + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_LOAD_KEY, sizeof(*req), req_packet, + &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*resp)); + WH_TEST_ASSERT_RETURN(resp->rc == WH_SHE_ERC_KEY_INVALID); + /* The slot is untouched: still oversized and still write-protected */ + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server->nvm, ids[2], &check) == + WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(check.len == sizeof(oversize)); + wh_She_Label2Meta(check.label, &count, &flags); + WH_TEST_ASSERT_RETURN((flags & WH_SHE_FLAG_WRITE_PROTECT) != 0); + + /* The server context and NVM are shared across server-group tests */ + WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 3, ids)); + memset(server->she, 0, sizeof(*server->she)); + server->comm->client_id = 0; + + WH_TEST_PRINT("SHE LoadKey oversized slot test SUCCESS\n"); + + return 0; +} + /* Send one SHE action through the server and return its response rc. */ static int32_t wh_She_SheActionRc(whServerContext* server, uint16_t action, diff --git a/test-refactor/wh_test_list.c b/test-refactor/wh_test_list.c index 130857ee8..a2667e4b7 100644 --- a/test-refactor/wh_test_list.c +++ b/test-refactor/wh_test_list.c @@ -53,6 +53,8 @@ WH_TEST_DECL(whTest_Log); WH_TEST_DECL(whTest_CertVerify); WH_TEST_DECL(whTest_CertNvmPolicy); WH_TEST_DECL(whTest_CertReadRejectsServerOnly); +WH_TEST_DECL(whTest_CertEraseCannotReachNonCert); +WH_TEST_DECL(whTest_CertPerClientIsolation); WH_TEST_DECL(whTest_CertReadTrusted); WH_TEST_DECL(whTest_HwKeystoreServer); WH_TEST_DECL(whTest_ServerImgMgr); @@ -88,6 +90,7 @@ WH_TEST_DECL(whTest_SheKeywrapInterop); WH_TEST_DECL(whTest_SheMasterEcuKeyFallback); WH_TEST_DECL(whTest_SheNoNvm); WH_TEST_DECL(whTest_SheReqSizeChecking); +WH_TEST_DECL(whTest_SheLoadKeyOversizedSlot); WH_TEST_DECL(whTest_SheStateGate); WH_TEST_DECL(whTest_SheUidClient); WH_TEST_DECL(whTest_SheUidCb); @@ -124,17 +127,20 @@ const whTestCase whTestsMisc[] = { const size_t whTestsMiscCount = ARRAY_SIZE(whTestsMisc); const whTestCase whTestsServer[] = { - { "whTest_CertVerify", whTest_CertVerify }, - { "whTest_CertNvmPolicy", whTest_CertNvmPolicy }, - { "whTest_CertReadRejectsServerOnly", whTest_CertReadRejectsServerOnly }, - { "whTest_ServerImgMgr", whTest_ServerImgMgr }, - { "whTest_CertReadTrusted", whTest_CertReadTrusted }, - { "whTest_NvmOptional", whTest_NvmOptional }, - { "whTest_NvmPolicyChecked", whTest_NvmPolicyChecked }, - { "whTest_SheMasterEcuKeyFallback", whTest_SheMasterEcuKeyFallback }, - { "whTest_SheReqSizeChecking", whTest_SheReqSizeChecking }, - { "whTest_HwKeystoreServer", whTest_HwKeystoreServer }, - { "whTest_SheStateGate", whTest_SheStateGate }, + {"whTest_CertVerify", whTest_CertVerify}, + {"whTest_CertNvmPolicy", whTest_CertNvmPolicy}, + {"whTest_CertReadRejectsServerOnly", whTest_CertReadRejectsServerOnly}, + {"whTest_CertEraseCannotReachNonCert", whTest_CertEraseCannotReachNonCert}, + {"whTest_CertPerClientIsolation", whTest_CertPerClientIsolation}, + {"whTest_ServerImgMgr", whTest_ServerImgMgr}, + {"whTest_CertReadTrusted", whTest_CertReadTrusted}, + {"whTest_NvmOptional", whTest_NvmOptional}, + {"whTest_NvmPolicyChecked", whTest_NvmPolicyChecked}, + {"whTest_SheMasterEcuKeyFallback", whTest_SheMasterEcuKeyFallback}, + {"whTest_SheReqSizeChecking", whTest_SheReqSizeChecking}, + {"whTest_SheLoadKeyOversizedSlot", whTest_SheLoadKeyOversizedSlot}, + {"whTest_HwKeystoreServer", whTest_HwKeystoreServer}, + {"whTest_SheStateGate", whTest_SheStateGate}, }; const size_t whTestsServerCount = ARRAY_SIZE(whTestsServer); diff --git a/test/Makefile b/test/Makefile index 1c0311952..70e5a3b33 100644 --- a/test/Makefile +++ b/test/Makefile @@ -177,6 +177,16 @@ ifeq ($(SHE_GLOBAL),1) DEF += -DWOLFHSM_CFG_SHE_GLOBAL_KEYS endif +# Build with the global keys feature disabled +ifeq ($(NOGLOBALKEYS),1) + DEF += -DWOLFHSM_CFG_TEST_NO_GLOBAL_KEYS +endif + +# Build with the legacy flat client NVM id space +ifeq ($(LEGACYNVM),1) + DEF += -DWOLFHSM_CFG_LEGACY_CLIENT_NVM +endif + # Enable per-client crypto affinity (HW/SW devId selection) and its unit test ifeq ($(CRYPTO_AFFINITY),1) DEF += -DWOLFHSM_CFG_CRYPTO_AFFINITY diff --git a/test/config/wolfhsm_cfg.h b/test/config/wolfhsm_cfg.h index 0ce3317dd..2b785b978 100644 --- a/test/config/wolfhsm_cfg.h +++ b/test/config/wolfhsm_cfg.h @@ -35,13 +35,16 @@ #define WOLFHSM_CFG_COMM_DATA_LEN (1024 * 8) -/* Enable global keys feature for testing */ +/* Enable global keys feature for testing. NOGLOBALKEYS=1 builds leave it + * disabled to cover that configuration. */ +#ifndef WOLFHSM_CFG_TEST_NO_GLOBAL_KEYS #define WOLFHSM_CFG_GLOBAL_KEYS +#endif /* Enable logging feature for testing */ #define WOLFHSM_CFG_LOGGING -#define WOLFHSM_CFG_NVM_OBJECT_COUNT 30 +#define WOLFHSM_CFG_NVM_OBJECT_COUNT 64 #define WOLFHSM_CFG_SERVER_KEYCACHE_COUNT 9 #define WOLFHSM_CFG_SERVER_KEYCACHE_BUFSIZE 300 #define WOLFHSM_CFG_DMAADDR_COUNT 8 @@ -64,6 +67,11 @@ /* Allow persistent NVM artifacts in tests */ #define WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS +/* Enable SHE preprogram/destroy test-only key-management APIs. These bypass + * the authenticated SHE key-update protocol and are not part of the SHE + * specification, so they must never be enabled in production builds. */ +#define WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT + #define WOLFHSM_CFG_ENABLE_TIMEOUT #endif /* WOLFHSM_CFG_H_ */ diff --git a/test/wh_test_cert.c b/test/wh_test_cert.c index 926a208fe..9e9ca94ac 100644 --- a/test/wh_test_cert.c +++ b/test/wh_test_cert.c @@ -349,14 +349,14 @@ static int whTest_CertServerTrustedRespectsNvmPolicy(whServerConfig* serverCfg) return rc; } -/* Keys and certs share the NVM id space, so a client that passes a trusted - * KEK's id to a cert read handler must be refused. The trusted flag alone (no - * NONEXPORTABLE) must be enough: the dispatcher is the only gate, since - * wh_Server_CertReadTrusted() does an unchecked NVM read. Provision a - * KEK-flagged object without NONEXPORTABLE and confirm both READTRUSTED and - * READTRUSTED_DMA return WH_ERROR_ACCESS and leak no bytes. Driven through - * wh_Server_HandleCertRequest() because the check lives in the dispatcher, - * not in the server cert API. */ +/* A client that passes a trusted KEK's id to a cert read handler must never + * get the KEK bytes. The handler checks and translates every client id into + * the cert namespace (TYPE=CERT), so the KEK's full internal id is rejected + * outright and its bare 8-bit id names a different (nonexistent) cert object. + * Within the cert namespace, a SERVER_ONLY object is refused by the flag + * check, which is the only gate there since wh_Server_CertReadTrusted() does + * an unchecked NVM read. Driven through wh_Server_HandleCertRequest() because + * the checks live in the handler, not in the server cert API. */ static int whTest_CertServerReadTrustedRejectsServerOnly(whServerConfig* serverCfg) { @@ -376,10 +376,16 @@ whTest_CertServerReadTrustedRejectsServerOnly(whServerConfig* serverCfg) WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, serverCfg)); WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server)); - WH_TEST_PRINT("Cert ReadTrusted rejects server-only KEK...\n"); + WH_TEST_PRINT("Cert ReadTrusted cannot reach a server-only KEK...\n"); - /* Provision a trusted KEK the way whnvmtool would, deliberately WITHOUT - * NONEXPORTABLE, to prove the trusted flag alone gates the read. */ + /* Provision a trusted KEK at a CRYPTO-typed id the way whnvmtool would. + * The cert read path checks and translates every client id into the cert + * namespace (TYPE=CERT), so this crypto-typed KEK is not nameable through + * it: its full internal id carries bits outside the client id fields and + * is rejected, and its bare 8-bit id lands on a different (nonexistent) + * cert object. Either way the KEK bytes never leave. This is stronger + * than the old flag-gated behavior, which returned ACCESS after actually + * finding the KEK. */ meta.id = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, 0, 0x5A); meta.access = WH_NVM_ACCESS_ANY; meta.flags = WH_NVM_FLAGS_TRUSTED | WH_NVM_FLAGS_USAGE_WRAP; @@ -387,7 +393,10 @@ whTest_CertServerReadTrustedRejectsServerOnly(whServerConfig* serverCfg) WH_TEST_RETURN_ON_FAIL( wh_Nvm_AddObject(server->nvm, &meta, sizeof(kek), kek)); - /* READTRUSTED must refuse the KEK id and return no cert bytes. */ + /* READTRUSTED with the KEK's full internal id is rejected as malformed, + * and with its bare id misses; neither returns cert bytes. The handler + * formats resp.rc and also returns it; resp.rc is the client-visible + * signal, so assert on that rather than the return. */ { whMessageCert_ReadTrustedRequest req = {0}; whMessageCert_ReadTrustedResponse resp = {0}; @@ -395,23 +404,61 @@ whTest_CertServerReadTrustedRejectsServerOnly(whServerConfig* serverCfg) req.id = meta.id; wh_MessageCert_TranslateReadTrustedRequest( magic, &req, (whMessageCert_ReadTrustedRequest*)req_packet); - - /* The handler formats resp.rc and also returns it; resp.rc is the - * client-visible signal, so assert on that rather than the return. */ (void)wh_Server_HandleCertRequest( server, magic, WH_MESSAGE_CERT_ACTION_READTRUSTED, /*seq=*/0, sizeof(req), req_packet, &resp_size, resp_packet); + wh_MessageCert_TranslateReadTrustedResponse( + magic, (whMessageCert_ReadTrustedResponse*)resp_packet, &resp); + WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_BADARGS); + WH_TEST_ASSERT_RETURN(resp.cert_len == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(resp)); + memset(&resp, 0, sizeof(resp)); + req.id = WH_KEYID_ID(meta.id); + wh_MessageCert_TranslateReadTrustedRequest( + magic, &req, (whMessageCert_ReadTrustedRequest*)req_packet); + (void)wh_Server_HandleCertRequest( + server, magic, WH_MESSAGE_CERT_ACTION_READTRUSTED, /*seq=*/0, + sizeof(req), req_packet, &resp_size, resp_packet); wh_MessageCert_TranslateReadTrustedResponse( magic, (whMessageCert_ReadTrustedResponse*)resp_packet, &resp); + WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_NOTFOUND); + WH_TEST_ASSERT_RETURN(resp.cert_len == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(resp)); + } + /* A SERVER_ONLY object inside the cert namespace is nameable but must + * be refused by the flag check. */ + { + whMessageCert_ReadTrustedRequest req = {0}; + whMessageCert_ReadTrustedResponse resp = {0}; + whNvmMetadata srv = {0}; + + srv.id = WH_MAKE_KEYID(WH_KEYTYPE_CERT, server->comm->client_id, 0x5B); + srv.access = WH_NVM_ACCESS_ANY; + srv.flags = WH_NVM_FLAGS_SERVER_ONLY; + srv.len = sizeof(kek); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &srv, sizeof(kek), kek)); + + req.id = 0x5B; + wh_MessageCert_TranslateReadTrustedRequest( + magic, &req, (whMessageCert_ReadTrustedRequest*)req_packet); + (void)wh_Server_HandleCertRequest( + server, magic, WH_MESSAGE_CERT_ACTION_READTRUSTED, /*seq=*/0, + sizeof(req), req_packet, &resp_size, resp_packet); + wh_MessageCert_TranslateReadTrustedResponse( + magic, (whMessageCert_ReadTrustedResponse*)resp_packet, &resp); WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_ACCESS); WH_TEST_ASSERT_RETURN(resp.cert_len == 0); WH_TEST_ASSERT_RETURN(resp_size == sizeof(resp)); + + WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &srv.id)); } #ifdef WOLFHSM_CFG_DMA - /* READTRUSTED_DMA must refuse it too and write nothing to the buffer. */ + /* READTRUSTED_DMA with the bare id must miss too and write nothing to + * the buffer. */ { whMessageCert_ReadTrustedDmaRequest req = {0}; whMessageCert_SimpleResponse resp = {0}; @@ -419,7 +466,7 @@ whTest_CertServerReadTrustedRejectsServerOnly(whServerConfig* serverCfg) size_t i; memset(out_buf, 0, sizeof(out_buf)); - req.id = meta.id; + req.id = WH_KEYID_ID(meta.id); req.cert_addr = (uint64_t)(uintptr_t)out_buf; req.cert_len = sizeof(out_buf); wh_MessageCert_TranslateReadTrustedDmaRequest( @@ -433,7 +480,7 @@ whTest_CertServerReadTrustedRejectsServerOnly(whServerConfig* serverCfg) wh_MessageCert_TranslateSimpleResponse( magic, (whMessageCert_SimpleResponse*)resp_packet, &resp); - WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_ACCESS); + WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_NOTFOUND); for (i = 0; i < sizeof(out_buf); i++) { WH_TEST_ASSERT_RETURN(out_buf[i] == 0); } @@ -443,7 +490,169 @@ whTest_CertServerReadTrustedRejectsServerOnly(whServerConfig* serverCfg) /* Server-internal unchecked destroy still works; clean up with it. */ WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &meta.id)); - WH_TEST_PRINT("Cert ReadTrusted server-only rejection PASSED\n"); + WH_TEST_PRINT("Cert ReadTrusted server-only unreachable PASSED\n"); + return rc; +} + +/* A client must not be able to destroy a non-certificate object through the + * cert erase path. EraseTrusted used to take a raw NVM id and refuse only + * objects flagged SERVER_ONLY or NONDESTROYABLE, so a client could erase the + * auth user database (which carries neither, only NONMODIFIABLE). Now the + * client id is checked and translated into the cert namespace: an id with + * TYPE bits set is rejected as malformed, and the real object is untouched. */ +static int whTest_CertEraseCannotReachNonCertObject(whServerConfig* serverCfg) +{ + int rc = WH_ERROR_OK; + whServerContext server[1] = {0}; + whNvmMetadata meta = {0}; + whNvmMetadata check = {0}; + const uint8_t secret[8] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04}; + const uint16_t magic = WH_COMM_MAGIC_NATIVE; + uint8_t req_packet[WOLFHSM_CFG_COMM_DATA_LEN] = {0}; + uint8_t resp_packet[WOLFHSM_CFG_COMM_DATA_LEN] = {0}; + uint16_t resp_size = 0; + /* The auth backend's user-index id (WH_NVM_ID_AUTH_USER_INDEX == 0xFE00): + * a TYPE nibble no client translation can produce, carrying only + * NONMODIFIABLE. */ + const whNvmId protectedId = WH_MAKE_KEYID(0xF, 0xE, 0x00); + + WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, serverCfg)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server)); + + WH_TEST_PRINT("Cert EraseTrusted cannot reach a non-cert object...\n"); + + meta.id = protectedId; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = + WH_NVM_FLAGS_NONMODIFIABLE; /* not server-only/nondestroyable */ + meta.len = sizeof(secret); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, sizeof(secret), secret)); + + /* Client erase naming the protected id must be rejected: its TYPE bits + * lie outside the client id fields, so it cannot be translated. */ + { + whMessageCert_EraseTrustedRequest req = {0}; + whMessageCert_SimpleResponse resp = {0}; + + req.id = protectedId; + wh_MessageCert_TranslateEraseTrustedRequest( + magic, &req, (whMessageCert_EraseTrustedRequest*)req_packet); + + (void)wh_Server_HandleCertRequest( + server, magic, WH_MESSAGE_CERT_ACTION_ERASETRUSTED, /*seq=*/0, + sizeof(req), req_packet, &resp_size, resp_packet); + + wh_MessageCert_TranslateSimpleResponse( + magic, (whMessageCert_SimpleResponse*)resp_packet, &resp); + WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_BADARGS); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(resp)); + } + + /* The protected object must still be present and intact. */ + WH_TEST_ASSERT_RETURN( + wh_Nvm_GetMetadata(server->nvm, protectedId, &check) == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(check.len == sizeof(secret)); + + /* Clean up with the server-internal API. */ + WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &protectedId)); + + WH_TEST_PRINT("Cert EraseTrusted non-cert confinement PASSED\n"); + return rc; +} + +/* Drive a client READTRUSTED for `id` at the server's current client_id and + * return the client-visible rc. */ +static int32_t _certReadRc(whServerContext* server, uint16_t magic, whNvmId id, + uint8_t* req_packet, uint8_t* resp_packet) +{ + whMessageCert_ReadTrustedRequest req = {0}; + whMessageCert_ReadTrustedResponse resp = {0}; + uint16_t resp_size = 0; + + req.id = id; + wh_MessageCert_TranslateReadTrustedRequest( + magic, &req, (whMessageCert_ReadTrustedRequest*)req_packet); + (void)wh_Server_HandleCertRequest( + server, magic, WH_MESSAGE_CERT_ACTION_READTRUSTED, 0, sizeof(req), + req_packet, &resp_size, resp_packet); + wh_MessageCert_TranslateReadTrustedResponse( + magic, (whMessageCert_ReadTrustedResponse*)resp_packet, &resp); + return resp.rc; +} + +/* Certificate ids follow the same per-client scheme as keys and NVM objects: a + * plain id is private to the calling client, and the GLOBAL flag selects the + * shared namespace. Two clients naming "cert 5" get distinct objects; neither + * sees the other's, and a global cert is visible to both. */ +static int whTest_CertPerClientIsolation(whServerConfig* serverCfg) +{ + int rc = WH_ERROR_OK; + whServerContext server[1] = {0}; + whNvmMetadata meta = {0}; + const uint16_t magic = WH_COMM_MAGIC_NATIVE; + uint8_t req_packet[WOLFHSM_CFG_COMM_DATA_LEN] = {0}; + uint8_t resp_packet[WOLFHSM_CFG_COMM_DATA_LEN] = {0}; + const whNvmId client1Cert = WH_MAKE_KEYID(WH_KEYTYPE_CERT, 1, 5); + + WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, serverCfg)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server)); + + WH_TEST_PRINT("Cert per-client isolation...\n"); + + /* Plant a cert in client 1's private namespace (server-direct, full id). */ + meta.id = client1Cert; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = ROOT_A_CERT_len; + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, ROOT_A_CERT_len, ROOT_A_CERT)); + + /* Client 2 asking for cert 5 lands in its own namespace: not found. */ + server->comm->client_id = 2; + WH_TEST_ASSERT_RETURN(_certReadRc(server, magic, 5, req_packet, + resp_packet) == WH_ERROR_NOTFOUND); + + /* Client 1 sees its own cert 5. */ + server->comm->client_id = 1; + WH_TEST_ASSERT_RETURN( + _certReadRc(server, magic, 5, req_packet, resp_packet) == WH_ERROR_OK); + +#ifdef WOLFHSM_CFG_GLOBAL_KEYS + /* A cert in the global namespace is reachable by any client that sets the + * GLOBAL flag, and not by a plain id. */ + memset(&meta, 0, sizeof(meta)); + meta.id = WH_MAKE_KEYID(WH_KEYTYPE_CERT, WH_KEYUSER_GLOBAL, 6); + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = ROOT_B_CERT_len; + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, ROOT_B_CERT_len, ROOT_B_CERT)); + + server->comm->client_id = 2; + WH_TEST_ASSERT_RETURN(_certReadRc(server, magic, + 6 | WH_KEYID_CLIENT_GLOBAL_FLAG, + req_packet, resp_packet) == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(_certReadRc(server, magic, 6, req_packet, + resp_packet) == WH_ERROR_NOTFOUND); + + { + whNvmId globalCert = + WH_MAKE_KEYID(WH_KEYTYPE_CERT, WH_KEYUSER_GLOBAL, 6); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_DestroyObjects(server->nvm, 1, &globalCert)); + } +#endif + + /* Subtests share one NVM backing via the server config, so clean up the + * planted certs and restore the unbound client id. */ + { + whNvmId c1 = client1Cert; + WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &c1)); + } + server->comm->client_id = 0; + + WH_TEST_PRINT("Cert per-client isolation PASSED\n"); return rc; } @@ -2027,6 +2236,22 @@ int whTest_CertRamSim(whTestNvmBackendType nvmType) } } + if (rc == WH_ERROR_OK) { + rc = whTest_CertEraseCannotReachNonCertObject(s_conf); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("Cert EraseTrusted non-cert confinement test " + "failed: %d\n", + rc); + } + } + + if (rc == WH_ERROR_OK) { + rc = whTest_CertPerClientIsolation(s_conf); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("Cert per-client isolation test failed: %d\n", rc); + } + } + #ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE if (rc == WH_ERROR_OK) { rc = whTest_CertServerVerifyCache(s_conf); diff --git a/test/wh_test_clientserver.c b/test/wh_test_clientserver.c index 29b1f2f48..93f4a4943 100644 --- a/test/wh_test_clientserver.c +++ b/test/wh_test_clientserver.c @@ -421,7 +421,9 @@ static int _testDma(whServerContext* server, whClientContext* client) * single-thread pump makes the old KeyCacheDma use-after-free deterministic. * On failure control jumps to cleanup so the callbacks are always unregistered. */ -#define BOUNCE_TEST_NVM_ID 0x4242 /* arbitrary id, destroyed at end of test */ +#define BOUNCE_TEST_NVM_ID \ + 0x42 /* arbitrary valid client NVM id (1-255), \ + * destroyed at end of test */ /* Local fail/assert helpers that unwind to cleanup instead of returning. */ #define BOUNCE_FAIL(expr) \ @@ -995,12 +997,9 @@ int whTest_ClientServerSequential(whTestNvmBackendType nvmType) WH_TEST_RETURN_ON_FAIL(_testInitClientIdBoundary(client, server, 17, 0)); WH_TEST_RETURN_ON_FAIL(_testInitClientIdBoundary(client, server, 32, 0)); WH_TEST_RETURN_ON_FAIL(_testInitClientIdBoundary(client, server, 255, 0)); -#ifdef WOLFHSM_CFG_GLOBAL_KEYS - /* USER=0 is reserved for global keys */ + /* client_id 0 (USER=0) names the shared/factory-provisioned namespace, + * so INIT rejects it in every build. */ WH_TEST_RETURN_ON_FAIL(_testInitClientIdBoundary(client, server, 0, 0)); -#else - WH_TEST_RETURN_ON_FAIL(_testInitClientIdBoundary(client, server, 0, 1)); -#endif WH_TEST_RETURN_ON_FAIL( _testInitClientIdBoundary(client, server, WH_CLIENT_ID_MAX, 1)); /* Restore default client_id so the rest of the sequential test runs with diff --git a/test/wh_test_crypto.c b/test/wh_test_crypto.c index c24040ddb..af7096c58 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -15935,7 +15935,7 @@ static int whTestCrypto_LmsCryptoCb(whClientContext* ctx, int devId, uint32_t lmsMagic = 0x4C4D5301u; /* 'LMS\1', see wh_crypto.c */ int32_t addRc = 0; int addRet; - whNvmId addId = 0x1042; /* An arbitrary ID in the NVM range */ + whNvmId addId = 0x42; /* arbitrary valid client NVM id (1-255) */ memset(fakeBlob, 0, sizeof(fakeBlob)); memcpy(fakeBlob, &lmsMagic, sizeof(lmsMagic)); fakeBlob[6] = 1; /* privLen field nonzero: a private-bearing blob */ @@ -16341,7 +16341,7 @@ static int whTestCrypto_XmssCryptoCb(whClientContext* ctx, int devId, uint32_t xmssMagic = 0x584D5301u; /* 'XMS\1', see wh_crypto.c */ int32_t addRc = 0; int addRet; - whNvmId addId = 0x1042; /* An arbitrary ID in the NVM range */ + whNvmId addId = 0x42; /* arbitrary valid client NVM id (1-255) */ memset(fakeBlob, 0, sizeof(fakeBlob)); memcpy(fakeBlob, &xmssMagic, sizeof(xmssMagic)); fakeBlob[6] = 1; /* privLen field nonzero: a private-bearing blob */ @@ -17992,10 +17992,6 @@ int whTest_CryptoClientConfig(whClientConfig* config) #endif /* HAVE_ECC */ #ifdef HAVE_ED25519 - if (ret != 0) { - WH_ERROR_PRINT("Pre-Ed25519 tests ret=%d\n", ret); - return ret; - } (void)wh_Client_SetDmaMode(client, 0); if (ret == 0) { ret = whTest_CryptoEd25519Inline(client, WH_CLIENT_DEVID(client), rng); diff --git a/test/wh_test_keywrap.c b/test/wh_test_keywrap.c index 8768fc523..e4672ee07 100644 --- a/test/wh_test_keywrap.c +++ b/test/wh_test_keywrap.c @@ -621,24 +621,32 @@ static int _AesGcm_TestTrustedKekPolicy(whClientContext* client, WC_RNG* rng) } /* (b) A client that provisions an NVM object carrying - * WH_NVM_FLAGS_TRUSTED at a crypto-key id (keys and NVM objects share - * the id space) must not obtain a trusted KEK either: the checked NVM - * add path strips the flag. */ + * WH_NVM_FLAGS_TRUSTED at a crypto-key id must not obtain a trusted KEK + * either. The forged id carries the crypto TYPE bits above the client id + * and flag fields, so the checked add rejects it and no KEK may appear + * at the crypto-key id. */ { - whKeyId nvmForgeId = WH_TEST_KEKID + 2; - whNvmId nvmObjId = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, - client->comm->client_id, nvmForgeId); - int32_t nvmRc = 0; + whKeyId nvmForgeId = WH_TEST_KEKID + 2; + whNvmId nvmObjId = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, + client->comm->client_id, nvmForgeId); + int32_t nvmRc = 0; + int32_t expectedAddRc = 0; + int expectedRc; + +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + expectedAddRc = WH_ERROR_BADARGS; +#endif ret = wh_Client_NvmAddObject( client, nvmObjId, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_TRUSTED | WH_NVM_FLAGS_USAGE_WRAP, sizeof(label), label, sizeof(srcKey), srcKey, &nvmRc); - if (ret != 0 || nvmRc != 0) { - WH_ERROR_PRINT("trusted-kek: NvmAddObject failed ret=%d rc=%d\n", - ret, (int)nvmRc); + if (ret != 0 || nvmRc != expectedAddRc) { + WH_ERROR_PRINT("trusted-kek: NvmAddObject expected rc=%d, got " + "ret=%d rc=%d\n", + (int)expectedAddRc, ret, (int)nvmRc); (void)wh_Client_KeyEvict(client, srcKeyId); - return (ret != 0) ? ret : (int)nvmRc; + return (ret != 0) ? ret : WH_TEST_FAIL; } wrappedKeySz = sizeof(wrappedKey); ret = wh_Client_KeyWrapExport(client, WC_CIPHER_AES_GCM, srcKeyId, @@ -648,10 +656,20 @@ static int _AesGcm_TestTrustedKekPolicy(whClientContext* client, WC_RNG* rng) int32_t destroyRc = 0; (void)wh_Client_NvmDestroyObjects(client, 1, &nvmObjId, &destroyRc); } - if (ret != WH_ERROR_ACCESS) { +#ifdef WOLFHSM_CFG_LEGACY_CLIENT_NVM + /* Legacy flat id space: the forged object lands at the crypto-key + * id; the checked NVM add stripped the TRUSTED flag, so KEK use is + * refused. */ + expectedRc = WH_ERROR_ACCESS; +#else + /* The rejected forge stored nothing, so no KEK exists at the + * crypto-key id. */ + expectedRc = WH_ERROR_NOTFOUND; +#endif + if (ret != expectedRc) { WH_ERROR_PRINT("trusted-kek: wrap-export with NVM-forged KEK " - "expected ACCESS, got %d\n", - ret); + "expected %d, got %d\n", + expectedRc, ret); (void)wh_Client_KeyEvict(client, srcKeyId); return WH_TEST_FAIL; } diff --git a/test/wh_test_multiclient.c b/test/wh_test_multiclient.c index 6811caf8f..4ace1b0b8 100644 --- a/test/wh_test_multiclient.c +++ b/test/wh_test_multiclient.c @@ -174,6 +174,82 @@ static int _testGlobalKeyBasic(whClientContext* client1, return 0; } +/* + * Test: Global counter shared across clients + * - Client 1 creates and increments a counter in the global namespace + * - Client 2 reads the same global counter id and sees the shared value + * - A plain (per-client) id of the same number is a different counter that + * does not exist for client 2 + * - A global counter has no owner: client 2 can increment, re-initialize and + * destroy it, and client 1 sees each change + */ +static int _testGlobalCounter(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId gCtr = WH_CLIENT_KEYID_MAKE_GLOBAL(7); + uint32_t val = 0; + int ret; + + WH_TEST_PRINT("Test: Global counter shared across clients\n"); + + /* Client 1 creates a global counter starting at 41 */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterInitRequest(client1, gCtr, 41)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterInitResponse(client1, &val)); + WH_TEST_ASSERT_RETURN(val == 41); + + /* Client 1 increments it -> 42 */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterIncrementRequest(client1, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterIncrementResponse(client1, &val)); + WH_TEST_ASSERT_RETURN(val == 42); + + /* Client 2 reads the SAME global counter and sees the shared value */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadRequest(client2, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadResponse(client2, &val)); + WH_TEST_ASSERT_RETURN(val == 42); + + /* Isolation: client 2's own plain counter 7 is a different object that was + * never created */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadRequest(client2, 7)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + ret = wh_Client_CounterReadResponse(client2, &val); + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_NOTFOUND); + + /* A global counter has no owner, exactly like a global key: client 2 + * increments it, resets it to 5, and client 1 sees both changes. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterIncrementRequest(client2, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterIncrementResponse(client2, &val)); + WH_TEST_ASSERT_RETURN(val == 43); + + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterInitRequest(client2, gCtr, 5)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterInitResponse(client2, &val)); + WH_TEST_ASSERT_RETURN(val == 5); + + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadRequest(client1, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadResponse(client1, &val)); + WH_TEST_ASSERT_RETURN(val == 5); + + /* Client 2 destroys it, and it is gone for client 1 too */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterDestroyRequest(client2, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterDestroyResponse(client2)); + + WH_TEST_RETURN_ON_FAIL(wh_Client_CounterReadRequest(client1, gCtr)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + ret = wh_Client_CounterReadResponse(client1, &val); + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_NOTFOUND); + + WH_TEST_PRINT(" PASS: Global counter shared across clients\n"); + return 0; +} + /* * Test 2: Local key isolation * - Both clients cache local keys with the same ID but different data @@ -1362,6 +1438,9 @@ static int _runGlobalKeysTests(whClientContext* client1, WH_TEST_RETURN_ON_FAIL( _testGlobalKeyBasic(client1, server1, client2, server2)); + WH_TEST_RETURN_ON_FAIL( + _testGlobalCounter(client1, server1, client2, server2)); + WH_TEST_RETURN_ON_FAIL( _testLocalKeyIsolation(client1, server1, client2, server2)); @@ -1420,6 +1499,948 @@ static int _runGlobalKeysTests(whClientContext* client1, #endif /* WOLFHSM_CFG_GLOBAL_KEYS */ +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM +/* ============================================================================ + * CLIENT NVM ID-TRANSLATION TEST SUITE + * + * These tests assert the per-client NVM id namespace: each client sees its own + * 1..255 id range plus a shared 1..255 global range. Cross-client raw access + * via the NVM api is impossible. + * + * Only meaningful when client NVM id translation is enabled (default). + * ========================================================================== */ + +static const uint8_t NVM_ISOLATION_PAYLOAD_A[] = "client-A-secret-NVM-payload"; +static const uint8_t NVM_ISOLATION_PAYLOAD_B[] = "client-B-different-payload"; + +/* + * Helper: add an NVM object via the explicit Request/Handle/Response + * pattern so that the matching server can be driven manually (multiclient + * sequential setup has no automatic dispatch). + */ +static int _nvmAddViaServer(whClientContext* client, whServerContext* server, + whNvmId id, whNvmSize len, const uint8_t* data, + int32_t* out_rc) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectRequest( + client, id, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, 0, NULL, len, data)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectResponse(client, out_rc)); + return WH_ERROR_OK; +} + +static int _nvmReadViaServer(whClientContext* client, whServerContext* server, + whNvmId id, whNvmSize len, int32_t* out_rc, + whNvmSize* out_len, uint8_t* buf) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmReadRequest(client, id, 0, len)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmReadResponse(client, out_rc, out_len, buf)); + return WH_ERROR_OK; +} + +static int _nvmDestroyViaServer(whClientContext* client, + whServerContext* server, whNvmId id, + int32_t* out_rc) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjectsRequest(client, 1, &id)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjectsResponse(client, out_rc)); + return WH_ERROR_OK; +} + +static int _nvmListViaServer(whClientContext* client, whServerContext* server, + whNvmId startId, int32_t* out_rc, + whNvmId* out_count, whNvmId* out_id) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmListRequest( + client, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, startId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmListResponse(client, out_rc, out_count, out_id)); + return WH_ERROR_OK; +} + +static int _nvmGetMetadataViaServer(whClientContext* client, + whServerContext* server, whNvmId id, + int32_t* out_rc) +{ + whNvmId got_id = 0; + whNvmAccess access = 0; + whNvmFlags flags = 0; + whNvmSize len = 0; + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadataRequest(client, id)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadataResponse( + client, out_rc, &got_id, &access, &flags, &len, 0, NULL)); + return WH_ERROR_OK; +} + +/* + * Client A adds NVM id 5 with secret payload. Client B reading id 5 must NOT + * see A's bytes (either NOTFOUND, or B's own value if B has one). + */ +static int _testNvmClientIsolation(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId shared_id = 5; + int32_t out_rc = 0; + uint8_t buf[64] = {0}; + whNvmSize out_len = 0; + + WH_TEST_PRINT("Testing NVM client isolation...\n"); + + /* Client A adds a secret object at id=5 */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, shared_id, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Client A confirms it can read its own bytes back */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) == 0); + + /* Client B reads id=5: must NOT find A's bytes */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client2, server2, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + /* B has no object 5 yet, so the read must miss and return no bytes */ + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_NOTFOUND); + WH_TEST_ASSERT_RETURN(out_len == 0); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) != 0); + + /* Client B adds its own object at the same client-facing id=5 */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client2, server2, shared_id, + sizeof(NVM_ISOLATION_PAYLOAD_B), + NVM_ISOLATION_PAYLOAD_B, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Client B reads id=5: gets ITS OWN value */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client2, server2, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_B)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_B, + sizeof(NVM_ISOLATION_PAYLOAD_B)) == 0); + + /* Client A still sees ITS OWN value, not B's */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) == 0); + + /* Cleanup: each client destroys its own */ + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, shared_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client2, server2, shared_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM client isolation: PASS\n"); + return WH_ERROR_OK; +} + +/* + * A connection that has not completed COMM INIT has client_id 0, which would + * translate every NVM request into the USER=0 namespace (shared/global and + * factory-provisioned objects). The server must refuse NVM requests from such + * an unbound client. Force the server connection's client_id to 0 and confirm + * a planted USER=0 object is never read, enumerated, added over, or destroyed. + */ +static int _testNvmUnboundClientRejected(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId planted_id = 8; + whNvmId planted_nvm_id; + whNvmMetadata meta = {0}; + uint8_t saved_id; + int32_t out_rc = 0; + int prc; + int leaked = 0; + whNvmId count = 0; + whNvmId list_id = 0; + whNvmSize out_len = 0; + uint8_t buf[64] = {0}; + + (void)client2; + (void)server2; + + WH_TEST_PRINT( + "Testing NVM reject of unbound (client_id 0) connection...\n"); + + /* Plant a USER=0 object directly, as provisioning would. */ + planted_nvm_id = + WH_MAKE_KEYID(WH_KEYTYPE_NVM, WH_KEYUSER_GLOBAL, planted_id); + meta.id = planted_nvm_id; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = sizeof(NVM_ISOLATION_PAYLOAD_B); + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, sizeof(NVM_ISOLATION_PAYLOAD_B), + NVM_ISOLATION_PAYLOAD_B) == WH_ERROR_OK); + + /* Force the server connection into the unbound (pre-COMM-INIT) state. */ + saved_id = server1->comm->client_id; + server1->comm->client_id = WH_KEYUSER_GLOBAL; + + /* Every verb must be refused with a properly shaped reply carrying + * WH_ERROR_ACCESS; a parse failure would be a malformed reply, not a + * refusal. Accumulate a "leaked" flag across all verbs, then restore + * client_id before asserting so a failure can't corrupt later tests + * sharing this server. */ + + /* Read: refused, and never returns the planted bytes. */ + prc = wh_Client_NvmReadRequest(client1, planted_id, 0, sizeof(buf)); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmReadResponse(client1, &out_rc, &out_len, buf); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + if (memcmp(buf, NVM_ISOLATION_PAYLOAD_B, sizeof(NVM_ISOLATION_PAYLOAD_B)) == + 0) { + leaked = 1; + } + + /* GetMetadata: refused. */ + out_rc = 0; + prc = wh_Client_NvmGetMetadataRequest(client1, planted_id); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + whNvmId got_id = 0; + whNvmAccess access = 0; + whNvmFlags flags = 0; + whNvmSize len = 0; + prc = wh_Client_NvmGetMetadataResponse(client1, &out_rc, &got_id, + &access, &flags, &len, 0, NULL); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + + /* List: refused (cannot enumerate the USER=0 namespace). */ + out_rc = 0; + prc = wh_Client_NvmListRequest(client1, WH_NVM_ACCESS_ANY, + WH_NVM_FLAGS_NONE, 0); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmListResponse(client1, &out_rc, &count, &list_id); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + + /* Destroy: refused. */ + out_rc = 0; + { + whNvmId destroy_id = planted_id; + prc = wh_Client_NvmDestroyObjectsRequest(client1, 1, &destroy_id); + } + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmDestroyObjectsResponse(client1, &out_rc); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + + /* Add: refused. */ + out_rc = 0; + prc = wh_Client_NvmAddObjectRequest( + client1, 5, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, 0, NULL, + sizeof(NVM_ISOLATION_PAYLOAD_A), NVM_ISOLATION_PAYLOAD_A); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmAddObjectResponse(client1, &out_rc); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + +#ifdef WOLFHSM_CFG_DMA + /* DMA add: refused (shares the same handler entry gate). */ + out_rc = 0; + { + whNvmMetadata dma_meta = {0}; + dma_meta.id = 5; + dma_meta.access = WH_NVM_ACCESS_ANY; + dma_meta.flags = WH_NVM_FLAGS_NONE; + dma_meta.len = sizeof(NVM_ISOLATION_PAYLOAD_A); + prc = wh_Client_NvmAddObjectDmaRequest(client1, &dma_meta, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmAddObjectDmaResponse(client1, &out_rc); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + } + + /* DMA read: refused, and never returns the planted bytes. */ + out_rc = 0; + memset(buf, 0, sizeof(buf)); + prc = wh_Client_NvmReadDmaRequest(client1, planted_id, 0, + sizeof(NVM_ISOLATION_PAYLOAD_B), buf); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_NvmReadDmaResponse(client1, &out_rc); + } + if ((prc != WH_ERROR_OK) || (out_rc != WH_ERROR_ACCESS)) { + leaked = 1; + } + if (memcmp(buf, NVM_ISOLATION_PAYLOAD_B, sizeof(NVM_ISOLATION_PAYLOAD_B)) == + 0) { + leaked = 1; + } +#endif /* WOLFHSM_CFG_DMA */ + + /* Restore the bound client id before asserting. */ + server1->comm->client_id = saved_id; + + WH_TEST_ASSERT_RETURN(leaked == 0); + + /* The refused adds must not have created USER=0 object 5 either. */ + { + whNvmId added_id = + WH_MAKE_KEYID(WH_KEYTYPE_NVM, WH_KEYUSER_GLOBAL, 5); + whNvmMetadata check = {0}; + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server1->nvm, added_id, + &check) == WH_ERROR_NOTFOUND); + } + + /* The planted USER=0 object must still exist, unchanged. */ + memset(buf, 0, sizeof(buf)); + WH_TEST_ASSERT_RETURN(wh_Nvm_Read(server1->nvm, planted_nvm_id, 0, + sizeof(NVM_ISOLATION_PAYLOAD_B), + buf) == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_B, + sizeof(NVM_ISOLATION_PAYLOAD_B)) == 0); + + /* Cleanup */ + WH_TEST_ASSERT_RETURN( + wh_Nvm_DestroyObjects(server1->nvm, 1, &planted_nvm_id) == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM unbound-client reject: PASS\n"); + return WH_ERROR_OK; +} + +#ifdef WOLFHSM_CFG_GLOBAL_KEYS +/* + * List has two namespaces: + * - own : startId without GLOBAL flag + * - global : startId with WH_KEYID_CLIENT_GLOBAL_FLAG set + * + * Client A populates 2 own ids and 2 global ids. Each scan must visit only + * the corresponding namespace. + */ +static int _testNvmGlobalNamespaceList(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int32_t out_rc = 0; + whNvmId count = 0; + whNvmId cur = 0; + int seen_own[256] = {0}; + int seen_global[256] = {0}; + int i; + int iters; + const whNvmId own_ids[2] = {3, 7}; + const whNvmId global_ids[2] = {2, 4}; + (void)server2; + (void)client2; + + WH_TEST_PRINT("Testing NVM list with global namespace...\n"); + + /* Populate own ids */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, own_ids[i], sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + /* Populate global ids */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, global_ids[i] | WH_KEYID_CLIENT_GLOBAL_FLAG, + sizeof(NVM_ISOLATION_PAYLOAD_B), NVM_ISOLATION_PAYLOAD_B, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + /* Iterate own namespace (no flag on startId). Expect ids in own_ids. + * Cap iterations to defend against an unintentional infinite loop. */ + cur = 0; + for (iters = 0; iters < 16; iters++) { + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + if (count == 0) { + break; + } + /* Must not carry the GLOBAL flag */ + WH_TEST_ASSERT_RETURN((cur & WH_KEYID_CLIENT_GLOBAL_FLAG) == 0); + seen_own[cur & WH_KEYID_MASK] = 1; + if (count == 1) { + break; + } + } + WH_TEST_ASSERT_RETURN(iters < 16); + + /* Iterate global namespace (GLOBAL flag on startId). Expect ids in + * global_ids, all returned with GLOBAL flag set. */ + cur = WH_KEYID_CLIENT_GLOBAL_FLAG; + for (iters = 0; iters < 16; iters++) { + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + if (count == 0) { + break; + } + WH_TEST_ASSERT_RETURN((cur & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0); + seen_global[cur & WH_KEYID_MASK] = 1; + if (count == 1) { + break; + } + } + WH_TEST_ASSERT_RETURN(iters < 16); + + for (i = 0; i < 2; i++) { + WH_TEST_ASSERT_RETURN(seen_own[own_ids[i]] == 1); + WH_TEST_ASSERT_RETURN(seen_global[global_ids[i]] == 1); + } + /* Crosscheck: globals never appear in the own scan and vice-versa */ + WH_TEST_ASSERT_RETURN(seen_own[global_ids[0]] == 0); + WH_TEST_ASSERT_RETURN(seen_own[global_ids[1]] == 0); + WH_TEST_ASSERT_RETURN(seen_global[own_ids[0]] == 0); + WH_TEST_ASSERT_RETURN(seen_global[own_ids[1]] == 0); + + /* Cleanup */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, own_ids[i], &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL(_nvmDestroyViaServer( + client1, server1, global_ids[i] | WH_KEYID_CLIENT_GLOBAL_FLAG, + &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + WH_TEST_PRINT(" NVM global namespace list: PASS\n"); + return WH_ERROR_OK; +} + +#else /* !WOLFHSM_CFG_GLOBAL_KEYS */ + +/* + * Without global keys there is no global namespace: AddObject must reject + * the GLOBAL flag and the other verbs must ignore it, resolving to the + * caller's own namespace. USER=0 objects (e.g. provisioned by whnvmtool) + * must stay unreachable, including via List. + */ +static int _testNvmGlobalFlagDisabled(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId own_id = 3; + const whNvmId planted_id = 6; + whNvmId planted_nvm_id; + whNvmMetadata meta = {0}; + int32_t out_rc = 0; + whNvmId count = 0; + whNvmId cur = 0; + whNvmSize out_len = 0; + uint8_t buf[64] = {0}; + + (void)client2; + (void)server2; + + WH_TEST_PRINT("Testing NVM GLOBAL flag with global keys disabled...\n"); + + /* Plant a USER=0 object directly, as provisioning would */ + planted_nvm_id = + WH_MAKE_KEYID(WH_KEYTYPE_NVM, WH_KEYUSER_GLOBAL, planted_id); + meta.id = planted_nvm_id; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = sizeof(NVM_ISOLATION_PAYLOAD_B); + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, sizeof(NVM_ISOLATION_PAYLOAD_B), + NVM_ISOLATION_PAYLOAD_B) == WH_ERROR_OK); + + /* AddObject with the GLOBAL flag fails loudly */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, 5 | WH_KEYID_CLIENT_GLOBAL_FLAG, + sizeof(NVM_ISOLATION_PAYLOAD_A), NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + +#ifdef WOLFHSM_CFG_DMA + /* The DMA add path shares the same id validation */ + { + whNvmMetadata dma_meta = {0}; + dma_meta.id = 5 | WH_KEYID_CLIENT_GLOBAL_FLAG; + dma_meta.access = WH_NVM_ACCESS_ANY; + dma_meta.flags = WH_NVM_FLAGS_NONE; + dma_meta.len = sizeof(NVM_ISOLATION_PAYLOAD_A); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectDmaRequest( + client1, &dma_meta, sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmAddObjectDmaResponse(client1, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + } +#endif + + /* Add an own object without the flag */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, own_id, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* A GLOBAL-flagged List ignores the flag: it walks the caller's own + * namespace and never surfaces the planted USER=0 object */ + cur = WH_KEYID_CLIENT_GLOBAL_FLAG; + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(count == 1); + WH_TEST_ASSERT_RETURN(cur == own_id); + + /* Read ignores the flag the same way: it resolves to the own object */ + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, own_id | WH_KEYID_CLIENT_GLOBAL_FLAG, sizeof(buf), + &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, out_len) == 0); + + /* The planted USER=0 object is not reachable via the flag */ + WH_TEST_RETURN_ON_FAIL(_nvmGetMetadataViaServer( + client1, server1, planted_id | WH_KEYID_CLIENT_GLOBAL_FLAG, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* Cleanup */ + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, own_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN( + wh_Nvm_DestroyObjects(server1->nvm, 1, &planted_nvm_id) == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM GLOBAL flag disabled semantics: PASS\n"); + return WH_ERROR_OK; +} +#endif /* WOLFHSM_CFG_GLOBAL_KEYS */ + +/* + * Reject malformed AddObject requests: + * - id portion == 0 (erased sentinel) + * - wrapped flag set + */ +static int _testNvmAddObjectRejections(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int32_t out_rc = 0; + (void)server2; + (void)client2; + + WH_TEST_PRINT("Testing NVM AddObject bad-id rejections...\n"); + + /* id=0 with own scope */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, 0, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* id=0 with global scope */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, + WH_KEYID_CLIENT_GLOBAL_FLAG, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* wrapped flag set */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, + 5 | WH_KEYID_CLIENT_WRAPPED_FLAG, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* bits above the id and client-flag fields (a legacy-style internal id) + * must fail, not silently truncate to id 0x42 */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, 0x1042, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + WH_TEST_PRINT(" NVM AddObject rejections: PASS\n"); + return WH_ERROR_OK; +} + +/* + * Non-add verbs must reject ids with bits above the id and client-flag + * fields instead of silently truncating them onto another object. Add object + * 0x42, then confirm the legacy-style id 0x1042 cannot read, stat, list-from, + * or destroy it. + */ +static int _testNvmNonAddVerbRejections(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId goodId = 0x42; + const whNvmId aliasId = 0x1042; /* truncates to 0x42 if not rejected */ + int32_t out_rc = 0; + whNvmSize out_len = 0; + whNvmId list_id = 0; + whNvmId count = 0; + uint8_t buf[64] = {0}; + + (void)client2; + (void)server2; + + WH_TEST_PRINT("Testing NVM bad-id rejection on non-add verbs...\n"); + + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, goodId, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + WH_TEST_RETURN_ON_FAIL( + _nvmGetMetadataViaServer(client1, server1, aliasId, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, aliasId, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmListViaServer(client1, server1, aliasId, &out_rc, + &count, &list_id)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + + /* The destructive verb is the dangerous one: without the per-verb check, + * destroying 0x1042 silently destroyed 0x42 */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, aliasId, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + +#ifdef WOLFHSM_CFG_DMA + /* The DMA read path shares the check */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmReadDmaRequest(client1, aliasId, 0, sizeof(buf), buf)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmReadDmaResponse(client1, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); +#endif + + /* The object the malformed id would have truncated onto is untouched */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, goodId, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, out_len) == 0); + + /* Cleanup */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, goodId, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM non-add verb rejections: PASS\n"); + return WH_ERROR_OK; +} + +/* + * The WRAPPED and HW client flags must not let the NVM API reach a + * differently-typed object. Plant a WRAPPED-typed and an HW-typed object in + * client1's own namespace at numeric id 9, then confirm the client NVM + * read/getmetadata/destroy verbs cannot touch them via those flags. + */ +static int _testNvmWrappedHwFlagIsolation(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId planted_id = 9; + const uint8_t secret[] = "planted-non-nvm-secret"; + const whNvmSize secretSz = (whNvmSize)sizeof(secret); + const whKeyId clientFlags[2] = {WH_KEYID_CLIENT_WRAPPED_FLAG, + WH_KEYID_CLIENT_HW_FLAG}; + int32_t out_rc = 0; + whNvmSize out_len = 0; + uint8_t buf[64] = {0}; + whNvmMetadata meta = {0}; + whKeyId wrappedId; + whKeyId hwId; + int i; + + (void)client2; + (void)server2; + + WH_TEST_PRINT("Testing NVM WRAPPED/HW flag type isolation...\n"); + + wrappedId = + WH_MAKE_KEYID(WH_KEYTYPE_WRAPPED, server1->comm->client_id, planted_id); + hwId = WH_MAKE_KEYID(WH_KEYTYPE_HW, server1->comm->client_id, planted_id); + + /* Plant the two forbidden objects directly in the shared store. */ + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = secretSz; + meta.id = wrappedId; + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, secretSz, secret) == WH_ERROR_OK); + meta.id = hwId; + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, secretSz, secret) == WH_ERROR_OK); + + for (i = 0; i < 2; i++) { + whNvmId flagged = (whNvmId)(planted_id | clientFlags[i]); + + /* Read: must not return the planted bytes. The flagged id is rejected + * up front; the translation-time strip is only a backstop. */ + memset(buf, 0, sizeof(buf)); + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, flagged, secretSz, &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(memcmp(buf, secret, secretSz) != 0); + + /* GetMetadata: must not surface the planted object. */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmGetMetadataViaServer(client1, server1, flagged, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* Destroy: must not reach the planted object. */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, flagged, &out_rc)); + } + + /* Both planted objects must still exist after the flagged destroys. */ + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server1->nvm, wrappedId, &meta) == + WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server1->nvm, hwId, &meta) == + WH_ERROR_OK); + + /* Clean up the planted objects. */ + (void)wh_Nvm_DestroyObjects(server1->nvm, 1, &wrappedId); + (void)wh_Nvm_DestroyObjects(server1->nvm, 1, &hwId); + + WH_TEST_PRINT(" NVM WRAPPED/HW flag isolation: PASS\n"); + return WH_ERROR_OK; +} + +static int _runNvmIdTranslationTests(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + WH_TEST_PRINT("=== NVM Id Translation Tests Begin ===\n"); + WH_TEST_RETURN_ON_FAIL( + _testNvmClientIsolation(client1, server1, client2, server2)); + WH_TEST_RETURN_ON_FAIL( + _testNvmUnboundClientRejected(client1, server1, client2, server2)); +#ifdef WOLFHSM_CFG_GLOBAL_KEYS + WH_TEST_RETURN_ON_FAIL( + _testNvmGlobalNamespaceList(client1, server1, client2, server2)); +#else + WH_TEST_RETURN_ON_FAIL( + _testNvmGlobalFlagDisabled(client1, server1, client2, server2)); +#endif + WH_TEST_RETURN_ON_FAIL( + _testNvmAddObjectRejections(client1, server1, client2, server2)); + WH_TEST_RETURN_ON_FAIL( + _testNvmNonAddVerbRejections(client1, server1, client2, server2)); + WH_TEST_RETURN_ON_FAIL( + _testNvmWrappedHwFlagIsolation(client1, server1, client2, server2)); + WH_TEST_PRINT("All NVM Id Translation Tests PASSED ===\n"); + return WH_ERROR_OK; +} + +#endif /* !WOLFHSM_CFG_LEGACY_CLIENT_NVM */ + +#ifndef WOLFHSM_CFG_NO_CRYPTO +/* + * The keystore group shares the pre-COMM-INIT gate, and its DMA replies carry + * rc behind a DMA address status rather than first. Force the unbound state + * and confirm every keystore verb surfaces WH_ERROR_ACCESS through a properly + * shaped reply, so a refused DMA cache or export is never reported as success. + */ +static int _testKeystoreUnboundClientRejected(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + /* Id no other test caches, so the absence checks below are meaningful */ + const whKeyId keyId = 0x3A; + const whKeyId globalId = + WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, WH_KEYUSER_GLOBAL, keyId); + uint8_t keyData[32] = "UnboundKeystoreRejectKey12!"; + uint8_t outBuf[32] = {0}; + uint8_t label[WH_NVM_LABEL_LEN] = {0}; + uint32_t readSz = sizeof(outBuf); + uint16_t outSz = 0; + uint16_t gotId = 0; + uint8_t saved_id; + int prc; + int leaked = 0; + + (void)client2; + (void)server2; + + WH_TEST_PRINT( + "Testing keystore reject of unbound (client_id 0) connection...\n"); + + /* Precondition: no USER=0 key under this id yet. */ + WH_TEST_ASSERT_RETURN( + wh_Server_KeystoreReadKey(server1, globalId, NULL, outBuf, &readSz) == + WH_ERROR_NOTFOUND); + + /* Force the server connection into the unbound (pre-COMM-INIT) state. */ + saved_id = server1->comm->client_id; + server1->comm->client_id = WH_KEYUSER_GLOBAL; + + /* Each verb must return WH_ERROR_ACCESS from a shaped reply: OK would be + * a false success, ABORTED a malformed reply. Accumulate a "leaked" flag, + * then restore client_id before asserting so a failure can't corrupt + * later tests sharing this server. */ + + /* Cache: refused. */ + prc = wh_Client_KeyCacheRequest_ex(client1, 0, (uint8_t*)"Unbound", + sizeof("Unbound"), keyData, + sizeof(keyData), keyId); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_KeyCacheResponse(client1, &gotId); + } + if (prc != WH_ERROR_ACCESS) { + leaked = 1; + } + + /* Export: refused. */ + outSz = sizeof(outBuf); + prc = wh_Client_KeyExportRequest(client1, keyId); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_KeyExportResponse(client1, label, sizeof(label), outBuf, + &outSz); + } + if (prc != WH_ERROR_ACCESS) { + leaked = 1; + } + +#ifdef WOLFHSM_CFG_DMA + /* DMA cache: rc sits behind the DMA address status in the reply. */ + prc = wh_Client_KeyCacheDmaRequest(client1, 0, (uint8_t*)"Unbound", + sizeof("Unbound"), keyData, + sizeof(keyData), keyId); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_KeyCacheDmaResponse(client1, &gotId); + } + if (prc != WH_ERROR_ACCESS) { + leaked = 1; + } + + /* DMA export: refused. */ + outSz = sizeof(outBuf); + prc = wh_Client_KeyExportDmaRequest(client1, keyId, outBuf, sizeof(outBuf)); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_KeyExportDmaResponse(client1, label, sizeof(label), + &outSz); + } + if (prc != WH_ERROR_ACCESS) { + leaked = 1; + } + + /* DMA public export: refused. */ + outSz = sizeof(outBuf); + prc = wh_Client_KeyExportPublicDmaRequest(client1, keyId, WH_KEY_ALGO_ECC, + outBuf, sizeof(outBuf)); + if (prc == WH_ERROR_OK) { + prc = wh_Server_HandleRequestMessage(server1); + } + if (prc == WH_ERROR_OK) { + prc = wh_Client_KeyExportPublicDmaResponse(client1, label, + sizeof(label), &outSz); + } + if (prc != WH_ERROR_ACCESS) { + leaked = 1; + } +#endif /* WOLFHSM_CFG_DMA */ + + /* Restore the bound client id before asserting. */ + server1->comm->client_id = saved_id; + + WH_TEST_ASSERT_RETURN(leaked == 0); + + /* The refused caches must not have left a USER=0 key behind. */ + readSz = sizeof(outBuf); + WH_TEST_ASSERT_RETURN( + wh_Server_KeystoreReadKey(server1, globalId, NULL, outBuf, &readSz) == + WH_ERROR_NOTFOUND); + + WH_TEST_PRINT(" Keystore unbound client rejection: PASS\n"); + return WH_ERROR_OK; +} +#endif /* !WOLFHSM_CFG_NO_CRYPTO */ + /* ============================================================================ * GLOBAL SHE KEYS TEST SUITE * @@ -1437,28 +2458,91 @@ static int _runGlobalKeysTests(whClientContext* client1, #define SHE_MC_PRIME_SLOT 8 #define SHE_MC_CTR_SLOT 9 -/* Provision a SHE slot in the shared NVM, the way ShePreProgramKey does but - * with the split API. Counter and SHE flags go in the object label. */ -static int _sheGlobalAddNvmKey(whClientContext* client, whServerContext* server, - uint8_t sheSlot, uint32_t counter, - uint32_t sheFlags, const uint8_t* key) +/* Provision a SHE slot through the dedicated pre-program message with the + * split API. Counter and SHE flags go in the object label. Raw NVM adds can + * no longer reach SHE slots since client NVM ids live in their own id space. */ +static int _sheGlobalPreProgramKey(whClientContext* client, + whServerContext* server, uint8_t sheSlot, + uint32_t counter, uint32_t sheFlags, + const uint8_t* key) { - int ret; - int32_t rc = 0; - uint8_t label[WH_NVM_LABEL_LEN] = {0}; - - wh_She_Meta2Label(counter, sheFlags, label); - ret = wh_Client_NvmAddObjectRequest( - client, WH_SHE_MAKE_KEYID(client->comm->client_id, sheSlot), 0, 0, - sizeof(label), label, WH_SHE_KEY_SZ, key); + int ret; + uint16_t group = 0; + uint16_t action = 0; + uint16_t dataSz = 0; + whMessageShe_PreProgramKeyRequest* req; + whMessageShe_PreProgramKeyResponse* resp; + uint8_t* reqBuf; + + reqBuf = (uint8_t*)wh_CommClient_GetDataPtr(client->comm); + req = (whMessageShe_PreProgramKeyRequest*)reqBuf; + req->keyId = sheSlot; + req->count = counter; + req->flags = sheFlags; + req->keySz = WH_SHE_KEY_SZ; + memcpy(reqBuf + sizeof(*req), key, WH_SHE_KEY_SZ); + + ret = wh_Client_SendRequest( + client, WH_MESSAGE_GROUP_SHE, WH_SHE_PRE_PROGRAM_KEY, + (uint16_t)(sizeof(*req) + WH_SHE_KEY_SZ), reqBuf); if (ret == 0) { ret = wh_Server_HandleRequestMessage(server); } if (ret == 0) { - ret = wh_Client_NvmAddObjectResponse(client, &rc); + resp = (whMessageShe_PreProgramKeyResponse*)wh_CommClient_GetDataPtr( + client->comm); + ret = wh_Client_RecvResponse(client, &group, &action, &dataSz, + WOLFHSM_CFG_COMM_DATA_LEN, (uint8_t*)resp); + if (ret == 0) { + if ((group != WH_MESSAGE_GROUP_SHE) || + (action != WH_SHE_PRE_PROGRAM_KEY) || + (dataSz != sizeof(*resp))) { + ret = WH_ERROR_ABORTED; + } + else { + ret = (int)resp->rc; + } + } } + return ret; +} + +/* Remove a SHE slot through the dedicated destroy message with the split API */ +static int _sheGlobalDestroyKey(whClientContext* client, + whServerContext* server, uint8_t sheSlot) +{ + int ret; + uint16_t group = 0; + uint16_t action = 0; + uint16_t dataSz = 0; + whMessageShe_DestroyKeyRequest* req; + whMessageShe_DestroyKeyResponse* resp; + + req = + (whMessageShe_DestroyKeyRequest*)wh_CommClient_GetDataPtr(client->comm); + memset(req, 0, sizeof(*req)); + req->keyId = sheSlot; + + ret = + wh_Client_SendRequest(client, WH_MESSAGE_GROUP_SHE, WH_SHE_DESTROY_KEY, + sizeof(*req), (uint8_t*)req); if (ret == 0) { - ret = (int)rc; + ret = wh_Server_HandleRequestMessage(server); + } + if (ret == 0) { + resp = (whMessageShe_DestroyKeyResponse*)wh_CommClient_GetDataPtr( + client->comm); + ret = wh_Client_RecvResponse(client, &group, &action, &dataSz, + WOLFHSM_CFG_COMM_DATA_LEN, (uint8_t*)resp); + if (ret == 0) { + if ((group != WH_MESSAGE_GROUP_SHE) || + (action != WH_SHE_DESTROY_KEY) || (dataSz != sizeof(*resp))) { + ret = WH_ERROR_ABORTED; + } + else { + ret = (int)resp->rc; + } + } } return ret; } @@ -1704,16 +2788,16 @@ static int _runSheGlobalTests(whClientContext* client1, * and the expected bootloader digest; same UID on both servers */ WH_TEST_RETURN_ON_FAIL(_sheGlobalComputeBootMac( bootloader, sizeof(bootloader), bootMacKey, bootDigest)); - WH_TEST_RETURN_ON_FAIL(_sheGlobalAddNvmKey( + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( client1, server1, WH_SHE_SECRET_KEY_ID, 0, 0, secretKey)); - WH_TEST_RETURN_ON_FAIL(_sheGlobalAddNvmKey( + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( client1, server1, WH_SHE_MASTER_ECU_KEY_ID, 0, 0, masterKey)); - WH_TEST_RETURN_ON_FAIL(_sheGlobalAddNvmKey( + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( client1, server1, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, bootMacKey)); - WH_TEST_RETURN_ON_FAIL(_sheGlobalAddNvmKey( + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( client1, server1, WH_SHE_BOOT_MAC, 0, 0, bootDigest)); - WH_TEST_RETURN_ON_FAIL( - _sheGlobalAddNvmKey(client1, server1, SHE_MC_USER_SLOT, 0, 0, userKey)); + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( + client1, server1, SHE_MC_USER_SLOT, 0, 0, userKey)); WH_TEST_RETURN_ON_FAIL( _sheGlobalSetUid(client1, server1, sheUid, sizeof(sheUid))); WH_TEST_RETURN_ON_FAIL( @@ -1801,7 +2885,7 @@ static int _runSheGlobalTests(whClientContext* client1, WH_TEST_PRINT(" PASS: Cross-client unwrap-and-cache prime\n"); /* Counter guard runs against the globally committed slot */ - WH_TEST_RETURN_ON_FAIL(_sheGlobalAddNvmKey( + WH_TEST_RETURN_ON_FAIL(_sheGlobalPreProgramKey( client1, server1, SHE_MC_CTR_SLOT, 5, 0, ctrKey)); blobSz = sizeof(blob); WH_TEST_RETURN_ON_FAIL(whTest_BuildSheKeyBlob( @@ -1859,20 +2943,18 @@ static int _runSheGlobalTests(whClientContext* client1, SHE_MC_USER_SLOT, SHE_MC_LOAD_SLOT, SHE_MC_PRIME_SLOT, SHE_MC_CTR_SLOT, WH_SHE_RAM_KEY_ID, }; - /* All SHE ids are global here, so the client id argument is moot */ - whNvmId destroyList[] = { - WH_SHE_MAKE_KEYID(0, WH_SHE_SECRET_KEY_ID), - WH_SHE_MAKE_KEYID(0, WH_SHE_MASTER_ECU_KEY_ID), - WH_SHE_MAKE_KEYID(0, WH_SHE_BOOT_MAC_KEY_ID), - WH_SHE_MAKE_KEYID(0, WH_SHE_BOOT_MAC), - WH_SHE_MAKE_KEYID(0, SHE_MC_USER_SLOT), - WH_SHE_MAKE_KEYID(0, SHE_MC_LOAD_SLOT), + static const uint8_t destroySlots[] = { + WH_SHE_SECRET_KEY_ID, + WH_SHE_MASTER_ECU_KEY_ID, + WH_SHE_BOOT_MAC_KEY_ID, + WH_SHE_BOOT_MAC, + SHE_MC_USER_SLOT, + SHE_MC_LOAD_SLOT, #if defined(WOLFHSM_CFG_KEYWRAP) && defined(HAVE_AESGCM) /* Only created by the keywrap sub-tests above */ - WH_SHE_MAKE_KEYID(0, SHE_MC_CTR_SLOT), + SHE_MC_CTR_SLOT, #endif }; - int32_t rc = 0; for (i = 0; i < (int)sizeof(evictSlots); i++) { ret = wh_Server_KeystoreEvictKey( @@ -1882,13 +2964,10 @@ static int _runSheGlobalTests(whClientContext* client1, return ret; } } - WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjectsRequest( - client1, (whNvmId)(sizeof(destroyList) / sizeof(destroyList[0])), - destroyList)); - WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); - WH_TEST_RETURN_ON_FAIL( - wh_Client_NvmDestroyObjectsResponse(client1, &rc)); - WH_TEST_ASSERT_RETURN(rc == 0); + for (i = 0; i < (int)sizeof(destroySlots); i++) { + WH_TEST_RETURN_ON_FAIL( + _sheGlobalDestroyKey(client1, server1, destroySlots[i])); + } } WH_TEST_PRINT("All Global SHE Keys Tests PASSED ===\n"); @@ -2142,6 +3221,17 @@ static int whTest_MultiClientSequential(void) _runSheGlobalTests(client1, server1, client2, server2)); #endif +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + WH_TEST_RETURN_ON_FAIL( + _runNvmIdTranslationTests(client1, server1, client2, server2)); +#endif + +#ifndef WOLFHSM_CFG_NO_CRYPTO + /* The COMM INIT gate is unconditional, so this runs in every build */ + WH_TEST_RETURN_ON_FAIL( + _testKeystoreUnboundClientRejected(client1, server1, client2, server2)); +#endif + /* Future test suites here */ /* Cleanup */ diff --git a/test/wh_test_posix_threadsafe_stress.c b/test/wh_test_posix_threadsafe_stress.c index 6df406abf..334394291 100644 --- a/test/wh_test_posix_threadsafe_stress.c +++ b/test/wh_test_posix_threadsafe_stress.c @@ -28,10 +28,21 @@ * Architecture: * - 1 shared NVM context with lock * - 4 server contexts sharing the NVM - * - 4 client threads (all doing both NVM and keystore ops) + * - 4 client threads (all doing both NVM and keystore ops), each with a + * distinct comm client_id so the server scopes ids per client * - Different contention phases to stress test contention patterns across * various different APIs * + * Every phase runs once per namespace variant: + * - global: all ids carry WH_KEYID_CLIENT_GLOBAL_FLAG, so every client + * operates on the same shared objects (cross-client contention). + * - local: bare ids resolve into each client's private namespace. Nobody + * else can touch a client's objects, so acceptable-error sets are strict + * and reads verify per-client data patterns. + * - mixed: clients 0-1 churn the shared global objects while clients 2-3 + * run deterministic operations on private objects. Any perturbation of + * the private objects is a hard failure. + * * NOTE: Uses PTHREAD_MUTEX_ERRORCHECK attribute to trap undefined behavior * errors (EDEADLK for deadlock, EPERM for non-owner unlock) which indicate * bugs in the locking implementation. @@ -211,11 +222,44 @@ static const whTransportServerCb serverTransportCb = WH_TRANSPORT_MEM_SERVER_CB; #define REVOKE_EXPORT_KEY_LOCAL 13 /* ID=13, local */ #define FRESHEN_KEY_GLOBAL WH_CLIENT_KEYID_MAKE_GLOBAL(14) /* ID=14, global */ #define FRESHEN_KEY_LOCAL 15 /* ID=15, local */ + +/* The mixed variant needs its own revoke keys: the keys revoked during the + * global and local runs are permanently NONMODIFIABLE and can't be reused */ +#define REVOKE_CACHE_KEY_MIXED_GLOBAL WH_CLIENT_KEYID_MAKE_GLOBAL(16) +#define REVOKE_CACHE_KEY_MIXED_LOCAL 17 +#define REVOKE_EXPORT_KEY_MIXED_GLOBAL WH_CLIENT_KEYID_MAKE_GLOBAL(18) +#define REVOKE_EXPORT_KEY_MIXED_LOCAL 19 + #define HOT_NVM_ID ((whNvmId)100) #define HOT_NVM_ID_2 ((whNvmId)101) #define HOT_NVM_ID_3 ((whNvmId)102) #define HOT_COUNTER_ID ((whNvmId)200) +/* Id ranges for the Add With Reclaim phase. Client NVM ids are limited to + * 1-255 (bits 8-10 are namespace/type flags), so the ids cycle within a + * bounded range. Re-adding an existing id retires the old copy as + * reclaimable, which keeps compaction busy for the whole phase. Ranges stay + * below HOT_NVM_ID so they never collide with the hot objects. */ +#define RECLAIM_ID_BASE ((whNvmId)10) +#define RECLAIM_IDS_PER_CLIENT 20 /* global: disjoint range per client */ +#define RECLAIM_IDS_LOCAL 40 /* local: cycle length per namespace */ +#define RECLAIM_SETUP_COUNT 10 /* objects pre-filled by setup */ + +/* Data-fill patterns. Writers fill objects with a single byte that encodes + * the writer and namespace side; readers verify content so torn reads and + * cross-namespace data bleed become hard failures. */ +#define GLOBAL_PATTERN_BASE 0x50 /* global-side writer: 0x50 | clientId */ +#define LOCAL_PATTERN_BASE 0xA0 /* local-side writer: 0xA0 | clientId */ + +/* Content expectation for doNvmRead/doNvmReadDma */ +#define NVM_EXPECT_NONE (-1) /* no content check */ +#define NVM_EXPECT_ANY_GLOBAL 0x100 /* uniform, from any global-side writer */ + +/* Test-local failure codes. Outside the wolfHSM error space so they are + * never in an acceptable-error set. */ +#define STRESS_ERR_DATA_MISMATCH (-77001) /* object content wrong/torn */ +#define STRESS_ERR_NAMESPACE_LEAK (-77002) /* list returned foreign-ns id */ + /* ============================================================================ * PHASE DEFINITIONS * ========================================================================== */ @@ -285,6 +329,16 @@ typedef enum { ROLE_OP_B, /* Perform operation B (e.g., Evict, Read) */ } ClientRole; +/* Namespace variant for a phase run. Selects which namespace each client's + * ids resolve into and how strictly per-operation results are judged. */ +typedef enum { + VARIANT_GLOBAL = 0, /* all clients share objects (global namespace) */ + VARIANT_LOCAL, /* each client uses its own private namespace */ + VARIANT_MIXED, /* clients 0-1 global, clients 2-3 private */ +} NamespaceVariant; + +#define VARIANT_COUNT 3 + /* Phase configuration */ typedef struct { ContentionPhase phase; @@ -495,6 +549,8 @@ typedef struct { volatile int stopFlag; volatile int errorCount; volatile int iterationCount; + volatile int successCount; + volatile int commInitFailed; /* Pointer back to shared context */ struct StressTestContext* sharedCtx; @@ -535,17 +591,91 @@ typedef struct StressTestContext { pthread_barrier_t setupCompleteBarrier; pthread_barrier_t streamStartBarrier; pthread_barrier_t streamEndBarrier; + pthread_barrier_t cleanupStartBarrier; /* Phase control */ - volatile int phaseRunning; - volatile ContentionPhase currentPhase; - volatile whKeyId currentKeyId; - volatile ClientRole clientRoles[NUM_CLIENTS]; + volatile int phaseRunning; + volatile ContentionPhase currentPhase; + volatile NamespaceVariant currentVariant; + volatile ClientRole clientRoles[NUM_CLIENTS]; } StressTestContext; /* Forward declarations */ static void* serverThread(void* arg); static void* contentionClientThread(void* arg); +static whKeyId selectKeyIdForPhase(ContentionPhase phase, + NamespaceVariant variant, int globalSide); + +/* ============================================================================ + * NAMESPACE VARIANT HELPERS + * ========================================================================== */ + +static const char* variantName(NamespaceVariant variant) +{ + switch (variant) { + case VARIANT_GLOBAL: + return "global"; + case VARIANT_LOCAL: + return "local"; + case VARIANT_MIXED: + return "mixed"; + default: + return "?"; + } +} + +/* True if this client targets the shared global namespace this run */ +static int isGlobalSide(const ClientServerPair* pair, NamespaceVariant variant) +{ + return (variant == VARIANT_GLOBAL) || + ((variant == VARIANT_MIXED) && (pair->clientId < (NUM_CLIENTS / 2))); +} + +/* NVM object id for this client under the current variant. Global-side ids + * carry WH_KEYID_CLIENT_GLOBAL_FLAG; local ids are bare and the server + * scopes them to this client's namespace. */ +static whNvmId nvmIdFor(const ClientServerPair* pair, NamespaceVariant variant, + whNvmId baseId) +{ + if (isGlobalSide(pair, variant)) { + return (whNvmId)(baseId | WH_KEYID_CLIENT_GLOBAL_FLAG); + } + return baseId; +} + +/* Data byte this client writes into NVM objects under the current variant */ +static uint8_t nvmPatternFor(const ClientServerPair* pair, + NamespaceVariant variant) +{ + if (isGlobalSide(pair, variant)) { + return (uint8_t)(GLOBAL_PATTERN_BASE | pair->clientId); + } + return (uint8_t)(LOCAL_PATTERN_BASE | pair->clientId); +} + +/* Content expectation for this client's reads under the current variant. + * Global-side objects may hold any global-side writer's pattern; a private + * object must hold exactly its owner's pattern. */ +static int nvmReadExpectFor(const ClientServerPair* pair, + NamespaceVariant variant) +{ + if (isGlobalSide(pair, variant)) { + return NVM_EXPECT_ANY_GLOBAL; + } + return (int)nvmPatternFor(pair, variant); +} + +/* Role for this client under the current variant. The mixed variant re-pairs + * roles by client parity so that both namespace sides get both operations + * (tables are laid out as {A,A,B,B} or all-A). */ +static ClientRole roleFor(const PhaseConfig* config, NamespaceVariant variant, + int clientId) +{ + if (variant == VARIANT_MIXED) { + return config->roles[(clientId % 2 == 0) ? 0 : (NUM_CLIENTS - 1)]; + } + return config->roles[clientId]; +} /* ============================================================================ * INITIALIZATION HELPERS @@ -706,9 +836,12 @@ static void* serverThread(void* arg) /* Wait for all threads to start */ pthread_barrier_wait(&ctx->startBarrier); - /* Process requests until stopped */ - while (!ATOMIC_LOAD_INT(&pair->stopFlag) && - !ATOMIC_LOAD_INT(&ctx->globalStopFlag)) { + /* Process requests until this pair is explicitly stopped. Do NOT exit + * on globalStopFlag: clients still send per-phase cleanup requests + * after the final phase, and a server that exits early leaves its + * client spinning forever for a response. join_threads sets stopFlag + * once the clients are done. */ + while (!ATOMIC_LOAD_INT(&pair->stopFlag)) { rc = wh_Server_HandleRequestMessage(&pair->server); if (rc == WH_ERROR_NOTREADY) { @@ -728,14 +861,63 @@ static void* serverThread(void* arg) * CLIENT OPERATIONS * ========================================================================== */ -static int doNvmAddObject(whClientContext* client, whNvmId id, int iteration) +/* Register this client's id with its server. Without this the server treats + * every request as USER=0 (the shared global namespace) and per-client + * isolation is never engaged. */ +static int doCommInit(ClientServerPair* pair) +{ + uint32_t outClientId = 0; + uint32_t outServerId = 0; + int rc; + + rc = wh_Client_CommInitRequest(&pair->client); + if (rc != WH_ERROR_OK) { + return rc; + } + + do { + rc = wh_Client_CommInitResponse(&pair->client, &outClientId, + &outServerId); + if (rc == WH_ERROR_NOTREADY) { + sched_yield(); + } + } while (rc == WH_ERROR_NOTREADY); + + return rc; +} + +/* Verify read-back object content. Data must be uniform (a torn read mixing + * two writers' patterns fails) and match the expectation: an exact owner + * pattern, or any global-side writer's pattern. */ +static int checkNvmData(const uint8_t* data, whNvmSize len, int expect) +{ + whNvmSize i; + + if (expect == NVM_EXPECT_NONE || len == 0) { + return WH_ERROR_OK; + } + for (i = 1; i < len; i++) { + if (data[i] != data[0]) { + return STRESS_ERR_DATA_MISMATCH; + } + } + if (expect == NVM_EXPECT_ANY_GLOBAL) { + return ((data[0] & 0xF0) == GLOBAL_PATTERN_BASE) + ? WH_ERROR_OK + : STRESS_ERR_DATA_MISMATCH; + } + return (data[0] == (uint8_t)expect) ? WH_ERROR_OK + : STRESS_ERR_DATA_MISMATCH; +} + +static int doNvmAddObject(whClientContext* client, whNvmId id, uint8_t pattern) { uint8_t data[NVM_OBJECT_DATA_SIZE]; int32_t out_rc; int rc; - /* Fill data with pattern */ - memset(data, (uint8_t)(iteration & 0xFF), sizeof(data)); + /* Fill data with this writer's pattern so readers can verify content */ + memset(data, pattern, sizeof(data)); /* Send request */ rc = wh_Client_NvmAddObjectRequest(client, id, WH_NVM_ACCESS_ANY, @@ -760,7 +942,9 @@ static int doNvmAddObject(whClientContext* client, whNvmId id, int iteration) return out_rc; } -static int doNvmRead(whClientContext* client, whNvmId id) +/* Read an object and, when expect is not NVM_EXPECT_NONE, verify its + * content (see checkNvmData) */ +static int doNvmRead(whClientContext* client, whNvmId id, int expect) { uint8_t data[NVM_OBJECT_DATA_SIZE]; whNvmSize outSz = sizeof(data); @@ -784,12 +968,19 @@ static int doNvmRead(whClientContext* client, whNvmId id) if (rc != WH_ERROR_OK) { return rc; } + if (out_rc != WH_ERROR_OK) { + return out_rc; + } - return out_rc; + return checkNvmData(data, outSz, expect); } -static int doNvmList(whClientContext* client) +/* List one namespace (listGlobal selects the shared global namespace via + * the GLOBAL flag on startId, else this client's own namespace) and verify + * any returned id belongs to the namespace that was asked for. */ +static int doNvmList(whClientContext* client, int listGlobal) { + whNvmId startId = listGlobal ? (whNvmId)WH_KEYID_CLIENT_GLOBAL_FLAG : 0; whNvmId outId; whNvmSize outCount; int32_t out_rc; @@ -797,7 +988,7 @@ static int doNvmList(whClientContext* client) /* Send request */ rc = wh_Client_NvmListRequest(client, WH_NVM_ACCESS_ANY, - WH_NVM_FLAGS_USAGE_ANY, 0); + WH_NVM_FLAGS_USAGE_ANY, startId); if (rc != WH_ERROR_OK) { return rc; } @@ -813,8 +1004,19 @@ static int doNvmList(whClientContext* client) if (rc != WH_ERROR_OK) { return rc; } + if (out_rc != WH_ERROR_OK) { + return out_rc; + } - return out_rc; + /* A local list must never surface a global id and vice versa */ + if (outCount > 0) { + int idIsGlobal = (outId & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0; + if (idIsGlobal != (listGlobal != 0)) { + return STRESS_ERR_NAMESPACE_LEAK; + } + } + + return WH_ERROR_OK; } static int doNvmDestroy(whClientContext* client, whNvmId id) @@ -1192,15 +1394,15 @@ static int doKeyExportDma(ClientServerPair* pair, whKeyId keyId) return rc; } -static int doNvmAddObjectDma(ClientServerPair* pair, whNvmId id, int iteration) +static int doNvmAddObjectDma(ClientServerPair* pair, whNvmId id, + uint8_t pattern) { whNvmMetadata meta; int32_t out_rc; int rc; - /* Fill DMA buffer with pattern */ - memset(pair->dmaNvmBuffer, (uint8_t)(iteration & 0xFF), - sizeof(pair->dmaNvmBuffer)); + /* Fill DMA buffer with this writer's pattern */ + memset(pair->dmaNvmBuffer, pattern, sizeof(pair->dmaNvmBuffer)); /* Set up metadata */ memset(&meta, 0, sizeof(meta)); @@ -1231,7 +1433,10 @@ static int doNvmAddObjectDma(ClientServerPair* pair, whNvmId id, int iteration) return out_rc; } -static int doNvmReadDma(ClientServerPair* pair, whNvmId id) +/* DMA read with optional content verification. The DMA response carries no + * length, so pass NVM_EXPECT_NONE for objects whose size can change (resize + * phases) since stale tail bytes can't be told apart from real data. */ +static int doNvmReadDma(ClientServerPair* pair, whNvmId id, int expect) { int32_t out_rc; int rc; @@ -1254,8 +1459,11 @@ static int doNvmReadDma(ClientServerPair* pair, whNvmId id) if (rc != WH_ERROR_OK) { return rc; } + if (out_rc != WH_ERROR_OK) { + return out_rc; + } - return out_rc; + return checkNvmData(pair->dmaNvmBuffer, sizeof(pair->dmaNvmBuffer), expect); } #endif /* WOLFHSM_CFG_DMA */ @@ -1264,12 +1472,47 @@ static int doNvmReadDma(ClientServerPair* pair, whNvmId id) * ========================================================================== */ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, - whKeyId keyId) + NamespaceVariant variant) { - whClientContext* client = &pair->client; + whClientContext* client = &pair->client; + int globalSide = isGlobalSide(pair, variant); + whKeyId keyId = selectKeyIdForPhase(phase, variant, globalSide); + whNvmId hotId = nvmIdFor(pair, variant, HOT_NVM_ID); + whNvmId counterId = nvmIdFor(pair, variant, HOT_COUNTER_ID); + uint8_t pattern = nvmPatternFor(pair, variant); + int provision; int rc; + /* Exactly one client provisions each shared (global-side) resource; + * every client provisions its own private resources. */ + switch (variant) { + case VARIANT_GLOBAL: + provision = (pair->clientId == 0); + break; + case VARIANT_LOCAL: + provision = 1; + break; + case VARIANT_MIXED: + default: + provision = (pair->clientId == 0) || !globalSide; + break; + } + if (!provision) { + return WH_ERROR_OK; + } + switch (phase) { + /* Counter phases: the owner creates the counter at 0. Global-side + * clients share one counter (GLOBAL flag), so their increments + * contend on the same object; private counters are per client. */ + case PHASE_COUNTER_CONCURRENT_INCREMENT: + case PHASE_COUNTER_INCREMENT_VS_READ: + (void)doCounterDestroy(client, counterId); + rc = doCounterInit(client, counterId, 0); + if (rc == WH_ERROR_NOSPACE) + rc = WH_ERROR_OK; + return rc; + /* Keystore phases that need clean state (evict first) */ case PHASE_KS_CONCURRENT_CACHE: rc = doKeyEvict(client, keyId); @@ -1290,7 +1533,7 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, /* NVM phases that need clean state (destroy first) */ case PHASE_NVM_CONCURRENT_ADD: - rc = doNvmDestroy(client, HOT_NVM_ID); + rc = doNvmDestroy(client, hotId); if (rc == WH_ERROR_NOTFOUND) rc = WH_ERROR_OK; return rc; @@ -1302,18 +1545,20 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, case PHASE_NVM_CONCURRENT_READ: case PHASE_NVM_CONCURRENT_DESTROY: /* First destroy any existing object to make space */ - (void)doNvmDestroy(client, HOT_NVM_ID); - return doNvmAddObject(client, HOT_NVM_ID, 0); + (void)doNvmDestroy(client, hotId); + return doNvmAddObject(client, hotId, pattern); /* List during modify needs multiple objects */ case PHASE_NVM_LIST_DURING_MODIFY: - rc = doNvmAddObject(client, HOT_NVM_ID, 0); + rc = doNvmAddObject(client, hotId, pattern); if (rc != WH_ERROR_OK && rc != WH_ERROR_NOSPACE) return rc; - rc = doNvmAddObject(client, HOT_NVM_ID_2, 0); + rc = doNvmAddObject(client, nvmIdFor(pair, variant, HOT_NVM_ID_2), + pattern); if (rc != WH_ERROR_OK && rc != WH_ERROR_NOSPACE) return rc; - rc = doNvmAddObject(client, HOT_NVM_ID_3, 0); + rc = doNvmAddObject(client, nvmIdFor(pair, variant, HOT_NVM_ID_3), + pattern); if (rc == WH_ERROR_NOSPACE) rc = WH_ERROR_OK; return rc; @@ -1324,7 +1569,7 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, rc = doKeyCache(client, keyId, 0); if (rc != WH_ERROR_OK) return rc; - rc = doNvmAddObject(client, HOT_NVM_ID, 0); + rc = doNvmAddObject(client, hotId, pattern); if (rc == WH_ERROR_NOSPACE) rc = WH_ERROR_OK; return rc; @@ -1341,7 +1586,7 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, if (rc != WH_ERROR_OK && rc != WH_ERROR_NOTFOUND) return rc; /* Add NVM object for the NVM modify operations */ - rc = doNvmAddObject(client, HOT_NVM_ID, 0); + rc = doNvmAddObject(client, hotId, pattern); if (rc == WH_ERROR_NOSPACE) rc = WH_ERROR_OK; return rc; @@ -1401,9 +1646,11 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, /* Add with reclaim: fill NVM with objects to trigger reclaim */ case PHASE_NVM_ADD_WITH_RECLAIM: { int i; - /* Add 10 objects to fill up NVM and trigger reclaim during test */ - for (i = 0; i < 10; i++) { - rc = doNvmAddObject(client, (whNvmId)(HOT_NVM_ID + i), 0); + /* Pre-fill NVM so reclaim triggers early during the test */ + for (i = 0; i < RECLAIM_SETUP_COUNT; i++) { + rc = doNvmAddObject( + client, nvmIdFor(pair, variant, (whNvmId)(HOT_NVM_ID + i)), + pattern); if (rc != WH_ERROR_OK && rc != WH_ERROR_NOSPACE) return rc; } @@ -1412,27 +1659,16 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, /* GetAvailable vs Add: add one object */ case PHASE_NVM_GETAVAILABLE_VS_ADD: - (void)doNvmDestroy(client, HOT_NVM_ID); - rc = doNvmAddObject(client, HOT_NVM_ID, 0); + (void)doNvmDestroy(client, hotId); + rc = doNvmAddObject(client, hotId, pattern); if (rc == WH_ERROR_NOSPACE) rc = WH_ERROR_OK; return rc; /* GetMetadata vs Destroy: destroy then add object */ case PHASE_NVM_GETMETADATA_VS_DESTROY: - (void)doNvmDestroy(client, HOT_NVM_ID); - rc = doNvmAddObject(client, HOT_NVM_ID, 0); - if (rc == WH_ERROR_NOSPACE) - rc = WH_ERROR_OK; - return rc; - - /* Counter phases - create counter with initial value 0 */ - case PHASE_COUNTER_CONCURRENT_INCREMENT: - case PHASE_COUNTER_INCREMENT_VS_READ: - /* Destroy any existing counter first */ - (void)doCounterDestroy(client, HOT_COUNTER_ID); - /* Initialize counter with value 0 */ - rc = doCounterInit(client, HOT_COUNTER_ID, 0); + (void)doNvmDestroy(client, hotId); + rc = doNvmAddObject(client, hotId, pattern); if (rc == WH_ERROR_NOSPACE) rc = WH_ERROR_OK; return rc; @@ -1441,9 +1677,9 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, case PHASE_NVM_READ_VS_RESIZE: case PHASE_NVM_CONCURRENT_RESIZE: /* Destroy any existing object first */ - (void)doNvmDestroy(client, HOT_NVM_ID); + (void)doNvmDestroy(client, hotId); /* Create object with initial size (64 bytes) */ - rc = doNvmAddObject(client, HOT_NVM_ID, 0); + rc = doNvmAddObject(client, hotId, pattern); if (rc == WH_ERROR_NOSPACE) rc = WH_ERROR_OK; return rc; @@ -1462,17 +1698,17 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, return rc; case PHASE_NVM_ADD_DMA_VS_READ: - (void)doNvmDestroy(client, HOT_NVM_ID); - return doNvmAddObject(client, HOT_NVM_ID, 0); + (void)doNvmDestroy(client, hotId); + return doNvmAddObject(client, hotId, pattern); case PHASE_NVM_READ_DMA_VS_DESTROY: - (void)doNvmDestroy(client, HOT_NVM_ID); - return doNvmAddObject(client, HOT_NVM_ID, 0); + (void)doNvmDestroy(client, hotId); + return doNvmAddObject(client, hotId, pattern); /* NVM Read DMA vs Resize */ case PHASE_NVM_READ_DMA_VS_RESIZE: - (void)doNvmDestroy(client, HOT_NVM_ID); - rc = doNvmAddObject(client, HOT_NVM_ID, 0); + (void)doNvmDestroy(client, hotId); + rc = doNvmAddObject(client, hotId, pattern); if (rc == WH_ERROR_NOSPACE) rc = WH_ERROR_OK; return rc; @@ -1483,14 +1719,165 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, } } +/* ============================================================================ + * PHASE CLEANUP + * ========================================================================== */ + +/* Best-effort teardown after each phase run so namespaces don't accumulate + * objects across the many phase/variant runs (a full object table would + * starve later phases). Return codes are intentionally ignored: revoked + * keys can't be erased and objects may already be gone. */ +static void doPhaseCleanup(ClientServerPair* pair, ContentionPhase phase, + NamespaceVariant variant) +{ + whClientContext* client = &pair->client; + int globalSide = isGlobalSide(pair, variant); + whKeyId keyId = selectKeyIdForPhase(phase, variant, globalSide); + whNvmId hotId = nvmIdFor(pair, variant, HOT_NVM_ID); + int owner; + int i; + + /* Same ownership rule as setup: one owner per shared resource, every + * client for its own */ + switch (variant) { + case VARIANT_GLOBAL: + owner = (pair->clientId == 0); + break; + case VARIANT_LOCAL: + owner = 1; + break; + case VARIANT_MIXED: + default: + owner = (pair->clientId == 0) || !globalSide; + break; + } + + switch (phase) { + /* The owner destroys the counter it provisioned */ + case PHASE_COUNTER_CONCURRENT_INCREMENT: + case PHASE_COUNTER_INCREMENT_VS_READ: + if (owner) { + (void)doCounterDestroy( + client, nvmIdFor(pair, variant, HOT_COUNTER_ID)); + } + return; + + /* Every client destroys the ids it added while streaming; the + * owner also removes the setup pre-fill */ + case PHASE_NVM_ADD_WITH_RECLAIM: + if (globalSide) { + for (i = 0; i < RECLAIM_IDS_PER_CLIENT; i++) { + (void)doNvmDestroy( + client, nvmIdFor(pair, variant, + (whNvmId)(RECLAIM_ID_BASE + + pair->clientId * + RECLAIM_IDS_PER_CLIENT + + i))); + } + } + else { + for (i = 0; i < RECLAIM_IDS_LOCAL; i++) { + (void)doNvmDestroy(client, (whNvmId)(RECLAIM_ID_BASE + i)); + } + } + if (owner) { + for (i = 0; i < RECLAIM_SETUP_COUNT; i++) { + (void)doNvmDestroy( + client, + nvmIdFor(pair, variant, (whNvmId)(HOT_NVM_ID + i))); + } + } + return; + + /* List phase uses three hot objects */ + case PHASE_NVM_LIST_DURING_MODIFY: + if (owner) { + (void)doNvmDestroy(client, hotId); + (void)doNvmDestroy(client, + nvmIdFor(pair, variant, HOT_NVM_ID_2)); + (void)doNvmDestroy(client, + nvmIdFor(pair, variant, HOT_NVM_ID_3)); + } + return; + + /* NVM phases: drop the hot object */ + case PHASE_NVM_CONCURRENT_ADD: + case PHASE_NVM_ADD_VS_READ: + case PHASE_NVM_ADD_VS_DESTROY: + case PHASE_NVM_READ_VS_DESTROY: + case PHASE_NVM_CONCURRENT_READ: + case PHASE_NVM_CONCURRENT_DESTROY: + case PHASE_NVM_GETAVAILABLE_VS_ADD: + case PHASE_NVM_GETMETADATA_VS_DESTROY: + case PHASE_NVM_READ_VS_RESIZE: + case PHASE_NVM_CONCURRENT_RESIZE: +#ifdef WOLFHSM_CFG_DMA + case PHASE_NVM_ADD_DMA_VS_READ: + case PHASE_NVM_READ_DMA_VS_DESTROY: + case PHASE_NVM_READ_DMA_VS_RESIZE: +#endif + if (owner) { + (void)doNvmDestroy(client, hotId); + } + return; + + /* Cross-subsystem: drop both the key and the hot object */ + case PHASE_CROSS_COMMIT_VS_ADD: + case PHASE_CROSS_COMMIT_VS_DESTROY: + case PHASE_CROSS_FRESHEN_VS_MODIFY: + if (owner) { + (void)doKeyEvict(client, keyId); + (void)doKeyErase(client, keyId); + (void)doNvmDestroy(client, hotId); + } + return; + + /* Keystore phases: evict the cache slot and erase any committed + * copy (fails harmlessly on revoked keys) */ + case PHASE_KS_CONCURRENT_CACHE: + case PHASE_KS_CACHE_VS_EVICT: + case PHASE_KS_CACHE_VS_EXPORT: + case PHASE_KS_EVICT_VS_EXPORT: + case PHASE_KS_CACHE_VS_COMMIT: + case PHASE_KS_COMMIT_VS_EVICT: + case PHASE_KS_CONCURRENT_EXPORT: + case PHASE_KS_CONCURRENT_EVICT: + case PHASE_KS_ERASE_VS_CACHE: + case PHASE_KS_ERASE_VS_EXPORT: + case PHASE_KS_REVOKE_VS_CACHE: + case PHASE_KS_REVOKE_VS_EXPORT: + case PHASE_KS_EXPLICIT_FRESHEN: +#ifdef WOLFHSM_CFG_DMA + case PHASE_KS_CACHE_DMA_VS_EXPORT: + case PHASE_KS_EXPORT_DMA_VS_EVICT: +#endif + if (owner) { + (void)doKeyEvict(client, keyId); + (void)doKeyErase(client, keyId); + } + return; + + /* GetUniqueId creates server-assigned ids we can't enumerate */ + case PHASE_KS_CONCURRENT_GETUNIQUEID: + default: + return; + } +} + /* ============================================================================ * PHASE OPERATION DISPATCH * ========================================================================== */ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, - ClientRole role, int iteration, whKeyId keyId) + ClientRole role, int iteration, whKeyId keyId, + NamespaceVariant variant) { - whClientContext* client = &pair->client; + whClientContext* client = &pair->client; + int globalSide = isGlobalSide(pair, variant); + whNvmId hotId = nvmIdFor(pair, variant, HOT_NVM_ID); + whNvmId counterId = nvmIdFor(pair, variant, HOT_COUNTER_ID); + uint8_t pattern = nvmPatternFor(pair, variant); + int readExpect = nvmReadExpectFor(pair, variant); switch (phase) { /* Keystore phases */ @@ -1535,56 +1922,58 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, /* NVM phases */ case PHASE_NVM_CONCURRENT_ADD: - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); case PHASE_NVM_ADD_VS_READ: if (role == ROLE_OP_A) - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); else - return doNvmRead(client, HOT_NVM_ID); + return doNvmRead(client, hotId, readExpect); case PHASE_NVM_ADD_VS_DESTROY: if (role == ROLE_OP_A) - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); case PHASE_NVM_READ_VS_DESTROY: if (role == ROLE_OP_A) - return doNvmRead(client, HOT_NVM_ID); + return doNvmRead(client, hotId, readExpect); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); case PHASE_NVM_CONCURRENT_READ: - return doNvmRead(client, HOT_NVM_ID); + return doNvmRead(client, hotId, readExpect); case PHASE_NVM_LIST_DURING_MODIFY: if (role == ROLE_OP_A) { /* Alternate between add and destroy */ if (iteration % 2 == 0) - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); } else { - return doNvmList(client); + /* Alternate between this client's namespace and the global + * one; both must only ever return their own ids */ + return doNvmList(client, iteration % 2); } case PHASE_NVM_CONCURRENT_DESTROY: - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); /* Cross-subsystem phases */ case PHASE_CROSS_COMMIT_VS_ADD: if (role == ROLE_OP_A) return doKeyCommit(client, keyId); else - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); case PHASE_CROSS_COMMIT_VS_DESTROY: if (role == ROLE_OP_A) return doKeyCommit(client, keyId); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); case PHASE_CROSS_FRESHEN_VS_MODIFY: if (role == ROLE_OP_A) { @@ -1594,9 +1983,9 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, else { /* Modify NVM while freshen might be happening */ if (iteration % 2 == 0) - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); } /* Erase vs Cache */ @@ -1636,50 +2025,60 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, case PHASE_KS_EXPLICIT_FRESHEN: return doKeyExport(client, keyId); - /* Add With Reclaim: all threads add unique objects to trigger reclaim - */ - case PHASE_NVM_ADD_WITH_RECLAIM: - /* Use client ID and iteration to create unique object IDs */ - return doNvmAddObject(client, - (whNvmId)(HOT_NVM_ID + 10 + - (pair->clientId * 1000) + - iteration), - iteration); + /* Add With Reclaim: all threads add objects from bounded id ranges. + * Client ids are limited to 1-255, so global-side clients cycle a + * disjoint per-client range in the shared namespace and local-side + * clients cycle a range in their own namespace. Re-adding an id + * retires the old copy as reclaimable, keeping compaction busy. */ + case PHASE_NVM_ADD_WITH_RECLAIM: { + whNvmId base; + if (globalSide) { + base = (whNvmId)(RECLAIM_ID_BASE + + pair->clientId * RECLAIM_IDS_PER_CLIENT + + (iteration % RECLAIM_IDS_PER_CLIENT)); + } + else { + base = (whNvmId)(RECLAIM_ID_BASE + + (iteration % RECLAIM_IDS_LOCAL)); + } + return doNvmAddObject(client, nvmIdFor(pair, variant, base), + pattern); + } /* GetAvailable vs Add */ case PHASE_NVM_GETAVAILABLE_VS_ADD: if (role == ROLE_OP_A) return doNvmGetAvailable(client); else - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); /* GetMetadata vs Destroy */ case PHASE_NVM_GETMETADATA_VS_DESTROY: if (role == ROLE_OP_A) - return doNvmGetMetadata(client, HOT_NVM_ID); + return doNvmGetMetadata(client, hotId); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); /* Counter Concurrent Increment */ case PHASE_COUNTER_CONCURRENT_INCREMENT: { uint32_t counter = 0; - return doCounterIncrement(client, HOT_COUNTER_ID, &counter); + return doCounterIncrement(client, counterId, &counter); } /* Counter Increment vs Read */ case PHASE_COUNTER_INCREMENT_VS_READ: { uint32_t counter = 0; if (role == ROLE_OP_A) - return doCounterIncrement(client, HOT_COUNTER_ID, &counter); + return doCounterIncrement(client, counterId, &counter); else - return doCounterRead(client, HOT_COUNTER_ID, &counter); + return doCounterRead(client, counterId, &counter); } /* NVM Read vs Resize - alternating object sizes */ case PHASE_NVM_READ_VS_RESIZE: if (role == ROLE_OP_A) { /* Read operation */ - return doNvmRead(client, HOT_NVM_ID); + return doNvmRead(client, hotId, readExpect); } else { /* Resize operation: destroy and re-add with different size @@ -1692,13 +2091,13 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, : (NVM_OBJECT_DATA_SIZE / 2); /* Destroy existing object */ - (void)doNvmDestroy(client, HOT_NVM_ID); + (void)doNvmDestroy(client, hotId); /* Re-add with new size */ - memset(data, (uint8_t)(iteration & 0xFF), newSize); + memset(data, pattern, newSize); rc = wh_Client_NvmAddObjectRequest( - client, HOT_NVM_ID, WH_NVM_ACCESS_ANY, - WH_NVM_FLAGS_USAGE_ANY, 0, NULL, newSize, data); + client, hotId, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_USAGE_ANY, 0, + NULL, newSize, data); if (rc != WH_ERROR_OK) { return rc; } @@ -1724,13 +2123,13 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, : (NVM_OBJECT_DATA_SIZE / 2); /* Destroy existing object */ - (void)doNvmDestroy(client, HOT_NVM_ID); + (void)doNvmDestroy(client, hotId); /* Re-add with new size */ - memset(data, (uint8_t)(iteration & 0xFF), newSize); - rc = wh_Client_NvmAddObjectRequest( - client, HOT_NVM_ID, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_USAGE_ANY, - 0, NULL, newSize, data); + memset(data, pattern, newSize); + rc = wh_Client_NvmAddObjectRequest(client, hotId, WH_NVM_ACCESS_ANY, + WH_NVM_FLAGS_USAGE_ANY, 0, NULL, + newSize, data); if (rc != WH_ERROR_OK) { return rc; } @@ -1763,47 +2162,42 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, /* DMA: Add DMA vs Read */ case PHASE_NVM_ADD_DMA_VS_READ: if (role == ROLE_OP_A) - return doNvmAddObjectDma(pair, HOT_NVM_ID, iteration); + return doNvmAddObjectDma(pair, hotId, pattern); else - return doNvmRead(client, HOT_NVM_ID); + return doNvmRead(client, hotId, readExpect); /* DMA: Read DMA vs Destroy */ case PHASE_NVM_READ_DMA_VS_DESTROY: if (role == ROLE_OP_A) - return doNvmReadDma(pair, HOT_NVM_ID); + return doNvmReadDma(pair, hotId, readExpect); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); /* NVM Read DMA vs Resize */ case PHASE_NVM_READ_DMA_VS_RESIZE: if (role == ROLE_OP_A) { - /* DMA Read operation */ - return doNvmReadDma(pair, HOT_NVM_ID); + /* DMA Read operation. No content check: the object's size + * changes under us and the DMA response carries no length, + * so stale tail bytes can't be told apart from data. */ + return doNvmReadDma(pair, hotId, NVM_EXPECT_NONE); } else { /* Resize operation: destroy and re-add with different size */ - int rc; - uint8_t data[NVM_OBJECT_DATA_SIZE]; - int32_t out_rc; - whNvmSize newSize = (iteration % 2 == 0) - ? NVM_OBJECT_DATA_SIZE - : (NVM_OBJECT_DATA_SIZE / 2); - whNvmMetadata meta; + int rc; + uint8_t data[NVM_OBJECT_DATA_SIZE]; + int32_t out_rc; + whNvmSize newSize = (iteration % 2 == 0) + ? NVM_OBJECT_DATA_SIZE + : (NVM_OBJECT_DATA_SIZE / 2); /* Destroy existing object */ - (void)doNvmDestroy(client, HOT_NVM_ID); + (void)doNvmDestroy(client, hotId); /* Re-add with new size */ - memset(data, (uint8_t)(iteration & 0xFF), newSize); - memset(&meta, 0, sizeof(meta)); - meta.id = HOT_NVM_ID; - meta.access = WH_NVM_ACCESS_ANY; - meta.flags = WH_NVM_FLAGS_USAGE_ANY; - meta.len = newSize; - + memset(data, pattern, newSize); rc = wh_Client_NvmAddObjectRequest( - client, HOT_NVM_ID, WH_NVM_ACCESS_ANY, - WH_NVM_FLAGS_USAGE_ANY, 0, NULL, newSize, data); + client, hotId, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_USAGE_ANY, 0, + NULL, newSize, data); if (rc != WH_ERROR_OK) { return rc; } @@ -1828,12 +2222,98 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, * RESULT VALIDATION * ========================================================================== */ -static int isAcceptableResult(ContentionPhase phase, int rc) +static int isAcceptableResult(ContentionPhase phase, ClientRole role, + int globalSide, int rc) { /* Always acceptable */ if (rc == WH_ERROR_OK) return 1; + /* Private-namespace (local-side) NVM expectations are strict: nobody + * else can touch this client's objects, so cross-client noise codes are + * real failures. What remains acceptable is self-inflicted (a client + * destroying its own object each iteration with nothing re-adding it) + * and NOSPACE (namespaces isolate ids, not capacity). Keystore and + * cross-subsystem phases keep the shared sets below: cache-slot + * dynamics make their self-inflicted effects role-independent. */ + if (!globalSide) { + switch (phase) { + case PHASE_NVM_CONCURRENT_ADD: + return (rc == WH_ERROR_NOSPACE); + + /* A adds its own object, B reads its own object */ + case PHASE_NVM_ADD_VS_READ: + return (role == ROLE_OP_A) && (rc == WH_ERROR_NOSPACE); + + /* B destroys its own object once, then sees NOTFOUND */ + case PHASE_NVM_ADD_VS_DESTROY: + return (role == ROLE_OP_A) ? (rc == WH_ERROR_NOSPACE) + : (rc == WH_ERROR_NOTFOUND); + + /* A's object is never destroyed by anyone: reads must succeed */ + case PHASE_NVM_READ_VS_DESTROY: + return (role == ROLE_OP_B) && (rc == WH_ERROR_NOTFOUND); + + case PHASE_NVM_CONCURRENT_READ: + return 0; + + /* A alternates add/destroy of its own object; B's lists must + * always succeed */ + case PHASE_NVM_LIST_DURING_MODIFY: + return (role == ROLE_OP_A) && + (rc == WH_ERROR_NOSPACE || rc == WH_ERROR_NOTFOUND); + + case PHASE_NVM_CONCURRENT_DESTROY: + return (rc == WH_ERROR_NOTFOUND); + + /* Readers are strict; resizers may see their own destroy fail + * after a NOSPACE'd re-add */ + case PHASE_NVM_READ_VS_RESIZE: + return (role == ROLE_OP_B) && + (rc == WH_ERROR_NOTFOUND || rc == WH_ERROR_NOSPACE); + + case PHASE_NVM_CONCURRENT_RESIZE: + return (rc == WH_ERROR_NOTFOUND || rc == WH_ERROR_NOSPACE); + + case PHASE_NVM_ADD_WITH_RECLAIM: + return (rc == WH_ERROR_NOSPACE || rc == WH_ERROR_ACCESS); + + case PHASE_NVM_GETAVAILABLE_VS_ADD: + return (role == ROLE_OP_B) && (rc == WH_ERROR_NOSPACE); + + /* A queries its own object, which nobody destroys */ + case PHASE_NVM_GETMETADATA_VS_DESTROY: + return (role == ROLE_OP_B) && (rc == WH_ERROR_NOTFOUND); + +#ifdef WOLFHSM_CFG_DMA + case PHASE_NVM_ADD_DMA_VS_READ: + return (role == ROLE_OP_A) && (rc == WH_ERROR_NOSPACE); + + case PHASE_NVM_READ_DMA_VS_DESTROY: + return (role == ROLE_OP_B) && (rc == WH_ERROR_NOTFOUND); + + case PHASE_NVM_READ_DMA_VS_RESIZE: + return (role == ROLE_OP_B) && + (rc == WH_ERROR_NOTFOUND || rc == WH_ERROR_NOSPACE); +#endif + + default: + break; /* keystore/cross/counter: shared sets below */ + } + } + + /* After setup the counter always exists (the owner created it), so only + * NOSPACE (shared capacity) is tolerated for increments and reads must + * succeed. */ + switch (phase) { + case PHASE_COUNTER_CONCURRENT_INCREMENT: + return (rc == WH_ERROR_NOSPACE); + case PHASE_COUNTER_INCREMENT_VS_READ: + return (role == ROLE_OP_A) && (rc == WH_ERROR_NOSPACE); + default: + break; + } + switch (phase) { /* Cache operations: NOSPACE acceptable (cache full) */ case PHASE_KS_CONCURRENT_CACHE: @@ -1901,13 +2381,6 @@ static int isAcceptableResult(ContentionPhase phase, int rc) case PHASE_NVM_GETMETADATA_VS_DESTROY: return (rc == WH_ERROR_NOTFOUND); - /* Counter phases - NOTFOUND marked acceptable to prevent test abort, - * but validation will catch it as a bug (counter < expectedMin). - * NOTFOUND shouldn't occur in CONCURRENT_INCREMENT (no destroys). */ - case PHASE_COUNTER_CONCURRENT_INCREMENT: - case PHASE_COUNTER_INCREMENT_VS_READ: - return (rc == WH_ERROR_NOTFOUND); - /* NVM Read vs Resize - NOTFOUND acceptable (object destroyed * during resize), NOSPACE acceptable (NVM full) */ case PHASE_NVM_READ_VS_RESIZE: @@ -1948,10 +2421,24 @@ static void* contentionClientThread(void* arg) StressTestContext* ctx = pair->sharedCtx; int rc; int localIteration; + ContentionPhase phase; + NamespaceVariant variant; + ClientRole role; + whKeyId keyId; + int globalSide; /* Wait for all threads to start */ pthread_barrier_wait(&ctx->startBarrier); + /* Register this client's id with its server. Without this the server + * treats every request as USER=0 (the shared global namespace) and + * per-client isolation is never engaged. */ + rc = doCommInit(pair); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("Client %d: CommInit failed: %d\n", pair->clientId, rc); + pair->commInitFailed = 1; + } + /* Always call barrier first, then check exit flag - prevents deadlock */ while (1) { /* ===== SETUP PHASE (once per phase) ===== */ @@ -1961,16 +2448,19 @@ static void* contentionClientThread(void* arg) pthread_barrier_wait(&ctx->setupCompleteBarrier); pthread_barrier_wait(&ctx->streamStartBarrier); pthread_barrier_wait(&ctx->streamEndBarrier); + pthread_barrier_wait(&ctx->cleanupStartBarrier); break; } - /* Only client 0 does setup */ - if (pair->clientId == 0) { - rc = doPhaseSetup(pair, ctx->currentPhase, ctx->currentKeyId); - if (rc != WH_ERROR_OK) { - WH_ERROR_PRINT("Setup failed for phase %d: %d\n", - ctx->currentPhase, rc); - } + phase = ctx->currentPhase; + variant = ctx->currentVariant; + + /* Every client runs setup; doPhaseSetup decides internally which + * resources this client is responsible for provisioning */ + rc = doPhaseSetup(pair, phase, variant); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("Client %d setup failed for phase %d (%s): %d\n", + pair->clientId, phase, variantName(variant), rc); } pthread_barrier_wait(&ctx->setupCompleteBarrier); @@ -1978,22 +2468,32 @@ static void* contentionClientThread(void* arg) /* ===== STREAMING PHASE (tight loop, no barriers) ===== */ pthread_barrier_wait(&ctx->streamStartBarrier); - ContentionPhase phase = ctx->currentPhase; - whKeyId keyId = ctx->currentKeyId; - ClientRole role = ctx->clientRoles[pair->clientId]; - localIteration = 0; + role = ctx->clientRoles[pair->clientId]; + globalSide = isGlobalSide(pair, variant); + keyId = selectKeyIdForPhase(phase, variant, globalSide); + localIteration = 0; /* Stream requests until phaseRunning becomes 0 */ while (ATOMIC_LOAD_INT(&ctx->phaseRunning)) { - rc = - executePhaseOperation(pair, phase, role, localIteration, keyId); + if (pair->commInitFailed) { + /* Poison the phase: without a registered client id the + * namespace machinery under test is not engaged */ + rc = WH_ERROR_ABORTED; + } + else { + rc = executePhaseOperation(pair, phase, role, localIteration, + keyId, variant); + } /* Count iteration */ localIteration++; ATOMIC_ADD_INT(&pair->iterationCount, 1); - /* Track unexpected errors */ - if (!isAcceptableResult(phase, rc)) { + /* Track successes (counter validation) and unexpected errors */ + if (rc == WH_ERROR_OK) { + ATOMIC_ADD_INT(&pair->successCount, 1); + } + else if (!isAcceptableResult(phase, role, globalSide, rc)) { ATOMIC_ADD_INT(&pair->errorCount, 1); } @@ -2002,6 +2502,11 @@ static void* contentionClientThread(void* arg) /* Wait for all clients to finish streaming */ pthread_barrier_wait(&ctx->streamEndBarrier); + + /* Main validates results between these barriers using our client + * context, so hold off cleanup until it signals completion */ + pthread_barrier_wait(&ctx->cleanupStartBarrier); + doPhaseCleanup(pair, phase, variant); } return NULL; @@ -2022,51 +2527,64 @@ static int allClientsReachedIterations(StressTestContext* ctx, int target) return 1; } -/* Post-phase validation for applicable tests - * Returns WH_ERROR_OK if validation passes, error code otherwise */ -static int validatePhaseResult(StressTestContext* ctx, ContentionPhase phase, - int totalIterations, int totalErrors) +/* Post-phase validation for applicable tests. Runs on the main thread while + * all clients are parked at cleanupStartBarrier, so it may safely use their + * client contexts. Returns WH_ERROR_OK if validation passes. */ +static int validatePhaseResult(StressTestContext* ctx, ContentionPhase phase) { - int rc; - switch (phase) { - case PHASE_COUNTER_CONCURRENT_INCREMENT: { - /* Validate counter value matches expected increments */ - uint32_t counter = 0; - - /* Read final counter value using client 0 */ - rc = doCounterRead(&ctx->pairs[0].client, HOT_COUNTER_ID, &counter); - if (rc != WH_ERROR_OK) { - WH_ERROR_PRINT( - " VALIDATION FAILED: Counter read failed: %d\n", rc); - return WH_ERROR_ABORTED; + case PHASE_COUNTER_CONCURRENT_INCREMENT: + case PHASE_COUNTER_INCREMENT_VS_READ: { + /* Global-side clients share one counter, so its value must equal + * the sum of their successful increments: less means a lost + * update (a missing NVM lock), more means a phantom increment. + * A private counter must exactly match its owner's count. */ + NamespaceVariant variant = ctx->currentVariant; + int failed = 0; + int expected = 0; + int sharedTotal = 0; + int i; + + for (i = 0; i < NUM_CLIENTS; i++) { + if (isGlobalSide(&ctx->pairs[i], variant) && + (phase == PHASE_COUNTER_CONCURRENT_INCREMENT || + ctx->clientRoles[i] == ROLE_OP_A)) { + sharedTotal += ATOMIC_LOAD_INT(&ctx->pairs[i].successCount); + } } - /* Calculate expected value: iterations per client × number of - * incrementing clients Each incrementing client did - * config->iterations increments - * Account for errors: totalIterations counts all attempts, - * but totalErrors counts unacceptable failures that didn't - * increment */ - uint32_t expectedMin = totalIterations - totalErrors; - - WH_TEST_PRINT(" Counter validation: value=%u, expected_min=%u " - "(iters=%d, errors=%d)\n", - counter, expectedMin, totalIterations, totalErrors); - - /* Counter must equal expectedMin. If counter < expectedMin, this - * indicates either: - * 1. Lost increments due to locking bug (race condition) - * 2. NOTFOUND occurred (shouldn't happen - no concurrent destroys) - */ - if (counter < expectedMin) { - WH_ERROR_PRINT(" VALIDATION FAILED: Counter value %u < " - "expected min %u\n", - counter, expectedMin); - return WH_ERROR_ABORTED; + for (i = 0; i < NUM_CLIENTS; i++) { + uint32_t counter = 0; + int rc; + + /* Reader roles never increment their counter */ + if (phase == PHASE_COUNTER_INCREMENT_VS_READ && + ctx->clientRoles[i] != ROLE_OP_A) { + continue; + } + + rc = doCounterRead(&ctx->pairs[i].client, + nvmIdFor(&ctx->pairs[i], variant, + HOT_COUNTER_ID), + &counter); + expected = isGlobalSide(&ctx->pairs[i], variant) + ? sharedTotal + : ATOMIC_LOAD_INT(&ctx->pairs[i].successCount); + + if (rc != WH_ERROR_OK || counter != (uint32_t)expected) { + WH_ERROR_PRINT(" VALIDATION FAILED: client %d counter " + "value=%u expected=%d (read rc=%d)\n", + i, counter, expected, rc); + failed = 1; + } + else { + WH_TEST_PRINT(" Counter validation: client %d " + "value=%u expected=%d\n", + i, counter, expected); + } } - return WH_ERROR_OK; + return failed ? WH_ERROR_ABORTED : WH_ERROR_OK; } /* Other phases don't need special validation yet */ @@ -2075,47 +2593,54 @@ static int validatePhaseResult(StressTestContext* ctx, ContentionPhase phase, } } -/* Select the appropriate keyId for a phase. - * Revoke-related phases need unique key IDs because revoked keys can't be - * erased or re-cached. Each phase type that might leave a key revoked needs - * its own key ID to avoid conflicts. - */ -static whKeyId selectKeyIdForPhase(ContentionPhase phase, int isGlobal) +/* Select the appropriate keyId for a phase, based on which namespace side + * this client is on. Revoke-related phases need unique key IDs per variant + * because revoked keys are permanently NONMODIFIABLE and can't be erased or + * re-cached by a later run. */ +static whKeyId selectKeyIdForPhase(ContentionPhase phase, + NamespaceVariant variant, int globalSide) { switch (phase) { case PHASE_KS_REVOKE_VS_CACHE: - return isGlobal ? REVOKE_CACHE_KEY_GLOBAL : REVOKE_CACHE_KEY_LOCAL; + if (variant == VARIANT_MIXED) { + return globalSide ? REVOKE_CACHE_KEY_MIXED_GLOBAL + : REVOKE_CACHE_KEY_MIXED_LOCAL; + } + return globalSide ? REVOKE_CACHE_KEY_GLOBAL + : REVOKE_CACHE_KEY_LOCAL; case PHASE_KS_REVOKE_VS_EXPORT: - return isGlobal ? REVOKE_EXPORT_KEY_GLOBAL - : REVOKE_EXPORT_KEY_LOCAL; + if (variant == VARIANT_MIXED) { + return globalSide ? REVOKE_EXPORT_KEY_MIXED_GLOBAL + : REVOKE_EXPORT_KEY_MIXED_LOCAL; + } + return globalSide ? REVOKE_EXPORT_KEY_GLOBAL + : REVOKE_EXPORT_KEY_LOCAL; /* Freshen uses HOT_KEY_ID (not unique IDs) so it can reuse keys that * are already committed by earlier phases - avoids NVM space issues */ default: - return isGlobal ? HOT_KEY_ID_GLOBAL : HOT_KEY_ID_LOCAL; + return globalSide ? HOT_KEY_ID_GLOBAL : HOT_KEY_ID_LOCAL; } } static int runPhase(StressTestContext* ctx, const PhaseConfig* config, - whKeyId keyId) + NamespaceVariant variant) { int i; int rc; #ifdef WOLFHSM_CFG_TEST_STRESS_PHASE_TIMEOUT_SEC time_t phaseStart; #endif - int totalIterations = 0; - int totalErrors = 0; - int timedOut = 0; - const char* keyScope = - (keyId & WH_KEYID_CLIENT_GLOBAL_FLAG) ? "global" : "local"; + int totalIterations = 0; + int totalErrors = 0; + int timedOut = 0; - WH_TEST_PRINT(" Phase: %s (%s key)\n", config->name, keyScope); + WH_TEST_PRINT(" Phase: %s (%s)\n", config->name, variantName(variant)); /* 1. Set phase info for all clients */ - ctx->currentPhase = config->phase; - ctx->currentKeyId = keyId; + ctx->currentPhase = config->phase; + ctx->currentVariant = variant; for (i = 0; i < NUM_CLIENTS; i++) { - ctx->clientRoles[i] = config->roles[i]; + ctx->clientRoles[i] = roleFor(config, variant, i); } /* 2. Signal clients to run setup */ @@ -2128,6 +2653,7 @@ static int runPhase(StressTestContext* ctx, const PhaseConfig* config, for (i = 0; i < NUM_CLIENTS; i++) { ATOMIC_STORE_INT(&ctx->pairs[i].iterationCount, 0); ATOMIC_STORE_INT(&ctx->pairs[i].errorCount, 0); + ATOMIC_STORE_INT(&ctx->pairs[i].successCount, 0); } /* 5. Signal clients to start streaming */ @@ -2170,8 +2696,14 @@ static int runPhase(StressTestContext* ctx, const PhaseConfig* config, WH_TEST_PRINT(" Total: %d iterations, %d errors\n", totalIterations, totalErrors); - /* 10. Run phase-specific validation */ - rc = validatePhaseResult(ctx, config->phase, totalIterations, totalErrors); + /* 10. Run phase-specific validation. Clients are parked at + * cleanupStartBarrier here, so their contexts are safe to use. */ + rc = validatePhaseResult(ctx, config->phase); + + /* 11. Release clients to run per-phase cleanup. They finish before the + * next phase's setup barrier. */ + pthread_barrier_wait(&ctx->cleanupStartBarrier); + if (rc != WH_ERROR_OK) { return rc; } @@ -2209,6 +2741,10 @@ int whTest_ThreadSafeStress(void) int testResult = 0; int phasesFailed = 0; size_t phaseIdx; + int variantIdx; + + static const NamespaceVariant variants[VARIANT_COUNT] = { + VARIANT_GLOBAL, VARIANT_LOCAL, VARIANT_MIXED}; memset(&ctx, 0, sizeof(ctx)); @@ -2216,7 +2752,7 @@ int whTest_ThreadSafeStress(void) WH_TEST_PRINT("Clients: %d, Phases: %zu, Iterations per phase: %d\n", NUM_CLIENTS, sizeof(phases) / sizeof(phases[0]), PHASE_ITERATIONS); - WH_TEST_PRINT("Key scopes: global, local\n"); + WH_TEST_PRINT("Namespace variants: global, local, mixed\n"); /* Initialize wolfCrypt */ rc = wolfCrypt_Init(); @@ -2282,6 +2818,13 @@ int whTest_ThreadSafeStress(void) goto cleanup; } + rc = pthread_barrier_init(&ctx.cleanupStartBarrier, NULL, NUM_CLIENTS + 1); + if (rc != 0) { + WH_ERROR_PRINT("Failed to init cleanupStart barrier: %d\n", rc); + testResult = rc; + goto cleanup; + } + WH_TEST_PRINT("Starting %d server threads and %d client threads...\n", NUM_CLIENTS, NUM_CLIENTS); @@ -2313,30 +2856,20 @@ int whTest_ThreadSafeStress(void) pthread_barrier_wait(&ctx.startBarrier); WH_TEST_PRINT("All threads started, running phases...\n\n"); - /* Run all phases */ + /* Run all phases, each once per namespace variant */ for (phaseIdx = 0; phaseIdx < sizeof(phases) / sizeof(phases[0]); phaseIdx++) { - whKeyId globalKey = selectKeyIdForPhase(phases[phaseIdx].phase, 1); - whKeyId localKey = selectKeyIdForPhase(phases[phaseIdx].phase, 0); - - rc = runPhase(&ctx, &phases[phaseIdx], globalKey); - if (rc != WH_ERROR_OK) { - WH_ERROR_PRINT("Phase %zu (global) failed: %d\n", phaseIdx, rc); - phasesFailed++; - if (testResult == 0) { - testResult = rc; /* Record first error */ - } - /* Continue to next phase - don't break */ - } - - rc = runPhase(&ctx, &phases[phaseIdx], localKey); - if (rc != WH_ERROR_OK) { - WH_ERROR_PRINT("Phase %zu (local) failed: %d\n", phaseIdx, rc); - phasesFailed++; - if (testResult == 0) { - testResult = rc; /* Record first error */ + for (variantIdx = 0; variantIdx < VARIANT_COUNT; variantIdx++) { + rc = runPhase(&ctx, &phases[phaseIdx], variants[variantIdx]); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("Phase %zu (%s) failed: %d\n", phaseIdx, + variantName(variants[variantIdx]), rc); + phasesFailed++; + if (testResult == 0) { + testResult = rc; /* Record first error */ + } + /* Continue to next phase - don't break */ } - /* Continue to next phase - don't break */ } } @@ -2350,6 +2883,7 @@ int whTest_ThreadSafeStress(void) ATOMIC_STORE_INT(&ctx.phaseRunning, 0); pthread_barrier_wait(&ctx.streamStartBarrier); pthread_barrier_wait(&ctx.streamEndBarrier); + pthread_barrier_wait(&ctx.cleanupStartBarrier); join_threads: /* Signal stop for servers */ @@ -2379,6 +2913,7 @@ int whTest_ThreadSafeStress(void) pthread_barrier_destroy(&ctx.setupCompleteBarrier); pthread_barrier_destroy(&ctx.streamStartBarrier); pthread_barrier_destroy(&ctx.streamEndBarrier); + pthread_barrier_destroy(&ctx.cleanupStartBarrier); cleanup: /* Cleanup client-server pairs */ diff --git a/test/wh_test_she.c b/test/wh_test_she.c index 5ea176a4e..25655f10d 100644 --- a/test/wh_test_she.c +++ b/test/wh_test_she.c @@ -107,17 +107,7 @@ enum { * destroy key API since SHE keys are supposed to be fixed hardware keys */ static int _destroySheKey(whClientContext* client, whNvmId clientSheKeyId) { - int rc = 0; - int32_t serverRc = 0; - - whNvmId id = WH_SHE_MAKE_KEYID(client->comm->client_id, clientSheKeyId); - - rc = wh_Client_NvmDestroyObjects(client, 1, &id, &serverRc); - if (rc == WH_ERROR_OK) { - rc = serverRc; - } - - return rc; + return wh_Client_SheDestroyKey(client, clientSheKeyId); } int whTest_SheClientConfig(whClientConfig* config) @@ -255,12 +245,15 @@ int whTest_SheClientConfig(whClientConfig* config) goto exit; } /* store cmac key */ - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, key, sizeof(key))) != 0) { + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, + key, sizeof(key))) != 0) { WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); goto exit; } /* store cmac digest */ - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, bootMacDigest, sizeof(bootMacDigest))) != 0) { + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, 0, + bootMacDigest, + sizeof(bootMacDigest))) != 0) { WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); goto exit; } @@ -289,12 +282,14 @@ int whTest_SheClientConfig(whClientConfig* config) } WH_TEST_PRINT("SHE secure boot SUCCESS\n"); /* load the secret key using pre program */ - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_SECRET_KEY_ID, 0, secretKey, sizeof(secretKey))) != 0) { + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_SECRET_KEY_ID, 0, 0, + secretKey, sizeof(secretKey))) != 0) { WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); goto exit; } /* load the prng seed using pre program */ - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_PRNG_SEED_ID, 0, prngSeed, sizeof(prngSeed))) != 0) { + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_PRNG_SEED_ID, 0, 0, + prngSeed, sizeof(prngSeed))) != 0) { WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); goto exit; } @@ -423,9 +418,9 @@ int whTest_SheClientConfig(whClientConfig* config) * to the M2 layout overlap between flags and count). Then * re-load the slot with an all-zero UID; the server must * accept it because the stored flags contain WILDCARD. */ - if ((ret = wh_Client_ShePreProgramKey(client, - SHE_WILDCARD_KEY_ID, WH_SHE_FLAG_WILDCARD, vectorRawKey, - sizeof(vectorRawKey))) != 0) { + if ((ret = wh_Client_ShePreProgramKey( + client, SHE_WILDCARD_KEY_ID, 0, WH_SHE_FLAG_WILDCARD, + vectorRawKey, sizeof(vectorRawKey))) != 0) { WH_ERROR_PRINT("Failed to preload wildcard key %d\n", ret); goto exit; } @@ -541,9 +536,7 @@ int whTest_SheClientConfig(whClientConfig* config) uint8_t ecbIn[WH_SHE_KEY_SZ]; uint8_t ecbOut[WH_SHE_KEY_SZ]; uint8_t ecbBack[WH_SHE_KEY_SZ]; - uint16_t outId = 0; - int32_t serverRc = 0; - uint8_t ctrLabel[WH_NVM_LABEL_LEN]; + uint16_t outId = 0; /* Wrap-export the cached RAM key (slot 14) by id; the blob must keep * TYPE=SHE and be the expected size. */ @@ -694,13 +687,8 @@ int whTest_SheClientConfig(whClientConfig* config) /* Counter guard on the SHE unwrap-and-cache path: seed an NVM SHE * slot with counter=5, then check a lower-counter prime is rejected * and an equal-counter prime is accepted. */ - wh_She_Meta2Label(5, 0, ctrLabel); - ret = wh_Client_NvmAddObject( - client, WH_SHE_MAKE_KEYID(client->comm->client_id, SHE_CTR_SLOT), 0, - 0, sizeof(ctrLabel), ctrLabel, sizeof(sheKey), sheKey, &serverRc); - if (ret == 0) { - ret = serverRc; - } + ret = wh_Client_ShePreProgramKey(client, SHE_CTR_SLOT, 5, 0, sheKey, + sizeof(sheKey)); if (ret != 0) { WH_ERROR_PRINT("SHE interop: seed counter slot failed %d\n", ret); goto exit; @@ -1030,12 +1018,12 @@ static int whTest_SheClientConfigBoundarySecureBoot(whClientConfig* config) goto exit_boundary; } - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, key, sizeof(key))) != 0) { WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); goto exit_boundary; } - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, 0, bootMacDigest, sizeof(bootMacDigest))) != 0) { WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); @@ -1189,16 +1177,16 @@ static int whTest_SheWriteProtect(whClientConfig* config) } /* pre-program boot MAC key and digest for secure boot */ - if ((ret = wh_Client_ShePreProgramKey( - client, WH_SHE_BOOT_MAC_KEY_ID, 0, - bootMacKey, sizeof(bootMacKey))) != 0) { + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, + bootMacKey, sizeof(bootMacKey))) != + 0) { WH_ERROR_PRINT( "Failed to pre-program boot MAC key %d\n", ret); goto exit_wp; } - if ((ret = wh_Client_ShePreProgramKey( - client, WH_SHE_BOOT_MAC, 0, - bootMacDigest, sizeof(bootMacDigest))) != 0) { + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, 0, + bootMacDigest, + sizeof(bootMacDigest))) != 0) { WH_ERROR_PRINT( "Failed to pre-program boot MAC digest %d\n", ret); @@ -1222,19 +1210,17 @@ static int whTest_SheWriteProtect(whClientConfig* config) } /* pre-program the secret key as auth key */ - if ((ret = wh_Client_ShePreProgramKey( - client, WH_SHE_SECRET_KEY_ID, 0, - secretKey, sizeof(secretKey))) != 0) { + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_SECRET_KEY_ID, 0, 0, + secretKey, sizeof(secretKey))) != 0) { WH_ERROR_PRINT( "Failed to pre-program secret key %d\n", ret); goto exit_wp; } /* pre-program the target key WITH write protect flag */ - if ((ret = wh_Client_ShePreProgramKey( - client, WP_TEST_KEY_ID, - WH_SHE_FLAG_WRITE_PROTECT, - rawKey, sizeof(rawKey))) != 0) { + if ((ret = wh_Client_ShePreProgramKey(client, WP_TEST_KEY_ID, 0, + WH_SHE_FLAG_WRITE_PROTECT, rawKey, + sizeof(rawKey))) != 0) { WH_ERROR_PRINT( "Failed to pre-program write-protected key %d\n", ret); @@ -1394,6 +1380,120 @@ static void* _whServerTask(void* cf) #if defined(WOLFHSM_CFG_TEST_POSIX) && defined(WOLFHSM_CFG_ENABLE_CLIENT) && \ defined(WOLFHSM_CFG_ENABLE_SERVER) +/* Every SHE request must be refused until COMM INIT binds a client id: the + * key-management verbs would otherwise target the USER=0 factory-provisioned + * namespace, and GET_ID would MAC under that namespace's MASTER_ECU_KEY. */ +static int whTest_ShePreInitKeyMgmtRejected(whClientConfig* config) +{ + int ret = 0; + whClientContext client[1] = {0}; + uint8_t key[WH_SHE_KEY_SZ] = {0}; + uint32_t outClientId = 0; + uint32_t outServerId = 0; + + if (config == NULL) { + return WH_ERROR_BADARGS; + } + + WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client, config)); + + /* Before COMM INIT the server must reject both actions */ + ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, key, + sizeof(key)); + if (ret != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("pre-init ShePreProgramKey: expected ACCESS, got %d\n", + ret); + ret = WH_ERROR_ABORTED; + goto exit_preinit; + } + ret = wh_Client_SheDestroyKey(client, WH_SHE_BOOT_MAC_KEY_ID); + if (ret != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("pre-init SheDestroyKey: expected ACCESS, got %d\n", + ret); + ret = WH_ERROR_ABORTED; + goto exit_preinit; + } + + /* The refusal is made in the dispatcher for every non-COMM group, so it + * also covers SET_UID and GET_ID, which carry no id of their own. Without + * it GET_ID would succeed here under the all-zero USER=0 key. The reply + * keeps the action's own layout, so the client sees a SHE error code. */ + { + uint8_t uid[WH_SHE_UID_SZ] = {0}; + uint8_t challenge[WH_SHE_KEY_SZ] = {0}; + uint8_t outUid[WH_SHE_UID_SZ] = {0}; + uint8_t sreg = 0; + uint8_t mac[WH_SHE_KEY_SZ] = {0}; + + ret = wh_Client_SheSetUid(client, uid, sizeof(uid)); + if (ret != WH_SHE_ERC_GENERAL_ERROR) { + WH_ERROR_PRINT( + "pre-init SheSetUid: expected GENERAL_ERROR, got %d\n", ret); + ret = WH_ERROR_ABORTED; + goto exit_preinit; + } + ret = wh_Client_SheGetId(client, challenge, sizeof(challenge), outUid, + &sreg, mac); + if (ret != WH_SHE_ERC_GENERAL_ERROR) { + WH_ERROR_PRINT( + "pre-init SheGetId: expected GENERAL_ERROR, got %d\n", ret); + ret = WH_ERROR_ABORTED; + goto exit_preinit; + } + } + + /* After COMM INIT all of them must succeed */ + ret = wh_Client_CommInit(client, &outClientId, &outServerId); + if (ret != 0) { + goto exit_preinit; + } + { + uint8_t uid[WH_SHE_UID_SZ] = {0}; + + ret = wh_Client_SheSetUid(client, uid, sizeof(uid)); + if (ret != 0) { + WH_ERROR_PRINT("post-init SheSetUid failed %d\n", ret); + goto exit_preinit; + } + } + ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, key, + sizeof(key)); + if (ret != 0) { + WH_ERROR_PRINT("post-init ShePreProgramKey failed %d\n", ret); + goto exit_preinit; + } + ret = wh_Client_SheDestroyKey(client, WH_SHE_BOOT_MAC_KEY_ID); + if (ret != 0) { + WH_ERROR_PRINT("post-init SheDestroyKey failed %d\n", ret); + goto exit_preinit; + } + { + uint8_t challenge[WH_SHE_KEY_SZ] = {0}; + uint8_t outUid[WH_SHE_UID_SZ] = {0}; + uint8_t sreg = 0; + uint8_t mac[WH_SHE_KEY_SZ] = {0}; + + ret = wh_Client_SheGetId(client, challenge, sizeof(challenge), outUid, + &sreg, mac); + if (ret != 0) { + WH_ERROR_PRINT("post-init SheGetId failed %d\n", ret); + } + } + +exit_preinit: + /* Tell server to close */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CommClose(client)); + + if (ret == 0) { + WH_TEST_RETURN_ON_FAIL(wh_Client_Cleanup(client)); + } + else { + wh_Client_Cleanup(client); + } + + return ret; +} + static void _whClientServerThreadTest(whClientConfig* c_conf, whServerConfig* s_conf, whTestSheClientFn clientFn) @@ -1708,6 +1808,10 @@ static int wh_She_TestReqSizeChecking(void) WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, s_conf)); WH_TEST_RETURN_ON_FAIL(wh_Server_SetConnected(server, WH_COMM_CONNECTED)); + /* Bind a client id the way COMM INIT would; the handlers are called + * directly here, so they see the id a real connection would carry. */ + server->comm->client_id = 1; + /* * Set SHE state so _ReportInvalidSheState allows requests through. * WH_SHE_SET_UID always passes the state gate, but most other handlers @@ -2379,6 +2483,11 @@ static int wh_She_TestGetId(void) WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, s_conf)); WH_TEST_RETURN_ON_FAIL(wh_Server_SetConnected(server, WH_COMM_CONNECTED)); + /* Bind a client id the way COMM INIT would; GET_ID's key lookup embeds + * it. (The dispatcher refuses requests from an unbound connection, but + * this test calls the handler directly.) */ + server->comm->client_id = 1; + /* UID is set (GET_ID returns it), but NO MASTER_ECU_KEY is ever loaded, so * the identity MAC must fall back to an all-zero key. */ server->she->uidSet = 1; @@ -2454,6 +2563,186 @@ static int wh_She_TestGetId(void) return 0; } + +/* LoadKey must refuse a slot whose NVM object is not exactly one SHE key, + * whether the slot is named as the auth key or as the target: reading it into + * the fixed key buffer fails, and an unchecked read would leave the metadata + * unset, so a zeroed label would clear the target's write protection. The + * slots are planted server-side, as a provisioning image would, because + * client NVM ids can no longer reach SHE slots. */ +static int wh_She_TestLoadKeyOversizedSlot(void) +{ + int ret = 0; + uint16_t resp_size = 0; + + uint8_t req_packet[WOLFHSM_CFG_COMM_DATA_LEN]; + uint8_t resp_packet[WOLFHSM_CFG_COMM_DATA_LEN]; + + uint8_t reqBuf[BUFFER_SIZE] = {0}; + uint8_t respBuf[BUFFER_SIZE] = {0}; + whTransportMemConfig tmcf[1] = {{ + .req = (whTransportMemCsr*)reqBuf, + .req_size = sizeof(reqBuf), + .resp = (whTransportMemCsr*)respBuf, + .resp_size = sizeof(respBuf), + }}; + whTransportServerCb tscb[1] = {WH_TRANSPORT_MEM_SERVER_CB}; + whTransportMemServerContext tmsc[1] = {0}; + whCommServerConfig cs_conf[1] = {{ + .transport_cb = tscb, + .transport_context = (void*)tmsc, + .transport_config = (void*)tmcf, + .server_id = 126, + }}; + + static uint8_t memory[FLASH_RAM_SIZE]; + whFlashRamsimCtx fc[1] = {0}; + whFlashRamsimCfg fc_conf[1] = {{0}}; + const whFlashCb fcb[1] = {WH_FLASH_RAMSIM_CB}; + + whNvmFlashConfig nf_conf[1] = {{ + .cb = fcb, + .context = fc, + .config = fc_conf, + }}; + whNvmFlashContext nfc[1] = {0}; + whNvmCb nfcb[1] = {WH_NVM_FLASH_CB}; + whNvmConfig n_conf[1] = {{ + .cb = nfcb, + .context = nfc, + .config = nf_conf, + }}; + whNvmContext nvm[1] = {{0}}; + + whServerCryptoContext crypto[1] = {0}; + whServerSheContext she[1]; + whServerContext server[1] = {0}; + + whServerConfig s_conf[1] = {{ + .comm_config = cs_conf, + .nvm = nvm, + .crypto = crypto, + .she = she, + .devId = INVALID_DEVID, + }}; + + const uint8_t SLOT_AUTH = 8; /* oversized, named as the auth key */ + const uint8_t SLOT_TARGET = 9; /* oversized, named as the target */ + const uint8_t SLOT_NEW = 7; /* empty target of the auth-key case */ + uint8_t uid[WH_SHE_UID_SZ] = {0}; + uint8_t secretKey[WH_SHE_KEY_SZ] = { + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, + 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c}; + uint8_t rawKey[WH_SHE_KEY_SZ] = { + 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, + 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00}; + uint8_t oversize[WH_SHE_KEY_SZ * 2]; + uint8_t m4[WH_SHE_M4_SZ]; + uint8_t m5[WH_SHE_M5_SZ]; + whNvmMetadata meta = {0}; + whNvmMetadata check = {0}; + uint32_t count = 0; + uint32_t flags = 0; + whNvmId secretId; + whNvmId authId; + whNvmId targetId; + whNvmId newId; + whMessageShe_LoadKeyRequest* req = + (whMessageShe_LoadKeyRequest*)req_packet; + whMessageShe_LoadKeyResponse* resp = + (whMessageShe_LoadKeyResponse*)resp_packet; + + memset(she, 0, sizeof(she)); + memset(memory, 0, sizeof(memory)); + memset(oversize, 0x5A, sizeof(oversize)); + + fc_conf->size = FLASH_RAM_SIZE; + fc_conf->sectorSize = FLASH_SECTOR_SIZE; + fc_conf->pageSize = FLASH_PAGE_SIZE; + fc_conf->erasedByte = ~(uint8_t)0; + fc_conf->memory = memory; + + WH_TEST_RETURN_ON_FAIL(wh_Nvm_Init(nvm, n_conf)); + WH_TEST_RETURN_ON_FAIL(wolfCrypt_Init()); + WH_TEST_RETURN_ON_FAIL(wc_InitRng_ex(crypto->rng, NULL, s_conf->devId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, s_conf)); + WH_TEST_RETURN_ON_FAIL(wh_Server_SetConnected(server, WH_COMM_CONNECTED)); + + /* Bind a client id the way COMM INIT would and open the state gate */ + server->comm->client_id = 1; + server->she->uidSet = 1; + server->she->sbState = TEST_SHE_SB_STATE_SUCCESS; + + secretId = WH_SHE_MAKE_KEYID(server->comm->client_id, WH_SHE_SECRET_KEY_ID); + authId = WH_SHE_MAKE_KEYID(server->comm->client_id, SLOT_AUTH); + targetId = WH_SHE_MAKE_KEYID(server->comm->client_id, SLOT_TARGET); + newId = WH_SHE_MAKE_KEYID(server->comm->client_id, SLOT_NEW); + + /* SECRET_KEY authorizes the target-slot case */ + meta.id = secretId; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = WH_SHE_KEY_SZ; + wh_She_Meta2Label(0, 0, meta.label); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, WH_SHE_KEY_SZ, secretKey)); + + /* An oversized object in a slot that will be named as the auth key */ + meta.id = authId; + meta.len = sizeof(oversize); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, sizeof(oversize), oversize)); + + /* An oversized, write-protected object in a slot named as the target */ + meta.id = targetId; + wh_She_Meta2Label(0, WH_SHE_FLAG_WRITE_PROTECT, meta.label); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, sizeof(oversize), oversize)); + + /* === Oversized auth key slot === */ + WH_TEST_RETURN_ON_FAIL(wh_She_GenerateLoadableKey( + SLOT_NEW, SLOT_AUTH, 1, 0, uid, rawKey, oversize, req->messageOne, + req->messageTwo, req->messageThree, m4, m5)); + memset(resp, 0, sizeof(*resp)); + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_LOAD_KEY, sizeof(*req), req_packet, + &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*resp)); + WH_TEST_ASSERT_RETURN(resp->rc == WH_SHE_ERC_KEY_INVALID); + /* Nothing was written to the target */ + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server->nvm, newId, &check) == + WH_ERROR_NOTFOUND); + WH_TEST_PRINT("SHE oversized auth key SUCCESS\n"); + + /* === Oversized target key slot === */ + WH_TEST_RETURN_ON_FAIL(wh_She_GenerateLoadableKey( + SLOT_TARGET, WH_SHE_SECRET_KEY_ID, 1, 0, uid, rawKey, secretKey, + req->messageOne, req->messageTwo, req->messageThree, m4, m5)); + memset(resp, 0, sizeof(*resp)); + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_LOAD_KEY, sizeof(*req), req_packet, + &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*resp)); + WH_TEST_ASSERT_RETURN(resp->rc == WH_SHE_ERC_KEY_INVALID); + /* The slot is untouched: still oversized and still write-protected */ + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server->nvm, targetId, &check) == + WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(check.len == sizeof(oversize)); + wh_She_Label2Meta(check.label, &count, &flags); + WH_TEST_ASSERT_RETURN((flags & WH_SHE_FLAG_WRITE_PROTECT) != 0); + WH_TEST_PRINT("SHE oversized target key SUCCESS\n"); + + WH_TEST_PRINT("SHE LoadKey oversized slot test SUCCESS\n"); + + wh_Server_Cleanup(server); + wh_Nvm_Cleanup(nvm); + wc_FreeRng(crypto->rng); + wolfCrypt_Cleanup(); + + return 0; +} #endif /* WOLFHSM_CFG_ENABLE_SERVER */ #if defined(WOLFHSM_CFG_TEST_POSIX) && defined(WOLFHSM_CFG_ENABLE_CLIENT) && \ @@ -2526,12 +2815,12 @@ static int _SheInteropSecureBoot(whClientContext* client) return ret; } - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, 0, bootMacKey, sizeof(bootMacKey))) != 0) { return ret; } - if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, digest, + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, 0, digest, sizeof(digest))) != 0) { return ret; } @@ -2595,8 +2884,8 @@ static int _SheInteropProvision(whClientConfig* config) /* Provision the secret key, then load the master ECU key (auth=secret) and * the target key (auth=master ECU) using offline-generated M1/M2/M3. */ - ret = wh_Client_ShePreProgramKey(client, WH_SHE_SECRET_KEY_ID, 0, secretKey, - sizeof(secretKey)); + ret = wh_Client_ShePreProgramKey(client, WH_SHE_SECRET_KEY_ID, 0, 0, + secretKey, sizeof(secretKey)); if (ret != 0) { goto exit; } @@ -2706,6 +2995,7 @@ static int _SheInteropRestore(whClientConfig* config) /* Drive the two sessions back-to-back. Each MemThreadTest call uses a fresh * server + NVM, modeling the power cycle between provision and restore. */ + static int wh_She_TestWrappedInterop(void) { int ret; @@ -2733,6 +3023,7 @@ int whTest_She(void) WH_TEST_RETURN_ON_FAIL(wh_She_TestStateGate()); WH_TEST_PRINT("Testing SHE: GET_ID empty-key / pre-secure-boot...\n"); WH_TEST_RETURN_ON_FAIL(wh_She_TestGetId()); + WH_TEST_RETURN_ON_FAIL(wh_She_TestLoadKeyOversizedSlot()); WH_TEST_PRINT("Testing SHE: (pthread) mem core flow...\n"); WH_TEST_RETURN_ON_FAIL( wh_ClientServer_MemThreadTest(whTest_SheClientConfig)); @@ -2742,6 +3033,9 @@ int whTest_She(void) WH_TEST_PRINT("Testing SHE: (pthread) mem write protect...\n"); WH_TEST_RETURN_ON_FAIL( wh_ClientServer_MemThreadTest(whTest_SheWriteProtect)); + WH_TEST_PRINT("Testing SHE: (pthread) mem pre-init key mgmt gate...\n"); + WH_TEST_RETURN_ON_FAIL( + wh_ClientServer_MemThreadTest(whTest_ShePreInitKeyMgmtRejected)); #if defined(WOLFHSM_CFG_KEYWRAP) && defined(HAVE_AESGCM) WH_TEST_PRINT("Testing SHE: (pthread) wrapped-key reboot interop...\n"); WH_TEST_RETURN_ON_FAIL(wh_She_TestWrappedInterop()); diff --git a/wolfhsm/wh_client.h b/wolfhsm/wh_client.h index e20475ec5..9e93dde2a 100644 --- a/wolfhsm/wh_client.h +++ b/wolfhsm/wh_client.h @@ -1582,7 +1582,17 @@ int wh_Client_DataUnwrapResponse(whClientContext* ctx, enum wc_CipherType cipherType, void* dataOut, uint32_t* dataSz); -/* Counter functions */ +/* Counter functions + * + * A counter id uses the client-facing key id encoding: bits 0-7 hold the + * counter number 1..255 (0 is invalid) and bit 8 + * (`WH_KEYID_CLIENT_GLOBAL_FLAG`, or `WH_CLIENT_KEYID_MAKE_GLOBAL()`) selects + * the shared global namespace when WOLFHSM_CFG_GLOBAL_KEYS is defined. A plain + * id is private to the calling client. Ids with any other bit set, or with + * the GLOBAL flag when global keys are disabled, are rejected with + * WH_ERROR_BADARGS. A global counter has no owner: any client that names it + * may initialize (rewind), increment, read, or destroy it, just as any client + * may use or erase a global key. */ int wh_Client_CounterInitRequest(whClientContext* c, whNvmId counterId, uint32_t counter); int wh_Client_CounterInitResponse(whClientContext* c, uint32_t* counter); @@ -1661,6 +1671,57 @@ int wh_Client_CounterDestroyResponse(whClientContext* c); */ int wh_Client_CounterDestroy(whClientContext* c, whNvmId counterId); +/** + * @section client_nvm_id_namespace Client NVM id semantics + * + * The `id` parameter passed to these NVM functions is a client-facing + * identifier in the same encoding used for keys (`wolfhsm/wh_keyid.h`): + * + * - Bits 0..7 (`WH_KEYID_MASK`) : numeric id, 1..255. Zero is + * reserved as the erased sentinel + * and is rejected by AddObject. + * - Bit 8 (`WH_KEYID_CLIENT_GLOBAL_FLAG`): when set, the request + * targets the shared global NVM + * namespace. When clear, it targets + * the calling client's own + * namespace. Requires + * WOLFHSM_CFG_GLOBAL_KEYS; without + * it AddObject rejects the flag and + * the other verbs ignore it. + * - Bit 9 (`WH_KEYID_CLIENT_WRAPPED_FLAG`): reserved; every NVM verb + * rejects ids with this flag set. + * - Bit 10 (`WH_KEYID_CLIENT_HW_FLAG`): reserved; every NVM verb + * rejects ids with this flag set. + * - Bits 11-15: must be zero; every NVM verb rejects ids with any of them + * set, since they cannot be + * translated. + * + * The server translates each request id into its TYPE/USER/ID internal + * encoding before it reaches the underlying NVM layer. This gives every + * client a private 1..255 namespace (plus a shared 1..255 global namespace + * when WOLFHSM_CFG_GLOBAL_KEYS is defined) and prevents the client NVM API + * from being used to reach keys, counters, SHE objects, or other clients' + * data. Without WOLFHSM_CFG_GLOBAL_KEYS, USER=0 objects such as + * factory-provisioned ones are unreachable through this API. + * + * The USER field is the client id bound by wh_Client_CommInit(), so the + * server refuses every request other than the COMM group until COMM INIT has + * completed; the refusal carries WH_ERROR_ACCESS in the refused action's own + * response layout. + * + * With WOLFHSM_CFG_GLOBAL_KEYS, `wh_Client_NvmList` honors the GLOBAL flag on + * `startId` as a namespace selector: pass 0 to iterate the calling client's + * own objects, or `WH_KEYID_CLIENT_GLOBAL_FLAG` to iterate the global + * namespace. Returned ids carry the appropriate flag so the caller can chain + * calls. Without it, the flag is ignored and List walks the caller's own + * namespace. + * + * Define `WOLFHSM_CFG_LEGACY_CLIENT_NVM` to disable translation for the NVM + * verbs only and fall back to the legacy global-flat 16-bit id space for + * them. Key, counter and certificate ids are translated regardless, and COMM + * INIT remains a prerequisite for every request. + */ + /** NVM functions */ /** * @brief Sends a non-volatile memory (NVM) initialization request to the @@ -2701,7 +2762,20 @@ int wh_Client_AuthUserSetCredentials( whClientContext* c, whUserId user_id, whAuthMethod method, const void* current_credentials, uint16_t current_credentials_len, const void* new_credentials, uint16_t new_credentials_len, int32_t* out_rc); -/* Certificate functions */ +/* Certificate functions + * + * Every certificate id below (the trusted-root id of the add, erase and read + * verbs and the root ids passed to the verify verbs) uses the client-facing + * key id encoding: bits 0-7 name a root 1..255 in the calling client's own + * trust store, and bit 8 (`WH_KEYID_CLIENT_GLOBAL_FLAG`, or + * `WH_CLIENT_KEYID_MAKE_GLOBAL()`) selects the shared global trust store when + * WOLFHSM_CFG_GLOBAL_KEYS is defined. The server stores roots as NVM objects + * of TYPE WH_KEYTYPE_CERT in that namespace, so a client cannot name a key, + * counter, or other object through these verbs. Ids with any other bit set + * are rejected with WH_ERROR_BADARGS, as is adding a root at id 0. Roots + * provisioned at build time or shared with server-internal users (such as the + * image manager) must be stored with that internal encoding; see the + * Trusted Root Storage section of the manual. */ /** * @brief Sends a request to initialize the certificate manager on the server. diff --git a/wolfhsm/wh_client_she.h b/wolfhsm/wh_client_she.h index b587f37d3..72291f7d8 100644 --- a/wolfhsm/wh_client_she.h +++ b/wolfhsm/wh_client_she.h @@ -69,13 +69,19 @@ /** SHE provisioning and identity functions */ +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT +/* Test/provisioning-only key management. These bypass the SHE M1-M5 + * authenticated key update and debug-authorization protocols and must NOT be + * exposed in production builds. Gated by WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT. + */ + /** * @brief Pre-programs a SHE key directly into NVM, bypassing the key update * protocol. * * This is a wolfHSM-specific provisioning helper that has no equivalent in the * AUTOSAR SHE command set. It writes @p key straight into the SHE NVM slot - * @p keyId with an update counter of zero, skipping the encrypted M1-M5 + * @p keyId with the update counter @p count, skipping the encrypted M1-M5 * CMD_LOAD_KEY protocol. It is intended for the initial provisioning of a * blank device (for example installing the MASTER_ECU_KEY or BOOT_MAC at * production) before any key-update authorization key exists; subsequent @@ -85,6 +91,8 @@ * * @param[in] c Pointer to the client context. * @param[in] keyId SHE key slot to write (0-15, e.g. WH_SHE_MASTER_ECU_KEY_ID). + * @param[in] count Initial SHE key counter to store with the key (0 for a + * factory-fresh slot). * @param[in] flags SHE key protection flags to store with the key * (WH_SHE_FLAG_WRITE_PROTECT, WH_SHE_FLAG_BOOT_PROTECT, etc.). * @param[in] key Pointer to the key material to store. @@ -95,7 +103,23 @@ * failure. */ int wh_Client_ShePreProgramKey(whClientContext* c, whNvmId keyId, - whNvmFlags flags, uint8_t* key, whNvmSize keySz); + uint32_t count, whNvmFlags flags, uint8_t* key, + whNvmSize keySz); + +/** + * @brief Destroys a pre-programmed SHE key (wolfHSM-specific). + * + * Removes the SHE key in slot @p keyId from the calling client's NVM + * namespace. Like wh_Client_ShePreProgramKey(), this is a provisioning helper + * with no AUTOSAR SHE equivalent, since the spec treats SHE keys as fixed + * hardware slots. + * + * @param[in] c Pointer to the client context. + * @param[in] keyId SHE key slot to destroy (0-15). + * @return int Returns 0 on success, or a negative error code on failure. + */ +int wh_Client_SheDestroyKey(whClientContext* c, whNvmId keyId); +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ /** * @brief Sends a request to set the ECU UID (wolfHSM-specific). diff --git a/wolfhsm/wh_keyid.h b/wolfhsm/wh_keyid.h index fe15742e4..524f2e2be 100644 --- a/wolfhsm/wh_keyid.h +++ b/wolfhsm/wh_keyid.h @@ -51,10 +51,9 @@ typedef uint16_t whKeyId; * namespace (WH_KEYUSER_GLOBAL), and larger values would be silently * truncated by WH_MAKE_KEYID, breaking per-client key isolation. * wh_Client_Init() rejects out-of-range ids (including 0) before any - * communication; the server rejects ids above the maximum at - * WH_MESSAGE_COMM_ACTION_INIT and, with WOLFHSM_CFG_GLOBAL_KEYS, also rejects - * 0. Derived from WH_KEYUSER_MASK so the bound stays in sync if the USER - * field is ever widened. */ + * communication, and the server rejects both 0 and ids above the maximum at + * WH_MESSAGE_COMM_ACTION_INIT. Derived from WH_KEYUSER_MASK so the bound stays + * in sync if the USER field is ever widened. */ #define WH_CLIENT_ID_MAX (WH_KEYUSER_MASK >> WH_KEYUSER_SHIFT) /* @@ -116,6 +115,7 @@ typedef uint16_t whKeyId; #define WH_KEYTYPE_COUNTER 0x3 /* Monotonic counter */ #define WH_KEYTYPE_WRAPPED 0x4 /* Wrapped key metadata */ #define WH_KEYTYPE_HW 0x5 /* HW-only key. Port-specific */ +#define WH_KEYTYPE_CERT 0x6 /* Trusted certificate object */ /* True when a key id carries no explicit identifier (ID field == 0) and so must * not be accepted as one - it would collide with the "assign me one" sentinel @@ -152,6 +152,57 @@ typedef uint16_t whKeyId; whKeyId wh_KeyId_TranslateFromClient(uint16_t type, uint16_t clientId, whKeyId reqId); +/** + * @brief Translate a client keyId for a fixed-type NVM-backed object. + * + * Like wh_KeyId_TranslateFromClient(), but the wrapped and hardware client + * flags do NOT override the type — they are stripped first — so the object + * always stays in the supplied TYPE namespace. The GLOBAL flag is still + * honored: the USER field is the connection's clientId by default, or + * WH_KEYUSER_GLOBAL when the client sets WH_KEYID_CLIENT_GLOBAL_FLAG (and + * WOLFHSM_CFG_GLOBAL_KEYS is enabled). Used by every subsystem whose objects + * have a fixed type: NVM objects, counters, and certificates. Keys use the + * plain translator, where the wrapped/hardware flags are meaningful sub-types. + * + * @param type Fixed object TYPE to stamp (e.g. WH_KEYTYPE_COUNTER) + * @param clientId Connection's client id for the USER field + * @param reqId Requested id from the client (GLOBAL flag honored; wrapped and + * hardware flags ignored) + * @return Server-internal keyId with the fixed TYPE, resolved USER, and ID. + */ +whKeyId wh_KeyId_TranslateObjectFromClient(uint16_t type, uint16_t clientId, + whKeyId reqId); + +/** + * @brief Check a client-supplied fixed-type object id before translating it. + * + * Shared by every fixed-type object API (NVM objects, counters, certificates) + * on every verb. Rejects bits above the id and client-flag fields, which + * translation would silently drop and so remap the request onto a different + * object, and the wrapped and hardware flags, which are not valid for these + * objects. The GLOBAL flag is allowed, as is an id portion of 0 (the NVM List + * start-from-beginning sentinel); creating verbs apply the stricter + * wh_KeyId_CheckClientObjectIdForCreate(). + * + * @param reqId Requested id from the client (may include flags) + * @return WH_ERROR_OK if the id is well formed, WH_ERROR_BADARGS otherwise. + */ +int wh_KeyId_CheckClientObjectId(whKeyId reqId); + +/** + * @brief Check a client-supplied id that creates a fixed-type object. + * + * Applies wh_KeyId_CheckClientObjectId() plus the creation rules: the id + * portion must be nonzero (0 is the erased sentinel and no object API + * auto-assigns ids), and without WOLFHSM_CFG_GLOBAL_KEYS the GLOBAL flag is + * rejected so the create fails loudly instead of silently landing in the + * caller's own namespace. + * + * @param reqId Requested id from the client (may include flags) + * @return WH_ERROR_OK if the id may be created, WH_ERROR_BADARGS otherwise. + */ +int wh_KeyId_CheckClientObjectIdForCreate(whKeyId reqId); + /** * @brief Translate server keyId to client keyId format (with flags) * diff --git a/wolfhsm/wh_message.h b/wolfhsm/wh_message.h index 1c9f7cba3..9518110f6 100644 --- a/wolfhsm/wh_message.h +++ b/wolfhsm/wh_message.h @@ -96,6 +96,8 @@ enum WH_SHE_ENUM { WH_SHE_GEN_MAC, WH_SHE_VERIFY_MAC, WH_SHE_GET_ID, + WH_SHE_PRE_PROGRAM_KEY, + WH_SHE_DESTROY_KEY, }; /* counter actions */ diff --git a/wolfhsm/wh_message_keystore.h b/wolfhsm/wh_message_keystore.h index b5702002c..cb5278b83 100644 --- a/wolfhsm/wh_message_keystore.h +++ b/wolfhsm/wh_message_keystore.h @@ -248,7 +248,9 @@ typedef struct { /* DMA address status structure */ typedef struct { /* If packet->rc == WH_ERROR_ACCESS, this field will contain the offending - * address/size pair. Invalid otherwise. */ + * address/size pair. Invalid otherwise. A request refused before any DMA + * access (before COMM INIT, or by authorization) also reports + * WH_ERROR_ACCESS but leaves this field zero. */ whMessageKeystore_DmaBuffer badAddr; } whMessageKeystore_DmaAddrStatus; diff --git a/wolfhsm/wh_message_she.h b/wolfhsm/wh_message_she.h index f8dc0ad5d..0a24eafe3 100644 --- a/wolfhsm/wh_message_she.h +++ b/wolfhsm/wh_message_she.h @@ -412,6 +412,61 @@ int wh_MessageShe_TranslateGetIdResponse( uint16_t magic, const whMessageShe_GetIdResponse* src, whMessageShe_GetIdResponse* dest); +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT +/* Pre-program Key Request. Persists a SHE-typed NVM entry under the calling + * client's USER namespace. Carried on the SHE message group so that NVM + * client-id translation does not need to special-case typed adds. + * + * NOTE: Bypasses the SHE M1-M5 authenticated key-update protocol. Not part of + * the SHE specification; gated behind WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT for + * test/provisioning use only. */ +typedef struct { + uint32_t keyId; + uint32_t count; + uint32_t flags; + uint32_t keySz; + /* Data follows: + * uint8_t key[keySz] + */ +} whMessageShe_PreProgramKeyRequest; + +typedef struct { + int32_t rc; + uint8_t WH_PAD[4]; +} whMessageShe_PreProgramKeyResponse; + +int wh_MessageShe_TranslatePreProgramKeyRequest( + uint16_t magic, const whMessageShe_PreProgramKeyRequest* src, + whMessageShe_PreProgramKeyRequest* dest); + +int wh_MessageShe_TranslatePreProgramKeyResponse( + uint16_t magic, const whMessageShe_PreProgramKeyResponse* src, + whMessageShe_PreProgramKeyResponse* dest); + +/* Destroy Key Request. Removes a SHE-typed NVM entry from the calling + * client's USER namespace. + * + * NOTE: Bypasses the SHE debug-authorization protocol. Test/provisioning use + * only; see WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT. */ +typedef struct { + uint32_t keyId; + uint8_t WH_PAD[4]; +} whMessageShe_DestroyKeyRequest; + +typedef struct { + int32_t rc; + uint8_t WH_PAD[4]; +} whMessageShe_DestroyKeyResponse; + +int wh_MessageShe_TranslateDestroyKeyRequest( + uint16_t magic, const whMessageShe_DestroyKeyRequest* src, + whMessageShe_DestroyKeyRequest* dest); + +int wh_MessageShe_TranslateDestroyKeyResponse( + uint16_t magic, const whMessageShe_DestroyKeyResponse* src, + whMessageShe_DestroyKeyResponse* dest); +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ + #endif /* WOLFHSM_CFG_SHE_EXTENSION */ #endif /* !WOLFHSM_WH_MESSAGE_SHE_H_ */ diff --git a/wolfhsm/wh_server_she.h b/wolfhsm/wh_server_she.h index d042f5595..b0dfe1e13 100644 --- a/wolfhsm/wh_server_she.h +++ b/wolfhsm/wh_server_she.h @@ -88,6 +88,25 @@ int wh_Server_HandleSheRequest(whServerContext* server, uint16_t magic, const void* req_packet, uint16_t* out_resp_size, void* resp_packet); +/** + * @brief Format an action-specific SHE error response. + * + * Used by the request dispatcher when a SHE request is refused before its + * handler runs (for example from a connection that has not completed COMM + * INIT), so the client receives the response layout it expects for that + * action rather than a bare error code. rc is a wolfHSM error code and is + * reported as the matching SHE error code; the test key-management actions + * report it unchanged. + * + * @param magic Request magic, used to byte-order the response. + * @param action SHE action the response is for. + * @param rc wolfHSM error code to report. + * @param resp_packet Response buffer to fill. + * @return Size of the formatted response, or 0 if the action is unknown. + */ +uint16_t wh_Server_SheFormatErrorResponse(uint16_t magic, uint16_t action, + int rc, void* resp_packet); + /** * @brief Register SHE UID storage callbacks at runtime. * diff --git a/wolfhsm/wh_settings.h b/wolfhsm/wh_settings.h index f9974d905..91646ed08 100644 --- a/wolfhsm/wh_settings.h +++ b/wolfhsm/wh_settings.h @@ -36,6 +36,12 @@ * WOLFHSM_CFG_SHE_EXTENSION - If defined, include AutoSAR SHE functionality * Default: Not defined * + * WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT - If defined, include the SHE + * pre-program/destroy key helpers, which write and remove SHE key slots + * directly, bypassing the M1-M5 update protocol. Test and provisioning + * builds only, never in production. Requires WOLFHSM_CFG_SHE_EXTENSION. + * Default: Not defined + * * WOLFHSM_CFG_GLOBAL_KEYS - If defined, enable global key support allowing * keys to be shared across multiple clients * Default: Not defined @@ -56,6 +62,16 @@ * WOLFHSM_CFG_KEYWRAP - If defined, include the key wrap functionality * Default: Not defined * + * WOLFHSM_CFG_LEGACY_CLIENT_NVM - If defined, the client-facing NVM message + * handlers pass ids to the NVM layer verbatim (the legacy flat id space + * shared by all clients). Applies to the NVM message group only: key, + * counter and certificate ids are always translated, and COMM INIT is + * always required first. When NOT defined (the default), every + * client-supplied NVM id is translated to the TYPE/USER/ID encoding so each + * client has its own private 1..255 namespace (plus a shared global one + * with WOLFHSM_CFG_GLOBAL_KEYS). + * Default: Not defined + * * WOLFHSM_CFG_KEYWRAP_MAX_KEY_SIZE - The maximum size (in bytes) of a key that * can be wrapped. Together with the request header it must fit within * WOLFHSM_CFG_COMM_DATA_LEN, which is checked at compile time