From 0b0695c06fa3f11f18082b5fd1e6a716c3bb55fe Mon Sep 17 00:00:00 2001 From: Brett Nicholas <7547222+bigbrett@users.noreply.github.com> Date: Wed, 13 May 2026 09:14:33 -0600 Subject: [PATCH 1/2] Major refactor of NVM object IDs making them per-client instead of global --- .../workflows/build-and-test-clientonly.yml | 10 +- .github/workflows/build-and-test-refactor.yml | 8 + .github/workflows/build-and-test-stress.yml | 2 +- .github/workflows/build-and-test.yml | 8 + docs/draft/auth.md | 2 +- docs/src/3-Quickstart.md | 2 +- docs/src/5-Features.md | 50 +- docs/src/6-Utilities.md | 2 + docs/src/8-Integration.md | 2 +- docs/src/9-Configuration.md | 2 + examples/posix/wh_posix_server/wolfhsm_cfg.h | 2 +- src/wh_client_she.c | 101 +- src/wh_keyid.c | 40 + src/wh_message_she.c | 51 +- src/wh_server.c | 183 ++- src/wh_server_cert.c | 114 +- src/wh_server_counter.c | 70 +- src/wh_server_nvm.c | 219 +++- src/wh_server_she.c | 464 +++++--- .../client-server/wh_test_crypto_lms.c | 2 +- .../client-server/wh_test_crypto_xmss.c | 2 +- test-refactor/client-server/wh_test_keywrap.c | 36 +- test-refactor/client-server/wh_test_she.c | 147 +-- test-refactor/config/wolfhsm_cfg.h | 8 +- test-refactor/misc/wh_test_multiclient.c | 1039 +++++++++++++++- test-refactor/misc/wh_test_she_keywrap.c | 138 ++- test-refactor/misc/wh_test_she_uid_cb.c | 3 + test-refactor/posix/Makefile | 10 + .../posix/wh_test_keygen_unique_id.c | 10 + test-refactor/posix/wh_test_keyread_race.c | 10 + test-refactor/server/wh_test_cert.c | 194 ++- .../server/wh_test_cert_readtrusted.c | 32 +- test-refactor/server/wh_test_she_server.c | 112 +- test-refactor/wh_test_list.c | 6 + test/Makefile | 10 + test/config/wolfhsm_cfg.h | 9 +- test/wh_test_cert.c | 226 +++- test/wh_test_clientserver.c | 8 +- test/wh_test_crypto.c | 8 +- test/wh_test_keywrap.c | 42 +- test/wh_test_multiclient.c | 1044 ++++++++++++++++- test/wh_test_posix_threadsafe_stress.c | 919 +++++++++++---- test/wh_test_she.c | 368 +++++- wolfhsm/wh_client.h | 47 +- wolfhsm/wh_client_she.h | 49 +- wolfhsm/wh_keyid.h | 53 +- wolfhsm/wh_message.h | 2 + wolfhsm/wh_message_keystore.h | 5 +- wolfhsm/wh_message_she.h | 49 + wolfhsm/wh_server_she.h | 17 + wolfhsm/wh_settings.h | 12 + 51 files changed, 5057 insertions(+), 892 deletions(-) diff --git a/.github/workflows/build-and-test-clientonly.yml b/.github/workflows/build-and-test-clientonly.yml index 22ee55ca9..d1d093c17 100644 --- a/.github/workflows/build-and-test-clientonly.yml +++ b/.github/workflows/build-and-test-clientonly.yml @@ -53,10 +53,11 @@ jobs: - 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 @@ -129,10 +130,11 @@ jobs: 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..4fbfc3bf2 100644 --- a/.github/workflows/build-and-test-refactor.yml +++ b/.github/workflows/build-and-test-refactor.yml @@ -140,6 +140,14 @@ jobs: if: matrix.group == 'wolfcrypt' run: cd test-refactor/posix && make clean && make -j SHE_GLOBAL=1 ASAN=1 WOLFSSL_DIR=../../wolfssl && make run + - 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 + + - 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..9bb15916f 100644 --- a/.github/workflows/build-and-test-stress.yml +++ b/.github/workflows/build-and-test-stress.yml @@ -22,7 +22,7 @@ jobs: if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} runs-on: ubuntu-latest - timeout-minutes: 10 + 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..87eebe568 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -128,6 +128,14 @@ jobs: if: matrix.group == 'wolfcrypt' run: cd test && make clean && make -j SHE_GLOBAL=1 ASAN=1 WOLFSSL_DIR=../wolfssl && make run + - 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 + + - 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..a2768ae6c 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 establishes the client ID used to namespace keys and objects. The server rejects other requests until COMM INIT completes. ### Processing Requests diff --git a/docs/src/5-Features.md b/docs/src/5-Features.md index c55183bda..d940fdb98 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 use the same object model through the `wh_Client_Nvm*` API, but their IDs are namespaced per client (see [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 + +Clients access the object store remotely through the `wh_Client_Nvm*` API. As with [key IDs](#key-cache-key-ids-and-nvm-backing-store), the server translates client-supplied NVM IDs into private per-client namespaces. + +This translation requires the client ID assigned during COMM INIT. The server rejects requests outside the COMM group with `WH_ERROR_ACCESS` until `wh_Client_CommInit()` completes. + +Client-facing NVM IDs use the same encoding as client-facing `whKeyId` values. + +- **Bits 0 to 7** contain the numeric object ID from 1 to 255. ID 0 is reserved as an erased sentinel and is rejected by `wh_Client_NvmAddObject`. +- **Bit 8** (`WH_KEYID_CLIENT_GLOBAL_FLAG`) selects the shared global namespace when `WOLFHSM_CFG_GLOBAL_KEYS` is defined. Without that define, `wh_Client_NvmAddObject` rejects IDs with this flag set, while other NVM functions ignore the flag and use the caller's private namespace. +- **Bits 9 and 10** represent wrapped and hardware key flags. These are invalid for NVM objects, and NVM functions reject IDs with these bits set. Bits above bit 10 are also rejected. + +On each request, the server expands the client ID to include TYPE = `WH_KEYTYPE_NVM` and USER set to the connection client ID (or 0 for global objects). Responses translate IDs back to the client format. + +- Each client has a 1 to 255 ID range. If two clients create an object with ID 5, they create separate objects that cannot be accessed across clients. +- Setting TYPE = `WH_KEYTYPE_NVM` prevents the client NVM API from accessing keys, counters, SHE slots, or server-internal storage. +- When `WOLFHSM_CFG_GLOBAL_KEYS` is defined, objects in the global namespace can be accessed by any client setting the global flag. Factory images from the [NVM provisioning tool](6-Utilities.md#nvm-provisioning-tool) place plain `obj` entries with IDs up to 255 into this namespace. Without this define, USER 0 objects cannot be reached through the client NVM API. + +When `WOLFHSM_CFG_GLOBAL_KEYS` is defined, `wh_Client_NvmList` uses the global flag on `startId` to select which namespace to enumerate. Pass 0 to list the client's own objects, or `WH_KEYID_CLIENT_GLOBAL_FLAG` to list global objects. Returned IDs include the flag so callers can pass them back as `startId`. Without `WOLFHSM_CFG_GLOBAL_KEYS`, the flag is ignored and the function lists the caller's private namespace. + +Access permissions and policy checks apply after translation. Server-local code using `wh_Nvm_*` directly continues to use internal IDs without translation. + +Defining `WOLFHSM_CFG_LEGACY_CLIENT_NVM` disables translation for the NVM group, restoring the legacy flat 16-bit ID space shared across all clients. Key, counter, and certificate IDs remain namespaced, and COMM INIT is still required before requests are accepted. 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 (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)) with TYPE `WH_KEYTYPE_CERT`. Each root is a DER-encoded X.509 certificate written into NVM with `whNvmMetadata` access bits, flags, and label so that standard access controls apply. + +Client-supplied certificate IDs use 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. An ID from 1 to 255 names a root in the client's own trust store. When `WOLFHSM_CFG_GLOBAL_KEYS` is defined, `WH_KEYID_CLIENT_GLOBAL_FLAG` selects the shared global trust store. The server expands IDs to TYPE `WH_KEYTYPE_CERT`, USER set to the client ID (or 0 for global), and the requested numeric ID. IDs with extra bits set, invalid flags (wrapped or hardware), or ID 0 on creation return `WH_ERROR_BADARGS`. `WOLFHSM_CFG_LEGACY_CLIENT_NVM` does not affect certificate IDs. + +Server-internal components like the [image manager](#image-manager) reference roots by full internal ID using `WH_MAKE_KEYID(WH_KEYTYPE_CERT, user, id)`. Roots provisioned at build time must use this format as well (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: +wolfHSM provides helper functions for features not covered by the AUTOSAR specification: + +- `wh_Client_SheSetUid`: programs the 15-byte ECU UID used by the key update protocol. AUTOSAR assumes this value is fixed in hardware. wolfHSM provides this software path and rejects most SHE operations until the UID is set. If the UID resides in hardware or NVM, the server can use [UID storage callbacks](#she-uid-storage), where `CMD_SET_UID` returns `WH_SHE_ERC_WRITE_PROTECTED` on read-only storage. +- `wh_Client_ShePreProgramKey`: writes a key, protection flags, and an initial counter directly into a SHE NVM slot without using the encrypted M1 to M5 protocol. This allows initial provisioning on a blank device. After `MASTER_ECU_KEY` is installed, subsequent updates use standard SHE protocols. +- `wh_Client_SheDestroyKey`: deletes a SHE key slot from the client's NVM namespace. -- `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. +Because `wh_Client_ShePreProgramKey` and `wh_Client_SheDestroyKey` bypass SHE authorization, they require `WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT` (see [Configuration](9-Configuration.md#cryptography-features)) and should only be used in provisioning or test builds. 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. @@ -814,7 +846,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. +In typical automotive deployments, the bootloader and `BOOT_MAC` are programmed into NVM during production using `whnvmtool` or `wh_Client_ShePreProgramKey`. The ECU UID is set on first boot with `wh_Client_SheSetUid`, secure boot runs on each reset via `wh_Client_SheSecureBoot`, runtime key updates use `CMD_LOAD_KEY`, and CAN message authentication uses `wh_Client_SheGenerateMac` and `wh_Client_SheVerifyMac` against pre-provisioned user-slot keys. ## Non-Volatile Monotonic Counters @@ -835,7 +867,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. +Counters use client-facing IDs where bits 0 to 7 specify the counter number from 1 to 255 (`WH_KEYID_ERASED`, 0, is invalid). When `WOLFHSM_CFG_GLOBAL_KEYS` is defined, bit 8 (`WH_KEYID_CLIENT_GLOBAL_FLAG`) selects the shared global namespace. The server encodes counters as `whKeyId` values with TYPE = `WH_KEYTYPE_COUNTER`, USER set to the client ID (or 0 for global), and the numeric ID. This isolates counters per client, and keeps counter 5 distinct from key 5. IDs with extra bits or key flags (wrapped or hardware) return `WH_ERROR_BADARGS`. When `WOLFHSM_CFG_GLOBAL_KEYS` is disabled, initialization with the global flag also returns `WH_ERROR_BADARGS`. + +Global counters are shared across all clients. Any client can initialize, increment, read, or destroy a global counter. Only use global counters when shared access is required. 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 edd2edaab..ac3b578d1 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, Intel HEX output details, and test workflow, see [`tools/whnvmtool/README.md`](https://github.com/wolfSSL/wolfHSM/blob/main/tools/whnvmtool/README.md). +Configuration files use server-internal IDs written directly into the image. Because the client NVM API uses per-client namespaces (see [Client NVM Access and Per-Client Namespaces](5-Features.md#client-nvm-access-and-per-client-namespaces)), an `obj` entry with an ID up to 255 resides in the global namespace, accessed by setting `WH_KEYID_CLIENT_GLOBAL_FLAG`. To provision an object for a specific client, encode the client ID into bits 8 to 11 of the object ID. Typed objects also require the type nibble in bits 12 to 15. For example, trusted root certificates use TYPE `WH_KEYTYPE_CERT` (`0x6`), where `obj 0x6005` provisions global root 5 and `obj 0x6105` provisions root 5 for client 1 (see [Trusted Root Storage](5-Features.md#trusted-root-storage)). Counters use TYPE `WH_KEYTYPE_COUNTER` (`0x3`). If `WOLFHSM_CFG_LEGACY_CLIENT_NVM` is enabled on the server, client NVM translation is disabled and clients use verbatim IDs. + ## Benchmark Suite ### Benchmark Suite Overview diff --git a/docs/src/8-Integration.md b/docs/src/8-Integration.md index 87f5b9715..6f5febece 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. Both the RSA-4096 and the ECC P-256 wolfBoot verify methods use this type. - `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 resides in the certificate namespace (TYPE = `WH_KEYTYPE_CERT`) referenced by both the client certificate API and the image manager `sigNvmId` (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..ca5ef9474 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 | Enables SHE test key-management functions (`wh_Client_ShePreProgramKey` and `wh_Client_SheDestroyKey`), which bypass M1 to M5 key updates. For provisioning and testing only. Do not enable in production. Requires `WOLFHSM_CFG_SHE_EXTENSION`. | | `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 | When defined, the server passes client NVM IDs directly to the NVM layer, restoring the flat 16-bit ID space for `wh_Client_Nvm*` requests. Key, counter, and certificate IDs remain namespaced, and COMM INIT is still required. When undefined, NVM IDs are translated into per-client namespaces (1 to 255) with optional global access via `WH_KEYID_CLIENT_GLOBAL_FLAG`. 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..5e36cf2ba 100644 --- a/src/wh_client_she.c +++ b/src/wh_client_she.c @@ -47,29 +47,100 @@ #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) { + 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) { + 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); + 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) { + 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) { + 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..8f59470d7 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,45 @@ whKeyId wh_KeyId_TranslateFromClient(uint16_t type, uint16_t clientId, return WH_MAKE_KEYID(type, user, id); } +whKeyId wh_KeyId_TranslateObjectIdFromClient(uint16_t type, uint16_t clientId, + whKeyId reqId) +{ + /* Strip wrapped and hardware flags so fixed types are preserved. */ + 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) +{ + /* Reject bits outside valid ID and flag ranges */ + if ((reqId & (whKeyId) ~(WH_KEYID_MASK | WH_CLIENT_KEYID_FLAGS_MASK)) != + 0) { + return WH_ERROR_BADARGS; + } + /* Wrapped and hardware flags are not valid for fixed-type objects */ + 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 invalid for object creation */ + if (WH_KEYID_ISERASED(reqId)) { + return WH_ERROR_BADARGS; + } +#ifndef WOLFHSM_CFG_GLOBAL_KEYS + /* Reject global flag when global keys are disabled */ + 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 7e4861601..2e88f4f2b 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 */ @@ -235,12 +237,18 @@ 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 */ + /* Clear client ID on disconnect so subsequent requests require COMM INIT */ + if (connected == WH_COMM_DISCONNECTED) { + server->comm->client_id = 0; + } + server->connected = connected; return WH_ERROR_OK; } @@ -282,17 +290,13 @@ 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 is reserved for global objects and cannot be bound by a + * client */ + 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; @@ -397,22 +401,34 @@ 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) { - uint16_t resp_size = sizeof(int32_t); /* Minimum: just the rc field */ + 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 refused before its handler runs. + * Response size matches the group and action expected by the client. */ +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); if (resp_packet == NULL) { 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 translated error code into first 32-bit word */ { int32_t translated_rc = (int32_t)wh_Translate32(magic, (uint32_t)error_code); @@ -554,6 +570,113 @@ 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: + resp_size = _FormatRcOnlyResponse( + magic, error_code, sizeof(whMessageCounter_ReadResponse), + resp_packet); + break; + +#ifndef WOLFHSM_CFG_NO_CRYPTO + case WH_MESSAGE_GROUP_KEY: { + /* Keystore error responses differ in size. DMA responses include + * badAddr status before the return code. */ + 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 + 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) { + 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 @@ -566,7 +689,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) @@ -600,6 +722,25 @@ int wh_Server_HandleRequestMessage(whServerContext* server) group = WH_MESSAGE_GROUP(kind); action = WH_MESSAGE_ACTION(kind); + /* Require COMM INIT before serving requests that resolve client IDs */ + 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 @@ -611,7 +752,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..da46a47e8 100644 --- a/src/wh_server_cert.c +++ b/src/wh_server_cert.c @@ -878,6 +878,26 @@ int wh_Server_CertVerifyAcert(whServerContext* server, const uint8_t* cert, } #endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT */ +/* Validate client certificate ID and translate into internal certificate + * namespace */ +static int _CertTranslateFromClient(whServerContext* server, whNvmId reqId, + int create, whNvmId* out_id) +{ + int rc; + + if (create) { + rc = wh_KeyId_CheckClientObjectIdForCreate(reqId); + } + else { + rc = wh_KeyId_CheckClientObjectId(reqId); + } + if (rc == WH_ERROR_OK) { + *out_id = wh_KeyId_TranslateObjectIdFromClient( + 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 +945,9 @@ 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)) { + 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); @@ -962,6 +983,15 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, wh_MessageCert_TranslateEraseTrustedRequest( magic, (whMessageCert_EraseTrustedRequest*)req_packet, &req); + rc = _CertTranslateFromClient(server, req.id, 0, &req.id); + if (rc != WH_ERROR_OK) { + resp.rc = rc; + wh_MessageCert_TranslateSimpleResponse( + magic, &resp, (whMessageCert_SimpleResponse*)resp_packet); + *out_resp_size = sizeof(resp); + break; + } + /* Process the delete trusted action */ rc = WH_SERVER_NVM_LOCK(server); if (rc == WH_ERROR_OK) { @@ -991,16 +1021,23 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, wh_MessageCert_TranslateReadTrustedRequest( magic, (whMessageCert_ReadTrustedRequest*)req_packet, &req); + rc = _CertTranslateFromClient(server, req.id, 0, &req.id); + if (rc != WH_ERROR_OK) { + resp.rc = rc; + wh_MessageCert_TranslateReadTrustedResponse( + magic, &resp, + (whMessageCert_ReadTrustedResponse*)resp_packet); + *out_resp_size = sizeof(resp); + break; + } + /* Get pointer to certificate data buffer */ cert_data = (uint8_t*)resp_packet + sizeof(resp); cert_len = WOLFHSM_CFG_MAX_CERT_SIZE > max_transport_cert_len ? 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. */ + /* Deny reading non-exportable or server-only certificate objects */ rc = WH_SERVER_NVM_LOCK(server); if (rc == WH_ERROR_OK) { rc = wh_Nvm_GetMetadata(server->nvm, req.id, &meta); @@ -1051,8 +1088,10 @@ 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)) { + 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 +1177,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 +1288,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 +1341,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 +1405,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 +1456,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 +1467,28 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, wh_MessageCert_TranslateVerifyMultiRootDmaRequest( magic, (whMessageCert_VerifyMultiRootDmaRequest*)req_packet, &req); + /* Validate numRoots and translate root IDs into cert namespace + */ + 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 +1542,10 @@ 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)) { + 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 +1597,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..29e4dc63c 100644 --- a/src/wh_server_counter.c +++ b/src/wh_server_counter.c @@ -68,10 +68,18 @@ 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); + ret = wh_KeyId_CheckClientObjectIdForCreate(req.counterId); + if (ret != WH_ERROR_OK) { + resp.rc = ret; + (void)wh_MessageCounter_TranslateInitResponse( + magic, &resp, (whMessageCounter_InitResponse*)resp_packet); + *out_resp_size = sizeof(resp); + break; + } + + /* Write initial counter value to NVM */ + meta->id = wh_KeyId_TranslateObjectIdFromClient( + WH_KEYTYPE_COUNTER, server->comm->client_id, req.counterId); /* use the label buffer to hold the counter value */ *counter = req.counter; @@ -109,15 +117,23 @@ int wh_Server_HandleCounter(whServerContext* server, uint16_t magic, (void)wh_MessageCounter_TranslateIncrementRequest( magic, (whMessageCounter_IncrementRequest*)req_packet, &req); + ret = wh_KeyId_CheckClientObjectId(req.counterId); + if (ret != WH_ERROR_OK) { + resp.rc = ret; + (void)wh_MessageCounter_TranslateIncrementResponse( + magic, &resp, + (whMessageCounter_IncrementResponse*)resp_packet); + *out_resp_size = sizeof(resp); + break; + } + + counterId = wh_KeyId_TranslateObjectIdFromClient( + WH_KEYTYPE_COUNTER, server->comm->client_id, req.counterId); + 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), - meta); + ret = wh_Nvm_GetMetadata(server->nvm, counterId, meta); /* increment and write the counter back */ if (ret == WH_ERROR_OK) { @@ -164,15 +180,22 @@ int wh_Server_HandleCounter(whServerContext* server, uint16_t magic, (void)wh_MessageCounter_TranslateReadRequest( magic, (whMessageCounter_ReadRequest*)req_packet, &req); + ret = wh_KeyId_CheckClientObjectId(req.counterId); + if (ret != WH_ERROR_OK) { + resp.rc = ret; + (void)wh_MessageCounter_TranslateReadResponse( + magic, &resp, (whMessageCounter_ReadResponse*)resp_packet); + *out_resp_size = sizeof(resp); + break; + } + + counterId = wh_KeyId_TranslateObjectIdFromClient( + WH_KEYTYPE_COUNTER, server->comm->client_id, req.counterId); + 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), - meta); + ret = wh_Nvm_GetMetadata(server->nvm, counterId, meta); /* return counter to the caller */ if (ret == WH_ERROR_OK) { @@ -206,9 +229,18 @@ 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); + ret = wh_KeyId_CheckClientObjectId(req.counterId); + if (ret != WH_ERROR_OK) { + resp.rc = ret; + (void)wh_MessageCounter_TranslateDestroyResponse( + magic, &resp, + (whMessageCounter_DestroyResponse*)resp_packet); + *out_resp_size = sizeof(resp); + break; + } + + counterId = wh_KeyId_TranslateObjectIdFromClient( + WH_KEYTYPE_COUNTER, server->comm->client_id, req.counterId); ret = WH_SERVER_NVM_LOCK(server); if (ret == WH_ERROR_OK) { diff --git a/src/wh_server_nvm.c b/src/wh_server_nvm.c index b73a767e1..eb1608a5c 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,31 @@ #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 + /* Translate client NVM ID to internal TYPE=NVM representation */ + return wh_KeyId_TranslateObjectIdFromClient( + 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 +184,73 @@ 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 + /* Global flag selects global namespace, otherwise client + * namespace */ + const uint16_t target_user = + ((req.startId & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0) + ? WH_KEYUSER_GLOBAL + : server->comm->client_id; +#else + const uint16_t target_user = server->comm->client_id; +#endif + /* ID 0 starts from beginning; otherwise translate to internal + * ID */ + 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; + /* Guard against infinite loop if backend fails to advance + */ + 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 +302,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 +350,46 @@ 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 { + resp.rc = WH_ERROR_ABORTED; } } /* Convert the response struct */ @@ -301,11 +412,28 @@ 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 + 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 +467,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; } @@ -394,30 +530,34 @@ int wh_Server_HandleNvmRequest(whServerContext* server, } } if (resp.rc == 0) { - /* A permit-all DMA config passes a zero metadata address through - * untouched, so reject it before it reaches the NVM layer. */ if (metadata == NULL) { resp.rc = WH_ERROR_BADARGS; } } if (resp.rc == 0) { + 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 +599,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 +637,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 ecadd6de1..4d035bcdf 100644 --- a/src/wh_server_she.c +++ b/src/wh_server_she.c @@ -131,6 +131,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); @@ -1894,6 +1902,286 @@ static int _GetId(whServerContext* server, uint16_t magic, uint16_t req_size, return ret; } +/* Pre-program a SHE key into NVM under the calling client's namespace */ +#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 keys must be exactly WH_SHE_KEY_SZ bytes */ + if (ret == 0 && + (req.keySz != WH_SHE_KEY_SZ || req_size != hdr_len + req.keySz)) { + ret = WH_ERROR_BADARGS; + } + /* Validate SHE slot ID range */ + 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) { + /* Evict cached entry before updating NVM */ + 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); + } + 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) { + /* Evict cached entry */ + 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 action-specific error response for SHE requests. Translates rc to + * SHE error codes, except for test key management actions which report rc + * directly. */ +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 @@ -1912,6 +2200,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); @@ -1949,159 +2241,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, @@ -2254,6 +2412,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..181fd5556 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; 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..0b6621c03 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; 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..139ca0904 100644 --- a/test-refactor/client-server/wh_test_keywrap.c +++ b/test-refactor/client-server/wh_test_keywrap.c @@ -186,24 +186,29 @@ static int _whTest_KeywrapTrustedKekPolicy(whClientContext* client) goto cleanup; } - /* (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 */ + /* (b) A client provisioning an NVM object with WH_NVM_FLAGS_TRUSTED + * at a crypto key ID must not obtain a trusted KEK. */ { 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 +219,17 @@ 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 + /* In legacy flat ID mode, TRUSTED flag is stripped on add */ + expectedRc = WH_ERROR_ACCESS; +#else + /* In translated mode, adding with crypto key type is rejected */ + 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 867659ff4..4483cc6bc 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; } @@ -741,10 +730,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; } @@ -769,7 +757,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, " @@ -777,7 +765,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, " @@ -785,7 +773,9 @@ 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 + /* Verify failed attempts left no object in NVM */ + (void)oversizeLabel; if ((ret = wh_Client_NvmGetMetadata( client, WH_SHE_MAKE_KEYID(client->comm->client_id, SHE_SIZE_CHECK_KEY_ID), @@ -799,89 +789,34 @@ 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_OVERSIZE_AUTH_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 === */ - - /* 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; +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + /* Verify SHE slots cannot be accessed through raw client NVM IDs */ + { + 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..b0df37934 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) +/* Disable global keys if WOLFHSM_CFG_TEST_NO_GLOBAL_KEYS is set */ +#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,9 @@ #define WOLFHSM_CFG_HWKEYSTORE #endif +/* Test-only SHE key management APIs. Bypass authenticated update protocols. */ +#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..7c290a117 100644 --- a/test-refactor/misc/wh_test_multiclient.c +++ b/test-refactor/misc/wh_test_multiclient.c @@ -177,6 +177,66 @@ static int _testGlobalKeyBasic(whClientContext* client1, return 0; } +/* Test global counter sharing across clients. */ +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"); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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 +1354,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 +1412,841 @@ 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; +} + +/* Verify client isolation for NVM objects with identical IDs. */ +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"); + + 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); + + 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); + + 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_NOTFOUND); + WH_TEST_ASSERT_RETURN(out_len == 0); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) != 0); + + 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); + + 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); + + 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 */ + 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; +} + +/* Verify that NVM requests from an unbound client (client_id 0) are rejected. + */ +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"); + + 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); + + saved_id = server1->comm->client_id; + server1->comm->client_id = WH_KEYUSER_GLOBAL; + + 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; + } + + 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; + } + + 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; + } + + 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; + } + + 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 + 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; + } + } + + 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 */ + + server1->comm->client_id = saved_id; + + WH_TEST_ASSERT_RETURN(leaked == 0); + + { + 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); + } + + 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 +/* Verify NVM list operations correctly separate own and global namespaces. */ +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"); + + 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); + } + + 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); + } + + 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; + } + 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); + + 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); + } + 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 */ + +/* + * When global keys are disabled, AddObject must reject the GLOBAL flag + * and other verbs must resolve to the caller's own namespace. + */ +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"); + + 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); + + 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 + { + 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 + + 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); + + 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); + + 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); + + 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 */ + +/* Verify AddObject rejects invalid IDs. */ +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"); + + 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); + + 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); + + 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); + + 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; +} + +/* Verify non-add verbs reject IDs with invalid high bits. */ +static int _testNvmNonAddVerbRejections(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId goodId = 0x42; + const whNvmId aliasId = 0x1042; + 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); + + 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 + 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 + + 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; +} + +/* + * Verify that WRAPPED and HW client flags cannot access differently typed + * objects through the NVM API. + */ +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); + + 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]); + + 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); + + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmGetMetadataViaServer(client1, server1, flagged, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, flagged, &out_rc)); + } + + 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); + + (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 */ + +/* Verify keystore operations reject requests from an unbound client. */ +static int _testKeystoreUnboundClientRejected(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + 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"); + + WH_TEST_ASSERT_RETURN( + wh_Server_KeystoreReadKey(server1, globalId, NULL, outBuf, &readSz) == + WH_ERROR_NOTFOUND); + + saved_id = server1->comm->client_id; + server1->comm->client_id = WH_KEYUSER_GLOBAL; + + 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; + } + + 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 + 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; + } + + 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; + } + + 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 */ + + server1->comm->client_id = saved_id; + + WH_TEST_ASSERT_RETURN(leaked == 0); + + 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 +2264,89 @@ 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 using the pre-program request message. */ +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 using the destroy key request message. */ +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 +2592,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 +2689,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 +2747,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 +2768,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 +3025,13 @@ 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 + 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..9c8cd8c7b 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,13 @@ 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)); + /* SHE key management is refused before COMM INIT binds a client ID */ + 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 +312,70 @@ 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 provisioning message */ +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; + 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) +/* Destroy a SHE key using the destroy request message */ +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 +451,36 @@ 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); +} + +/* Verify GET_ID is rejected when no client ID is bound. */ +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 +609,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 +658,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 +672,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 +826,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 +1022,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..3df4f4a25 100644 --- a/test-refactor/misc/wh_test_she_uid_cb.c +++ b/test-refactor/misc/wh_test_she_uid_cb.c @@ -190,6 +190,9 @@ 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)); + /* Set client ID for direct handler invocation. */ + 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..80eb425fe 100644 --- a/test-refactor/posix/wh_test_keygen_unique_id.c +++ b/test-refactor/posix/wh_test_keygen_unique_id.c @@ -615,6 +615,16 @@ int whTest_KeygenUniqueIdConcurrent(void* ctx_arg) goto stop_servers; } + /* Initialize communication for each client pair. */ + 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..40dd5912c 100644 --- a/test-refactor/posix/wh_test_keyread_race.c +++ b/test-refactor/posix/wh_test_keyread_race.c @@ -568,6 +568,16 @@ int whTest_KeyReadRace(void* ctx_arg) goto stop_servers; } + /* Initialize communication for each client pair. */ + 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..2f7683d55 100644 --- a/test-refactor/server/wh_test_cert.c +++ b/test-refactor/server/wh_test_cert.c @@ -182,14 +182,7 @@ int whTest_CertNvmPolicy(whServerContext* ctx) return 0; } -/* - * 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. - */ +/* Verify cert read handler rejects non-cert IDs and SERVER_ONLY objects. */ int whTest_CertReadRejectsServerOnly(whServerContext* ctx) { whServerContext* server = (whServerContext*)ctx; @@ -205,9 +198,7 @@ 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. */ 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 +206,6 @@ 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. */ { whMessageCert_ReadTrustedRequest req = {0}; whMessageCert_ReadTrustedResponse resp = {0}; @@ -225,21 +213,58 @@ 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)); + } + /* Verify SERVER_ONLY object in cert namespace cannot be read. */ + { + 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. */ { whMessageCert_ReadTrustedDmaRequest req = {0}; whMessageCert_SimpleResponse resp = {0}; @@ -247,7 +272,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,17 +286,148 @@ 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); } } #endif /* WOLFHSM_CFG_DMA */ - /* Server-internal unchecked destroy still works; clean up. */ WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &meta.id)); return 0; } +/* Verify EraseTrusted cannot destroy non-certificate objects. */ +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; + /* Protected ID that no client translation can produce */ + 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)); + + { + 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)); + } + + WH_TEST_ASSERT_RETURN( + wh_Nvm_GetMetadata(server->nvm, protectedId, &check) == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(check.len == sizeof(secret)); + + WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &protectedId)); + + return 0; +} + +/* Issue a client READTRUSTED request for `id` and return the response 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; +} + +/* Verify certificate ID isolation between clients and shared global access. */ +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)); + + /* Add cert to client 1 namespace */ + 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 cannot see client 1 cert */ + server->comm->client_id = 2; + WH_TEST_ASSERT_RETURN(_certReadRc(server, magic, 5, req_packet, + resp_packet) == WH_ERROR_NOTFOUND); + + /* Client 1 can read its own cert */ + 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 + /* Verify global cert access */ + 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 + + /* Clean up and restore 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..a069df98b 100644 --- a/test-refactor/server/wh_test_cert_readtrusted.c +++ b/test-refactor/server/wh_test_cert_readtrusted.c @@ -65,7 +65,10 @@ 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; + /* Plant the cert in the client's cert namespace. */ + 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 +82,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 +108,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 +134,18 @@ 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; + 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 +163,19 @@ 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; + /* Set client ID for handler testing */ + 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 ace103d0f..dbbc3f406 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,9 @@ int whTest_SheReqSizeChecking(whServerContext* server) return WH_ERROR_BADARGS; } + /* Set client ID for direct handler invocation. */ + 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 +495,119 @@ 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 clean SHE context and unbound client ID */ memset(server->she, 0, sizeof(*server->she)); + server->comm->client_id = 0; WH_TEST_PRINT("SHE req_size checking test SUCCESS\n"); return 0; } +/* Test that LoadKey rejects oversized NVM objects in auth or target 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; + const uint8_t SLOT_TARGET = 9; + 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]; + 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)); + + 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); + + 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)); + + meta.id = ids[1]; + meta.len = sizeof(oversize); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, sizeof(oversize), oversize)); + + 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)); + + /* Test oversized auth key slot */ + WH_TEST_RETURN_ON_FAIL(wh_She_GenerateLoadableKey( + SLOT_AUTH, 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); + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server->nvm, ids[1], &check) == + WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(check.len == sizeof(oversize)); + + /* Test 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); + 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); + + /* Cleanup */ + 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 27465d87c..fa2994089 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_ShePrngSeedPersistence); WH_TEST_DECL(whTest_SheUidClient); @@ -128,12 +131,15 @@ const whTestCase whTestsServer[] = { {"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}, {"whTest_ShePrngSeedPersistence", whTest_ShePrngSeedPersistence}, 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..64d2d39d4 100644 --- a/test/config/wolfhsm_cfg.h +++ b/test/config/wolfhsm_cfg.h @@ -35,13 +35,15 @@ #define WOLFHSM_CFG_COMM_DATA_LEN (1024 * 8) -/* Enable global keys feature for testing */ +/* Disable global keys if WOLFHSM_CFG_TEST_NO_GLOBAL_KEYS is set */ +#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 +66,9 @@ /* Allow persistent NVM artifacts in tests */ #define WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS +/* Test-only SHE key management APIs. Bypass authenticated update protocols. */ +#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..1d953febc 100644 --- a/test/wh_test_cert.c +++ b/test/wh_test_cert.c @@ -349,21 +349,13 @@ 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. */ +/* Verify cert read handler rejects non-cert IDs and SERVER_ONLY objects. */ static int whTest_CertServerReadTrustedRejectsServerOnly(whServerConfig* serverCfg) { int rc = WH_ERROR_OK; whServerContext server[1] = {0}; whNvmMetadata meta = {0}; - /* Recognizable KEK bytes so any leak into the response is obvious. */ const uint8_t kek[32] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, @@ -376,10 +368,9 @@ 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. */ 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 +378,6 @@ 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. */ { whMessageCert_ReadTrustedRequest req = {0}; whMessageCert_ReadTrustedResponse resp = {0}; @@ -395,23 +385,58 @@ 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)); + } + /* Verify SERVER_ONLY object in cert namespace cannot be read. */ + { + 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. */ { whMessageCert_ReadTrustedDmaRequest req = {0}; whMessageCert_SimpleResponse resp = {0}; @@ -419,7 +444,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,17 +458,160 @@ 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); } } #endif /* WOLFHSM_CFG_DMA */ - /* 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; +} + +/* Verify EraseTrusted cannot destroy non-certificate objects. */ +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; + /* Protected ID that no client translation can produce */ + 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; + meta.len = sizeof(secret); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, sizeof(secret), secret)); + + { + 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)); + } + + WH_TEST_ASSERT_RETURN( + wh_Nvm_GetMetadata(server->nvm, protectedId, &check) == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(check.len == sizeof(secret)); + + WH_TEST_RETURN_ON_FAIL(wh_Nvm_DestroyObjects(server->nvm, 1, &protectedId)); + + WH_TEST_PRINT("Cert EraseTrusted non-cert confinement PASSED\n"); + return rc; +} + +/* Issue a client READTRUSTED request for `id` and return the response 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; +} + +/* Verify certificate ID isolation between clients and shared global access. */ +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"); + + /* Add cert to client 1 namespace */ + 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 cannot see client 1 cert */ + server->comm->client_id = 2; + WH_TEST_ASSERT_RETURN(_certReadRc(server, magic, 5, req_packet, + resp_packet) == WH_ERROR_NOTFOUND); + + /* Client 1 can read its own cert */ + 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 + /* Verify global cert access */ + 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 + + /* Clean up and restore 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 +2195,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..994720c96 100644 --- a/test/wh_test_clientserver.c +++ b/test/wh_test_clientserver.c @@ -421,7 +421,7 @@ 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 /* Local fail/assert helpers that unwind to cleanup instead of returning. */ #define BOUNCE_FAIL(expr) \ @@ -995,12 +995,8 @@ 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 is reserved and rejected */ 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..4e9dd7d0b 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; 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; 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..4f079a438 100644 --- a/test/wh_test_keywrap.c +++ b/test/wh_test_keywrap.c @@ -620,25 +620,30 @@ static int _AesGcm_TestTrustedKekPolicy(whClientContext* client, WC_RNG* rng) return WH_TEST_FAIL; } - /* (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. */ + /* (b) A client provisioning an NVM object with WH_NVM_FLAGS_TRUSTED + * at a crypto key ID must not obtain a trusted KEK. */ { - 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 +653,17 @@ 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 + /* In legacy flat ID mode, TRUSTED flag is stripped on add */ + expectedRc = WH_ERROR_ACCESS; +#else + /* In translated mode, adding with crypto key type is rejected */ + 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..2c72b4c30 100644 --- a/test/wh_test_multiclient.c +++ b/test/wh_test_multiclient.c @@ -174,6 +174,66 @@ static int _testGlobalKeyBasic(whClientContext* client1, return 0; } +/* Test global counter sharing across clients. */ +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"); + + 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); + + 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); + + 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); + + 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); + + 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); + + 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 +1422,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 +1483,843 @@ 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; +} + +/* Verify client isolation for NVM objects with identical IDs. */ +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"); + + 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); + + 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); + + 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_NOTFOUND); + WH_TEST_ASSERT_RETURN(out_len == 0); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) != 0); + + 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); + + 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); + + 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 */ + 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; +} + +/* Verify that NVM requests from an unbound client (client_id 0) are rejected. + */ +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"); + + 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); + + saved_id = server1->comm->client_id; + server1->comm->client_id = WH_KEYUSER_GLOBAL; + + 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; + } + + 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; + } + + 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; + } + + 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; + } + + 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 + 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; + } + } + + 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 */ + + server1->comm->client_id = saved_id; + + WH_TEST_ASSERT_RETURN(leaked == 0); + + { + 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); + } + + 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 +/* Verify NVM list operations correctly separate own and global namespaces. */ +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"); + + 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); + } + + 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); + } + + 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; + } + 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); + + 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); + } + 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 */ + +/* + * When global keys are disabled, AddObject must reject the GLOBAL flag + * and other verbs must resolve to the caller's own namespace. + */ +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"); + + 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); + + 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 + { + 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 + + 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); + + 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); + + 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); + + 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 */ + +/* Verify AddObject rejects invalid IDs. */ +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"); + + 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); + + 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); + + 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); + + 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; +} + +/* Verify non-add verbs reject IDs with invalid high bits. */ +static int _testNvmNonAddVerbRejections(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId goodId = 0x42; + const whNvmId aliasId = 0x1042; + 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); + + 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 + 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 + + 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; +} + +/* + * Verify that WRAPPED and HW client flags cannot access differently typed + * objects through the NVM API. + */ +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); + + 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]); + + 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); + + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmGetMetadataViaServer(client1, server1, flagged, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, flagged, &out_rc)); + } + + 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); + + (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 +/* Verify keystore operations reject requests from an unbound client. */ +static int _testKeystoreUnboundClientRejected(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + 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"); + + WH_TEST_ASSERT_RETURN( + wh_Server_KeystoreReadKey(server1, globalId, NULL, outBuf, &readSz) == + WH_ERROR_NOTFOUND); + + saved_id = server1->comm->client_id; + server1->comm->client_id = WH_KEYUSER_GLOBAL; + + 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; + } + + 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 + 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; + } + + 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; + } + + 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 */ + + server1->comm->client_id = saved_id; + + WH_TEST_ASSERT_RETURN(leaked == 0); + + 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 +2337,89 @@ 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 using the pre-program request message. */ +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 using the destroy key request message. */ +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; } @@ -1704,16 +2665,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 +2762,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 +2820,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 +2841,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 +3098,16 @@ 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 + 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..938b21ee5 100644 --- a/test/wh_test_posix_threadsafe_stress.c +++ b/test/wh_test_posix_threadsafe_stress.c @@ -28,9 +28,13 @@ * Architecture: * - 1 shared NVM context with lock * - 4 server contexts sharing the NVM - * - 4 client threads (all doing both NVM and keystore ops) - * - Different contention phases to stress test contention patterns across - * various different APIs + * - 4 client threads with distinct comm client IDs + * - Contention phases across various APIs + * + * Phase namespace variants: + * - global: all IDs carry WH_KEYID_CLIENT_GLOBAL_FLAG (shared objects) + * - local: bare IDs resolve into private namespaces + * - mixed: clients 0-1 use global namespace, clients 2-3 use private namespaces * * NOTE: Uses PTHREAD_MUTEX_ERRORCHECK attribute to trap undefined behavior * errors (EDEADLK for deadlock, EPERM for non-owner unlock) which indicate @@ -211,11 +215,36 @@ 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 */ + +/* Distinct revoke keys for mixed variant */ +#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 Add With Reclaim phase */ +#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 patterns encoding client ID and namespace */ +#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 */ +#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 +314,15 @@ typedef enum { ROLE_OP_B, /* Perform operation B (e.g., Evict, Read) */ } ClientRole; +/* Namespace variant for phase run */ +typedef enum { + VARIANT_GLOBAL = 0, /* all clients share global namespace */ + VARIANT_LOCAL, /* each client uses 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 +533,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 +575,85 @@ 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))); +} + +/* Return NVM object ID for client under current variant */ +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 client reads under current variant */ +static int nvmReadExpectFor(const ClientServerPair* pair, + NamespaceVariant variant) +{ + if (isGlobalSide(pair, variant)) { + return NVM_EXPECT_ANY_GLOBAL; + } + return (int)nvmPatternFor(pair, variant); +} + +/* Role for client under current variant */ +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 +814,8 @@ 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 stopFlag is set */ + while (!ATOMIC_LOAD_INT(&pair->stopFlag)) { rc = wh_Server_HandleRequestMessage(&pair->server); if (rc == WH_ERROR_NOTREADY) { @@ -728,14 +835,59 @@ static void* serverThread(void* arg) * CLIENT OPERATIONS * ========================================================================== */ -static int doNvmAddObject(whClientContext* client, whNvmId id, int iteration) +/* Register client ID with server */ +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 matches expected 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 writer pattern */ + memset(data, pattern, sizeof(data)); /* Send request */ rc = wh_Client_NvmAddObjectRequest(client, id, WH_NVM_ACCESS_ANY, @@ -760,7 +912,8 @@ static int doNvmAddObject(whClientContext* client, whNvmId id, int iteration) return out_rc; } -static int doNvmRead(whClientContext* client, whNvmId id) +/* Read an object and optionally verify its content */ +static int doNvmRead(whClientContext* client, whNvmId id, int expect) { uint8_t data[NVM_OBJECT_DATA_SIZE]; whNvmSize outSz = sizeof(data); @@ -784,12 +937,17 @@ 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 objects in client or global namespace and verify returned ID scope */ +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 +955,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 +971,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; + /* Verify returned ID belongs to requested namespace */ + 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 +1361,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 writer pattern */ + memset(pair->dmaNvmBuffer, pattern, sizeof(pair->dmaNvmBuffer)); /* Set up metadata */ memset(&meta, 0, sizeof(meta)); @@ -1231,7 +1400,8 @@ 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 */ +static int doNvmReadDma(ClientServerPair* pair, whNvmId id, int expect) { int32_t out_rc; int rc; @@ -1254,8 +1424,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 +1437,45 @@ 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: initialize counter to 0 */ + 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 +1496,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 +1508,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 +1532,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 +1549,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 +1609,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 +1622,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 +1640,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 +1661,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 +1682,162 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, } } +/* ============================================================================ + * PHASE CLEANUP + * ========================================================================== */ + +/* Best-effort teardown after each phase run */ +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 +1882,57 @@ 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 client and global namespaces */ + 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 +1942,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 +1984,56 @@ 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: add objects from bounded ID ranges */ + 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 +2046,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 +2078,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 +2117,40 @@ 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); + 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 +2175,82 @@ 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; + /* Local namespace error checks */ + if (!globalSide) { + switch (phase) { + case PHASE_NVM_CONCURRENT_ADD: + return (rc == WH_ERROR_NOSPACE); + + case PHASE_NVM_ADD_VS_READ: + return (role == ROLE_OP_A) && (rc == WH_ERROR_NOSPACE); + + case PHASE_NVM_ADD_VS_DESTROY: + return (role == ROLE_OP_A) ? (rc == WH_ERROR_NOSPACE) + : (rc == WH_ERROR_NOTFOUND); + + case PHASE_NVM_READ_VS_DESTROY: + return (role == ROLE_OP_B) && (rc == WH_ERROR_NOTFOUND); + + case PHASE_NVM_CONCURRENT_READ: + return 0; + + 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); + + 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); + + 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; + } + } + + /* Counter phases: accept NOSPACE */ + 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 +2318,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 +2358,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 +2385,18 @@ 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; + + /* Run per-client setup */ + 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 +2404,31 @@ 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) { + /* Fail if CommInit failed */ + 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 +2437,10 @@ static void* contentionClientThread(void* arg) /* Wait for all clients to finish streaming */ pthread_barrier_wait(&ctx->streamEndBarrier); + + /* Wait for validation to complete before cleanup */ + pthread_barrier_wait(&ctx->cleanupStartBarrier); + doPhaseCleanup(pair, phase, variant); } return NULL; @@ -2022,51 +2461,59 @@ 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 */ +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: { + /* Validate counter value against successful increments */ + 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 +2522,51 @@ 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 key ID for phase and namespace variant */ +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 +2579,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 */ @@ -2171,7 +2623,11 @@ static int runPhase(StressTestContext* ctx, const PhaseConfig* config, totalErrors); /* 10. Run phase-specific validation */ - rc = validatePhaseResult(ctx, config->phase, totalIterations, totalErrors); + rc = validatePhaseResult(ctx, config->phase); + + /* 11. Release clients to run per-phase cleanup */ + pthread_barrier_wait(&ctx->cleanupStartBarrier); + if (rc != WH_ERROR_OK) { return rc; } @@ -2209,6 +2665,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 +2676,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 +2742,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 +2780,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 +2807,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 +2837,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 d601a1f59..86e7fddca 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; } @@ -745,9 +740,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. */ @@ -898,13 +891,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; @@ -1234,12 +1222,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); @@ -1393,16 +1381,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); @@ -1426,19 +1414,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); @@ -1598,6 +1584,114 @@ static void* _whServerTask(void* cf) #if defined(WOLFHSM_CFG_TEST_POSIX) && defined(WOLFHSM_CFG_ENABLE_CLIENT) && \ defined(WOLFHSM_CFG_ENABLE_SERVER) +/* Verify SHE requests are rejected before COMM INIT */ +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)); + + 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; + } + + /* Verify SET_UID and GET_ID are also rejected before COMM INIT */ + { + 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) @@ -1912,6 +2006,9 @@ 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)); + /* Set mock client ID for direct handler invocation */ + 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 @@ -2583,6 +2680,9 @@ 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)); + /* Set mock client ID for direct handler invocation */ + 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; @@ -2836,6 +2936,175 @@ static int wh_She_TestPrngSeedPersistence(void) return ret; } +/* Verify LoadKey rejects slots whose NVM objects are not exactly WH_SHE_KEY_SZ + */ +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; + const uint8_t SLOT_TARGET = 9; + 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; + 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)); + + /* Set mock client ID and state for direct handler invocation */ + 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); + + 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)); + + meta.id = authId; + meta.len = sizeof(oversize); + WH_TEST_RETURN_ON_FAIL( + wh_Nvm_AddObject(server->nvm, &meta, sizeof(oversize), oversize)); + + 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)); + + /* Test oversized auth key slot */ + WH_TEST_RETURN_ON_FAIL(wh_She_GenerateLoadableKey( + SLOT_AUTH, 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); + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server->nvm, authId, &check) == + WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(check.len == sizeof(oversize)); + WH_TEST_PRINT("SHE oversized auth key SUCCESS\n"); + + /* Test 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); + 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) && \ @@ -2908,12 +3177,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; } @@ -2977,8 +3246,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; } @@ -3088,6 +3357,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; @@ -3117,6 +3387,7 @@ int whTest_She(void) WH_TEST_RETURN_ON_FAIL(wh_She_TestGetId()); WH_TEST_PRINT("Testing SHE: PRNG seed persistence...\n"); WH_TEST_RETURN_ON_FAIL(wh_She_TestPrngSeedPersistence()); + 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)); @@ -3126,6 +3397,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..439c19d96 100644 --- a/wolfhsm/wh_client.h +++ b/wolfhsm/wh_client.h @@ -1582,7 +1582,14 @@ int wh_Client_DataUnwrapResponse(whClientContext* ctx, enum wc_CipherType cipherType, void* dataOut, uint32_t* dataSz); -/* Counter functions */ +/* Counter functions + * + * Counter IDs use client-facing key ID encoding. Bits 0 to 7 hold the + * counter number from 1 to 255 (0 is invalid). Bit 8 + * (WH_KEYID_CLIENT_GLOBAL_FLAG) selects the shared global namespace when + * WOLFHSM_CFG_GLOBAL_KEYS is enabled. Standard IDs are private to the calling + * client. IDs with other bits set, or with the global flag when global keys are + * disabled, return WH_ERROR_BADARGS. Any client can access global counters. */ int wh_Client_CounterInitRequest(whClientContext* c, whNvmId counterId, uint32_t counter); int wh_Client_CounterInitResponse(whClientContext* c, uint32_t* counter); @@ -1661,6 +1668,35 @@ 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 uses the client-facing + * key ID encoding (see wolfhsm/wh_keyid.h). + * + * - Bits 0 to 7 (WH_KEYID_MASK): numeric ID from 1 to 255. ID 0 is + * reserved as the erased sentinel and is rejected by AddObject. + * - Bit 8 (WH_KEYID_CLIENT_GLOBAL_FLAG): selects the shared global NVM + * namespace when WOLFHSM_CFG_GLOBAL_KEYS is enabled. Otherwise, the ID + * targets the calling client's private namespace. + * - Bits 9 and 10 (WH_KEYID_CLIENT_WRAPPED_FLAG and WH_KEYID_CLIENT_HW_FLAG): + * invalid for NVM objects. Requests with these flags return WH_ERROR_BADARGS. + * - Bits 11 to 15: must be zero. + * + * The server translates each request ID into internal TYPE/USER/ID encoding, + * providing private per-client namespaces (1 to 255) and an optional global + * namespace. The client ID is assigned during wh_Client_CommInit(). Requests + * outside the COMM group sent before COMM INIT are rejected with WH_ERROR_ACCESS. + * + * With WOLFHSM_CFG_GLOBAL_KEYS enabled, wh_Client_NvmList() uses the global + * flag on startId to select the target namespace. Returned IDs retain the + * flag for pagination. + * + * Defining WOLFHSM_CFG_LEGACY_CLIENT_NVM disables translation for the NVM + * group, restoring the flat 16-bit ID space. Key, counter, and certificate + * IDs remain translated. + */ + /** NVM functions */ /** * @brief Sends a non-volatile memory (NVM) initialization request to the @@ -2701,7 +2737,14 @@ 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 + * + * Certificate IDs use client-facing key ID encoding. Bits 0 to 7 name a root + * from 1 to 255 in the client trust store. Bit 8 (WH_KEYID_CLIENT_GLOBAL_FLAG) + * selects the shared global trust store when WOLFHSM_CFG_GLOBAL_KEYS is enabled. + * Roots are stored as NVM objects of type WH_KEYTYPE_CERT. IDs with other bits + * set or ID 0 return WH_ERROR_BADARGS. Build-time provisioned roots must use + * internal encoding (see Trusted Root Storage in documentation). */ /** * @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..672a20234 100644 --- a/wolfhsm/wh_client_she.h +++ b/wolfhsm/wh_client_she.h @@ -69,33 +69,40 @@ /** SHE provisioning and identity functions */ +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT +/* Test or provisioning key management functions that bypass the SHE + * authenticated update protocol. 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 - * 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 - * in-field updates should use the spec-compliant wh_Client_SheLoadKey(). The - * key is scoped to the calling client via the keyId USER field, or to the - * shared global namespace when WOLFHSM_CFG_SHE_GLOBAL_KEYS is defined. - * - * @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] 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. - * @param[in] keySz Length of the key material in bytes. Must be exactly - * WH_SHE_KEY_SZ (16); any other length is rejected. - * @return int Returns 0 on success, WH_ERROR_BADARGS if @p c or @p key is NULL - * or @p keySz is not WH_SHE_KEY_SZ, or a negative error code on - * failure. + * Writes key material directly into the SHE NVM slot keyId with the + * specified update counter and flags. Intended for initial device provisioning. + * + * @param[in] c Pointer to the client context. + * @param[in] keyId SHE key slot to write (0-15). + * @param[in] count Initial SHE key counter value. + * @param[in] flags SHE key protection flags. + * @param[in] key Pointer to key material. + * @param[in] keySz Length of key material in bytes (must be WH_SHE_KEY_SZ). + * @return int Returns 0 on success, or a negative error code on 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. + * + * Removes the SHE key in slot keyId from the calling client's NVM namespace. + * + * @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..153754e45 100644 --- a/wolfhsm/wh_keyid.h +++ b/wolfhsm/wh_keyid.h @@ -47,14 +47,10 @@ typedef uint16_t whKeyId; #define WH_KEYTYPE_SHIFT 12 /* Maximum valid client_id. The USER field of whKeyId is 4 bits, so client_id - * must fit in [1, WH_CLIENT_ID_MAX]: value 0 is reserved for the global-keys - * 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. */ + * must be between 1 and WH_CLIENT_ID_MAX. Value 0 is reserved for the global + * namespace (WH_KEYUSER_GLOBAL). wh_Client_Init() rejects out-of-range IDs + * before communication, and the server rejects 0 and IDs above the maximum at + * WH_MESSAGE_COMM_ACTION_INIT. */ #define WH_CLIENT_ID_MAX (WH_KEYUSER_MASK >> WH_KEYUSER_SHIFT) /* @@ -116,6 +112,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 +149,46 @@ 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. + * + * Translates reqId into server encoding using the provided TYPE. The wrapped + * and hardware flags are stripped, ensuring the object stays in the supplied + * TYPE namespace. The global flag sets the USER field to WH_KEYUSER_GLOBAL + * when WOLFHSM_CFG_GLOBAL_KEYS is enabled, or to clientId otherwise. Used + * by NVM objects, counters, and certificates. + * + * @param type Fixed object TYPE to set (such as WH_KEYTYPE_COUNTER). + * @param clientId Connection client ID for the USER field. + * @param reqId Requested ID from the client. + * @return Server-internal keyId with fixed TYPE, USER, and ID fields. + */ +whKeyId wh_KeyId_TranslateObjectIdFromClient(uint16_t type, uint16_t clientId, + whKeyId reqId); + +/** + * @brief Check a client-supplied fixed-type object ID before translating it. + * + * Validates object IDs for NVM objects, counters, and certificates. Rejects + * bits above valid ID and flag ranges, as well as wrapped and hardware flags. + * The global flag and ID 0 are permitted. + * + * @param reqId Requested ID from the client. + * @return WH_ERROR_OK if valid, or WH_ERROR_BADARGS otherwise. + */ +int wh_KeyId_CheckClientObjectId(whKeyId reqId); + +/** + * @brief Check a client-supplied ID for creating a fixed-type object. + * + * Validates client object ID for creation by requiring a non-zero ID and + * rejecting the global flag when WOLFHSM_CFG_GLOBAL_KEYS is disabled. + * + * @param reqId Requested ID from the client. + * @return WH_ERROR_OK if valid for creation, or 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..1283a3a29 100644 --- a/wolfhsm/wh_message_keystore.h +++ b/wolfhsm/wh_message_keystore.h @@ -247,8 +247,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. */ + /* When packet->rc is WH_ERROR_ACCESS, contains the offending address and + * size pair. If the request failed before DMA access, this field is zero. + * Invalid for other return codes. */ whMessageKeystore_DmaBuffer badAddr; } whMessageKeystore_DmaAddrStatus; diff --git a/wolfhsm/wh_message_she.h b/wolfhsm/wh_message_she.h index f8dc0ad5d..69b4a10a0 100644 --- a/wolfhsm/wh_message_she.h +++ b/wolfhsm/wh_message_she.h @@ -412,6 +412,55 @@ 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. Writes a SHE key into NVM under the calling client's + * namespace without using the SHE M1-M5 update protocol. Test and 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 key from the calling client's NVM + * namespace without using the SHE debug-authorization protocol. Test and + * provisioning use only. */ +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..24421b929 100644 --- a/wolfhsm/wh_server_she.h +++ b/wolfhsm/wh_server_she.h @@ -88,6 +88,23 @@ 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. + * + * Formats a response buffer with the expected layout when a SHE request + * fails before its handler runs, such as before COMM INIT. Standard SHE + * actions translate rc to a SHE error code, while test key management + * actions report rc directly. + * + * @param magic Request magic for endianness translation. + * @param action SHE action to format. + * @param rc Error code to report. + * @param resp_packet Output response buffer. + * @return Size of the 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..09d32840b 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 SHE test + * key-management helpers (pre-program and destroy) that bypass the M1 to M5 + * update protocol. For test and provisioning builds only. 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,12 @@ * WOLFHSM_CFG_KEYWRAP - If defined, include the key wrap functionality * Default: Not defined * + * WOLFHSM_CFG_LEGACY_CLIENT_NVM - If defined, client NVM requests use + * untranslated 16-bit IDs across all clients. Only applies to the NVM group. + * Key, counter, and certificate IDs remain translated. By default, NVM IDs + * are translated into per-client namespaces (1 to 255). + * 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 From 83a35110d128d4843992d006527e1e7f5b04525d Mon Sep 17 00:00:00 2001 From: Brett Nicholas <7547222+bigbrett@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:06:28 -0600 Subject: [PATCH 2/2] remove draft docs --- docs/draft/README.md | 8 - docs/draft/THREADSAFE.md | 218 ---------- docs/draft/async-crypto.md | 773 ---------------------------------- docs/draft/auth.md | 516 ----------------------- docs/draft/certificates.md | 635 ---------------------------- docs/draft/crypto_affinity.md | 96 ----- docs/draft/posix-shm.md | 67 --- docs/draft/timeout.md | 122 ------ 8 files changed, 2435 deletions(-) delete mode 100644 docs/draft/README.md delete mode 100644 docs/draft/THREADSAFE.md delete mode 100644 docs/draft/async-crypto.md delete mode 100644 docs/draft/auth.md delete mode 100644 docs/draft/certificates.md delete mode 100644 docs/draft/crypto_affinity.md delete mode 100644 docs/draft/posix-shm.md delete mode 100644 docs/draft/timeout.md diff --git a/docs/draft/README.md b/docs/draft/README.md deleted file mode 100644 index 5db146b1b..000000000 --- a/docs/draft/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Draft Documentation - -This directory holds documentation for individual features added to wolfHSM. - -Eventually these documents should be refined and incorporated into the "V2" -wolfHSM documentation and the online manual. For now we leave them here so they -can inform code review and still be helpful so users, despite being in "draft" -state. diff --git a/docs/draft/THREADSAFE.md b/docs/draft/THREADSAFE.md deleted file mode 100644 index 4b1c939e5..000000000 --- a/docs/draft/THREADSAFE.md +++ /dev/null @@ -1,218 +0,0 @@ -# Thread Safety in wolfHSM - -## Overview - -wolfHSM supports server-side thread safety via the `WOLFHSM_CFG_THREADSAFE` build flag. When enabled, the server can safely process requests from multiple clients concurrently, with each request processing loop running on a separate thread and communicating through its own server context. - -The thread safety model serializes access to shared resources like NVM storage and the global key cache using a single lock embedded in the NVM context. Multiple server contexts can safely share a single NVM context, with the lock ensuring atomic access to shared state. - -The thread safety feature does NOT imply that a single server context can be shared across threads, as it only serializes internal access to shared resources between multiple server contexts that would occur internally as a consequence of using the server API. Concurrent access to a single server context is not currently supported. - -## Build Configuration - -Thread safety is enabled when building with `WOLFHSM_CFG_THREADSAFE` defined. - -To enable thread safety in the posix test harness, you can build with: - -```bash -make -C test THREADSAFE=1 -``` - -When `WOLFHSM_CFG_THREADSAFE` is not defined, all locking operations compile to no-ops with zero overhead. - -## What Is Protected - -Currently only the global NVM context is protected by a lock, guaranteeing thread safe access to: - -- **NVM operations**: All operations on non-volatile storage -- **Global key cache**: Keys marked as global (shared across clients) via `WOLFHSM_CFG_GLOBAL_KEYS` -- **Local key cache**: Per-server key cache operations that must synchronize with NVM - -The lock does **not** protect: - -- Transport layer operations (each server has its own comm context) -- Per-request scratch memory (allocated per-handler) -- Server context fields that are not shared - -## Architecture - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ Server 1 │ │ Server 2 │ │ Server 3 │ -│ (localCache)│ │ (localCache)│ │ (localCache)│ -└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ - │ │ │ - └────────────────┼────────────────┘ - │ - ▼ - ┌───────────────────┐ - │ NVM Context │ - │ ┌─────────────┐ │ - │ │ Lock │ │ - │ ├─────────────┤ │ - │ │ Global Keys │ │ - │ ├─────────────┤ │ - │ │ NVM Backend│ │ - │ └─────────────┘ │ - └───────────────────┘ -``` - -## Lock Lifecycle - -The lock is initialized and cleaned up with the NVM context: - -```c -whNvmConfig nvmConfig = { - .cb = &nvmFlashCb, - .context = &flashContext, - .config = &flashConfig, -#ifdef WOLFHSM_CFG_THREADSAFE - .lockConfig = &lockConfig, /* Platform-specific lock config */ -#endif -}; - -wh_Nvm_Init(&nvmContext, &nvmConfig); /* Initializes lock */ -/* ... use NVM ... */ -wh_Nvm_Cleanup(&nvmContext); /* Cleans up lock */ -``` - -## Request Handler Locking - -Request handlers in the server acquire the lock around compound operations. The pattern is: - -```c -case SOME_ACTION: { - /* Translate request (no lock needed) */ - wh_MessageFoo_TranslateRequest(magic, req_packet, &req); - - /* Acquire lock for atomic compound operation */ - ret = WH_SERVER_NVM_LOCK(server); - if (ret == WH_ERROR_OK) { - /* Perform work while holding lock */ - ret = wh_Server_KeystoreXxx(server, ...); - if (ret == WH_ERROR_OK) { - ret = wh_Nvm_Xxx(server->nvm, ...); - } - - /* Release lock */ - (void)WH_SERVER_NVM_UNLOCK(server); - } - resp.rc = ret; - - /* Translate response (no lock needed) */ - wh_MessageFoo_TranslateResponse(magic, &resp, resp_packet); -} -``` - -Key points: -- Lock acquired **after** request translation -- Lock released **before** response translation -- All NVM and keystore operations within the lock are atomic -- The lock ensures multi-step operations (e.g., check-then-modify) are not interleaved - -## Server-Side Development - -When developing server-side code that accesses shared resources outside the request handling pipeline, you must manually acquire the lock. - -### Using the Server Lock Macros - -```c -int my_server_function(whServerContext* server) -{ - int ret; - - ret = WH_SERVER_NVM_LOCK(server); - if (ret != WH_ERROR_OK) { - return ret; - } - - /* Access NVM or global keystore while holding lock */ - ret = wh_Nvm_Read(server->nvm, id, offset, len, buffer); - if (ret == WH_ERROR_OK) { - ret = wh_Server_KeystoreCacheKey(server, &meta, keyData); - } - - (void)WH_SERVER_NVM_UNLOCK(server); - return ret; -} -``` - -### Using the NVM Lock Macros Directly - -If you only have access to the NVM context: - -```c -int my_nvm_function(whNvmContext* nvm) -{ - int ret; - - ret = WH_NVM_LOCK(nvm); - if (ret != WH_ERROR_OK) { - return ret; - } - - /* Access NVM while holding lock */ - ret = wh_Nvm_GetMetadata(nvm, id, &meta); - if (ret == WH_ERROR_OK) { - ret = wh_Nvm_Read(nvm, id, 0, meta.len, buffer); - } - - (void)WH_NVM_UNLOCK(nvm); - return ret; -} -``` - -### Lock Macro Behavior - -| Macro | THREADSAFE defined | THREADSAFE not defined | -|-------|-------------------|------------------------| -| `WH_SERVER_NVM_LOCK(server)` | Calls `wh_Server_NvmLock()` | Returns `WH_ERROR_OK` | -| `WH_SERVER_NVM_UNLOCK(server)` | Calls `wh_Server_NvmUnlock()` | Returns `WH_ERROR_OK` | -| `WH_NVM_LOCK(nvm)` | Calls `wh_Nvm_Lock()` | Returns `WH_ERROR_OK` | -| `WH_NVM_UNLOCK(nvm)` | Calls `wh_Nvm_Unlock()` | Returns `WH_ERROR_OK` | - -Using these macros ensures code compiles and runs correctly regardless of whether thread safety is enabled. - -## Platform-Specific Lock Implementation - -The lock abstraction is a generic interface that relies on callbacks for the actual implementation, allowing platform-specific implementations. wolfHSM provides a reference POSIX implementation using pthreads for use in the POSIX port: - -```c -#include "port/posix/posix_lock.h" - -static posixLockContext lockCtx; -static const whLockCb lockCb = POSIX_LOCK_CB; - -whLockConfig lockConfig = { - .cb = &lockCb, - .context = &lockCtx, - .config = NULL, /* Use default mutex attributes */ -}; -``` - -To implement for another platform, you can implement your own callbacks matching the `whLockCb` interface: - -```c -typedef struct whLockCb_t { - whLockInitCb init; /* Initialize lock resources */ - whLockCleanupCb cleanup; /* Free lock resources */ - whLockAcquireCb acquire; /* Acquire exclusive lock (blocking) */ - whLockReleaseCb release; /* Release exclusive lock */ -} whLockCb; -``` - -## Testing Thread Safety on the POSIX port - -Run the standard test suite with DMA, SHE, and thread safety enabled - -```bash -make -C test clean && make -j -C test DMA=1 THREADSAFE=1 && make -C test run -``` - -Run the multithreaded stress test with the same functionality under ThreadSanitizer to detect data races: - -```bash -make -C test clean && make -j -C test STRESS=1 TSAN=1 DMA=1 SHE=1 THREADSAFE=1 && make -C test run TSAN=1 -``` - -The stress test runs multiple client threads against multiple server contexts sharing a single NVM context, exercising contention patterns across keystore, NVM, counter, and certificate APIs. diff --git a/docs/draft/async-crypto.md b/docs/draft/async-crypto.md deleted file mode 100644 index 92fdb3caf..000000000 --- a/docs/draft/async-crypto.md +++ /dev/null @@ -1,773 +0,0 @@ -# Async Crypto API - -**Status: Work in Progress** - -## Background: Crypto in wolfHSM Today - -wolfHSM offloads cryptographic operations from a client application to a secure -server (typically running on an HSM or trusted core) using a request/response -protocol over a shared communication buffer. Today this works through -wolfCrypt's **crypto callback** mechanism: - -1. The application initializes a wolfCrypt context with `devId = WH_DEV_ID`. -2. When a wolfCrypt function (`wc_Sha256Update`, `wc_AesCbcEncrypt`, etc.) is - called, wolfCrypt invokes the registered callback `wh_Client_CryptoCb`. -3. The callback serializes the operation into the comm buffer, sends the request - to the server, and **blocks** polling `wh_Client_RecvResponse()` until the - server replies. -4. The result is deserialized and returned to the caller. - -This is transparent to application code -- standard wolfCrypt API calls "just -work" -- but every crypto operation is **synchronous and blocking**. The client -thread cannot do useful work while the server is processing. On embedded -targets where the transport is shared memory and the server runs on a different -core, this means the client core sits idle for the entire round-trip. - -### Why Blocking is a Problem - -- **CPU waste**: the client spins in a polling loop while the HSM computes. -- **No pipelining**: multi-step operations (e.g., hashing a large file followed - by signing the digest) cannot overlap. -- **RTOS integration**: a blocking call cannot yield to higher-priority tasks - or cooperate with event-driven schedulers. - -## The Async Crypto API - -The async crypto API introduces a **non-blocking request/response split** for -each cryptographic operation. Every blocking function is decomposed into: - -- **`*Request()`** -- serializes and sends the request. Returns immediately. -- **`*Response()`** -- attempts a single non-blocking receive. Returns - `WH_ERROR_NOTREADY` if the server has not yet replied, or the final result - on completion. - -The existing blocking functions are retained as thin wrappers that call -`Request()` then poll `Response()` in a loop. The crypto callback path -(`wh_Client_CryptoCb`) continues to use these blocking wrappers, so existing -application code is unaffected. - -``` - +-----------+ - Application | | - (async) | wolfHSM | - | | Server | - |-- Request() ---->| | - | | (compute)| - | (do other work) | | - | | | - |<-- Response() ---| | - | WH_ERROR_NOTREADY | - | | | - |<-- Response() ---| | - | WH_ERROR_OK | | - | (result) +-----------+ -``` - -### Design Principles - -- **Stateless responses**: output buffers are passed as parameters to the - Response function, not stored in `whClientContext`. -- **No server-side changes**: the server already handles each request - independently -- it doesn't know or care whether the client blocked. -- **Preserve existing wire formats where possible**: for operations whose - request/response layout is already suitable, the async API only changes the - client-side calling pattern. Some algorithms (notably the SHA family) still - require new message layouts to carry async-specific inputs such as - intermediate state, variable-length trailing input, and DMA metadata. -- **Pre-cached keys required**: async Request functions require keys to already - be cached on the server. The blocking wrappers retain automatic key import - for convenience. -- **One outstanding request per client context**: only one async crypto - request may be in flight at a time on a given `whClientContext`. - -### Usage Pattern - -```c -/* Send the request */ -ret = wh_Client_EccSignRequest(ctx, key, hash, hashLen); -if (ret != WH_ERROR_OK) { /* handle error */ } - -/* ... do other work while server computes ... */ - -/* Poll for completion */ -do { - ret = wh_Client_EccSignResponse(ctx, sig, &sigLen); - if (ret == WH_ERROR_NOTREADY) { - /* yield to scheduler, do other work, etc. */ - } -} while (ret == WH_ERROR_NOTREADY); -/* ret has final result, sig/sigLen are populated */ -``` - -## SHA: The First Async Algorithm - -SHA hash functions are the first algorithm family to receive the async -treatment. All four SHA-2 variants are supported: SHA-224, SHA-256, SHA-384, -and SHA-512. - -SHA is a particularly interesting case because hashing is inherently a -**streaming, multi-call** operation (`Init` / `Update*` / `Final`), unlike -single-shot operations like RSA sign or AES-CBC encrypt where one -request/response round-trip suffices. The async SHA API must handle: - -- Inputs that vastly exceed the communication buffer size -- Partial-block buffering on the client -- Intermediate hash state that must be preserved across round-trips -- A stateless server that reconstructs state from each request - -### Wire Protocol - -Each SHA request carries the **full intermediate hash state** inline so the -server can process the data statelessly. The wire layout in the comm buffer -is: - -``` -+------------------------------------------+ -| GenericRequestHeader (12 bytes) | algo type, affinity -+------------------------------------------+ -| Sha256Request / Sha512Request | resumeState + control fields -| resumeState.hiLen (4 bytes) | -| resumeState.loLen (4 bytes) | -| resumeState.hash (32 or 64 bytes) | intermediate digest -| [resumeState.hashType (4 bytes)] | SHA-512 family only -| isLastBlock (4 bytes) | -| inSz (4 bytes) | -+------------------------------------------+ -| uint8_t in[inSz] | variable-length input data -+------------------------------------------+ -``` - -The response carries the updated state (or final digest) back: - -``` -+------------------------------------------+ -| GenericResponseHeader (12 bytes) | algo type, return code -+------------------------------------------+ -| Sha2Response | -| hiLen, loLen (8 bytes) | -| hash (64 bytes) | updated/final digest -| hashType (4 bytes) | -+------------------------------------------+ -``` - -### Block Alignment and MTU Filling - -The comm buffer has a fixed size (`WOLFHSM_CFG_COMM_DATA_LEN`, default 1280 -bytes). The async SHA design maximizes throughput by packing as many **whole -hash blocks** into each message as possible. - -SHA-256 and SHA-224 use a 64-byte block size. SHA-384 and SHA-512 use 128 -bytes. The maximum inline data capacity per message is: - -```c -#define WH_MESSAGE_CRYPTO_SHA256_MAX_INLINE_UPDATE_SZ \ - (((WOLFHSM_CFG_COMM_DATA_LEN \ - - sizeof(whMessageCrypto_GenericRequestHeader) \ - - sizeof(whMessageCrypto_Sha256Request)) \ - / 64u) * 64u) -``` - -This rounds **down** to the nearest block boundary so that non-final Update -messages always carry whole blocks. - -With the default 1280-byte comm buffer: - -| Variant | Header Overhead | Block Size | Max Inline Data | Blocks/Message | -|----------------|-----------------|------------|-----------------|----------------| -| SHA-256/224 | 60 bytes | 64 bytes | 1216 bytes | 19 blocks | -| SHA-512/384 | 96 bytes | 128 bytes | 1152 bytes | 9 blocks | - -> *Header overhead = GenericRequestHeader (12 bytes) + algorithm-specific -> request struct (48 bytes for SHA-256, 84 bytes for SHA-512).* - -The per-call capacity is slightly larger than the inline wire capacity because -the client can absorb up to `BLOCK_SIZE - 1` additional tail bytes into its -local buffer without needing to send them: - -```c -capacity = MAX_INLINE_UPDATE_SZ + (BLOCK_SIZE - 1 - sha->buffLen) -``` - -### Client-Side Partial-Block Buffering - -The SHA block cipher operates on fixed-size blocks (64 or 128 bytes). When the -caller provides input that isn't block-aligned, the client must buffer the -partial tail locally until enough data arrives to form a complete block. This -buffering uses the `buffer` and `buffLen` fields already present in wolfCrypt's -`wc_Sha256` (and related) structures -- no additional memory is needed. - -The Update request function performs three steps: - -1. **Top up the existing partial block**: if there are already bytes buffered - from a previous call (`buffLen > 0`), pull bytes from the new input until - either a full block is assembled or the input is exhausted. If a full block - is formed, it becomes the first inline block on the wire. - -2. **Pack whole blocks from input**: copy as many remaining complete blocks from - the caller's input as fit in the inline data area. - -3. **Stash the tail**: any leftover bytes (less than one block) go into the - local buffer for the next call. - -``` - Caller input (e.g., 200 bytes, buffLen=30 from prior call): - ┌──────────────────────────────────────────────────────────┐ - │ input data (200 bytes) │ - └──────────────────────────────────────────────────────────┘ - - Step 1: Top up partial block (34 bytes from input complete the block) - ┌────────┬──────────────────────────────────────────────────┐ - │buff(30)│ 34 bytes │ │ - └────────┴──────────┘ remaining: 166 bytes │ - ↓ │ - [Block 0: 64 bytes] → wire │ - │ - Step 2: Pack whole blocks (2 more blocks = 128 bytes) │ - [Block 1: 64 bytes] → wire │ - [Block 2: 64 bytes] → wire │ - │ - Step 3: Stash tail (166 - 128 = 38 bytes) │ - buffLen = 38 │ - │ - Wire payload: 192 bytes (3 blocks) │ - └───────────────────────────────────────────────────────────┘ -``` - -If the total input is small enough to fit entirely in the partial-block buffer -without completing a block, no server round-trip is issued at all. The -`requestSent` output flag tells the caller whether a matching `*Response()` call -is needed: - -```c -bool requestSent; -ret = wh_Client_Sha256UpdateRequest(ctx, sha, smallData, 10, &requestSent); -/* requestSent == false: data absorbed locally, no Response needed */ -``` - -### State Rollback on Send Failure - -Before mutating the buffer state, the Request function snapshots `buffLen` and -the partial buffer contents. If `wh_Client_SendRequest()` fails (e.g., -transport error), the snapshot is restored so the caller can retry without data -loss: - -```c -/* Save state before mutation */ -savedBuffLen = sha->buffLen; -memcpy(savedBuffer, sha->buffer, sha->buffLen); - -/* ... mutate buffer, assemble wire payload ... */ - -ret = wh_Client_SendRequest(...); -if (ret != 0) { - /* Restore -- SHA state is as if the call never happened */ - sha->buffLen = savedBuffLen; - memcpy(sha->buffer, savedBuffer, savedBuffLen); -} -``` - -### Finalization - -The Final request sends whatever partial data remains in the client's buffer -(0 to `BLOCK_SIZE - 1` bytes) with `isLastBlock = 1`. The server handles -the padding and produces the final digest. The Final response copies the -digest to the caller's output buffer and resets the `wc_Sha*` context (via -`wc_InitSha*_ex`, preserving `devId`). - -### Stateless Server - -The server is fully stateless with respect to SHA operations. Each request -carries the complete intermediate hash state (`digest`, `loLen`, `hiLen`) in -the `resumeState` field. The server: - -1. Initializes a fresh `wc_Sha256` (or variant) context. -2. Restores `digest`, `loLen`, `hiLen` from the request. -3. Calls `wc_Sha256Update()` with the inline data. -4. If `isLastBlock`, calls `wc_Sha256Final()` and returns the digest. -5. Otherwise, returns the updated intermediate state. - -This design has a key benefit: **no server-side per-client hash state is -needed**. The server can handle SHA requests from multiple clients -interleaved without any context tracking. The tradeoff is larger messages -(~40-84 bytes of state overhead per request), which is negligible relative to -the data payload. - -The server also enforces invariants: -- Non-final updates: `inSz` must be a multiple of the block size. -- Final: `inSz` must be strictly less than one block. -- After processing a non-final update, `buffLen` must be 0 (sanity check). - -### Blocking Wrapper - -The existing `wh_Client_Sha256()` function is retained as a blocking wrapper -that loops over the async primitives: - -```c -int wh_Client_Sha256(whClientContext* ctx, wc_Sha256* sha256, - const uint8_t* in, uint32_t inLen, uint8_t* out) -{ - /* Update phase: chunk input to fit per-call capacity */ - while (consumed < inLen) { - capacity = _Sha256UpdatePerCallCapacity(sha256); - chunk = min(remaining, capacity); - - wh_Client_Sha256UpdateRequest(ctx, sha256, in + consumed, chunk, &sent); - if (sent) { - do { - ret = wh_Client_Sha256UpdateResponse(ctx, sha256); - } while (ret == WH_ERROR_NOTREADY); - } - consumed += chunk; - } - - /* Final phase */ - wh_Client_Sha256FinalRequest(ctx, sha256); - do { - ret = wh_Client_Sha256FinalResponse(ctx, sha256, out); - } while (ret == WH_ERROR_NOTREADY); -} -``` - -The crypto callback (`wh_Client_CryptoCb`) calls this blocking wrapper, so -existing code using `wc_Sha256Update()` / `wc_Sha256Final()` with -`devId = WH_DEV_ID` continues to work identically. - -### DMA Variant - -When `WOLFHSM_CFG_DMA` is enabled, a parallel set of DMA async functions is -available. The DMA variant differs from the inline variant in how bulk data -reaches the server: - -- **Inline (non-DMA)**: all input data is copied into the comm buffer message. -- **DMA**: whole blocks are referenced by address via a `DmaBuffer` descriptor - (the server reads them directly from client memory). Only the assembled first - block (from the partial buffer) or the final tail travels inline. - -The hash state (`resumeState`) always travels **inline**, not via DMA, for -cross-architecture concerns (endian translation, etc.) - -DMA async functions require the client to stash the translated DMA address -across the Request/Response boundary for POST cleanup. This context is stored -in `whClientContext.dma.asyncCtx.sha`: - -```c -typedef struct { - uintptr_t ioAddr; /* translated DMA address for POST */ - uintptr_t clientAddr; /* original client address for POST */ - uint64_t ioSz; /* DMA'd size for POST */ -} whClientDmaAsyncSha; -``` - -### API Reference - -All variants follow the same pattern. SHA-224 uses the SHA-256 wire format -(same block size); SHA-384 uses the SHA-512 wire format. - -#### Non-DMA - -```c -/* SHA-256 */ -int wh_Client_Sha256UpdateRequest(whClientContext* ctx, wc_Sha256* sha, - const uint8_t* in, uint32_t inLen, - bool* requestSent); -int wh_Client_Sha256UpdateResponse(whClientContext* ctx, wc_Sha256* sha); -int wh_Client_Sha256FinalRequest(whClientContext* ctx, wc_Sha256* sha); -int wh_Client_Sha256FinalResponse(whClientContext* ctx, wc_Sha256* sha, - uint8_t* out); - -/* SHA-224: identical pattern, s/256/224/ */ -/* SHA-384: identical pattern, s/256/384/, uses SHA-512 wire format */ -/* SHA-512: identical pattern, s/256/512/ */ -``` - -#### DMA - -```c -/* SHA-256 DMA (requires WOLFHSM_CFG_DMA) */ -int wh_Client_Sha256DmaUpdateRequest(whClientContext* ctx, wc_Sha256* sha, - const uint8_t* in, uint32_t inLen, - bool* requestSent); -int wh_Client_Sha256DmaUpdateResponse(whClientContext* ctx, wc_Sha256* sha); -int wh_Client_Sha256DmaFinalRequest(whClientContext* ctx, wc_Sha256* sha); -int wh_Client_Sha256DmaFinalResponse(whClientContext* ctx, wc_Sha256* sha, - uint8_t* out); - -/* SHA-224, SHA-384, SHA-512: same pattern */ -``` - -#### Blocking (unchanged, now wraps async internally) - -```c -int wh_Client_Sha256(whClientContext* ctx, wc_Sha256* sha, const uint8_t* in, - uint32_t inLen, uint8_t* out); -int wh_Client_Sha256Dma(whClientContext* ctx, wc_Sha256* sha, const uint8_t* in, - uint32_t inLen, uint8_t* out); -/* SHA-224, SHA-384, SHA-512: same pattern */ -``` - -### Design Tradeoffs - -| Decision | Tradeoff | -|----------|----------| -| **State on wire** | Larger messages (~40-84 bytes overhead), but the server is fully stateless and needs no per-client hash context | -| **Whole-block alignment** | Wastes up to `BLOCK_SIZE - 1` bytes of comm buffer capacity per message, but guarantees the server never has a partial block (simplifies server logic and invariant checking) | -| **Client-side partial buffering** | Requires wolfCrypt's buffer/buffLen fields, but avoids allocating separate storage and enables the `requestSent` optimization for small inputs | -| **Per-call capacity limit** | Callers of the async API must respect the capacity and chunk large inputs themselves (the blocking wrapper handles this automatically), but each call is bounded and predictable | -| **`requestSent` flag** | Adds a parameter to the API, but avoids unnecessary round-trips when input is absorbed entirely into the local buffer | -| **Snapshot/rollback on send failure** | Small CPU cost to copy the partial buffer, but guarantees SHA state consistency even on transport failures | - -## RNG: Single-Shot with Caller-Driven Chunking - -The RNG generate operation is the second algorithm to receive the async -treatment. Unlike SHA, RNG is **single-shot** -- there is no intermediate -state to carry, no partial-block buffering, and no multi-call Init/Update/Final -sequence. Each Request asks for N random bytes and the matching Response -delivers them. - -RNG is still interesting because the existing blocking API silently chunks -large requests into multiple round-trips when the caller asks for more bytes -than fit in one comm-buffer message. The async split has to decide where -that chunking logic lives. - -### Chunking Policy - -The async Request/Response pair is **single-shot per call**: one Request -produces one Response. Callers requesting more bytes than fit in a single -inline message must loop themselves. The per-call inline cap is exposed as: - -```c -#define WH_MESSAGE_CRYPTO_RNG_MAX_INLINE_SZ \ - (WOLFHSM_CFG_COMM_DATA_LEN - \ - (uint32_t)sizeof(whMessageCrypto_GenericResponseHeader) - \ - (uint32_t)sizeof(whMessageCrypto_RngResponse)) -``` - -Requests exceeding this cap (or of size zero) are rejected with -`WH_ERROR_BADARGS` before any bytes hit the wire. - -The existing blocking `wh_Client_RngGenerate()` function is retained as a -thin wrapper that chunks internally against the cap, so application code -using the wolfCrypt RNG callback path continues to work without changes: - -```c -int wh_Client_RngGenerate(whClientContext* ctx, uint8_t* out, uint32_t size) -{ - while (remaining > 0) { - uint32_t chunk = min(remaining, WH_MESSAGE_CRYPTO_RNG_MAX_INLINE_SZ); - uint32_t got = chunk; - wh_Client_RngGenerateRequest(ctx, chunk); - do { - ret = wh_Client_RngGenerateResponse(ctx, out, &got); - } while (ret == WH_ERROR_NOTREADY); - out += got; remaining -= got; - } -} -``` - -This keeps the async primitives predictable (each call is bounded by a single -round trip) and pushes the scheduling decision -- "when should I yield -between chunks?" -- up to the async caller, who is the only one with enough -context to answer it. - -### Response Size Negotiation - -The Response function takes an `inout_size` parameter: on entry it is the -capacity of the output buffer; on exit it is the actual number of bytes the -server wrote. This lets the caller distinguish short reads from bugs: - -```c -uint32_t got = requested; -ret = wh_Client_RngGenerateResponse(ctx, out, &got); -/* got may be < requested if the server returned a shorter reply */ -``` - -If the server somehow returns more bytes than the caller's buffer can hold -(should not happen, but defended against), the Response returns -`WH_ERROR_ABORTED` instead of overflowing. - -### DMA Variant - -The DMA variant bypasses the comm buffer entirely for the data payload: the -server writes random bytes directly into the client's output buffer via -translated DMA addresses. The Request/Response split introduces the same -address-stashing pattern used by SHA DMA: - -```c -typedef struct { - uintptr_t outAddr; /* translated DMA address */ - uintptr_t clientAddr; /* original client address (for POST) */ - uint64_t outSz; /* DMA'd size (0 means "nothing to clean up") */ -} whClientDmaAsyncRng; -``` - -Stored in `whClientContext.dma.asyncCtx.rng`, this context carries the -translated address across the Request/Response boundary so the Response can -perform the matching POST cleanup. - -Two points worth calling out: - -- **Fail-fast on occupied transport**: the DMA Request checks - `wh_CommClient_IsRequestPending()` *before* acquiring the DMA mapping. - Without this check, a request that would be rejected by `SendRequest` would - still leave a leaked DMA mapping behind, because the Response (which - normally releases the mapping) would never run. -- **POST runs on every non-NOTREADY exit**: once the Response receives a - reply -- success or otherwise -- it performs the POST cleanup - unconditionally, so the client buffer is safe to read regardless of the - final return code. - -Unlike the non-DMA variant, the DMA variant has no per-call size cap: the -server writes directly to client memory, so a single DMA call can fulfill -arbitrarily large requests. - -### API Reference - -```c -/* Non-DMA */ -int wh_Client_RngGenerateRequest(whClientContext* ctx, uint32_t size); -int wh_Client_RngGenerateResponse(whClientContext* ctx, uint8_t* out, - uint32_t* inout_size); - -/* DMA (requires WOLFHSM_CFG_DMA) */ -int wh_Client_RngGenerateDmaRequest(whClientContext* ctx, uint8_t* out, - uint32_t size); -int wh_Client_RngGenerateDmaResponse(whClientContext* ctx); - -/* Blocking (unchanged; now wraps the async primitives and chunks internally) */ -int wh_Client_RngGenerate(whClientContext* ctx, uint8_t* out, uint32_t size); -int wh_Client_RngGenerateDma(whClientContext* ctx, uint8_t* out, uint32_t size); -``` - -## AES: One-Shot with DMA Support - -AES modes (CBC, CTR, ECB, GCM) are all **one-shot** operations — every call -consumes a fixed buffer of input and returns a fixed buffer of output in a -single round-trip. There is no client-side partial-block accumulation across -Request/Response pairs the way SHA's `Update` family has, which makes the -async split significantly simpler than SHA. (CBC and CTR still carry -inter-call IV / counter state on the `Aes` struct — see *Mutable state* -below — but that state is updated atomically by the Response, not buffered -across calls.) - -- **No partial-block buffering** on the client. The entire plaintext or - ciphertext is handed to one Request and the full result comes back in - one Response. -- **No `requestSent` flag.** Each call sends exactly one request and - expects exactly one response. If the request's serialised size would - exceed `WOLFHSM_CFG_COMM_DATA_LEN`, the inline Request returns - `WH_ERROR_BADARGS` up front; DMA variants bypass the cap for payload - data. -- **No snapshot/rollback.** There is no local buffer to corrupt: the key - lives on `aes->devKey` (or as a cached keyId), the IV on `aes->reg`, and - these are read-only until the Response arrives. - -### Mutable state: IV and counter - -For **CBC** and **CTR**, the Response updates mutable state on the `Aes` -struct so subsequent calls chain correctly: - -- **CBC** — `aes->reg` is updated with the last ciphertext block. For - decryption, the Request captures the last ciphertext block from the - input buffer into `aes->reg` *before* sending, so in-place (input - pointer == output pointer) operation still produces the right chaining - state after the Response overwrites the plaintext. -- **CTR** — `aes->reg`, `aes->tmp`, and `aes->left` are updated from the - Response so the counter advances correctly for subsequent calls. CTR - is symmetric: callers should use `AES_ENCRYPTION` for the key schedule - and pass `enc = 1` in both directions. - -**ECB** and **GCM** carry no inter-call state on the `Aes` struct. For -GCM, the IV, AAD, and (on decrypt) the expected tag are passed as explicit -arguments on each call. - -### DMA variant contract - -The DMA pairs follow the same pattern as SHA DMA: - -1. **Fail-fast** on `wh_CommClient_IsRequestPending()` before acquiring - any DMA mapping, so a Request cannot be issued while another call is - still outstanding and cannot leak a translated address if - `wh_Client_SendRequest` later rejects the call. -2. **PRE-translate** input, output, and (for GCM) AAD buffers. Non-DMA - payload fields (key material, IV, auth tag) stay inline in the - request message. -3. **Stash** the translated addresses in `ctx->dma.asyncCtx.aes` so the - matching Response can issue POST cleanup. -4. **POST cleanup** runs on every non-`WH_ERROR_NOTREADY` return from the - Response, so the caller's buffers are safe to read regardless of - success or error. -5. The caller must keep the input, output, and AAD buffers valid until - the Response returns something other than `WH_ERROR_NOTREADY`. - -### API Reference - -Inline (non-DMA) pairs: - -- `wh_Client_AesCbcRequest` / `wh_Client_AesCbcResponse` -- `wh_Client_AesCtrRequest` / `wh_Client_AesCtrResponse` -- `wh_Client_AesEcbRequest` / `wh_Client_AesEcbResponse` -- `wh_Client_AesGcmRequest` / `wh_Client_AesGcmResponse` - -DMA pairs (require `WOLFHSM_CFG_DMA`): - -- `wh_Client_AesCbcDmaRequest` / `wh_Client_AesCbcDmaResponse` -- `wh_Client_AesCtrDmaRequest` / `wh_Client_AesCtrDmaResponse` -- `wh_Client_AesEcbDmaRequest` / `wh_Client_AesEcbDmaResponse` -- `wh_Client_AesGcmDmaRequest` / `wh_Client_AesGcmDmaResponse` - -The existing blocking wrappers (`wh_Client_AesCbc`, `wh_Client_AesCtr`, -`wh_Client_AesEcb`, `wh_Client_AesGcm`, and their `*Dma` variants) are now -thin shells that call the new async primitives in a poll loop, so blocking -and async paths share identical wire behaviour. - -## CMAC - -CMAC is the latest algorithm to receive native async support. Unlike SHA, the -existing blocking `wh_Client_Cmac` was already a single oneshot wire round -trip when called with a complete message + key + output. To preserve that -1-RTT behavior while also exposing streaming Update/Final pairs, CMAC ships -with **three** async pairs (six functions total per non-DMA / DMA variant): - -```c -/* Oneshot: full message + key + output in a single round trip */ -int wh_Client_CmacGenerateRequest (whClientContext*, Cmac*, CmacType, - const uint8_t* key, uint32_t keyLen, - const uint8_t* in, uint32_t inLen, - uint32_t outMacLen); -int wh_Client_CmacGenerateResponse(whClientContext*, Cmac*, - uint8_t* outMac, uint32_t* outMacLen); - -/* Streaming: separate Update and Final phases, possibly multiple Updates */ -int wh_Client_CmacUpdateRequest (whClientContext*, Cmac*, CmacType, - const uint8_t* key, uint32_t keyLen, - const uint8_t* in, uint32_t inLen, - bool* requestSent); -int wh_Client_CmacUpdateResponse (whClientContext*, Cmac*); - -int wh_Client_CmacFinalRequest (whClientContext*, Cmac*); -int wh_Client_CmacFinalResponse (whClientContext*, Cmac*, - uint8_t* outMac, uint32_t* outMacLen); -``` - -Each function has a DMA counterpart that transfers the input via DMA. The -output MAC is always returned inline (16 bytes max). Note the slight naming -asymmetry: the oneshot is `wh_Client_CmacGenerateDmaRequest`/`Response`, -while streaming uses `wh_Client_CmacDmaUpdate{Request,Response}` and -`wh_Client_CmacDmaFinal{Request,Response}`. - -### Why three pairs - -- **Generate** is a true oneshot. The server dispatches to - `wc_AesCmacGenerate_ex`, which performs init / update / final in one - call. This preserves the 1-RTT performance of the existing blocking API - for callers that have the full message in hand at once. -- **Update / Final** form the streaming pair. Each Update sends the - current input chunk plus the full CMAC state (`buffer`, `bufferSz`, - `digest`, `totalSz`); each Response carries back the updated state. - Final sends an empty input with `outSz = AES_BLOCK_SIZE`, telling the - server to finalize and return the MAC. - -### Why no client-side partial-block buffering - -SHA does client-side partial-block buffering because `wc_Sha256Update` -processes complete blocks immediately and leaves bufferSz = 0 after -absorbing whole blocks. CMAC is different: `wc_CmacUpdate` deliberately -withholds the *last* whole block in its partial buffer until the next -Update arrives (or Final is called), because the last block has special -key-derived XOR handling. As a result, after a server-side Update the -CMAC buffer can hold any value from 0..AES_BLOCK_SIZE bytes. Imposing a -"bufferSz must be 0 on the wire" invariant (as SHA does) would break -correctness, so CMAC instead round-trips the entire CMAC state on every -Update Request/Response pair. - -### Blocking-wrapper dispatch - -`wh_Client_Cmac` and `wh_Client_CmacDma` retain their existing signatures -and now auto-detect the oneshot case at the top of the function. The -wrapper delegates to `CmacGenerate*` for a single round trip when all of -the following hold: - -- a complete message is supplied (`in`/`inLen` non-NULL/non-zero); -- an output buffer is supplied (`outMac`/`outMacLen` non-NULL, `*outMacLen > 0`); -- for non-DMA, `inLen` fits the inline cap - `WH_MESSAGE_CRYPTO_CMAC_MAX_INLINE_GENERATE_SZ` (DMA has no per-call cap); -- *and* either an explicit key is provided (`key`/`keyLen`), which matches - `wc_AesCmacGenerate_ex` semantics — prior cmac state is irrelevant and may - even be uninitialized — *or* the cmac struct is in fresh state - (`bufferSz == 0 && totalSz == 0`) so an HSM-cached keyId can be used - without losing in-progress data. - -Otherwise (incremental usage, mid-stream state, Final-only call, or -oversize input on non-DMA), the wrapper falls back to the streaming -`CmacUpdate*` + `CmacFinal*` pair. - -### Per-request key - -CMAC's server is stateless: every Request must carry the key. HSM-cached -keys are referenced by `keyId` (set on the cmac via -`wh_Client_CmacSetKeyId`). For inline keys, the bytes are stashed into -`cmac->aes.devKey` on the first Request and replayed on subsequent -Update/Final Requests automatically. - -### DMA wire format - -The CMAC DMA request struct (`whMessageCrypto_CmacAesDmaRequest`) gained -a new `inlineInSz` field carrying inline trailing input (for an -assembled first block, when client-side buffering is desired by some -caller). The current async clients always pass `inlineInSz = 0` and -route input either via DMA (for Update/Generate) or omit input -(Final). The field is reserved for future client-side buffering use -without another wire-format change. - -## Roadmap: Remaining Algorithms - -The async split pattern will be applied algorithm by algorithm to all crypto -operations currently handled by `wh_Client_CryptoCb`. The table below shows -the full set of operations and their planned async status. - -**Completed:** - -| Algorithm | Functions | Notes | -|----------------|----------------------------------|-------| -| SHA-256 | Update/Final Request/Response | Non-DMA and DMA variants | -| SHA-224 | Update/Final Request/Response | Shares SHA-256 wire format | -| SHA-384 | Update/Final Request/Response | Shares SHA-512 wire format | -| SHA-512 | Update/Final Request/Response | Non-DMA and DMA variants | -| RNG Generate | `wh_Client_RngGenerate{Request,Response}` and DMA variants | Single-shot per call; non-DMA callers chunk against `WH_MESSAGE_CRYPTO_RNG_MAX_INLINE_SZ`, DMA has no per-call cap | -| AES-CBC | `wh_Client_AesCbc{,Dma}{Request,Response}` | Non-DMA and DMA variants | -| AES-CTR | `wh_Client_AesCtr{,Dma}{Request,Response}` | Non-DMA and DMA variants | -| AES-ECB | `wh_Client_AesEcb{,Dma}{Request,Response}` | Non-DMA and DMA variants | -| AES-GCM | `wh_Client_AesGcm{,Dma}{Request,Response}` | Non-DMA and DMA variants; AAD supports DMA | -| CMAC | Generate / Update / Final Request/Response | Three async pairs: oneshot Generate (1-RTT) plus streaming Update/Final. Non-DMA and DMA variants. Blocking wrapper auto-dispatches to oneshot when conditions allow. | - -**Planned:** - -| Algorithm | Functions | Complexity | Notes | -|-------------------|--------------------------------------------|------------|-------| -| RSA Sign/Verify | `wh_Client_RsaFunction{Request,Response}` | Low | Single-shot; may need auto-import removed from Request | -| RSA Get Size | `wh_Client_RsaGetSize{Request,Response}` | Low | Trivial query | -| ECDSA Sign | `wh_Client_EccSign{Request,Response}` | Low | Single-shot | -| ECDSA Verify | `wh_Client_EccVerify{Request,Response}` | Low | Single-shot | -| ECDH | `wh_Client_EccSharedSecret{Request,Response}` | Low | Single-shot | -| Curve25519 | `wh_Client_Curve25519SharedSecret{Request,Response}` | Low | Single-shot | -| Ed25519 Sign | `wh_Client_Ed25519Sign{Request,Response}` | Low | Single-shot | -| Ed25519 Verify | `wh_Client_Ed25519Verify{Request,Response}`| Low | Single-shot | -| ML-DSA Sign | `wh_Client_MlDsaSign{Request,Response}` | Low | Post-quantum; single-shot | -| ML-DSA Verify | `wh_Client_MlDsaVerify{Request,Response}` | Low | Post-quantum; single-shot | - -Most remaining algorithms are **single-shot** operations (one request, one -response) and are straightforward to split compared to SHA's streaming -semantics. SHA was done first because it exercises the hardest design -constraints: multi-round-trip streaming, partial-block buffering, and state -resumption. - -### Future: Async Crypto Callbacks - -The long-term goal is to also make the **crypto callback path itself -asynchronous**, so that standard wolfCrypt API calls (`wc_Sha256Update`, -`wc_AesCbcEncrypt`, etc.) can return a "not ready" indicator and be resumed -later, rather than blocking. This requires changes in wolfCrypt's crypto -callback infrastructure and is outside the scope of the current native async -API work. The native async API being introduced here is a prerequisite: it -establishes the per-algorithm Request/Response split that a future async -callback mechanism will build upon. diff --git a/docs/draft/auth.md b/docs/draft/auth.md deleted file mode 100644 index fcb67f13f..000000000 --- a/docs/draft/auth.md +++ /dev/null @@ -1,516 +0,0 @@ -# wolfHSM Authentication Manager — PR #270 Overview (v2) - ---- - -## 1. TL;DR - -PR #270 introduces a **PKCS11-flavored Authentication/Authorization Manager** to wolfHSM. It provides: - -- **Login/logout** with two credential methods: **PIN** (SHA-256 hashed) or **X.509 certificate**. -- A **user database** managed via add/delete/get and set-permissions / set-credentials APIs. -- A **permission model** of (admin flag) + (per-group allow boolean) + (per-group bitmap of 256 allowed actions) + (a small per-user list of accessible key IDs — not yet wired into crypto paths). -- A **server-side request gate** that, on every incoming request, consults the Auth Manager and rejects messages the current session is not permitted to run. -- A **message group** `WH_MESSAGE_GROUP_AUTH = 0x0D00` with 7 new actions (login, logout, user add/delete/get, set-permissions, set-credentials), complete with endian/magic translation functions. -- A **pluggable backend**: everything goes through a `whAuthCb` callback vtable. A default backend lives in `src/wh_auth_base.c` (up to 5 users, credential storage up to 2 KiB per user, optional NVM persistence with credentials split into per-user objects, used by examples and tests). -- The feature is **opt-in**: entire subsystem is guarded by `WOLFHSM_CFG_ENABLE_AUTHENTICATION`. With it compiled in but no context configured (`server->auth == NULL`), the server logs a SECEVENT and processes all requests without any authorization check — preserving backwards compatibility. - -Design notes called out by the author: -- The "check key use" callback (`CheckKeyAuthorization`) is wired into the interface but **not yet invoked** on the key paths — it's a TODO placeholder. -- The base user list lives in a RAM index and is optionally persisted to NVM (config `whAuthBaseConfig.nvm`; NULL = RAM-only). Credentials are split into per-user NVM objects, and a corrupt/incompatible persisted database is a fatal init error; see [6.1 Persistence (NVM)](#61-persistence-nvm). -- Logging of auth events (login success/failure, crypto actions) is another TODO, though authorization failures already log via `WH_LOG_ON_ERROR_F`. - ---- - -## 2. High-level architecture - -``` - ┌──────────────────────┐ - │ Client App │ - │ wh_Client_Auth*() │ client-side request/response helpers - └──────────┬───────────┘ - │ WH_MESSAGE_GROUP_AUTH (0x0D00) - │ - ┌──────────────▼────────────────────────────────────────────┐ - │ Server dispatch │ - │ wh_Server_HandleRequestMessage() (src/wh_server.c) │ - │ 1. Recv packet -> extract (group, action) │ - │ 2. wh_Auth_CheckRequestAuthorization(group, action) │ <-- the gate - │ 3. Dispatch by group │ - └──────────────┬─────────────────────────┬──────────────────┘ - │ │ - WH_MESSAGE_GROUP_AUTH any other group - │ │ - ▼ ▼ - ┌─────────────────────────┐ (NVM/key/crypto/SHE/etc. handlers; - │ wh_Server_HandleAuth- │ they do not re-check auth — the gate - │ Request() │ above has already vetted the call) - │ (src/wh_server_auth.c) │ - └───────┬─────────────────┘ - │ wh_Auth_Login / _Logout / _UserAdd / _UserDelete / - │ _UserGet / _UserSetPermissions / _UserSetCredentials - ▼ - ┌─────────────────────────┐ - │ Auth Manager core │ transport/protocol-agnostic wrappers - │ src/wh_auth.c │ that take the lock and delegate to cb - └───────┬─────────────────┘ - │ whAuthCb->Login / ->UserAdd / ... - ▼ - ┌─────────────────────────┐ - │ Pluggable backend │ - │ default: wh_auth_base │ user db (RAM index + optional NVM), SHA-256 PIN hashing, - │ (src/wh_auth_base.c) │ optional wolfSSL cert verification - └─────────────────────────┘ -``` - -Key separation of concerns: - -1. `wh_auth.h` / `wh_auth.c` — **the "front end":** public API, session state, locking, policy decisions (the default group+action bitmap check). Always compiled when auth is on; does not depend on any specific user-store format. -2. `wh_auth_base.h` / `wh_auth_base.c` — **reference backend:** owns the user list, hashes PINs, verifies certificates, stores permissions. Can be swapped for a custom backend by registering a different `whAuthCb` vtable. -3. `wh_message_auth.h` / `wh_message_auth.c` — wire format and endian translation for all 7 auth messages (plus a flatten/unflatten pair for the permissions struct, which is too large and array-heavy for the usual `WH_T*()` helpers). -4. `wh_server_auth.c` / `wh_client_auth.c` — the message handlers on each side, each of which lives under both `WOLFHSM_CFG_ENABLE_{SERVER,CLIENT}` and `WOLFHSM_CFG_ENABLE_AUTHENTICATION` guards. - ---- - -## 3. Files touched (grouped) - -| Area | Files | -|------|-------| -| New public headers | `wolfhsm/wh_auth.h`, `wolfhsm/wh_auth_base.h`, `wolfhsm/wh_message_auth.h`, `wolfhsm/wh_server_auth.h` | -| Core & base impl | `src/wh_auth.c`, `src/wh_auth_base.c`, `src/wh_message_auth.c` | -| Server integration | `src/wh_server.c`, `src/wh_server_auth.c`, `wolfhsm/wh_server.h` | -| Client integration | `src/wh_client.c`, `src/wh_client_auth.c`, `wolfhsm/wh_client.h` | -| Error / message enums | `wolfhsm/wh_error.h` (3 new codes), `wolfhsm/wh_message.h` (new group + actions) | -| Examples | `examples/posix/wh_posix_server/wh_posix_server_cfg.c`, `wh_posix_server_cfg.h`, `wh_posix_server.c`; `examples/posix/wh_posix_client/Makefile`; `examples/posix/wh_posix_server/Makefile`; `examples/demo/client/wh_demo_client_auth.{c,h}`; `examples/demo/client/wh_demo_client_all.c` | -| Tests | `test/wh_test_auth.{c,h}` (1440 LOC), hook-ins in `test/wh_test.c`, `wh_test_clientserver.c`, `wh_test_crypto.c`, `wh_test_keywrap.c`, `wh_test_she.c`, `wh_test_log.c`, `wh_test_posix_threadsafe_stress.c`, `wh_test_common.h`, `test/Makefile` | -| Misc | `src/wh_server_she.c` (SHE tests now log in as admin), `src/wh_utils.c` + `wolfhsm/wh_utils.h` (new `wh_Utils_ForceZero` and `wh_Utils_ConstantCompare`), `port/posix/posix_transport_tls.c`, CI workflows, `docs/src/chapter09.md` (new docs chapter) | - ---- - -## 4. Data model (what an "individual user" looks like) - -### 4.1 Identity: `whUserId` - -`whUserId` is a `uint16_t`. Zero is reserved as `WH_USER_ID_INVALID`. The base backend assigns IDs 1..`WH_AUTH_BASE_MAX_USERS` (5 by default), where the ID is literally the 1-based slot in the static users array (`id = slot_index + 1`). - -### 4.2 Credentials: `whAuthMethod` - -```c -typedef enum { - WH_AUTH_METHOD_NONE = 0, - WH_AUTH_METHOD_PIN, // SHA-256 hashed when crypto is enabled - WH_AUTH_METHOD_CERTIFICATE, // wolfSSL cert verification; gated on WOLFHSM_CFG_CERTIFICATE_MANAGER -} whAuthMethod; -``` - -- PIN: the base backend stores the 32-byte SHA-256 of the PIN (falls back to a direct copy when `WOLFHSM_CFG_NO_CRYPTO` is set). Comparison uses `wh_Utils_ConstantCompare` — a new utility added by this PR. -- Certificate: the user's stored "credential" is a CA in DER; login presents a leaf cert, which the base backend feeds through `wolfSSL_CertManagerLoadCABuffer` + `wolfSSL_CertManagerVerifyBuffer`. - -### 4.3 Permissions: `whAuthPermissions` - -#### 4.3.1 The two-tier concept - -Every wolfHSM request on the wire is identified by a 16-bit `kind` that splits into: - -- **Group** (high byte): the category of operation. `WH_MESSAGE_GROUP_*` defines 13 groups today (`wolfhsm/wh_message.h:38-50`): `COMM=0x0100`, `NVM=0x0200`, `KEY=0x0300`, `CRYPTO=0x0400`, `IMAGE=0x0500`, `PKCS11=0x0600`, `SHE=0x0700`, `COUNTER=0x0800`, `CUSTOM=0x0A00`, `CRYPTO_DMA=0x0B00`, `CERT=0x0C00`, `AUTH=0x0D00`. -- **Action** (low byte): the specific operation within that group. Action enums are *group-local* — `KEY_CACHE` and `CRYPTO_SIGN` may both be value `0`, but they live in different groups so they're unambiguous in context. - -The auth manager mirrors that split in the user's `whAuthPermissions` struct as **two independent filters that both must pass** before a request is admitted to its handler: - -```c -typedef struct { - uint8_t groupPermissions[WH_NUMBER_OF_GROUPS + 1]; // boolean allow per group; last byte = admin flag - uint32_t actionPermissions[WH_NUMBER_OF_GROUPS][WH_AUTH_ACTION_WORDS]; // 256 bits per group (8 x uint32_t) - uint16_t keyIdCount; - uint32_t keyIds[WH_AUTH_MAX_KEY_IDS]; // small allowlist; default WH_AUTH_MAX_KEY_IDS = 2 -} whAuthPermissions; -``` - -#### 4.3.2 Filter 1 — group boolean - -`groupPermissions[groupIndex]` (where `groupIndex = (group >> 8) & 0xFF`) is a single byte: nonzero means "this user is allowed to talk to this group at all." If it's 0, the request is denied without ever looking at the bitmap. It's a fast reject path *and* a coarse on/off switch — useful for "this user only ever uses NVM, never crypto." - -The `+1` slot at the end (`groupPermissions[WH_NUMBER_OF_GROUPS]`) is reused as the **admin flag** — `WH_AUTH_IS_ADMIN(p)` reads it. Admin isn't a group; it's a separate capability that gates things like `UserAdd` of another admin and cross-user logout. - -#### 4.3.3 Filter 2 — action bitmap - -If the group passes, the gate then checks the per-group **256-bit bitmap** stored as 8 × `uint32_t`. The mapping is straightforward (`wolfhsm/wh_auth.h:79`): - -```c -wordIdx = action / 32 -bitMask = 1U << (action % 32) -allowed = actionPermissions[groupIndex][wordIdx] & bitMask -``` - -Wire actions are `uint16_t`, so 65 536 are theoretically possible — the model caps at 256 and rejects anything beyond. That's a deliberate trade: 256 bits per group keeps the struct flat and copy-friendly (the whole `whAuthPermissions` flattens to ~473 bytes for the wire) at the cost of an upper limit on actions per group. Today no group comes close. - -#### 4.3.4 Derived constants and byte shape - -- `WH_NUMBER_OF_GROUPS = (WH_MESSAGE_GROUP_MAX >> 8) + 1` — currently 14, since `WH_MESSAGE_GROUP_MAX = WH_MESSAGE_GROUP_AUTH = 0x0D00`. -- `WH_AUTH_ACTIONS_PER_GROUP = 256`, `WH_AUTH_ACTION_WORDS = 8`. - -Shape in bytes (exactly what gets flattened on the wire, `WH_FLAT_PERMISSIONS_LEN`): -``` -(WH_NUMBER_OF_GROUPS + 1) // group booleans + admin -+ 4 * WH_NUMBER_OF_GROUPS * WH_AUTH_ACTION_WORDS // action bitmap (per-group) -+ 2 // keyIdCount -+ 4 * WH_AUTH_MAX_KEY_IDS // keyIds -= 15 + (4 * 14 * 8) + 2 + 8 = 473 bytes // with current defaults -``` - -#### 4.3.5 Helper macros - -The two filters compose through the macros in `wolfhsm/wh_auth.h:86-125`: - -| Macro | Group byte | Action bitmap | -|-------|------------|---------------| -| `WH_AUTH_IS_ADMIN(p)` | reads admin slot (byte index `WH_NUMBER_OF_GROUPS`) | — | -| `WH_AUTH_SET_IS_ADMIN(p, v)` | writes admin slot (`v ? 1 : 0`) | — | -| `WH_AUTH_SET_ALLOWED_GROUP(p, group)` | set to 1 | set all 256 bits | -| `WH_AUTH_SET_ALLOWED_ACTION(p, group, action)` | set to 1 | OR in one bit (existing bits preserved — Copilot flagged a mismatch with the header comment that says "only the given action bit") | -| `WH_AUTH_CLEAR_ALLOWED_GROUP(p, group)` | set to 0 | zero all bits | -| `WH_AUTH_CLEAR_ALLOWED_ACTION(p, group, action)` | left alone | clear one bit | - -So enabling a single action also implicitly enables its group, but disabling a single action leaves the group enabled (you can still use *other* actions in it). Disabling the group nukes everything. - -#### 4.3.6 Worked examples - -**Crypto-only signer:** group byte set for `CRYPTO`, only the `SIGN` action bit set in the CRYPTO bitmap, every other group byte = 0. Any NVM/KEY/IMAGE/etc. request hits filter 1 and is denied; any CRYPTO request other than `SIGN` passes filter 1 but fails filter 2. - -**Admin everything:** the example POSIX server seeds admin with `memset(&permissions, 0xFF, sizeof(permissions))` (`examples/posix/wh_posix_server/wh_posix_server_cfg.c:719`). That sets every group byte, every action bit, and the admin slot in one shot. - -#### 4.3.7 Exceptions to the bitmap - -The gate (`src/wh_auth.c:206`) hard-codes a few unconditional allows that bypass both filters: - -- Unauthenticated session → all of group `COMM`, plus `(AUTH, LOGIN)`. Without these, no one could ever open a connection or log in. -- Authenticated session → `(AUTH, LOGOUT)` always succeeds. You can always log yourself out regardless of permission state. - -Everything else flows through the two-tier filter described above, optionally followed by the backend's `cb->CheckRequestAuthorization` override (see §7.1) which sees the tentative verdict and can flip it either way. - -### 4.4 Session: `whAuthUser` / `whAuthContext` - -```c -typedef struct { - whUserId user_id; - char username[32]; - whAuthPermissions permissions; - bool is_active; -} whAuthUser; - -struct whAuthContext_t { - whAuthCb* cb; // backend vtable - whAuthUser user; // *the* currently-logged-in user for this connection - void* context; // opaque backend state -#ifdef WOLFHSM_CFG_THREADSAFE - whLock lock; -#endif -}; -``` - -A single `whAuthContext` holds **one** logged-in user at a time (src/wh_auth.c:139: "allowing only one user logged in to an open connection at a time"). A second login attempt while someone is already logged in returns success at the protocol level with `loggedIn=0` set — i.e. the call was processed fine, authentication simply didn't happen. - ---- - -## 5. The plugin contract: `whAuthCb` - -The core wraps every operation, acquires the lock, and then calls into this vtable: - -```c -typedef struct { - int (*Init)(void* ctx, const void* cfg); - int (*Cleanup)(void* ctx); - - int (*Login)(void* ctx, uint8_t client_id, whAuthMethod method, - const char* username, const void* auth_data, uint16_t auth_data_len, - whUserId* out_user_id, whAuthPermissions* out_permissions, - int* loggedIn); - int (*Logout)(void* ctx, whUserId current_user_id, whUserId user_id); - - /* Optional authorization-decision overrides */ - int (*CheckRequestAuthorization)(void* ctx, int err, uint16_t user_id, - uint16_t group, uint16_t action); - int (*CheckKeyAuthorization)(void* ctx, int err, uint16_t user_id, - uint32_t key_id, uint16_t action); - - /* User management */ - int (*UserAdd)(void* ctx, const char* username, whUserId* out_user_id, - whAuthPermissions permissions, whAuthMethod method, - const void* credentials, uint16_t credentials_len); - int (*UserDelete)(void* ctx, whUserId current_user_id, whUserId user_id); - int (*UserSetPermissions)(void* ctx, whUserId current_user_id, - whUserId user_id, whAuthPermissions permissions); - int (*UserGet)(void* ctx, const char* username, - whUserId* out_user_id, whAuthPermissions* out_permissions); - int (*UserSetCredentials)(void* ctx, whUserId user_id, whAuthMethod method, - const void* current_credentials, uint16_t current_credentials_len, - const void* new_credentials, uint16_t new_credentials_len); -} whAuthCb; -``` - -The two "Check*" callbacks are **overrides, not gates** — see §7 below for exactly how they're layered over the default decision. - ---- - -## 6. Default backend (`wh_auth_base.c`) - -- **Storage:** `static whAuthBase_User users[WH_AUTH_BASE_MAX_USERS]` (=5). Each slot is a metadata-only index entry: the public `whAuthUser`, the chosen `method`, and `credentials_len` (0 = none). Credential material is not held in this array; it lives separately (per-user NVM object, or a RAM fallback table when no NVM is configured) and is pulled into a force-zeroed scratch buffer only for the duration of an auth or credential-change operation. Optionally persisted to NVM; see [6.1 Persistence (NVM)](#61-persistence-nvm). -- **Thread safety:** explicitly documented (src/wh_auth_base.c:54) — the global array is protected by the auth context's lock which the core `wh_Auth_*` wrappers acquire before calling any backend entry. The backend itself does no locking. -- **PIN path:** `wh_Auth_BaseCheckPin` hashes the incoming PIN with `wc_Sha256Hash_ex` and compares to the stored digest using `wh_Utils_ConstantCompare`. Hash buffer is `wh_Utils_ForceZero`d on exit whether the compare succeeded or not. When `WOLFHSM_CFG_NO_CRYPTO` is set, the PIN is stored verbatim (bounded by `WH_AUTH_BASE_MAX_CREDENTIALS_LEN`). -- **Certificate path:** guarded by `WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO`. Uses a per-call `WOLFSSL_CERT_MANAGER` seeded with the user's stored DER as a CA and then verifies the supplied leaf. -- **Admin enforcement:** `wh_Auth_BaseUserDelete` and `wh_Auth_BaseUserSetPermissions` both require `current_user_id` (the caller session) to have the admin flag. `wh_Auth_BaseLogout` allows logging out someone *other* than yourself only if you're admin. -- **Set-credentials:** if the target user already has credentials, the old ones must be presented and match (constant-time compare, PIN hashed first); otherwise `current_credentials` must be NULL. PINs are rehashed before replacement, and intermediate hash buffers are force-zeroed. -- **User ID policy:** 1-based indexes into `users[]`; 0 reserved. Duplicate usernames are rejected by `wh_Auth_BaseUserAdd` with `WH_ERROR_BADARGS`. `keyIdCount` is clamped to `WH_AUTH_MAX_KEY_IDS` and unused `keyIds` entries are zeroed (done both on add and on set-permissions). - -### 6.1 Persistence (NVM) - -The user database is optionally persisted to NVM. Persistence is selected purely by the config passed to `wh_Auth_BaseInit`: - -```c -whAuthBaseConfig cfg = {0}; -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. 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)`. - -Modes: - -- NVM not configured (`cfg.nvm == NULL`, or a NULL config): the backend runs RAM-only. `s_auth_base_nvm` stays NULL, no NVM operations are performed (nothing read at init, nothing written on change), and credentials live in the `s_auth_base_ram_cred` fallback table. All state is lost on restart. -- NVM configured (`cfg.nvm != NULL`): the index is loaded at init; the index and the affected credential object are rewritten on every change. - -> Note on `WOLFHSM_CFG_AUTH_BASE_NVM_ONLY` (RAM savings): the RAM-only mode requires a static fallback table sized to hold every user's full credential blob at once, `s_auth_base_ram_cred[WH_AUTH_BASE_MAX_USERS][WH_AUTH_BASE_MAX_CREDENTIALS_LEN]`, i.e. `5 x 2048 = 10 KiB` at the defaults. On a memory-constrained target that always configures an NVM backend, define `WOLFHSM_CFG_AUTH_BASE_NVM_ONLY` to compile that table out entirely (and drop support for the `nvm == NULL` configuration). Credentials then only ever occupy the single shared `s_auth_base_cred_buf` scratch buffer (`WH_AUTH_BASE_MAX_CREDENTIALS_LEN` = 2 KiB, force-zeroed after each use) while one credential is being read or written, rather than a per-user resident copy. That reclaims roughly `(MAX_USERS - 1) x MAX_CREDENTIALS_LEN` of RAM. - -There is no separate "create/format the database" step. Setup is just init followed by adding the first user: - -1. First boot on blank NVM: `wh_Auth_BaseInit` -> `wh_Auth_BaseLoadFromNvm` -> `wh_Nvm_GetMetadata(WH_NVM_ID_AUTH_USER_INDEX)` returns `WH_ERROR_NOTFOUND`, and init succeeds with an empty in-RAM index. No NVM write happens yet. -2. First `wh_Auth_BaseUserAdd(...)` materializes the on-NVM database: it persists the index object and writes the new user's credential object. Every subsequent add, delete, set-permissions, or set-credentials rewrites the index (via `wh_Nvm_AddObjectWithReclaim`, so superseded copies are reclaimed) and the touched credential object. Failed multi-object updates roll back from an in-RAM backup of the affected index entry and credential blob. -3. Later boots: the index is read back into `users[]`; credentials are pulled on demand per login. Session state is not persisted. `is_active` is forced to `false` for every slot on load, so a restart always starts with everyone logged out. - -On-NVM index format: a 4-byte magic (`0x57484142`, "WHAB") followed by one serialized `whAuthUser` record per slot. The magic is the sole format sentinel; there is intentionally no version field and no migration path. The stored index length must equal `WH_AUTH_BASE_NVM_INDEX_SIZE`, derived from `sizeof(whAuthUser)` and `WH_AUTH_BASE_MAX_USERS`, so any change to the persisted record layout or the max-user count changes the expected length. - -Corrupt or incompatible database is fatal. If NVM is configured and an index object exists under `WH_NVM_ID_AUTH_USER_INDEX` but its length does not match `WH_AUTH_BASE_NVM_INDEX_SIZE`, or its magic does not match, `wh_Auth_BaseLoadFromNvm` returns `WH_ERROR_ABORTED` and `wh_Auth_BaseInit` fails. This is deliberate: silently starting with an empty user database (e.g. after a firmware upgrade that changed the index layout) could drop every provisioned user or lock out the admin, a security-relevant surprise for a trusted component. The operator must reprovision deliberately. This is distinct from the blank-NVM case (`WH_ERROR_NOTFOUND`), which is not an error. - -Locking: `wh_Auth_Base*` backend entries are already called under the auth context lock. The NVM accesses additionally take the NVM lock (`WH_NVM_LOCK`/`WH_NVM_UNLOCK`) around the metadata/read/write, since the NVM API does not lock internally. - ---- - -## 7. Authorization — how the server enforces it on *every* request - -The integration point is **`wh_Server_HandleRequestMessage`** in `src/wh_server.c`. After a packet is received and the `(group, action)` are extracted, before the switch on `group`: - -```c -#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION - if (server->auth != NULL) { - rc = wh_Auth_CheckRequestAuthorization(server->auth, group, action); - if (rc != WH_ERROR_OK) { - int32_t error_code = (int32_t)WH_AUTH_PERMISSION_ERROR; - uint16_t resp_size = _FormatAuthErrorResponse(magic, group, action, - error_code, 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_AUTH_PERMISSION_ERROR, - "Authorization failed for (group=%d, action=%d, seq=%d)", - group, action, seq); - return rc; - } - } -#endif -``` - -Two deliberate design points here: - -1. **The check happens once per request**, up-front, on the front end — as explicitly requested by @bigbrett in review ("the actual authorization check … should be part of the generic 'front end' and not delegated to the back-end"). Backends only affect authorization through the optional override callback. -2. **When `server->auth == NULL`** (auth compiled in but not configured) the check is skipped entirely and a SECEVENT log line is emitted at init time to announce this. That keeps existing code/tests working without having to introduce logins everywhere. - -### 7.1 The default decision (`wh_Auth_CheckRequestAuthorization`) - -In `src/wh_auth.c` the flow inside the lock is: - -1. Read `user_id = context->user.user_id`. -2. **If no user is logged in** (`user_id == WH_USER_ID_INVALID`): - - Allow `WH_MESSAGE_GROUP_COMM` (so a client can still perform comm handshakes/echo/close). - - Allow `WH_MESSAGE_GROUP_AUTH` + `WH_MESSAGE_AUTH_ACTION_LOGIN` (so a client can actually log in). - - Deny everything else → `WH_ERROR_ACCESS`. -3. **If a user is logged in:** - - Always allow `WH_MESSAGE_GROUP_AUTH` + `WH_MESSAGE_AUTH_ACTION_LOGOUT` (you can always log yourself out). - - Otherwise, look up `groupIndex = (group >> 8) & 0xFF`: - - Bounds-check `groupIndex < WH_NUMBER_OF_GROUPS`. - - If `permissions.groupPermissions[groupIndex] == 0` → deny. - - If `action >= WH_AUTH_ACTIONS_PER_GROUP` → deny. - - Map `action → (wordIdx, bitMask)` via `WH_AUTH_ACTION_TO_WORD_AND_BITMASK`. Allow iff the bit is set in `permissions.actionPermissions[groupIndex][wordIdx]`. -4. **Override hook:** if `cb->CheckRequestAuthorization != NULL`, invoke it with the preliminary `rc`, the user id, and the (group, action). Its return becomes the final decision. This is what `test/wh_test_auth.c` exercises to confirm backends can see the result and flip it either direction. - -If this stage denies the request, the server synthesizes a per-group/per-action error response via `_FormatAuthErrorResponse` (new helper) so the client always gets a well-formed reply carrying `WH_AUTH_PERMISSION_ERROR`. The helper handles the three auth responses that are bigger than `SimpleResponse` (Login / UserAdd / UserGet), the oversized NVM ones, the Cert group, and falls back to writing just a translated `int32_t` rc for everything else. - -### 7.2 Key-level authorization — deferred - -`wh_Auth_CheckKeyAuthorization` and the `CheckKeyAuthorization` callback are defined and tested (presence-of-callback), but **no current request handler calls it**. The PR author called this out explicitly: "I added a callback function framework for checking authorization of key use based on key ID and user permissions but did not tie in that check yet." The reviewer should confirm no crypto/key handler was modified to call it — otherwise callers silently skip that layer today. - -### 7.3 Admin gating in auth operations - -Two additional checks live above the backend in `wh_Auth_UserAdd` (src/wh_auth.c): - -- `WH_AUTH_IS_ADMIN(permissions_to_assign) && !WH_AUTH_IS_ADMIN(current_session_permissions)` → `WH_AUTH_PERMISSION_ERROR`. That is, **a non-admin session can never promote another user to admin**. This is enforced in the core, not the backend, so any custom backend inherits it. - -The backend `wh_Auth_BaseUserDelete` and `wh_Auth_BaseUserSetPermissions` additionally require the caller to be admin. `wh_Auth_BaseLogout` requires admin for cross-user logouts. - -### 7.4 Auto-logout on disconnect - -The COMM group's `CLOSE` action handler (src/wh_server.c:270) now logs the current user out when the comm channel is torn down. This prevents a stale session from persisting across client reconnects on the same server. - ---- - -## 8. Wire protocol — the auth message group - -Added to `wolfhsm/wh_message.h`: - -```c -WH_MESSAGE_GROUP_AUTH = 0x0D00 -WH_MESSAGE_GROUP_MAX = 0x0D00 // bumped so WH_NUMBER_OF_GROUPS reflects it - -enum { - WH_MESSAGE_AUTH_ACTION_LOGIN, - WH_MESSAGE_AUTH_ACTION_LOGOUT, - WH_MESSAGE_AUTH_ACTION_USER_ADD, - WH_MESSAGE_AUTH_ACTION_USER_DELETE, - WH_MESSAGE_AUTH_ACTION_USER_GET, - WH_MESSAGE_AUTH_ACTION_USER_SET_PERMISSIONS, - WH_MESSAGE_AUTH_ACTION_USER_SET_CREDENTIALS, -}; -``` - -The 7 request/response pairs live in `wh_message_auth.h`. Three of them carry **variable-length payloads** after a fixed header (login auth data, user-add credentials, set-credentials' two credential buffers). Those use `Translate*Request(void* src_packet, uint16_t src_size, ...)` helpers that validate `src_size` against the header-plus-declared-payload length (returning `WH_ERROR_BUFFER_SIZE` on mismatch), and each type has its own cap: - -```c -WH_MESSAGE_AUTH_LOGIN_MAX_AUTH_DATA_LEN - = COMM_DATA_LEN - sizeof(LoginRequest) -WH_MESSAGE_AUTH_USERADD_MAX_CREDENTIALS_LEN - = COMM_DATA_LEN - sizeof(UserAddRequest) -WH_MESSAGE_AUTH_SETCREDS_MAX_CREDENTIALS_LEN - = (COMM_DATA_LEN - sizeof(UserSetCredentialsRequest)) / 2 -``` - -`whAuthPermissions` is large and contains nested arrays, so the PR adds `wh_MessageAuth_FlattenPermissions` / `_Unflatten…` to marshal it into a fixed-size little-endian byte buffer (`WH_FLAT_PERMISSIONS_LEN` bytes) that's embedded in the UserAdd, UserGet, and UserSetPermissions messages. Everything else uses the standard `WH_T16`/`WH_T32` magic-aware translation helpers. - -Responses either use a dedicated type (Login → user_id, UserAdd → user_id, UserGet → user_id + flat permissions) or the shared `whMessageAuth_SimpleResponse { int32_t rc; }`. - ---- - -## 9. Client API (`src/wh_client_auth.c`) - -Every action comes in the wolfHSM-standard three flavors — a non-blocking send, a non-blocking receive, and a blocking loop on `WH_ERROR_NOTREADY`: - -```c -/* One-shot helpers */ -int wh_Client_AuthLogin(whClientContext* c, whAuthMethod method, - const char* username, const void* auth_data, - uint16_t auth_data_len, - int32_t* out_rc, whUserId* out_user_id); -int wh_Client_AuthLogout(whClientContext* c, whUserId user_id, int32_t* out_rc); -int wh_Client_AuthUserAdd(whClientContext* c, const char* username, - whAuthPermissions permissions, whAuthMethod method, - const void* credentials, uint16_t credentials_len, - int32_t* out_rc, whUserId* out_user_id); -int wh_Client_AuthUserDelete(whClientContext*, whUserId, int32_t* out_rc); -int wh_Client_AuthUserGet(whClientContext*, const char* username, - int32_t* out_rc, whUserId* out_user_id, - whAuthPermissions* out_permissions); -int wh_Client_AuthUserSetPermissions(whClientContext*, whUserId, - whAuthPermissions, int32_t* out_rc); -int wh_Client_AuthUserSetCredentials(whClientContext*, whUserId, whAuthMethod, - const void* current, uint16_t current_len, - const void* new, uint16_t new_len, - int32_t* out_rc); -``` - -Client-side defensive behavior to notice during review: - -- **Username validity** (`_UserNameIsValid` in wh_client_auth.c) requires non-NULL, non-empty, `< WH_MESSAGE_AUTH_MAX_USERNAME_LEN` (32) chars. -- The client stages all credential-carrying requests in a **stack buffer of size `WOLFHSM_CFG_COMM_DATA_LEN`** and calls `wh_Utils_ForceZero(buffer, sizeof(buffer))` before returning. This is the client-side mirror of the zeroization the server does after processing. -- `wh_Client_AuthLoginResponse` tolerates a server that responds with a `SimpleResponse` instead of a `LoginResponse` — that's how the server signals `WH_AUTH_NOT_ENABLED` to older/simpler clients; the demo (`wh_demo_client_auth.c`) keys off this to skip the demo cleanly. -- Every response handler validates `(resp_group, resp_action, resp_size)` before trusting the buffer. This is defense-in-depth against a desynchronized server — important since responses are returned in the same memory the request was written to. - ---- - -## 10. A concrete end-to-end: "user logs in and does a crypto op" - -To make the per-request/per-user flow concrete, here's what happens when a client does a `Login` followed by, say, a cached-key crypto call. Assume the server has been seeded with an admin `admin/1234` and a non-admin `demo` with `CRYPTO` group access (the exact setup in the POSIX example and the demo): - -1. **Client sends `Login("demo", PIN="…")`.** - - `wh_Client_AuthLogin` packs `whMessageAuth_LoginRequest` + PIN bytes into the comm buffer and sends with `(group=AUTH, action=LOGIN)`. - - The buffer is `ForceZero`d before return. -2. **Server front end** (`wh_Server_HandleRequestMessage`) receives the packet, extracts `(AUTH, LOGIN)`, and calls `wh_Auth_CheckRequestAuthorization`. No one is logged in yet, but the gate explicitly whitelists `(AUTH, LOGIN)` — passes. -3. **`wh_Server_HandleAuthRequest`** is dispatched, which for `LOGIN`: - - Translates the header (endian/magic) via `wh_MessageAuth_TranslateLoginRequest`. - - Calls `wh_Auth_Login(server->auth, comm->client_id, method, username, auth_data, auth_data_len, &loggedIn)`. -4. **`wh_Auth_Login`** acquires the auth lock. If someone is already logged in on this context, it returns `WH_ERROR_OK` with `loggedIn=0` (the slot is "busy"). Otherwise it calls the backend `Login`. On success it stashes `user.user_id`, `user.permissions`, and `user.is_active = true` inside the `whAuthContext`. -5. **`wh_Auth_BaseLogin`** (PIN path) hashes the provided PIN with `wc_Sha256Hash_ex`, looks the username up in the static array, constant-time compares the digests, sets `loggedIn=1` and copies out the user id and permissions on match. The hash scratch buffer is `ForceZero`d on every exit path. -6. **Server sends `LoginResponse`** containing either the new `user_id` or `WH_AUTH_LOGIN_FAILED`. The request packet's `auth_data` region is `ForceZero`d before the server returns. -7. **Client receives**, pulls out `out_rc` and `out_user_id` for later `Logout`. -8. **Client now does a crypto call**, e.g. `wc_…` which goes through the crypto-callback layer and ultimately sends `(group=CRYPTO, action=)`. -9. **Server front end** runs `wh_Auth_CheckRequestAuthorization(CRYPTO, op)`. Inside: - - `user_id` is non-invalid. - - `groupIndex = (WH_MESSAGE_GROUP_CRYPTO >> 8) & 0xFF = 4`. - - `permissions.groupPermissions[4]` is 1 → proceed to action bitmap. - - `(wordIdx, bitMask)` is computed from the action enum; allowed iff the bit is set. - - If a `CheckRequestAuthorization` callback is registered, it gets the tentative verdict and can flip it. In the example server it is `NULL`, so the callback step is skipped. -10. **If allowed** — the normal crypto handler runs; **no additional auth check** is performed today, even when the operation names a specific `keyId`. That's the TODO: the `CheckKeyAuthorization` callback and per-user `keyIds` allowlist exist in the data model and public API but the PR does not wire them into the crypto path. -11. **If denied** — `_FormatAuthErrorResponse` writes a group-appropriate error response carrying `WH_AUTH_PERMISSION_ERROR` (-2301), `wh_CommServer_SendResponse` ships it, and `WH_LOG_ON_ERROR_F` logs "Authorization failed for (group=%d, action=%d, seq=%d)". The request never reaches the crypto handler. -12. **Eventually the client sends `Close` on the comm channel.** The server's COMM close handler detects a live user, calls `wh_Auth_Logout`, which clears the `user` field inside `whAuthContext`. This happens even if the client forgets to call `AuthLogout` explicitly. - ---- - -## 11. Noteworthy security posture - -Things that are present and worth confirming during review: - -- **Constant-time credential compare** (`wh_Utils_ConstantCompare`, new utility) used for PIN hashes and cert buffers in the base backend. -- **Force-zero of sensitive buffers** (`wh_Utils_ForceZero`, new utility) on both client and server: PIN hash scratch, request-packet credential regions after processing, response packets with credentials, entire staging buffer on the client before return, and the user array on base-backend cleanup. Several of the later commits in the PR were exactly to add more of these. -- **Single-session-per-connection** semantics reduce the attack surface for cross-user confusion inside one comm channel. -- **Admin promotion guard** sits in the generic front end (`wh_Auth_UserAdd`), so backends can't accidentally allow it even if their `UserAdd` doesn't check. -- **Lock discipline**: all `wh_Auth_*` wrappers take the lock before calling into the backend; `wh_Auth_BaseLogin` and friends document that they expect to be called under the lock. Reviewer should verify any new call sites honor this. -- **Graceful fallback responses**: clients that get `WH_AUTH_NOT_ENABLED` when auth isn't configured server-side still see a well-formed message, not a malformed/oversized frame. - -Things that are **explicit open items** (per PR body and code comments): - -- The base user list is now optionally NVM-backed (metadata index + per-user credential objects; see [6.1 Persistence (NVM)](#61-persistence-nvm)). With no NVM configured it remains RAM-only and is lost on reboot. -- `CheckKeyAuthorization` is wired but not called anywhere in the request-handling paths in this PR. -- Logging of login attempts (successes and failures) is a TODO — only authorization denials are logged today. -- The `WH_AUTH_SET_ALLOWED_ACTION` macro comment says "and only the given action bit," but the implementation ORs (Copilot raised this during review). Either the comment or the semantics should change. - ---- - -## 12. Review checklist (suggested focus areas) - -1. **Front-end gate placement** — confirm every server-side request path goes through `wh_Server_HandleRequestMessage` before reaching a group handler. In particular, check DMA, SHE, PKCS11, CERT, and custom handler dispatch paths; anything that inserts a second dispatch could bypass the gate. -2. **`server->auth == NULL` semantics** — tests should confirm (a) auth-compiled + no context lets all non-auth requests through and (b) auth requests in that state return `WH_AUTH_NOT_ENABLED`, not `WH_ERROR_BADARGS`. -3. **Cross-user logout semantics** — `wh_Auth_Logout` in the core still wipes local session memory only if `user_id == context->user.user_id`, but the backend `wh_Auth_BaseLogout` can also deactivate a different user if the caller is admin. That asymmetry is intentional but worth double-checking against the test cases. -4. **Message validation against `WOLFHSM_CFG_COMM_DATA_LEN`** — particularly UserSetCredentials, which packs *two* variable-length buffers back to back. The per-message caps exist (`WH_MESSAGE_AUTH_SETCREDS_MAX_CREDENTIALS_LEN`) but verify both client and server reject an aggregate-size overrun. -5. **Macro behavior vs docs** on `WH_AUTH_SET_ALLOWED_ACTION` (OR vs assign) — the project should decide intended semantics since callers (including the demo) rely on OR behavior. -6. **`keyIdCount` clamping + unused slot zeroing** is duplicated between `UserAdd` and `UserSetPermissions` — worth a small helper to keep these in sync. -7. **Thread-safety contract** — backend docs say "protected by the auth context lock"; confirm any future backend author can't easily step outside the lock by, e.g., calling `wh_Auth_BaseFindUser` directly. -8. **Auto-logout on close** — confirm tests cover a client that crashes mid-session and reconnects; `CLOSE` isn't the only path to disconnection. -9. **`CheckKeyAuthorization` TODO** — decide whether merging without at least a scaffolded call site in the key/crypto handlers is acceptable, or whether it should be added (even if defaulting to allow) before merge so customers don't build against an interface that changes behavior later. -10. **Force-zero coverage** — spot-check that every function that stages credentials or PIN digests on the stack `ForceZero`s before return, even on error paths. The commit log shows several late additions here, suggesting it's easy to miss. - ---- - -## 13. Build & try it locally - -Per `docs/src/chapter09.md`, enable the feature via `WOLFHSM_CFG_ENABLE_AUTHENTICATION` and use the `AUTH=1` Make flag in the POSIX examples/tests: - -```bash -# Tests -cd test && make clean && make -j AUTH=1 && make run - -# Example server + demo client -cd examples/posix/wh_posix_server && make AUTH=1 -cd examples/posix/wh_posix_client && make AUTH=1 -``` - -The example server seeds an `admin/1234` user with everything-allowed permissions at startup (see `wh_PosixServer_ExampleAuthConfig` in `examples/posix/wh_posix_server/wh_posix_server_cfg.c`). The demo client (`examples/demo/client/wh_demo_client_auth.c`) logs in as admin, adds `demo/1234` with USER_SET_CREDENTIALS permission, rotates the `demo` PIN to `5678`, verifies the old PIN fails and the new PIN works, then logs out. Running the demo against an auth-disabled server returns `WH_AUTH_NOT_ENABLED` and the demo prints "Authentication not enabled on server, skipping …" and returns `WH_ERROR_OK` — useful for CI matrix coverage without separate test binaries. diff --git a/docs/draft/certificates.md b/docs/draft/certificates.md deleted file mode 100644 index 490f862ff..000000000 --- a/docs/draft/certificates.md +++ /dev/null @@ -1,635 +0,0 @@ -# Certificate Chain Validation in wolfHSM - -## 1. Overview - -wolfHSM provides a server-resident X.509 certificate manager that lets clients -provision trusted root anchors into NVM and then verify candidate certificate -chains against those anchors over the standard wolfHSM client/server protocol. -The chain walk, signature checks, anchor selection, and any custom verify -callbacks all run inside the trusted server environment; the client only ever -ships DER bytes and trust-anchor identifiers, never private key material or -root certificates that have been provisioned with the non-exportable flag. - -The feature set is layered. Each layer below adds capability without -invalidating the layer above it, and each is independently gated by a -compile-time configuration macro. - -| Capability | Macro | Notes | -|-----------------------------------------|----------------------------------------------------|-------| -| Trusted-root NVM CRUD + chain verify | `WOLFHSM_CFG_CERTIFICATE_MANAGER` | Base feature. Requires crypto. | -| Multi-root chain verify | (always available with the base feature) | Bounded by `WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS`. | -| Trusted CA verify-result cache | `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE` | Per-server cache by default. | -| Cross-client (global) verify cache | `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL` | Layered on top of the verify cache. | -| Cache leaf public key after verify | `WH_CERT_FLAGS_CACHE_LEAF_PUBKEY` request flag | Available on every verify variant. | -| Attribute-certificate (X.509 ACERT) | `WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT` | Single-root verify only. | -| DMA transport for large chains | `WOLFHSM_CFG_DMA` | Available on every cert API. | -| User-supplied verify callback | `whServerCertConfig.verifyCb` (server-side only) | Applied per cert manager. | - -The remaining sections walk the client API for each operation, then dive into -the multi-root and trusted-cache features in detail and describe the precise -semantics that result when both are enabled together. - -## 2. Build Configuration - -### Required - -- `WOLFHSM_CFG_CERTIFICATE_MANAGER` — enables every API in this document. - Requires `!WOLFHSM_CFG_NO_CRYPTO` (the implementation depends on - `WOLFSSL_CERT_MANAGER` and the wolfCrypt ASN.1 decoder). - -### Optional - -- `WOLFHSM_CFG_DMA` — enables the `*Dma*` variants that pass the candidate - chain by client address rather than copying it through the comm buffer. -- `WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT` — enables `wh_Client_CertVerifyAcert` - / `wh_Client_CertVerifyAcertDma`. Requires wolfSSL built with `WOLFSSL_ACERT` - and `WOLFSSL_ASN_TEMPLATE`. -- `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE` — enables the trusted CA verify cache - (Section 6). Pulls in `wh_Client_CertVerifyCacheClear` and - `wh_Client_CertVerifyCacheSetEnabled` on the client. -- `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL` — relocates the verify cache - from the per-server context into the shared NVM context so it is reused - across every client connected to the server. Requires - `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE`. - -### Bounds - -- `WOLFHSM_CFG_MAX_CERT_SIZE` — maximum DER size of any single certificate - read from or written to NVM. Defaults to `WOLFHSM_CFG_COMM_DATA_LEN` when - DMA is off, `4096` when DMA is on. -- `WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS` — upper bound on the number of trusted - root NVM IDs that may be supplied to a single multi-root verify call. - Defaults to `8`. This bound also sizes the inline root-id array in the - multi-root DMA request and the per-slot root binding in the verify cache. - Fails a static assert if the resulting DMA request struct would exceed - `WOLFHSM_CFG_COMM_DATA_LEN`. -- `WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT` — number of slots in the verify - cache (FIFO ring). Defaults to `16`. - -## 3. Common Concepts - -### 3.1 Trust anchors live in NVM - -Every trusted root is a regular NVM object identified by a `whNvmId`. The -client provisions roots with `wh_Client_CertAddTrusted`, removes them with -`wh_Client_CertEraseTrusted`, and reads them back with -`wh_Client_CertReadTrusted`. Verification operations take the NVM ID(s) of -the root(s) to anchor against — the root certificate bytes themselves are -never sent inline with a verify request. - -Roots respect normal NVM access and flag policy. A root provisioned with -`WH_NVM_FLAGS_NONEXPORTABLE` cannot be read back via -`wh_Client_CertReadTrusted` (the server returns `WH_ERROR_ACCESS`) but is -still usable as a verify anchor. - -### 3.2 Verification flags (`whCertFlags`) - -Defined in `wolfhsm/wh_common.h`: - -- `WH_CERT_FLAGS_NONE` — verify only. -- `WH_CERT_FLAGS_CACHE_LEAF_PUBKEY` — on a successful verify, extract the - leaf certificate's `SubjectPublicKeyInfo` and cache it in the server's - key cache so subsequent crypto operations can address it by `whKeyId`. - -### 3.3 Cached leaf key id - -Verify variants whose name contains `AndCacheLeafPubKey` take an `inout_keyId` -argument. On entry, supply either an explicit `whKeyId` or `WH_KEYID_ERASED` -to let the server pick a unique id; on success, the caller-side id is updated -with the value the server actually used. Failed verifies leave the prior id -contents undisturbed and do not populate the key cache. - -`cachedKeyFlags` carries the NVM usage flags applied to the cached key — -typically `WH_NVM_FLAGS_USAGE_VERIFY` for a leaf certificate's public key. - -### 3.4 Async (request/response) split - -Every verify and trusted-root mutation API has three forms: - -- A single blocking call (e.g. `wh_Client_CertVerify`). -- A non-blocking `*Request` call that returns as soon as the request is on - the wire. -- A non-blocking `*Response` call that returns `WH_ERROR_NOTREADY` until the - server has replied, then yields `out_rc`. - -The blocking forms loop on `WH_ERROR_NOTREADY` internally. Use the split -pair when the calling thread needs to remain responsive (for example, to -service a separate request). - -### 3.5 Return-code conventions - -All client functions return a wolfHSM transport-layer `int`: -`WH_ERROR_OK` if the request and response cycle completed, or a negative -error code if the comm layer itself failed. - -The server's verify result is returned separately via `out_rc`: - -| `out_rc` | Meaning | -|-------------------------|----------------------------------------------------------------------| -| `WH_ERROR_OK` (0) | Chain anchored successfully. | -| `WH_ERROR_CERT_VERIFY` | Chain did not anchor (signature, expiry, or path failure). | -| `WH_ERROR_NOTFOUND` | (Multi-root only) every supplied root id was absent from NVM. | -| `WH_ERROR_BADARGS` | Argument shape or wire-payload size violation. | -| `WH_ERROR_ACCESS` | Read-trusted on a non-exportable cert. | -| Other negative codes | Underlying NVM, transport, or cert-manager environment errors. | - -This separation lets callers distinguish a real trust failure -(`WH_ERROR_CERT_VERIFY`) from "the trust store itself is empty" -(`WH_ERROR_NOTFOUND`) and from infrastructure errors. - -## 4. Client API - -All prototypes below live in `wolfhsm/wh_client.h`. The `*Request` / -`*Response` split forms are omitted from the listing for brevity but exist -for every blocking entry point shown. - -### 4.1 Initialization - -```c -int wh_Client_CertInit(whClientContext* c, int32_t* out_rc); -``` - -Initializes the server's certificate manager subsystem. Required once per -server before any other cert call. When the trusted-cert verify cache is -enabled in per-client mode, `CertInit` clears the calling client's cache -(see Section 6.4). - -### 4.2 Trusted root provisioning - -```c -int wh_Client_CertAddTrusted(whClientContext* c, whNvmId id, - whNvmAccess access, whNvmFlags flags, - uint8_t* label, whNvmSize label_len, - const uint8_t* cert, uint32_t cert_len, - int32_t* out_rc); - -int wh_Client_CertEraseTrusted(whClientContext* c, whNvmId id, int32_t* out_rc); - -int wh_Client_CertReadTrusted(whClientContext* c, whNvmId id, uint8_t* cert, - uint32_t* cert_len, int32_t* out_rc); -``` - -`CertAddTrusted` writes a DER root certificate into NVM under the supplied -`whNvmId` with the given access and flag policy. `CertEraseTrusted` removes -it. `CertReadTrusted` reads it back, with `*cert_len` updated to the actual -stored size on success (or, on `WH_ERROR_BUFFER_SIZE`, the size needed). - -When the verify cache is enabled, both `AddTrusted` and `EraseTrusted` also -trigger a cache eviction for the affected root id (Section 6.4). - -DMA variants: - -```c -int wh_Client_CertAddTrustedDma(whClientContext* c, whNvmId id, - whNvmAccess access, whNvmFlags flags, - uint8_t* label, whNvmSize label_len, - const void* cert, uint32_t cert_len, - int32_t* out_rc); - -int wh_Client_CertReadTrustedDma(whClientContext* c, whNvmId id, void* cert, - uint32_t cert_len, int32_t* out_rc); -``` - -### 4.3 Single-root chain verify - -```c -int wh_Client_CertVerify(whClientContext* c, const uint8_t* cert, - uint32_t cert_len, whNvmId trustedRootNvmId, - int32_t* out_rc); -``` - -Walks the chain in `cert` (concatenated DER in leaf-last certificate order), -anchoring against the single root identified by `trustedRootNvmId`. The server -constructs a fresh `WOLFSSL_CERT_MANAGER` for the call, loads the root, walks -the chain via `wolfSSL_CertManagerVerifyBuffer`, and returns the result via -`out_rc`. - -DMA variant: - -```c -int wh_Client_CertVerifyDma(whClientContext* c, const void* cert, - uint32_t cert_len, whNvmId trustedRootNvmId, - int32_t* out_rc); -``` - -### 4.4 Single-root verify with leaf-key caching - -```c -int wh_Client_CertVerifyAndCacheLeafPubKey( - whClientContext* c, const uint8_t* cert, uint32_t cert_len, - whNvmId trustedRootNvmId, whNvmFlags cachedKeyFlags, whKeyId* inout_keyId, - int32_t* out_rc); -``` - -Same chain walk as `wh_Client_CertVerify`, plus on success the leaf -certificate's public key is copied into the server's key cache under -`*inout_keyId` (or a server-chosen id if the input was `WH_KEYID_ERASED`) -with `cachedKeyFlags` as its NVM usage policy. Subsequent crypto operations -can address the key by id. - -DMA variant: `wh_Client_CertVerifyDmaAndCacheLeafPubKey`. - -### 4.5 Multi-root chain verify - -```c -int wh_Client_CertVerifyMultiRoot(whClientContext* c, const uint8_t* cert, - uint32_t cert_len, - const whNvmId* trustedRootNvmIds, - uint16_t numRoots, int32_t* out_rc); -``` - -Identical to the single-root call, except the server loads up to -`numRoots` roots (`1 .. WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS`) into a single -cert manager and the chain succeeds if it anchors to *any* of them. See -Section 5 for the full semantics. - -DMA variant: `wh_Client_CertVerifyMultiRootDma`. - -### 4.6 Multi-root verify with leaf-key caching - -```c -int wh_Client_CertVerifyMultiRootAndCacheLeafPubKey( - whClientContext* c, const uint8_t* cert, uint32_t cert_len, - const whNvmId* trustedRootNvmIds, uint16_t numRoots, - whNvmFlags cachedKeyFlags, whKeyId* inout_keyId, int32_t* out_rc); -``` - -DMA variant: `wh_Client_CertVerifyMultiRootDmaAndCacheLeafPubKey`. - -### 4.7 Attribute certificate verify - -```c -int wh_Client_CertVerifyAcert(whClientContext* c, const void* cert, - uint32_t cert_len, whNvmId trustedRootNvmId, - int32_t* out_rc); -``` - -Verifies an X.509 attribute certificate's signature against the public key -of the trusted root identified by `trustedRootNvmId`. Available only when -the server is built with `WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT`. There is -no multi-root or leaf-cache variant — attribute certificates are signed -directly by an attribute authority and the call carries a single anchor. - -A signature mismatch is reported as `WH_ERROR_CERT_VERIFY` in `out_rc`, the -same convention as the standard verify path. - -DMA variant: `wh_Client_CertVerifyAcertDma`. - -### 4.8 Verify-cache management - -Available only when `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE` is enabled: - -```c -int wh_Client_CertVerifyCacheClear(whClientContext* c, int32_t* out_rc); - -int wh_Client_CertVerifyCacheSetEnabled(whClientContext* c, uint8_t enable, - int32_t* out_rc); -``` - -`CertVerifyCacheClear` drops every entry from the server's verify cache. In -per-client mode this clears only the calling client's cache; in global mode -(Section 6.5) it clears the shared cache for all clients. Subsequent verifies -fall back to running the full wolfSSL signature path until the cache is -repopulated. - -`CertVerifyCacheSetEnabled` toggles the cache at runtime. The cache is -initialized enabled at server (and, in global mode, NVM) init, so this call -is only needed to opt out or to re-enable after opting out. Disabling -flushes all existing entries and makes subsequent Lookup miss / Insert a -no-op until the cache is re-enabled. The scope of the toggle matches the -cache mode: per-client mode affects this client's server only, global mode -affects the shared cache observed by every connected client. - -Both APIs have the usual `*Request` / `*Response` split forms. - -## 5. The Multi-Root Feature - -### 5.1 Why it exists - -The single-root entry point couples each verify to exactly one trust anchor. -Callers needing to validate a chain against any of several acceptable roots -otherwise have to either fold every acceptable root under a single super-root -(operationally awkward when the root infrastructures are independent) or -loop over `wh_Client_CertVerify` per root, parsing the chain again each -attempt and inferring at the application layer whether a per-anchor failure -should trigger a retry against the next anchor. - -`wh_Client_CertVerifyMultiRoot` collapses both of those into a single -request: hand the server an array of up to `WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS` -NVM ids, the server loads each one as a CA into a single cert manager, and -the chain is walked exactly once. If it anchors to any of the supplied -roots the verify succeeds; otherwise it fails with `WH_ERROR_CERT_VERIFY`. - -### 5.2 Order independence - -The cert manager picks an issuer for each child cert by subject/issuer -matching during chain walk, not by load order. Listing root A before root B -does not "prefer" A. - -### 5.3 Mixed-failure semantics - -Multi-root distinguishes three failure modes via `out_rc`: - -| Outcome | `out_rc` | -|-----------------------------------------------------------|-------------------------| -| Chain anchors to ≥ 1 loaded root | `WH_ERROR_OK` | -| ≥ 1 anchor loaded; chain does not anchor to any of them | `WH_ERROR_CERT_VERIFY` | -| Every supplied root id is absent from NVM | `WH_ERROR_NOTFOUND` | -| Any non-absent failure reading or loading a supplied root | underlying error code | - -Roots that are absent from NVM are skipped silently — they do not abort the -operation and do not count against the chain's chance of anchoring. A read -or load failure on an *existing* root, by contrast, is treated as an -environment error and aborts the call. - -## 6. The Trusted Verify Cache - -### 6.1 Overview - -When `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE` is enabled, the server keeps a -fixed-size FIFO ring of slots, each holding: - -- A SHA-256 hash of a successfully-verified DER-encoded **CA** certificate. -- The set of trusted root NVM ids that were loaded into the cert manager - when that cert was verified. - -On a subsequent verify, before the server invokes -`wolfSSL_CertManagerVerifyBuffer` for a CA cert in the chain, it hashes -that cert and looks the hash up in the cache. A hit short-circuits the -public-key signature check; the rest of the chain walk (CA decode, store -load for downstream certs, leaf pubkey extract) continues unchanged. - -Only CA certs are ever inserted. Leaves are deliberately excluded. - -The verify cache is never stored in NVM and does not persist across power -cycles. - -The cache also carries a runtime enable flag. It is initialized **enabled** -at server (and, in global mode, NVM) init, so the cache is active out of the -box on builds that compile it in. Clients can toggle it at runtime via -`wh_Client_CertVerifyCacheSetEnabled`; disabling clears all entries and -suppresses subsequent Lookup / Insert until re-enabled. Deployments that -want to fail-safe should disable the cache immediately after init and -re-enable it only when the threat-model implications below have been -accepted. - -This feature is intended to provide a substantial performance enhancement by -eliminating multiple potentially redundant and expensive public key verification -operations, however it does so at the expense of security in some scenarios. If -deploying this feature in production it is paramount that the nuances regarding -the trust anchor consequences are fully understood and align with the threat -model of the application. **This feature should be used with caution and for most -scenarios is NOT recommended.** - -### 6.2 Internals - -The wolfHSM trusted certificate cache binds each entry to the *set* of -trusted roots that were actually loaded when the verify occured, and lookups -require the cached set to be a **subset of the caller's currently loaded set**. - -The soundness argument rests on the monotonicity of X.509 verification: adding -more trusted roots should never invalidate a previously successful verify, so a -chain that validated under set `S` still validates under any superset `T ⊇ S`. -A cache hit therefore implies the cached verify's anchor (whichever root in `S` -actually closed the chain) is currently trusted, regardless of which element of -`S` it was, since every element of `S` is known to be in `T`. - -### 6.3 Hits, misses, and recording the loaded set - -Crucially, the *loaded* set is recorded — not the caller-supplied set. If a -caller passes three roots but only two are present in NVM, the cache slot -records the two-element loaded set. Forwarding the three-element supplied -set instead would let a stale entry under the missing root match a verify -whose effective trust store does not contain that root. - -Insertion is deduplicated on exact `(set, hash)` match under the cache lock, so -concurrent inserts of the same verify collapse to a single slot. Two -entries with the same hash but different sets coexist: each is an -independent claim about a distinct verify, and dropping either could lose -hit coverage for callers whose loaded set is a superset of one but not the -other. - -The ring overwrites using a FIFO pattern once full. - -### 6.4 Cache lifecycle and eviction - -Five mutation paths interact with the cache: - -- **`wh_Client_CertAddTrusted`** evicts every cache slot whose stored set - contains the affected root id. `AddObject` supersedes any prior object at - that id, so cached verifies anchored at the previous root would otherwise - short-circuit a verify under the new (different) root resident at that id. -- **`wh_Client_CertEraseTrusted`** evicts the same way. Without this, a - later `AddTrusted` reusing the freed id would inherit phantom cache hits - from the now-departed root. -- **`wh_Client_CertVerifyCacheClear`** drops every slot. -- **`wh_Client_CertVerifyCacheSetEnabled`** with `enable=0` drops every - slot and suppresses subsequent Lookup / Insert until re-enabled. - Re-enabling resumes caching from an empty state. -- **`wh_Client_CertInit`** drops every slot in **per-client mode only**. - Under `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL` the cache lives in - the shared NVM context and is initialized exactly once in `wh_Nvm_Init` — - clearing it on a per-client `CertInit` would wipe entries populated by - other clients. - -Eviction happens on success only. Otherwise, a failed `AddTrusted` or -`EraseTrusted` leaves the prior root and any cache entries bound to it in -place. - -### 6.5 Per-client vs global mode - -By default the cache lives in `whServerCertContext` and is per-server (and -therefore per-client connection). Each client connection sees its own slots -and its own hit rate; a verify under client A does not warm client B's -cache. - -`WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL` relocates the cache into the -shared NVM context, where every connected client shares one `whCertVerify­ -CacheContext`. Hits then apply across client boundaries: once any client -has verified a CA against root R, every client whose loaded root set -contains R hits the cache for that CA. - -Global mode adds a dedicated lock embedded in the cache so cache operations -do not serialize behind general NVM I/O. In per-client mode the cache -piggybacks on the NVM lock — adequate given the cache is private to one -server and `CertInit` resets it on each (re)connect. - -In global mode the dedicated lock must be initialized by the deployment. -`whNvmConfig` gains a `certVerifyCacheLockConfig` field (`whLockConfig*`) -that is wired into the shared cache at `wh_Nvm_Init`. It must reference a -distinct platform lock context from the NVM `lockConfig` (e.g. a separate -`posixLockContext`) so the two locks back independent mutexes; pointing -both at the same context would re-serialize cache operations behind NVM -I/O and defeat the point. Pass `NULL` for no-op locking on single-threaded -builds (`WOLFHSM_CFG_THREADSAFE` undefined drops the field entirely). - -### 6.6 The user-supplied verify callback - -The verify callback is independent of the verify cache — it is available on -any build with `WOLFHSM_CFG_CERTIFICATE_MANAGER` and is documented here -because its only subtle behavior is how it interacts with the cache. - -**Registration.** `whServerConfig` carries an optional -`whServerCertConfig* certConfig` field whose `verifyCb` member is the -callback applied to every per-request `WOLFSSL_CERT_MANAGER`. `certConfig` -is optional; leaving it `NULL` (or leaving `verifyCb` `NULL` inside it) is -the no-callback default. The signature matches wolfSSL's `VerifyCallback` -exactly, so a callback that works with `wolfSSL_CertManagerSetVerify` works -here unchanged. - -```c -/* Server-side registration at init */ -whServerCertConfig certCfg = { .verifyCb = my_verify_cb }; -whServerConfig srvCfg = { /* ... */, .certConfig = &certCfg }; -wh_Server_Init(server, &srvCfg); -``` - -The callback can also be replaced (or removed) at runtime: - -```c -int wh_Server_CertSetVerifyCb(whServerContext* server, VerifyCallback cb); -``` - -Pass `NULL` to unregister. The change applies to subsequent verify requests; -in-flight requests continue to use whichever callback was registered when -they entered `wh_Server_CertVerifyMultiRoot`. Both registration paths are -server-side only — there is no client API for installing or replacing the -callback, because the callback executes in the trusted server context. - -**Lifecycle.** The callback is installed on a fresh per-request -`WOLFSSL_CERT_MANAGER` via `wolfSSL_CertManagerSetVerify` immediately after -the cert manager is constructed and before any roots are loaded. wolfSSL -invokes it during chain walk inside `wolfSSL_CertManagerVerifyBuffer`. - -**Cache interaction.** Verify-cache hits short-circuit -`wolfSSL_CertManagerVerifyBuffer` and so deliberately **do not** invoke the -callback. Callbacks that gate on properties other than wolfSSL's standard -chain validation (e.g. policy OIDs, EKU pinning, application-specific -revocation checks) therefore see only the first verify of a given CA cert -under a given root set — subsequent hits silently bypass them. Deployments -that rely on the callback as a hard gate should disable the verify cache -via `wh_Client_CertVerifyCacheSetEnabled(c, 0, ...)` (or omit -`WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE` at build time). - -## 7. Multi-Root and the Verify Cache Together - -When both features are compiled in, the cache participates in both -single-root and multi-root verifies. The combination preserves the -single-root behavior exactly while extending hit semantics to the larger -trust-set landscape multi-root callers create. The interaction has three -corner cases worth being explicit about. - -### 7.1 Cache entries record which roots were actually used, not which were asked for - -A multi-root verify request lists the trusted roots the caller is willing -to trust, but some of those roots may not currently exist in NVM. The -server skips any missing roots and only loads the ones it finds, so the -trust store the chain is actually verified against can be smaller than the -list the caller supplied. - -When a successful verify produces a new cache entry, the entry remembers -that smaller, real trust store — not the original request. For example, a -caller that asks for eight roots but only has three present in NVM -produces a three-root cache entry, not an eight-root one. - -This matters because cache lookups use the subset rule: an entry hits only -when its recorded roots are all present in the looking-up caller's -currently-loaded set. Recording roots that were never actually loaded -would let a future verify hit an entry under a root that wasn't part of -the trust store when the cached chain originally validated, and the -subset rule's soundness argument would no longer hold. - -### 7.2 Single-root verifies populate the cache too — and produce the broadest entries - -The single-root path is implemented as a one-element multi-root call. -Successful single-root verifies therefore insert one-element entries -(`{R}`) — the narrowest possible set. Under the subset rule, those entries -are also the most reusable: any future multi-root call whose loaded set -contains `R` (e.g. `{R, R₂}`, `{R, R₂, R₃}`) hits. - -Multi-root entries with larger sets (`{R₁, R₂, R₃}`) have correspondingly -narrower reuse — only future verifies whose loaded set is a superset -(`{R₁, R₂, R₃}` itself, or `{R₁, R₂, R₃, R₄}`, etc.) will hit. They are -still useful: they capture verifies that pure single-root traffic would -not generate. - -A practical consequence: if a deployment runs both single-root traffic -against `R₁` and multi-root traffic against `{R₁, R₂}`, the single-root -verifies populate `{R₁}`-bound entries that the multi-root traffic also -hits, while the multi-root verifies populate `{R₁, R₂}`-bound entries that -do *not* serve future single-root `{R₁}` traffic. The cache is therefore -biased toward maximizing reuse from single-root callers. - -### 7.3 A single root rotation invalidates entries across both paths - -`AddTrusted` and `EraseTrusted` call `CertVerifyCache_EvictRoot(id)`, which -drops every slot whose recorded set *contains* `id`. This does the right -thing for both single- and multi-root populated entries: - -- A `{R₁}` entry is dropped on a rotation of `R₁` and is unaffected by - rotations of any other root — exactly what monotonicity demands. -- A `{R₁, R₂}` entry is dropped on a rotation of either `R₁` or `R₂`. The - original verify may have anchored at the rotated root, and the remaining - set is no longer a sound claim about which stores still validate the - chain. Stripping just the rotated id from the set would leave a slot - that falsely claims `{R_other}` validated this chain on its own. - -A multi-root caller's "live" cache footprint therefore depends on the -stability of every root in its supplied sets, not just the one that -ultimately anchored. This is intrinsic to the soundness argument — the -cache cannot identify which anchor closed any given chain after the fact — -and is the trade-off paid for cross-anchor cache reuse. - -### 7.4 Cache miss falls back to the regular multi-root path - -A miss does not change semantics relative to a no-cache build: the server -runs `wolfSSL_CertManagerVerifyBuffer` against the populated cert manager -just as it would have without the cache. There is no path by which a miss -weakens the verify; the cache is a pure performance optimization. - -### 7.5 Recommendations - -- Provision long-lived roots with stable NVM ids when targeting a high - cache hit rate. Frequent rotations will keep the cache cold. -- Prefer per-client mode (the default) when client trust stores diverge - significantly. Prefer global mode when most clients verify against the - same set of roots (e.g. fleet-uniform PKI). -- Single-root callers benefit from the cache without any additional design. - Multi-root callers benefit most when the supplied set is reasonably - stable across calls — the recorded set is what determines hit eligibility - for downstream traffic. - -## 8. Worked Example - -Provision two roots, verify a chain against either, and cache the leaf -public key for subsequent signing-key lookup: - -```c -whClientContext* c = /* ... */; -whNvmId rootIds[2] = { 100, 101 }; -int32_t rc; -whKeyId leafKeyId = WH_KEYID_ERASED; - -/* One-time provisioning (can also be done offline) */ -wh_Client_CertInit(c, &rc); -wh_Client_CertAddTrusted(c, rootIds[0], WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, - (uint8_t*)"primary", 7, - primary_root_der, primary_root_len, &rc); -wh_Client_CertAddTrusted(c, rootIds[1], WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, - (uint8_t*)"backup", 6, - backup_root_der, backup_root_len, &rc); - -/* Verify a chain against either root and cache the leaf public key */ -int ret = wh_Client_CertVerifyMultiRootAndCacheLeafPubKey( - c, chain_der, chain_len, rootIds, 2, - WH_NVM_FLAGS_USAGE_VERIFY, &leafKeyId, &rc); - -if (ret == WH_ERROR_OK && rc == WH_ERROR_OK) { - /* leafKeyId now refers to the leaf cert's public key in the server's - * key cache; subsequent crypto operations can use it by id. With the verify - * cache enabled, a repeat verify of this chain against this root set hits - * the cache for every CA in the chain and skips the wolfSSL signature path. - */ -} -``` - diff --git a/docs/draft/crypto_affinity.md b/docs/draft/crypto_affinity.md deleted file mode 100644 index e35339f48..000000000 --- a/docs/draft/crypto_affinity.md +++ /dev/null @@ -1,96 +0,0 @@ -# Crypto Affinity Client API - -The crypto affinity feature allows a client to control whether the server uses **software** or **hardware** cryptographic implementations on a per-request basis. - -> **Compile-time guard:** The feature is gated behind `WOLFHSM_CFG_CRYPTO_AFFINITY` (default off). When the macro is not defined, `wh_Client_SetCryptoAffinity`/`wh_Client_GetCryptoAffinity` are not compiled, so the client cannot change affinity, and the server ignores the request header affinity field and always uses its configured `devId`. The affinity field is still present in the client context and carried in every crypto request header for wire-format compatibility; without the setter it simply retains its default value. - -Affinity is stored as **client-local state** and is transmitted to the server in every crypto request message header. There is no dedicated round-trip required to change affinity -- setting it is instantaneous and takes effect on the next crypto operation. Affinity persists for all subsequent requests once changed. - -## Affinity Values - -```c -enum WH_CRYPTO_AFFINITY_ENUM { - WH_CRYPTO_AFFINITY_HW = 0, // Attempt to use hardware crypto (devId = configured value) - WH_CRYPTO_AFFINITY_SW = 1, // Use software crypto (devId = INVALID_DEVID) -}; -``` - -The default affinity after client initialization is `WH_CRYPTO_AFFINITY_HW`. - -## API - -### SetCryptoAffinity - -```c -int wh_Client_SetCryptoAffinity(whClientContext* c, uint32_t affinity); -``` - -Sets the client's crypto affinity. This is a **local operation** that does not communicate with the server. The new affinity value will be included in all subsequent crypto request messages. - -**Parameters:** -- `c` -- Client context -- `affinity` -- `WH_CRYPTO_AFFINITY_SW` or `WH_CRYPTO_AFFINITY_HW` - -**Returns:** -- `WH_ERROR_OK` -- Affinity set successfully -- `WH_ERROR_BADARGS` -- NULL context or invalid affinity value - -### GetCryptoAffinity - -```c -int wh_Client_GetCryptoAffinity(whClientContext* c, uint32_t* out_affinity); -``` - -Retrieves the client's current crypto affinity. This is a **local operation** that does not communicate with the server. - -**Parameters:** -- `c` -- Client context -- `out_affinity` -- Pointer to receive the current affinity value - -**Returns:** -- `WH_ERROR_OK` -- Affinity retrieved successfully -- `WH_ERROR_BADARGS` -- NULL context or NULL output pointer - -## Usage Example - -```c -uint32_t affinity; - -/* Default affinity is WH_CRYPTO_AFFINITY_HW after wh_Client_Init() */ -wh_Client_GetCryptoAffinity(client, &affinity); -/* affinity == WH_CRYPTO_AFFINITY_HW */ - -/* Perform a crypto operation -- affinity is sent in the request header */ -wc_AesCbcEncrypt(&aes, out, in, len); -/* If server has a valid devId, hardware crypto callback is used */ - -/* Switch to software crypto -- takes effect immediately, no round-trip */ -int rc = wh_Client_SetCryptoAffinity(client, WH_CRYPTO_AFFINITY_SW); -if (rc == WH_ERROR_OK) { - /* All subsequent crypto operations will use software implementation */ -} - -/* Switch back to hardware crypto */ -wh_Client_SetCryptoAffinity(client, WH_CRYPTO_AFFINITY_HW); -/* Subsequent crypto operations request HW acceleration */ -``` - -## Server Behavior - -When the server receives a crypto request, it reads the affinity field from the generic crypto request header and selects the appropriate `devId`: - -| Affinity in Request | Server Action | -|---------------------|---------------| -| `WH_CRYPTO_AFFINITY_SW` | Uses `INVALID_DEVID` (wolfCrypt software implementation) | -| `WH_CRYPTO_AFFINITY_HW` | Uses `server->devId` if valid, otherwise falls back to `INVALID_DEVID` | - -The `devId` is configured at server initialization from `config->devId`. If the server was not configured with a valid hardware `devId`, hardware affinity requests will silently fall back to software crypto. - -## Protocol Details - -Affinity is transmitted in the `affinity` field of `whMessageCrypto_GenericRequestHeader`, which is included at the start of every crypto request message. This means: - -- Each crypto operation independently specifies its desired affinity -- Multiple clients can use different affinities concurrently without interference -- No server-side affinity state is maintained per-client -- Changing affinity has zero latency (no communication overhead) diff --git a/docs/draft/posix-shm.md b/docs/draft/posix-shm.md deleted file mode 100644 index be4035a84..000000000 --- a/docs/draft/posix-shm.md +++ /dev/null @@ -1,67 +0,0 @@ -# POSIX SHM DMA Transport - -## Overview - -There are two independent features at play in the POSIX SHM transport port. Understanding which is which is key. - -## 1. The Transport: POSIX Shared Memory (`posix_transport_shm`) - -This is purely a **transport layer** -- it moves request/response messages between client and server processes. It works like this: - -- **Server** creates a POSIX shared memory object (`shm_open`) with a layout of: - ``` - [ 64-byte header | request buffer | response buffer | optional DMA section ] - ``` -- **Client** opens the same named object and `mmap`s it into its address space -- Both sides then delegate to `wh_transport_mem` (the generic memory-based transport) for actual message passing via CSR registers in the request/response buffers -- The header contains PIDs for RT-signal-based async notification - -The transport's job is **only** to shuttle serialized request/response packets. It knows nothing about crypto, keys, or DMA semantics. - -The optional **DMA section** at the end of the shared memory region is the transport providing a chunk of shared address space that *both* processes can access. This is just raw shared memory -- the transport allocates it but doesn't use it itself. It's plumbing for the DMA feature. - -## 2. The Feature: DMA (`WOLFHSM_CFG_DMA`) - -DMA is a **separate, transport-agnostic feature** in wolfHSM core (`wh_dma.h`, `wh_server_dma.c`, `wh_client_dma.c`). It allows crypto operations to reference client memory **by address** rather than copying data into the transport's request/response buffers. This matters because: - -- Standard messages are limited by `WOLFHSM_CFG_COMM_DATA_LEN` (typically ~4KB) -- DMA messages send *addresses* in the request, and the server reads/writes client memory directly - -The DMA feature has a callback-based architecture: -- `wh_Server_DmaProcessClientAddress()` -- server calls this with a client address, the registered callback transforms it to something the server can dereference -- `wh_Client_DmaProcessClientAddress()` -- client calls this to transform its local address into whatever the server will receive in the message -- PRE/POST operations handle setup and teardown (cache flush/invalidate, temporary buffer allocation, etc.) - -On real hardware (e.g. Infineon TC3xx), this is literal hardware DMA -- client and server are on different cores with different address maps, and the callbacks handle the MMU/bus address translation. - -## 3. The Glue: Static Memory Pool Allocator in the SHM DMA Callbacks - -The `posixTransportShm_ClientStaticMemDmaCallback` and `posixTransportShm_ServerStaticMemDmaCallback` in `posix_transport_shm.c` are the **port-specific DMA callbacks** that bridge the POSIX SHM transport with the DMA feature. Here's the clever part: - -**Problem:** On POSIX, client and server are separate processes with separate virtual address spaces. A raw client pointer like `0x7fff12345000` means nothing to the server. But the DMA section in shared memory is mapped into *both* processes (at potentially different virtual addresses). - -**Solution using the pool allocator:** - -1. wolfCrypt's `WOLFSSL_STATIC_MEMORY` pool allocator (`wc_LoadStaticMemory_ex`) is initialized with the DMA section as its backing memory pool -2. When the client DMA callback gets a PRE operation with a client address that's **not** already in the DMA area, it: - - Allocates a temporary buffer from the pool (`XMALLOC` with the heap hint) - - Copies client data into it - - Returns an **offset** from the DMA base (not a pointer) -- this is what gets sent to the server -3. The server DMA callback simply takes that offset, validates it's in bounds, and returns `dma_base + offset` -4. On POST, the client callback copies results back (for writes) and frees the temporary buffer - -If the client address **is already** in the DMA section (the client allocated directly from the pool), it skips the copy and just computes the offset -- zero-copy. - -The pool allocator here is used as a **bump/slab allocator for the shared DMA region**. It has nothing to do with the transport itself -- it's the DMA callback's strategy for managing the shared buffer. wolfHSM could use a different allocator; the pool allocator was chosen because it's already available in wolfCrypt and works without `malloc`. - -## Summary Table - -| Aspect | Transport (SHM) | DMA Feature | Pool Allocator | -|--------|-----------------|-------------|----------------| -| **Layer** | Communication | Application/Crypto | Memory management | -| **Scope** | Port-specific (POSIX) | Core wolfHSM | DMA callback impl detail | -| **Purpose** | Move request/response packets | Let server access client memory by address | Manage temporary buffers in shared DMA area | -| **Config** | `posixTransportShmConfig` | `WOLFHSM_CFG_DMA` | `WOLFSSL_STATIC_MEMORY` | -| **Without it** | No communication | Data must fit in request/response buffers | Would need a different allocator for DMA region | - -The DMA section is **allocated by the transport** but **used by the DMA callbacks**. The pool allocator is **used by the DMA callbacks** to subdivide that DMA section. Three layers, three concerns. diff --git a/docs/draft/timeout.md b/docs/draft/timeout.md deleted file mode 100644 index 4f9583fb0..000000000 --- a/docs/draft/timeout.md +++ /dev/null @@ -1,122 +0,0 @@ -# Timeout Functionality: Client Perspective - -## Overview - -The response timeout feature is primarily designed to prevent the client from blocking indefinitely when the server fails to respond to a request. This is most relevant for **blocking operations** such as wolfCrypt cryptographic calls, where the client sends a request and polls for a response in a tight loop. Without a timeout, a non-responsive server would cause the client to hang forever. - -Since the timeout is checked inside `wh_CommClient_RecvResponse`, it **can** also apply to the split (async) API where the caller manually polls `RecvResponse`. However, in the async case the timeout is only evaluated each time the caller invokes `RecvResponse` -- it does not proactively notify the caller or fire asynchronously. If the caller is not actively polling, the timeout has no effect. - -## 1. Configuration at Init Time - -The timeout feature uses a callback-based abstraction (similar to the lock feature) that allows platform-specific timer implementations without introducing OS dependencies in core wolfHSM code. A platform port provides a callback table implementing the timer operations, and the core timeout module delegates to these callbacks. - -The timeout lives in the comm layer. When creating a client, you provide a `whTimeoutConfig` in the `whCommClientConfig`: -```c -/* Platform-specific setup (e.g. POSIX) */ -posixTimeoutContext posixCtx = {0}; -posixTimeoutConfig posixCfg = {.timeoutUs = WH_SEC_TO_USEC(5)}; -whTimeoutCb timeoutCbTable = POSIX_TIMEOUT_CB; - -/* NOTE: The callback table, platform context, and expiredCtx must remain valid - * for the lifetime of the whCommClient/whTimeout instance. Do not use stack - * locals that go out of scope while the client is still in use. */ -whTimeoutConfig timeoutCfg = { - .cb = &timeoutCbTable, /* platform callback table */ - .context = &posixCtx, /* platform context */ - .config = &posixCfg, /* platform-specific config */ - .expiredCb = myTimeoutHandler, /* optional app callback on expiry */ - .expiredCtx = myAppContext, /* context passed to app callback */ -}; -whCommClientConfig commConfig = { - .transport_cb = &transportCb, - .transport_context = &transportCtx, - .transport_config = &transportCfg, - .client_id = 1, - .respTimeoutConfig = &timeoutCfg, /* attach timeout config */ -}; -whClientConfig clientCfg = { - .comm = &commConfig, -}; -wh_Client_Init(&clientCtx, &clientCfg); -``` - -During `wh_CommClient_Init`, the timeout is initialized via `wh_Timeout_Init()`. This calls the platform `init` callback to set up timer resources but doesn't start any timer yet. -If `respTimeoutConfig` is NULL (or `cb` is NULL), the timeout enters no-op mode and never expires. - -## 2. How the Timeout Works - -The timeout is handled transparently in the comm layer: - -1. **`wh_CommClient_SendRequest`**: After a successful send, starts the response timer via `wh_Timeout_Start()`. -2. **`wh_CommClient_RecvResponse`**: When the transport returns `WH_ERROR_NOTREADY`, checks `wh_Timeout_Expired()`. If expired, returns `WH_ERROR_TIMEOUT`. On successful receive, stops the timer via `wh_Timeout_Stop()`. - -For blocking (synchronous) client APIs, this means the internal `do { ... } while (ret == WH_ERROR_NOTREADY)` polling loop automatically gets timeout support -- the client will return `WH_ERROR_TIMEOUT` instead of spinning forever if the server does not respond within the configured deadline. - -For split (async) APIs where the application calls `SendRequest` and `RecvResponse` separately, the timeout check occurs each time `RecvResponse` is called and returns `WH_ERROR_NOTREADY`. The timeout does **not** interrupt the caller or provide out-of-band notification -- it is purely poll-based. - -``` -Client App CommClient whTimeout - | | | - |-- wh_Client_AesCbc() -------->| | - | |-- SendRequest ------> cb->start() - | | | - | |-- RecvResponse (NOTREADY) | - | |-- Expired? -------> cb->expired() -> no - | |-- RecvResponse (NOTREADY) | - | |-- Expired? -------> cb->expired() -> no - | | ... | - | |-- RecvResponse (NOTREADY) | - | |-- Expired? -------> cb->expired() -> YES - | | |-- expiredCb() - |<-- WH_ERROR_TIMEOUT -----------| | -``` - -## 3. What the Client Sees - -From the application's perspective, any client API that waits for a server response can now return `WH_ERROR_TIMEOUT` (-2010) instead of hanging indefinitely. The application can then decide how to handle it -- retry, log, fail gracefully, etc. -The `expiredCb` fires *before* the error is returned, so you can use it for logging or cleanup without needing to check the return code first. - -## 4. Overriding Expiration via the Callback - -The application expired callback receives a pointer to the `isExpired` flag and can override it by setting `*isExpired = 0`. This suppresses the expiration for the current check, allowing the polling loop to continue. A common use case is to extend the timeout deadline: clear the flag, then call `wh_Timeout_Start()` to restart the timer. - -The callback can also return a non-zero error code to signal a failure. When it does, `wh_Timeout_Expired()` propagates that error directly to the caller instead of returning the expired flag. - -```c -static int myOverrideCb(whTimeout* timeout, int* isExpired) -{ - int* retryCount = (int*)timeout->expiredCtx; - if (retryCount == NULL) { - return WH_ERROR_BADARGS; - } - - (*retryCount)++; - - if (*retryCount <= 1) { - /* First expiration: suppress and restart the timer */ - *isExpired = 0; - wh_Timeout_Start(timeout); - } - /* Subsequent expirations: allow the timeout to fire */ - return WH_ERROR_OK; -} - -int retryCount = 0; -posixTimeoutContext posixCtx = {0}; -posixTimeoutConfig posixCfg = {.timeoutUs = WH_SEC_TO_USEC(5)}; -whTimeoutCb timeoutCbTable = POSIX_TIMEOUT_CB; - -whTimeoutConfig timeoutCfg = { - .cb = &timeoutCbTable, - .context = &posixCtx, - .config = &posixCfg, - .expiredCb = myOverrideCb, - .expiredCtx = &retryCount, -}; -``` - -## 5. Design Notes -- **Primary use case: blocking wolfCrypt operations.** The timeout is designed to prevent indefinite hangs when the server fails to respond to blocking client API calls, which currently only exist when using the wolfCrypt API for crypto. These calls internally poll `RecvResponse` in a tight loop, and the timeout provides automatic protection against a non-responsive server. -- **Async API compatibility.** The timeout mechanism also works with the split wolfHSM `SendRequest`/`RecvResponse` API, but only checks for expiration when `RecvResponse` is called by the application. It is purely poll-driven, and there is no callback, signal, or interrupt that fires independently. If the application stops calling `RecvResponse`, the timeout will not trigger. -- **The timeout is per-comm-client, not per-call.** All operations for a given client share the same `respTimeout` context with the same duration. You can call `wh_Timeout_Set()` to change the duration between calls, but there's no per-operation override. -- **Timer starts on send, checks on receive.** The timer window begins when a request is successfully sent, measuring the full round-trip wait.