diff --git a/.github/workflows/build-and-test-clientonly.yml b/.github/workflows/build-and-test-clientonly.yml index 1c7e92308..e920af0f5 100644 --- a/.github/workflows/build-and-test-clientonly.yml +++ b/.github/workflows/build-and-test-clientonly.yml @@ -34,14 +34,18 @@ jobs: repository: wolfssl/wolfssl path: wolfssl - # Build example server + # Build example server. The SHE client tests use the test-only + # preprogram/destroy key-management messages, so the CI server must be + # built with WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT (never enable this in + # production builds). - name: Build POSIX server run: | cd examples/posix/wh_posix_server + SHE_TEST_CFLAGS="-Wextra -DWOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT" if [ "${{ matrix.transport }}" = "tcp" ]; then - make -j SHE=1 WOLFSSL_DIR=../../../wolfssl + make -j SHE=1 WOLFSSL_DIR=../../../wolfssl CFLAGS_EXTRA="$SHE_TEST_CFLAGS" else - make -j TLS=1 SHE=1 WOLFSSL_DIR=../../../wolfssl + make -j TLS=1 SHE=1 WOLFSSL_DIR=../../../wolfssl CFLAGS_EXTRA="$SHE_TEST_CFLAGS" fi # Start the server in the background @@ -108,16 +112,18 @@ jobs: make -j CLIENT_ONLY=1 TLS=1 SHE=1 AUTH=1 WOLFSSL_DIR=../wolfssl && make run fi - # Rebuild the server with AUTH support and restart + # Rebuild the server with AUTH support and restart. Keep the SHE test + # key-management define so the SHE client tests still work. - name: Rebuild and restart POSIX server for AUTH run: | kill $SERVER_PID || true cd examples/posix/wh_posix_server make clean + SHE_TEST_CFLAGS="-Wextra -DWOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT" if [ "${{ matrix.transport }}" = "tcp" ]; then - make -j SHE=1 AUTH=1 WOLFSSL_DIR=../../../wolfssl + make -j SHE=1 AUTH=1 WOLFSSL_DIR=../../../wolfssl CFLAGS_EXTRA="$SHE_TEST_CFLAGS" else - make -j TLS=1 SHE=1 AUTH=1 WOLFSSL_DIR=../../../wolfssl + make -j TLS=1 SHE=1 AUTH=1 WOLFSSL_DIR=../../../wolfssl CFLAGS_EXTRA="$SHE_TEST_CFLAGS" fi rm -f *.bin || true ./Build/wh_posix_server.elf --type ${{ matrix.transport }} & diff --git a/.github/workflows/build-and-test-refactor.yml b/.github/workflows/build-and-test-refactor.yml index 4656b4a5a..a959d234c 100644 --- a/.github/workflows/build-and-test-refactor.yml +++ b/.github/workflows/build-and-test-refactor.yml @@ -73,6 +73,12 @@ jobs: - name: Build and test refactor ASAN SHE run: cd test-refactor/posix && make clean && make -j SHE=1 ASAN=1 WOLFSSL_DIR=../../wolfssl && make run + # Build and test with the global keys feature disabled (exercises the NVM + # id translation paths, including the DMA add path, with + # WOLFHSM_CFG_GLOBAL_KEYS undefined) + - name: Build and test refactor ASAN NOGLOBALKEYS DMA + run: cd test-refactor/posix && make clean && make -j NOGLOBALKEYS=1 DMA=1 ASAN=1 WOLFSSL_DIR=../../wolfssl && make run + # Build and test with DEBUG=1 - name: Build and test refactor with DEBUG run: cd test-refactor/posix && make clean && make -j DEBUG=1 WOLFSSL_DIR=../../wolfssl && make run diff --git a/.github/workflows/build-and-test-stress.yml b/.github/workflows/build-and-test-stress.yml index b14d8ede4..7b05241fb 100644 --- a/.github/workflows/build-and-test-stress.yml +++ b/.github/workflows/build-and-test-stress.yml @@ -13,7 +13,9 @@ jobs: build: runs-on: ubuntu-latest - timeout-minutes: 10 + # The stress suite runs every phase in three namespace variants + # (global/local/mixed), so give it headroom beyond the old 10 minutes + timeout-minutes: 20 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index d1af2729b..9d597b577 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -73,6 +73,12 @@ jobs: - name: Build and test ASAN SHE run: cd test && make clean && make -j SHE=1 ASAN=1 WOLFSSL_DIR=../wolfssl && make run + # Build and test with the global keys feature disabled (exercises the NVM + # id translation paths, including the DMA add path, with + # WOLFHSM_CFG_GLOBAL_KEYS undefined) + - name: Build and test ASAN NOGLOBALKEYS DMA + run: cd test && make clean && make -j NOGLOBALKEYS=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/src/5-Features.md b/docs/src/5-Features.md index 34e3aabc0..036b798cf 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) @@ -177,7 +178,7 @@ The NVM library presents non-volatile storage as a collection of opaque, variabl - A fixed-size **metadata** record describing the object (see [Object Metadata and Access Attributes](#object-metadata-and-access-attributes)) - A variable-length **payload** of arbitrary bytes -Applications and higher-level subsystems do not address NVM by byte offset; they create, read, enumerate, and destroy whole objects through the `wh_Nvm_*` API. This object orientation is what allows the keystore, certificate manager, and counter subsystems to share a single backing store without colliding: each subsystem owns a range of identifiers and a set of metadata flags, and the underlying NVM layer is unaware of what the objects mean. +Applications and higher-level subsystems do not address NVM by byte offset; they create, read, enumerate, and destroy whole objects through the `wh_Nvm_*` API. This object orientation is what allows the keystore, certificate manager, and counter subsystems to share a single backing store without colliding: each subsystem owns a range of identifiers and a set of metadata flags, and the underlying NVM layer is unaware of what the objects mean. Remote clients get the same object model through the `wh_Client_Nvm*` request API, with one additional layer on top: the ids they supply are namespaced per client, as described in [Client NVM Access and Per-Client Namespaces](#client-nvm-access-and-per-client-namespaces). The core operations exposed by the interface are: @@ -219,6 +220,28 @@ The NVM library exposes both a raw and a policy-checked variant of the mutating The access field is used to express coarser-grained permissions (owner / other / user buckets, with read/write/exec/special bits) that higher layers may consult, and is the primary filter used by `wh_Nvm_List()` when enumerating objects. +### Client NVM Access and Per-Client Namespaces + +Everything above describes the server's view of the object store. Clients reach it remotely through the `wh_Client_Nvm*` request API — `wh_Client_NvmAddObject`, `wh_Client_NvmRead`, `wh_Client_NvmList`, `wh_Client_NvmGetMetadata`, `wh_Client_NvmDestroyObjects`, `wh_Client_NvmGetAvailable`, and the [DMA variants](#dma-support) of add and read — which the server executes against the same `wh_Nvm_*` interface. The id a client supplies, however, is not the raw internal `whNvmId`: exactly as with [key ids](#key-cache-key-ids-and-nvm-backing-store), the server translates every client-supplied NVM id at the request boundary, giving each client a private namespace of objects. + +A client-facing NVM id uses the same encoding as a client-facing `whKeyId`: + +- **Bits 0–7**: the numeric object id, `1` through `255`. Zero is reserved as the erased sentinel and is rejected by `wh_Client_NvmAddObject` — unlike a key cache request, an NVM add never auto-assigns an id. +- **Bit 8** (`WH_KEYID_CLIENT_GLOBAL_FLAG`): selects the **shared global namespace** instead of the calling client's private one. This is the same flag-and-translation mechanism used by [global keys](#global-keys), and like global keys it is honored when `WOLFHSM_CFG_GLOBAL_KEYS` is defined. Without that define there is no global namespace: `wh_Client_NvmAddObject` rejects ids carrying the flag, and the remaining verbs (including `wh_Client_NvmList`) ignore it and operate on the caller's own namespace. +- **Bits 9–10** (the wrapped and hardware-only key flags): not meaningful for NVM objects; `wh_Client_NvmAddObject` rejects ids carrying either flag. + +On each request the server expands the client's id into the full internal form with TYPE = `WH_KEYTYPE_NVM` and USER = the connection's client id (or `0` for the global namespace), and collapses it back in responses. The isolation consequences mirror the keystore's: + +- Every client sees the same `[1, 255]` id range, but two clients that both write "object 5" create two distinct objects; neither can read, overwrite, enumerate, or destroy the other's. +- Because the translation always stamps TYPE = `WH_KEYTYPE_NVM` and the caller's own USER, the client NVM API cannot name keys, counters, SHE slots, or another client's objects at all. +- When `WOLFHSM_CFG_GLOBAL_KEYS` is defined, objects in the global namespace are visible to every client that sets the GLOBAL flag. Factory images produced by the [NVM provisioning tool](6-Utilities.md#nvm-provisioning-tool) place plain `obj` entries with ids up to 255 in exactly this namespace, so provisioned objects are reachable by all clients out of the box. On builds without the define, these USER=0 objects are not reachable through the client NVM API at all. + +When `WOLFHSM_CFG_GLOBAL_KEYS` is defined, `wh_Client_NvmList` treats the GLOBAL flag on its `startId` argument as a namespace selector: pass `0` to enumerate the calling client's own objects from the beginning, or `WH_KEYID_CLIENT_GLOBAL_FLAG` to enumerate the global namespace. The returned count covers only the selected namespace, and each returned id carries the appropriate flag, so it can be passed back unchanged as the next `startId` to continue the walk. Without the define, the flag on `startId` is ignored and the walk always covers the caller's own namespace. + +Access- and flags-based filtering and the policy-checked NVM variants apply unchanged after translation, and server-local code with direct access to the `wh_Nvm_*` API is unaffected — it continues to address objects by full internal id. + +For integrations that depend on the historical behavior, defining `WOLFHSM_CFG_LEGACY_CLIENT_NVM` disables the translation entirely and restores the flat 16-bit id space shared by all clients — along with the cross-client reachability that comes with it. 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): @@ -298,7 +321,7 @@ Keys are named by a 16-bit identifier (`whKeyId`), which has two forms — a sim The server-side `whKeyId` packs three fields into its 16 bits: -- **TYPE** (top 4 bits): the kind of object — `WH_KEYTYPE_CRYPTO` for ordinary crypto keys, `WH_KEYTYPE_SHE` for AUTOSAR SHE keys, `WH_KEYTYPE_COUNTER` for monotonic counters, `WH_KEYTYPE_WRAPPED` for wrapped-key metadata, and `WH_KEYTYPE_NVM` for non-key NVM objects that share the same id space. +- **TYPE** (top 4 bits): the kind of object — `WH_KEYTYPE_CRYPTO` for ordinary crypto keys, `WH_KEYTYPE_SHE` for AUTOSAR SHE keys, `WH_KEYTYPE_COUNTER` for monotonic counters, `WH_KEYTYPE_WRAPPED` for wrapped-key metadata, and `WH_KEYTYPE_NVM` for non-key NVM objects that share the same id space (see [Client NVM Access and Per-Client Namespaces](#client-nvm-access-and-per-client-namespaces)). - **USER** (middle 4 bits): the owning client. Value `0` is reserved for the global-key namespace when `WOLFHSM_CFG_GLOBAL_KEYS` is enabled. - **ID** (low 8 bits): the number the client chose. @@ -670,10 +693,13 @@ The SHE client API is declared in `wolfhsm/wh_client_she.h` and maps one-to-one - **MAC**: `wh_Client_SheGenerateMac` / `wh_Client_SheVerifyMac` (`CMD_GENERATE_MAC` / `CMD_VERIFY_MAC`) — CMAC generation and verification against a selected key slot - **Status**: `wh_Client_SheGetStatus` (`CMD_GET_STATUS`) — reads the SHE status register (SREG) -In addition to the spec commands, wolfHSM exposes two non-standard helpers that fill gaps left by the spec's assumption of dedicated hardware: +In addition to the spec commands, wolfHSM exposes non-standard helpers that fill gaps left by the spec's assumption of dedicated hardware: - `wh_Client_SheSetUid`: explicitly programs the 15-byte ECU UID that the key update protocol binds against. The AUTOSAR spec assumes this value is hardware-fused; wolfHSM needs a software path to install it, and rejects most SHE operations until it has been set. -- `wh_Client_ShePreProgramKey`: writes a key directly into a SHE NVM slot, bypassing the encrypted M1–M5 protocol. This exists to support initial provisioning on a blank device — once a `MASTER_ECU_KEY` exists, all subsequent updates can go through the spec-compliant protocol. +- `wh_Client_ShePreProgramKey`: writes a key, its protection flags, and its initial key counter directly into a SHE NVM slot over a dedicated SHE message, bypassing the encrypted M1–M5 protocol. This exists to support initial provisioning on a blank device — once a `MASTER_ECU_KEY` exists, all subsequent updates can go through the spec-compliant protocol. +- `wh_Client_SheDestroyKey`: removes a SHE key slot from the calling client's NVM namespace, a capability the spec omits because it treats key slots as fixed hardware. + +Because `wh_Client_ShePreProgramKey` and `wh_Client_SheDestroyKey` bypass the SHE key-update and authorization protocols, both are compiled only when `WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT` is defined (see [Configuration](9-Configuration.md#cryptography-features)) and are intended for provisioning and test builds, not production firmware. All SHE commands return one of the spec's `WH_SHE_ERC_*` error codes (`SEQUENCE_ERROR`, `KEY_NOT_AVAILABLE`, `WRITE_PROTECTED`, `KEY_UPDATE_ERROR`, etc.) alongside the wolfHSM transport return code, so applications can distinguish protocol-level failures from communication failures. @@ -746,7 +772,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**: not supported for SHE keys — every SHE keyId carries the connection's client ID in the USER field, and the SHE command set has no way to name the [global](#global-keys) (`WH_KEYUSER_GLOBAL`) namespace. Applications that need to share a key across clients must provision it into each client's SHE namespace separately. -A typical automotive deployment uses the SHE extension end-to-end: the bootloader and `BOOT_MAC` are programmed into NVM at production using `wh_Client_ShePreProgramKey`, the device's UID is set on first boot with `wh_Client_SheSetUid`, secure boot is run on every reset via `wh_Client_SheSecureBoot`, in-field key updates flow through the encrypted `CMD_LOAD_KEY` protocol, and CAN message authentication uses `wh_Client_SheGenerateMac` / `wh_Client_SheVerifyMac` against pre-provisioned user-slot keys. +A typical automotive deployment uses the SHE extension end-to-end: the bootloader and `BOOT_MAC` are programmed into NVM at production via whnvmtool or via `wh_Client_ShePreProgramKey`, the device's UID is set on first boot with `wh_Client_SheSetUid`, secure boot is run on every reset via `wh_Client_SheSecureBoot`, in-field key updates flow through the encrypted `CMD_LOAD_KEY` protocol, and CAN message authentication uses `wh_Client_SheGenerateMac` / `wh_Client_SheVerifyMac` against pre-provisioned user-slot keys. ## Non-Volatile Monotonic Counters diff --git a/docs/src/6-Utilities.md b/docs/src/6-Utilities.md index 42eb2368f..09c809191 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 the `--size` argument must match the server's `whNvmFlash` partition size. For the full configuration file schema, command-line options, hex conversion recipe, and test workflow, see [`tools/whnvmtool/README.md`](https://github.com/wolfSSL/wolfHSM/blob/main/tools/whnvmtool/README.md). +The ids in the configuration file are **server-internal** ids, written into the image verbatim. This matters when the provisioned objects are meant to be reached through the client NVM API, which namespaces ids per client (see [Client NVM Access and Per-Client Namespaces](5-Features.md#client-nvm-access-and-per-client-namespaces)): a plain `obj` entry with an id of `255` or below lands in the shared global namespace, which clients address by setting `WH_KEYID_CLIENT_GLOBAL_FLAG` on the request id. To pre-provision an object into one specific client's private namespace, encode the owning client id into bits 8–11 of the entry's id — the same encoding the tool's `key` entries construct from their separate `clientId` field. Servers built with `WOLFHSM_CFG_LEGACY_CLIENT_NVM` skip client-id translation, and their clients address provisioned objects by verbatim id instead. + ## Benchmark Suite ### Benchmark Suite Overview diff --git a/docs/src/9-Configuration.md b/docs/src/9-Configuration.md index e1cd9563a..9303f1a25 100644 --- a/docs/src/9-Configuration.md +++ b/docs/src/9-Configuration.md @@ -72,6 +72,7 @@ These macros enable or tune optional cryptographic subsystems built on top of wo | Macro | Default | Description | |---|---|---| | `WOLFHSM_CFG_SHE_EXTENSION` | Undefined | If defined, compile the AUTOSAR SHE subsystem (SHE message types, SHE key slots, M1-M5 update protocol, SHE-specific RNG and SREG handling). Requires wolfCrypt built with AES, `WOLFSSL_CMAC`, `WOLFSSL_AES_DIRECT`, and `HAVE_AES_ECB`. | +| `WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT` | Undefined | If defined, compile the SHE provisioning/test key-management helpers `wh_Client_ShePreProgramKey` and `wh_Client_SheDestroyKey` and their server handlers, which write and remove SHE key slots directly, bypassing the M1-M5 encrypted update protocol. Intended for provisioning and test builds only; leave undefined in production. Only meaningful when `WOLFHSM_CFG_SHE_EXTENSION` is defined. | | `WOLFHSM_CFG_KEYWRAP` | Undefined | If defined, compile the key-wrap subsystem (`wh_Client_KeyWrap*` / server counterparts). Uses AES-GCM internally and therefore requires wolfCrypt built with AES and `HAVE_AESGCM`. Incompatible with `WOLFHSM_CFG_NO_CRYPTO`. | | `WOLFHSM_CFG_KEYWRAP_MAX_KEY_SIZE` | `2000` | 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. | | `WOLFHSM_CFG_KEYWRAP_MAX_DATA_SIZE` | `2000` | 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. | @@ -98,6 +99,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_SERVER_NVM_FLASH_LOG` | Undefined | If defined, compile the log-structured NVM flash backend (`wh_nvm_flash_log`). When enabled it can be selected at runtime as an alternative to the regular flash backend; useful for flash parts that tolerate fewer erases or that prefer append-only update patterns. | +| `WOLFHSM_CFG_LEGACY_CLIENT_NVM` | Undefined | If defined, the server's client-facing NVM handlers pass object ids to the NVM layer verbatim, restoring the legacy flat 16-bit id space shared by all clients. When left undefined (the default), every client-supplied NVM id is translated into the server-internal TYPE/USER/ID encoding, giving each client a private `1`–`255` object namespace plus a shared global namespace selected with `WH_KEYID_CLIENT_GLOBAL_FLAG`, and preventing the client NVM API from reaching keys, counters, or other clients' objects. See [Client NVM Access and Per-Client Namespaces](5-Features.md#client-nvm-access-and-per-client-namespaces). | ## Certificate Manager diff --git a/examples/posix/wh_posix_server/wolfhsm_cfg.h b/examples/posix/wh_posix_server/wolfhsm_cfg.h index 4bb3fec11..f29422b2e 100644 --- a/examples/posix/wh_posix_server/wolfhsm_cfg.h +++ b/examples/posix/wh_posix_server/wolfhsm_cfg.h @@ -37,7 +37,7 @@ /* Must match client WOLFHSM_CFG_COMM_DATA_LEN */ #define WOLFHSM_CFG_COMM_DATA_LEN (1024 * 8) -#define WOLFHSM_CFG_NVM_OBJECT_COUNT 30 +#define WOLFHSM_CFG_NVM_OBJECT_COUNT 64 #define WOLFHSM_CFG_SERVER_KEYCACHE_COUNT 9 #define WOLFHSM_CFG_SERVER_KEYCACHE_SIZE 1024 diff --git a/src/wh_client_she.c b/src/wh_client_she.c index 716934f83..7ba9983d7 100644 --- a/src/wh_client_she.c +++ b/src/wh_client_she.c @@ -47,22 +47,98 @@ #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 = {0}; + 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); - /* Create a key with 0 counter */ - wh_She_Meta2Label(0, flags, label); - ret = wh_Client_NvmAddObject(c, - WH_MAKE_KEYID(WH_KEYTYPE_SHE, c->comm->client_id, keyId), - 0, 0, sizeof(label), label, keySz, key, (int32_t*)&outRc); - if (ret == 0) - ret = outRc; + ret = wh_Client_SendRequest(c, WH_MESSAGE_GROUP_SHE, WH_SHE_PRE_PROGRAM_KEY, + sizeof(*req) + keySz, reqBuf); + if (ret == 0) { + do { + ret = wh_Client_RecvResponse(c, &group, &action, &dataSz, + (uint8_t*)&resp); + } while (ret == WH_ERROR_NOTREADY); + } + if (ret == 0) { + /* Validate the response. A server built without + * WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT sends an empty response for + * this action; without this check the zero-initialized resp.rc + * would read as success even though no key was stored. */ + 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 = {0}; + + if (c == NULL) { + return WH_ERROR_BADARGS; + } + + req = (whMessageShe_DestroyKeyRequest*)wh_CommClient_GetDataPtr(c->comm); + req->keyId = keyId; + + ret = wh_Client_SendRequest(c, WH_MESSAGE_GROUP_SHE, WH_SHE_DESTROY_KEY, + sizeof(*req), (uint8_t*)req); + if (ret == 0) { + do { + ret = wh_Client_RecvResponse(c, &group, &action, &dataSz, + (uint8_t*)&resp); + } while (ret == WH_ERROR_NOTREADY); + } + if (ret == 0) { + /* Validate the response (see wh_Client_ShePreProgramKey) */ + if ((group != WH_MESSAGE_GROUP_SHE) || (action != WH_SHE_DESTROY_KEY) || + (dataSz != sizeof(resp))) { + ret = WH_ERROR_ABORTED; + } + else { + ret = resp.rc; + } + } return ret; } +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ int wh_Client_SheSetUidRequest(whClientContext* c, uint8_t* uid, uint32_t uidSz) { diff --git a/src/wh_message_she.c b/src/wh_message_she.c index 659673128..1fdaa6f53 100644 --- a/src/wh_message_she.c +++ b/src/wh_message_she.c @@ -429,4 +429,53 @@ int wh_MessageShe_TranslateVerifyMacResponse( return 0; } +#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 */ \ No newline at end of file diff --git a/src/wh_server_nvm.c b/src/wh_server_nvm.c index 9481db048..cb0ebb547 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,64 @@ #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 + /* Clear the WRAPPED and HW client flags before translating. They are not + * valid for NVM objects and would otherwise override the WH_KEYTYPE_NVM + * type below, letting a client reach a wrapped-key or hardware object + * through the NVM API. Stripping them here keeps every request path + * TYPE=NVM. AddObject still rejects them outright (see + * _NvmValidateClientId) for a clear error; the GLOBAL namespace selector + * is preserved (honored only when WOLFHSM_CFG_GLOBAL_KEYS is enabled). */ + clientId &= + (whNvmId) ~(WH_KEYID_CLIENT_WRAPPED_FLAG | WH_KEYID_CLIENT_HW_FLAG); + return wh_KeyId_TranslateFromClient(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 +} + +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM +/* Reject ids that are invalid in the translated scheme: the bare id portion + * must be non-zero (id=0 is the erased sentinel; auto-generation is not + * supported here) and the WRAPPED and HW flags are not valid for NVM + * objects. Without global keys the GLOBAL flag is also rejected, so an add + * fails loudly instead of silently landing in the caller's own namespace. */ +static int _NvmValidateClientId(whNvmId clientId) +{ + if ((clientId & WH_KEYID_MASK) == WH_KEYID_ERASED) { + return WH_ERROR_BADARGS; + } + if ((clientId & (WH_KEYID_CLIENT_WRAPPED_FLAG | WH_KEYID_CLIENT_HW_FLAG)) != + 0) { + return WH_ERROR_BADARGS; + } +#ifndef WOLFHSM_CFG_GLOBAL_KEYS + /* No global namespace in this build */ + if ((clientId & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0) { + return WH_ERROR_BADARGS; + } +#endif + return WH_ERROR_OK; +} +#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, @@ -160,9 +219,64 @@ int wh_Server_HandleNvmRequest(whServerContext* server, rc = WH_SERVER_NVM_LOCK(server); if (rc == WH_ERROR_OK) { +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM +#ifdef WOLFHSM_CFG_GLOBAL_KEYS + /* The GLOBAL flag on startId selects which namespace to + * iterate: set => global (USER=0), clear => this client's own + * objects (USER=client_id). The flag rides through both + * translation helpers, so iterating with the previously + * returned id stays in the same namespace. */ + const uint16_t target_user = + ((req.startId & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0) + ? WH_KEYUSER_GLOBAL + : server->comm->client_id; +#else + /* Without global keys the GLOBAL flag is inert, here and in + * the translation helpers: only the caller's own namespace + * can be listed. */ + const uint16_t target_user = server->comm->client_id; +#endif + /* startId id-portion of 0 is the start-from-beginning + * sentinel; pass it through unchanged. Otherwise translate to + * the server-internal id so wh_Nvm_List resumes after it. */ + whNvmId cur = + ((req.startId & WH_KEYID_MASK) == 0) + ? 0 + : _NvmTranslateFromClient(server, req.startId); + whNvmId hit_id = 0; + whNvmId total = 0; + + for (;;) { + whNvmId next_id = 0; + whNvmId remaining = 0; + 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() */ @@ -217,13 +331,15 @@ int wh_Server_HandleNvmRequest(whServerContext* server, rc = WH_SERVER_NVM_LOCK(server); 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 +369,42 @@ 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)); - - rc = WH_ERROR_OK; +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + int validate_rc = _NvmValidateClientId(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; #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; } } /* Convert the response struct */ @@ -301,11 +427,19 @@ int wh_Server_HandleNvmRequest(whServerContext* server, (whMessageNvm_DestroyObjectsRequest*)req_packet, &req); if (req.list_count <= WH_MESSAGE_NVM_MAX_DESTROY_OBJECTS_COUNT) { + whNvmId + translated_ids[WH_MESSAGE_NVM_MAX_DESTROY_OBJECTS_COUNT]; + whNvmId i; + for (i = 0; i < req.list_count; i++) { + translated_ids[i] = + _NvmTranslateFromClient(server, req.list[i]); + } + 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() */ @@ -342,7 +476,8 @@ int wh_Server_HandleNvmRequest(whServerContext* server, rc = WH_SERVER_NVM_LOCK(server); 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,23 +529,31 @@ int wh_Server_HandleNvmRequest(whServerContext* server, } } if (resp.rc == 0) { + /* Take a local copy of the metadata so we can rewrite the id + * field without touching host memory. */ + whNvmMetadata local_meta = *(const whNvmMetadata*)metadata; +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + resp.rc = _NvmValidateClientId(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() */ @@ -455,7 +598,8 @@ int wh_Server_HandleNvmRequest(whServerContext* server, 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) { @@ -485,7 +629,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 22c9acace..f453eea2d 100644 --- a/src/wh_server_she.c +++ b/src/wh_server_she.c @@ -125,6 +125,14 @@ static int _GenerateMac(whServerContext* server, uint16_t magic, static int _VerifyMac(whServerContext* server, uint16_t magic, uint16_t req_size, const void* req_packet, uint16_t* out_resp_size, void* resp_packet); +#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 _ReportInvalidSheState(whServerContext* server, uint16_t magic, uint16_t action, uint16_t req_size, @@ -1637,6 +1645,89 @@ static int _VerifyMac(whServerContext* server, uint16_t magic, return ret; } +/* Pre-program a SHE-typed NVM entry under the calling client's USER namespace. + * Replaces the historical client-side use of wh_Client_NvmAddObject with a + * hand-constructed SHE-typed id, which is incompatible with the new client-id + * translation on the NVM message path. */ +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT +static int _PreProgramKey(whServerContext* server, uint16_t magic, + uint16_t req_size, const void* req_packet, + uint16_t* out_resp_size, void* resp_packet) +{ + int ret = 0; + whMessageShe_PreProgramKeyRequest req = {0}; + whMessageShe_PreProgramKeyResponse resp = {0}; + whNvmMetadata meta = {0}; + const uint8_t* key_data; + const uint16_t hdr_len = sizeof(req); + + if (req_size < hdr_len) { + ret = WH_ERROR_BUFFER_SIZE; + } + if (ret == 0) { + ret = wh_MessageShe_TranslatePreProgramKeyRequest(magic, req_packet, + &req); + } + if (ret == 0 && (req.keySz == 0 || req_size != hdr_len + req.keySz)) { + ret = WH_ERROR_BADARGS; + } + if (ret == 0) { + key_data = (const uint8_t*)req_packet + hdr_len; + + meta.id = WH_MAKE_KEYID(WH_KEYTYPE_SHE, 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) { + 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) { + id = WH_MAKE_KEYID(WH_KEYTYPE_SHE, server->comm->client_id, + (whNvmId)req.keyId); + ret = WH_SERVER_NVM_LOCK(server); + 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 */ + /* 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 @@ -1651,7 +1742,7 @@ static int _ReportInvalidSheState(whServerContext* server, uint16_t magic, (void)req_size; if (action == WH_SHE_GET_STATUS) { - /* Status read is always permitted per AUTOSAR spec, even before boot + /* Status read is always permitted per AUTOSAR spec, even before boot *or UID setup. */ } else if (action == WH_SHE_SET_UID) { @@ -1660,6 +1751,10 @@ static int _ReportInvalidSheState(whServerContext* server, uint16_t magic, ret = WH_SHE_ERC_SEQUENCE_ERROR; } } + 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 if (server->she->uidSet == 0) { /* Every remaining command needs a provisioned UID. */ ret = WH_SHE_ERC_SEQUENCE_ERROR; @@ -1963,6 +2058,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_keywrap.c b/test-refactor/client-server/wh_test_keywrap.c index 174a9f811..9ef84ba79 100644 --- a/test-refactor/client-server/wh_test_keywrap.c +++ b/test-refactor/client-server/wh_test_keywrap.c @@ -231,23 +231,44 @@ static int _whTest_KeywrapTrustedKekPolicy(whClientContext* client) } /* (b) A client that provisions an NVM object carrying - * WH_NVM_FLAGS_TRUSTED at a crypto-key id (keys and NVM objects share - * the id space) must not - * obtain a trusted KEK either: the checked NVM add path strips the flag */ + * WH_NVM_FLAGS_TRUSTED at a crypto-key id must not obtain a trusted KEK + * either. The forged id embeds the client id in the USER bits, which + * overlap the client WRAPPED/HW flag bits, so depending on the client id + * the checked add either rejects the id outright or stores the object + * outside the crypto-key id space; either way no KEK may appear at the + * crypto-key id. */ { whNvmId nvmObjId = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, client->comm->client_id, WH_TEST_KW_NVM_FORGE_ID); - int32_t nvmRc = 0; + int32_t nvmRc = 0; + int32_t expectedAddRc = 0; + int expectedRc; + +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + if ((nvmObjId & + (WH_KEYID_CLIENT_WRAPPED_FLAG | WH_KEYID_CLIENT_HW_FLAG)) != 0) { + /* Forged id aliases a client flag bit; the checked add rejects + * it before storing anything. */ + expectedAddRc = WH_ERROR_BADARGS; + } +#ifndef WOLFHSM_CFG_GLOBAL_KEYS + if ((nvmObjId & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0) { + /* Without global keys an aliased GLOBAL bit is rejected too */ + expectedAddRc = WH_ERROR_BADARGS; + } +#endif +#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); @@ -258,10 +279,21 @@ static int _whTest_KeywrapTrustedKekPolicy(whClientContext* client) int32_t destroyRc = 0; (void)wh_Client_NvmDestroyObjects(client, 1, &nvmObjId, &destroyRc); } - if (ret != WH_ERROR_ACCESS) { +#ifdef WOLFHSM_CFG_LEGACY_CLIENT_NVM + /* Legacy flat id space: the forged object lands at the crypto-key + * id; the checked NVM add stripped the TRUSTED flag, so KEK use is + * refused. */ + expectedRc = WH_ERROR_ACCESS; +#else + /* An accepted forge is stored as a plain NVM object in the caller's + * namespace and a rejected one stores nothing, so no KEK exists at + * the crypto-key id either way. */ + 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 f5883e403..d78e79c4e 100644 --- a/test-refactor/client-server/wh_test_she.c +++ b/test-refactor/client-server/wh_test_she.c @@ -63,17 +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_MAKE_KEYID(WH_KEYTYPE_SHE, 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); } @@ -220,12 +210,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); @@ -258,12 +248,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; @@ -374,9 +364,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; } @@ -490,10 +480,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; } diff --git a/test-refactor/config/wolfhsm_cfg.h b/test-refactor/config/wolfhsm_cfg.h index ac428acfe..dda03dd50 100644 --- a/test-refactor/config/wolfhsm_cfg.h +++ b/test-refactor/config/wolfhsm_cfg.h @@ -32,11 +32,14 @@ #define WOLFHSM_CFG_COMM_DATA_LEN (1024 * 8) +/* NOGLOBALKEYS=1 builds leave global keys disabled to cover that config */ +#ifndef WOLFHSM_CFG_TEST_NO_GLOBAL_KEYS #define WOLFHSM_CFG_GLOBAL_KEYS +#endif #define WOLFHSM_CFG_LOGGING -#define WOLFHSM_CFG_NVM_OBJECT_COUNT 30 +#define WOLFHSM_CFG_NVM_OBJECT_COUNT 64 #define WOLFHSM_CFG_SERVER_KEYCACHE_COUNT 9 #define WOLFHSM_CFG_SERVER_KEYCACHE_BUFSIZE 300 #define WOLFHSM_CFG_DMAADDR_COUNT 8 @@ -52,6 +55,11 @@ #define WOLFHSM_CFG_HWKEYSTORE #endif +/* Enable SHE preprogram/destroy test-only key-management APIs. These bypass + * the authenticated SHE key-update protocol and are not part of the SHE + * specification, so they must never be enabled in production builds. */ +#define WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT + /* Test log-based NVM flash backend */ #define WOLFHSM_CFG_SERVER_NVM_FLASH_LOG diff --git a/test-refactor/misc/wh_test_multiclient.c b/test-refactor/misc/wh_test_multiclient.c index 34427e82e..46d754752 100644 --- a/test-refactor/misc/wh_test_multiclient.c +++ b/test-refactor/misc/wh_test_multiclient.c @@ -1338,6 +1338,522 @@ static int _runGlobalKeysTests(whClientContext* client1, #endif /* WOLFHSM_CFG_GLOBAL_KEYS */ +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM +/* ============================================================================ + * CLIENT NVM ID-TRANSLATION TEST SUITE + * + * These tests assert the per-client NVM id namespace: each client sees its own + * 1..255 id range plus a shared 1..255 global range. Cross-client raw access + * via the NVM api is impossible. + * + * Only meaningful when client NVM id translation is enabled (default). + * ========================================================================== */ + +static const uint8_t NVM_ISOLATION_PAYLOAD_A[] = "client-A-secret-NVM-payload"; +static const uint8_t NVM_ISOLATION_PAYLOAD_B[] = "client-B-different-payload"; + +/* + * Helper: add an NVM object via the explicit Request/Handle/Response + * pattern so that the matching server can be driven manually (multiclient + * sequential setup has no automatic dispatch). + */ +static int _nvmAddViaServer(whClientContext* client, whServerContext* server, + whNvmId id, whNvmSize len, const uint8_t* data, + int32_t* out_rc) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectRequest( + client, id, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, 0, NULL, len, data)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectResponse(client, out_rc)); + return WH_ERROR_OK; +} + +static int _nvmReadViaServer(whClientContext* client, whServerContext* server, + whNvmId id, whNvmSize len, int32_t* out_rc, + whNvmSize* out_len, uint8_t* buf) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmReadRequest(client, id, 0, len)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmReadResponse(client, out_rc, out_len, buf)); + return WH_ERROR_OK; +} + +static int _nvmDestroyViaServer(whClientContext* client, + whServerContext* server, whNvmId id, + int32_t* out_rc) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjectsRequest(client, 1, &id)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjectsResponse(client, out_rc)); + return WH_ERROR_OK; +} + +static int _nvmListViaServer(whClientContext* client, whServerContext* server, + whNvmId startId, int32_t* out_rc, + whNvmId* out_count, whNvmId* out_id) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmListRequest( + client, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, startId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmListResponse(client, out_rc, out_count, out_id)); + return WH_ERROR_OK; +} + +static int _nvmGetMetadataViaServer(whClientContext* client, + whServerContext* server, whNvmId id, + int32_t* out_rc) +{ + whNvmId got_id = 0; + whNvmAccess access = 0; + whNvmFlags flags = 0; + whNvmSize len = 0; + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadataRequest(client, id)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadataResponse( + client, out_rc, &got_id, &access, &flags, &len, 0, NULL)); + return WH_ERROR_OK; +} + +/* + * Client A adds NVM id 5 with secret payload. Client B reading id 5 must NOT + * see A's bytes (either NOTFOUND, or B's own value if B has one). + */ +static int _testNvmClientIsolation(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId shared_id = 5; + int32_t out_rc = 0; + uint8_t buf[64] = {0}; + whNvmSize out_len = 0; + + WH_TEST_PRINT("Testing NVM client isolation...\n"); + + /* Client A adds a secret object at id=5 */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, shared_id, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Client A confirms it can read its own bytes back */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) == 0); + + /* Client B reads id=5: must NOT find A's bytes */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client2, server2, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + /* Either NOTFOUND or read returned different bytes; never A's plaintext. */ + if (out_rc == WH_ERROR_OK) { + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) != 0); + } + + /* Client B adds its own object at the same client-facing id=5 */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client2, server2, shared_id, + sizeof(NVM_ISOLATION_PAYLOAD_B), + NVM_ISOLATION_PAYLOAD_B, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Client B reads id=5: gets ITS OWN value */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client2, server2, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_B)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_B, + sizeof(NVM_ISOLATION_PAYLOAD_B)) == 0); + + /* Client A still sees ITS OWN value, not B's */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) == 0); + + /* Cleanup: each client destroys its own */ + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, shared_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client2, server2, shared_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM client isolation: PASS\n"); + return WH_ERROR_OK; +} + +#ifdef WOLFHSM_CFG_GLOBAL_KEYS +/* + * List has two namespaces: + * - own : startId without GLOBAL flag + * - global : startId with WH_KEYID_CLIENT_GLOBAL_FLAG set + * + * Client A populates 2 own ids and 2 global ids. Each scan must visit only + * the corresponding namespace. + */ +static int _testNvmGlobalNamespaceList(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int32_t out_rc = 0; + whNvmId count = 0; + whNvmId cur = 0; + int seen_own[256] = {0}; + int seen_global[256] = {0}; + int i; + int iters; + const whNvmId own_ids[2] = {3, 7}; + const whNvmId global_ids[2] = {2, 4}; + (void)server2; + (void)client2; + + WH_TEST_PRINT("Testing NVM list with global namespace...\n"); + + /* Populate own ids */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, own_ids[i], sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + /* Populate global ids */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, global_ids[i] | WH_KEYID_CLIENT_GLOBAL_FLAG, + sizeof(NVM_ISOLATION_PAYLOAD_B), NVM_ISOLATION_PAYLOAD_B, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + /* Iterate own namespace (no flag on startId). Expect ids in own_ids. + * Cap iterations to defend against an unintentional infinite loop. */ + cur = 0; + for (iters = 0; iters < 16; iters++) { + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + if (count == 0) { + break; + } + /* Must not carry the GLOBAL flag */ + WH_TEST_ASSERT_RETURN((cur & WH_KEYID_CLIENT_GLOBAL_FLAG) == 0); + WH_TEST_ASSERT_RETURN((cur & WH_KEYID_MASK) <= WH_KEYID_IDMAX); + seen_own[cur & WH_KEYID_MASK] = 1; + if (count == 1) { + break; + } + } + WH_TEST_ASSERT_RETURN(iters < 16); + + /* Iterate global namespace (GLOBAL flag on startId). Expect ids in + * global_ids, all returned with GLOBAL flag set. */ + cur = WH_KEYID_CLIENT_GLOBAL_FLAG; + for (iters = 0; iters < 16; iters++) { + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + if (count == 0) { + break; + } + WH_TEST_ASSERT_RETURN((cur & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0); + seen_global[cur & WH_KEYID_MASK] = 1; + if (count == 1) { + break; + } + } + WH_TEST_ASSERT_RETURN(iters < 16); + + for (i = 0; i < 2; i++) { + WH_TEST_ASSERT_RETURN(seen_own[own_ids[i]] == 1); + WH_TEST_ASSERT_RETURN(seen_global[global_ids[i]] == 1); + } + /* Crosscheck: globals never appear in the own scan and vice-versa */ + WH_TEST_ASSERT_RETURN(seen_own[global_ids[0]] == 0); + WH_TEST_ASSERT_RETURN(seen_own[global_ids[1]] == 0); + WH_TEST_ASSERT_RETURN(seen_global[own_ids[0]] == 0); + WH_TEST_ASSERT_RETURN(seen_global[own_ids[1]] == 0); + + /* Cleanup */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, own_ids[i], &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL(_nvmDestroyViaServer( + client1, server1, global_ids[i] | WH_KEYID_CLIENT_GLOBAL_FLAG, + &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + WH_TEST_PRINT(" NVM global namespace list: PASS\n"); + return WH_ERROR_OK; +} + +#else /* !WOLFHSM_CFG_GLOBAL_KEYS */ + +/* + * Without global keys there is no global namespace: AddObject must reject + * the GLOBAL flag and the other verbs must ignore it, resolving to the + * caller's own namespace. USER=0 objects (e.g. provisioned by whnvmtool) + * must stay unreachable, including via List. + */ +static int _testNvmGlobalFlagDisabled(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId own_id = 3; + const whNvmId planted_id = 6; + whNvmId planted_nvm_id; + whNvmMetadata meta = {0}; + int32_t out_rc = 0; + whNvmId count = 0; + whNvmId cur = 0; + whNvmSize out_len = 0; + uint8_t buf[64] = {0}; + + (void)client2; + (void)server2; + + WH_TEST_PRINT("Testing NVM GLOBAL flag with global keys disabled...\n"); + + /* Plant a USER=0 object directly, as provisioning would */ + planted_nvm_id = + WH_MAKE_KEYID(WH_KEYTYPE_NVM, WH_KEYUSER_GLOBAL, planted_id); + meta.id = planted_nvm_id; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = sizeof(NVM_ISOLATION_PAYLOAD_B); + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, sizeof(NVM_ISOLATION_PAYLOAD_B), + NVM_ISOLATION_PAYLOAD_B) == WH_ERROR_OK); + + /* AddObject with the GLOBAL flag fails loudly */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, 5 | WH_KEYID_CLIENT_GLOBAL_FLAG, + sizeof(NVM_ISOLATION_PAYLOAD_A), NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + +#ifdef WOLFHSM_CFG_DMA + /* The DMA add path shares the same id validation */ + { + whNvmMetadata dma_meta = {0}; + dma_meta.id = 5 | WH_KEYID_CLIENT_GLOBAL_FLAG; + dma_meta.access = WH_NVM_ACCESS_ANY; + dma_meta.flags = WH_NVM_FLAGS_NONE; + dma_meta.len = sizeof(NVM_ISOLATION_PAYLOAD_A); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectDmaRequest( + client1, &dma_meta, sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmAddObjectDmaResponse(client1, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + } +#endif + + /* Add an own object without the flag */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, own_id, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* A GLOBAL-flagged List ignores the flag: it walks the caller's own + * namespace and never surfaces the planted USER=0 object */ + cur = WH_KEYID_CLIENT_GLOBAL_FLAG; + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(count == 1); + WH_TEST_ASSERT_RETURN(cur == own_id); + + /* Read ignores the flag the same way: it resolves to the own object */ + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, own_id | WH_KEYID_CLIENT_GLOBAL_FLAG, sizeof(buf), + &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, out_len) == 0); + + /* The planted USER=0 object is not reachable via the flag */ + WH_TEST_RETURN_ON_FAIL(_nvmGetMetadataViaServer( + client1, server1, planted_id | WH_KEYID_CLIENT_GLOBAL_FLAG, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* Cleanup */ + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, own_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN( + wh_Nvm_DestroyObjects(server1->nvm, 1, &planted_nvm_id) == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM GLOBAL flag disabled semantics: PASS\n"); + return WH_ERROR_OK; +} +#endif /* WOLFHSM_CFG_GLOBAL_KEYS */ + +/* + * Reject malformed AddObject requests: + * - id portion == 0 (erased sentinel) + * - wrapped flag set + */ +static int _testNvmAddObjectRejections(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int32_t out_rc = 0; + (void)server2; + (void)client2; + + WH_TEST_PRINT("Testing NVM AddObject bad-id rejections...\n"); + + /* id=0 with own scope */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, 0, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* id=0 with global scope */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, + WH_KEYID_CLIENT_GLOBAL_FLAG, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* wrapped flag set */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, + 5 | WH_KEYID_CLIENT_WRAPPED_FLAG, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + WH_TEST_PRINT(" NVM AddObject rejections: PASS\n"); + return WH_ERROR_OK; +} + +/* + * The WRAPPED and HW client flags must not let the NVM API reach a + * differently-typed object. Plant a WRAPPED-typed and an HW-typed object in + * client1's own namespace at numeric id 9, then confirm the client NVM + * read/getmetadata/destroy verbs cannot touch them via those flags. + */ +static int _testNvmWrappedHwFlagIsolation(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId planted_id = 9; + const uint8_t secret[] = "planted-non-nvm-secret"; + const whNvmSize secretSz = (whNvmSize)sizeof(secret); + const whKeyId clientFlags[2] = {WH_KEYID_CLIENT_WRAPPED_FLAG, + WH_KEYID_CLIENT_HW_FLAG}; + int32_t out_rc = 0; + whNvmSize out_len = 0; + uint8_t buf[64] = {0}; + whNvmMetadata meta = {0}; + whKeyId wrappedId; + whKeyId hwId; + int i; + + (void)client2; + (void)server2; + + WH_TEST_PRINT("Testing NVM WRAPPED/HW flag type isolation...\n"); + + wrappedId = + WH_MAKE_KEYID(WH_KEYTYPE_WRAPPED, server1->comm->client_id, planted_id); + hwId = WH_MAKE_KEYID(WH_KEYTYPE_HW, server1->comm->client_id, planted_id); + + /* Plant the two forbidden objects directly in the shared store. */ + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = secretSz; + meta.id = wrappedId; + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, secretSz, secret) == WH_ERROR_OK); + meta.id = hwId; + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, secretSz, secret) == WH_ERROR_OK); + + for (i = 0; i < 2; i++) { + whNvmId flagged = (whNvmId)(planted_id | clientFlags[i]); + + /* Read: must not return the planted bytes. With the flag stripped this + * resolves to a nonexistent NVM object 9 and fails. */ + memset(buf, 0, sizeof(buf)); + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, flagged, secretSz, &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(memcmp(buf, secret, secretSz) != 0); + + /* GetMetadata: must not surface the planted object. */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmGetMetadataViaServer(client1, server1, flagged, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* Destroy: must not reach the planted object. */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, flagged, &out_rc)); + } + + /* Both planted objects must still exist after the flagged destroys. */ + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server1->nvm, wrappedId, &meta) == + WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server1->nvm, hwId, &meta) == + WH_ERROR_OK); + + /* Clean up the planted objects. */ + (void)wh_Nvm_DestroyObjects(server1->nvm, 1, &wrappedId); + (void)wh_Nvm_DestroyObjects(server1->nvm, 1, &hwId); + + WH_TEST_PRINT(" NVM WRAPPED/HW flag isolation: PASS\n"); + return WH_ERROR_OK; +} + +static int _runNvmIdTranslationTests(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + WH_TEST_PRINT("=== NVM Id Translation Tests Begin ===\n"); + WH_TEST_RETURN_ON_FAIL( + _testNvmClientIsolation(client1, server1, client2, server2)); +#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( + _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 */ + /* ============================================================================ * MULTI-CLIENT SEQUENTIAL TEST FRAMEWORK * ========================================================================== */ @@ -1562,6 +2078,11 @@ static int _whTest_MultiClient(void) _runGlobalKeysTests(client1, server1, client2, server2)); #endif +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + WH_TEST_RETURN_ON_FAIL( + _runNvmIdTranslationTests(client1, server1, client2, server2)); +#endif + /* 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 8a8521674..46a80552b 100644 --- a/test-refactor/misc/wh_test_she_keywrap.c +++ b/test-refactor/misc/wh_test_she_keywrap.c @@ -300,30 +300,39 @@ static int _ComputeBootMac(const uint8_t* bootloader, uint32_t bootloaderSz, /* Sequential wrappers: send the request, pump the server once, then collect * the response */ -static int _NvmAddObject(TestCtx* t, whNvmId id, whNvmAccess access, - whNvmFlags flags, whNvmSize labelLen, uint8_t* label, - whNvmSize len, const uint8_t* data) +/* Pre-program a SHE key via the dedicated SHE provisioning message: the + * split equivalent of wh_Client_ShePreProgramKey(), pumping the server + * between the request and the response */ +static int _ShePreProgramKey(TestCtx* t, whNvmId keyId, uint32_t count, + uint32_t sheFlags, const uint8_t* key, + whNvmSize keySz) { - int32_t rc = 0; + whMessageShe_PreProgramKeyRequest* req; + whMessageShe_PreProgramKeyResponse resp = {0}; + 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; -} - -/* 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) -{ - uint8_t label[WH_NVM_LABEL_LEN] = {0}; - - wh_She_Meta2Label(0, sheFlags, label); - return _NvmAddObject( - t, WH_MAKE_KEYID(WH_KEYTYPE_SHE, t->client->comm->client_id, keyId), 0, - 0, sizeof(label), label, keySz, key); + WH_TEST_RETURN_ON_FAIL(wh_Client_RecvResponse(t->client, &group, &action, + &dataSz, (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; } static int _KeyWrapExport(TestCtx* t, whKeyId keyId, uint16_t keyType, @@ -524,12 +533,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; } @@ -573,7 +582,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]; @@ -590,13 +598,13 @@ static int _SheKeywrapInterop(TestCtx* t) /* 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); @@ -723,11 +731,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_MAKE_KEYID(WH_KEYTYPE_SHE, 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; @@ -862,7 +866,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/posix/Makefile b/test-refactor/posix/Makefile index 81ed43f74..2f465ed9a 100644 --- a/test-refactor/posix/Makefile +++ b/test-refactor/posix/Makefile @@ -115,6 +115,11 @@ ifeq ($(SHE),1) DEF += -DWOLFHSM_CFG_SHE_EXTENSION endif +# Build with the global keys feature disabled +ifeq ($(NOGLOBALKEYS),1) + DEF += -DWOLFHSM_CFG_TEST_NO_GLOBAL_KEYS +endif + # Support an authentication-capable build ifeq ($(AUTH),1) DEF += -DWOLFHSM_CFG_ENABLE_AUTHENTICATION diff --git a/test/Makefile b/test/Makefile index faf251850..2c0899258 100644 --- a/test/Makefile +++ b/test/Makefile @@ -157,6 +157,11 @@ ifeq ($(SHE),1) DEF += -DWOLFHSM_CFG_SHE_EXTENSION endif +# Build with the global keys feature disabled +ifeq ($(NOGLOBALKEYS),1) + DEF += -DWOLFHSM_CFG_TEST_NO_GLOBAL_KEYS +endif + # Enable per-client crypto affinity (HW/SW devId selection) and its unit test ifeq ($(CRYPTO_AFFINITY),1) DEF += -DWOLFHSM_CFG_CRYPTO_AFFINITY diff --git a/test/config/wolfhsm_cfg.h b/test/config/wolfhsm_cfg.h index 0ce3317dd..2b785b978 100644 --- a/test/config/wolfhsm_cfg.h +++ b/test/config/wolfhsm_cfg.h @@ -35,13 +35,16 @@ #define WOLFHSM_CFG_COMM_DATA_LEN (1024 * 8) -/* Enable global keys feature for testing */ +/* Enable global keys feature for testing. NOGLOBALKEYS=1 builds leave it + * disabled to cover that configuration. */ +#ifndef WOLFHSM_CFG_TEST_NO_GLOBAL_KEYS #define WOLFHSM_CFG_GLOBAL_KEYS +#endif /* Enable logging feature for testing */ #define WOLFHSM_CFG_LOGGING -#define WOLFHSM_CFG_NVM_OBJECT_COUNT 30 +#define WOLFHSM_CFG_NVM_OBJECT_COUNT 64 #define WOLFHSM_CFG_SERVER_KEYCACHE_COUNT 9 #define WOLFHSM_CFG_SERVER_KEYCACHE_BUFSIZE 300 #define WOLFHSM_CFG_DMAADDR_COUNT 8 @@ -64,6 +67,11 @@ /* Allow persistent NVM artifacts in tests */ #define WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS +/* Enable SHE preprogram/destroy test-only key-management APIs. These bypass + * the authenticated SHE key-update protocol and are not part of the SHE + * specification, so they must never be enabled in production builds. */ +#define WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT + #define WOLFHSM_CFG_ENABLE_TIMEOUT #endif /* WOLFHSM_CFG_H_ */ diff --git a/test/wh_test_clientserver.c b/test/wh_test_clientserver.c index 56caebc70..b799262e6 100644 --- a/test/wh_test_clientserver.c +++ b/test/wh_test_clientserver.c @@ -421,7 +421,8 @@ static int _testDma(whServerContext* server, whClientContext* client) * single-thread pump makes the old KeyCacheDma use-after-free deterministic. * On failure control jumps to cleanup so the callbacks are always unregistered. */ -#define BOUNCE_TEST_NVM_ID 0x4242 /* arbitrary id, destroyed at end of test */ +#define BOUNCE_TEST_NVM_ID 0x42 /* arbitrary valid client NVM id (1-255), \ + * destroyed at end of test */ /* Local fail/assert helpers that unwind to cleanup instead of returning. */ #define BOUNCE_FAIL(expr) \ diff --git a/test/wh_test_crypto.c b/test/wh_test_crypto.c index 3dcf8918b..8c1d586e9 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -16313,10 +16313,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 a129d2326..dfc7f1be6 100644 --- a/test/wh_test_keywrap.c +++ b/test/wh_test_keywrap.c @@ -621,24 +621,45 @@ static int _AesGcm_TestTrustedKekPolicy(whClientContext* client, WC_RNG* rng) } /* (b) A client that provisions an NVM object carrying - * WH_NVM_FLAGS_TRUSTED at a crypto-key id (keys and NVM objects share - * the id space) must not obtain a trusted KEK either: the checked NVM - * add path strips the flag. */ + * WH_NVM_FLAGS_TRUSTED at a crypto-key id must not obtain a trusted KEK + * either. The forged id embeds the client id in the USER bits, which + * overlap the client WRAPPED/HW flag bits, so depending on the client id + * the checked add either rejects the id outright or stores the object + * outside the crypto-key id space; either way no KEK may appear at the + * crypto-key id. */ { - whKeyId nvmForgeId = WH_TEST_KEKID + 2; - whNvmId nvmObjId = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, - client->comm->client_id, nvmForgeId); - int32_t nvmRc = 0; + whKeyId nvmForgeId = WH_TEST_KEKID + 2; + whNvmId nvmObjId = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, + client->comm->client_id, nvmForgeId); + int32_t nvmRc = 0; + int32_t expectedAddRc = 0; + int expectedRc; + +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + if ((nvmObjId & + (WH_KEYID_CLIENT_WRAPPED_FLAG | WH_KEYID_CLIENT_HW_FLAG)) != 0) { + /* Forged id aliases a client flag bit; the checked add rejects + * it before storing anything. */ + expectedAddRc = WH_ERROR_BADARGS; + } +#ifndef WOLFHSM_CFG_GLOBAL_KEYS + if ((nvmObjId & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0) { + /* Without global keys an aliased GLOBAL bit is rejected too */ + expectedAddRc = WH_ERROR_BADARGS; + } +#endif +#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 +669,21 @@ static int _AesGcm_TestTrustedKekPolicy(whClientContext* client, WC_RNG* rng) int32_t destroyRc = 0; (void)wh_Client_NvmDestroyObjects(client, 1, &nvmObjId, &destroyRc); } - if (ret != WH_ERROR_ACCESS) { +#ifdef WOLFHSM_CFG_LEGACY_CLIENT_NVM + /* Legacy flat id space: the forged object lands at the crypto-key + * id; the checked NVM add stripped the TRUSTED flag, so KEK use is + * refused. */ + expectedRc = WH_ERROR_ACCESS; +#else + /* An accepted forge is stored as a plain NVM object in the caller's + * namespace and a rejected one stores nothing, so no KEK exists at + * the crypto-key id either way. */ + 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 796c7df88..3eb949d25 100644 --- a/test/wh_test_multiclient.c +++ b/test/wh_test_multiclient.c @@ -1412,6 +1412,522 @@ static int _runGlobalKeysTests(whClientContext* client1, #endif /* WOLFHSM_CFG_GLOBAL_KEYS */ +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM +/* ============================================================================ + * CLIENT NVM ID-TRANSLATION TEST SUITE + * + * These tests assert the per-client NVM id namespace: each client sees its own + * 1..255 id range plus a shared 1..255 global range. Cross-client raw access + * via the NVM api is impossible. + * + * Only meaningful when client NVM id translation is enabled (default). + * ========================================================================== */ + +static const uint8_t NVM_ISOLATION_PAYLOAD_A[] = "client-A-secret-NVM-payload"; +static const uint8_t NVM_ISOLATION_PAYLOAD_B[] = "client-B-different-payload"; + +/* + * Helper: add an NVM object via the explicit Request/Handle/Response + * pattern so that the matching server can be driven manually (multiclient + * sequential setup has no automatic dispatch). + */ +static int _nvmAddViaServer(whClientContext* client, whServerContext* server, + whNvmId id, whNvmSize len, const uint8_t* data, + int32_t* out_rc) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectRequest( + client, id, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, 0, NULL, len, data)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectResponse(client, out_rc)); + return WH_ERROR_OK; +} + +static int _nvmReadViaServer(whClientContext* client, whServerContext* server, + whNvmId id, whNvmSize len, int32_t* out_rc, + whNvmSize* out_len, uint8_t* buf) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmReadRequest(client, id, 0, len)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmReadResponse(client, out_rc, out_len, buf)); + return WH_ERROR_OK; +} + +static int _nvmDestroyViaServer(whClientContext* client, + whServerContext* server, whNvmId id, + int32_t* out_rc) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjectsRequest(client, 1, &id)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjectsResponse(client, out_rc)); + return WH_ERROR_OK; +} + +static int _nvmListViaServer(whClientContext* client, whServerContext* server, + whNvmId startId, int32_t* out_rc, + whNvmId* out_count, whNvmId* out_id) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmListRequest( + client, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, startId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmListResponse(client, out_rc, out_count, out_id)); + return WH_ERROR_OK; +} + +static int _nvmGetMetadataViaServer(whClientContext* client, + whServerContext* server, whNvmId id, + int32_t* out_rc) +{ + whNvmId got_id = 0; + whNvmAccess access = 0; + whNvmFlags flags = 0; + whNvmSize len = 0; + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadataRequest(client, id)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server)); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadataResponse( + client, out_rc, &got_id, &access, &flags, &len, 0, NULL)); + return WH_ERROR_OK; +} + +/* + * Client A adds NVM id 5 with secret payload. Client B reading id 5 must NOT + * see A's bytes (either NOTFOUND, or B's own value if B has one). + */ +static int _testNvmClientIsolation(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId shared_id = 5; + int32_t out_rc = 0; + uint8_t buf[64] = {0}; + whNvmSize out_len = 0; + + WH_TEST_PRINT("Testing NVM client isolation...\n"); + + /* Client A adds a secret object at id=5 */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, shared_id, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Client A confirms it can read its own bytes back */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) == 0); + + /* Client B reads id=5: must NOT find A's bytes */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client2, server2, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + /* Either NOTFOUND or read returned different bytes; never A's plaintext. */ + if (out_rc == WH_ERROR_OK) { + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) != 0); + } + + /* Client B adds its own object at the same client-facing id=5 */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client2, server2, shared_id, + sizeof(NVM_ISOLATION_PAYLOAD_B), + NVM_ISOLATION_PAYLOAD_B, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Client B reads id=5: gets ITS OWN value */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client2, server2, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_B)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_B, + sizeof(NVM_ISOLATION_PAYLOAD_B)) == 0); + + /* Client A still sees ITS OWN value, not B's */ + out_len = 0; + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, shared_id, sizeof(buf), &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, + sizeof(NVM_ISOLATION_PAYLOAD_A)) == 0); + + /* Cleanup: each client destroys its own */ + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, shared_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client2, server2, shared_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM client isolation: PASS\n"); + return WH_ERROR_OK; +} + +#ifdef WOLFHSM_CFG_GLOBAL_KEYS +/* + * List has two namespaces: + * - own : startId without GLOBAL flag + * - global : startId with WH_KEYID_CLIENT_GLOBAL_FLAG set + * + * Client A populates 2 own ids and 2 global ids. Each scan must visit only + * the corresponding namespace. + */ +static int _testNvmGlobalNamespaceList(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int32_t out_rc = 0; + whNvmId count = 0; + whNvmId cur = 0; + int seen_own[256] = {0}; + int seen_global[256] = {0}; + int i; + int iters; + const whNvmId own_ids[2] = {3, 7}; + const whNvmId global_ids[2] = {2, 4}; + (void)server2; + (void)client2; + + WH_TEST_PRINT("Testing NVM list with global namespace...\n"); + + /* Populate own ids */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, own_ids[i], sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + /* Populate global ids */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, global_ids[i] | WH_KEYID_CLIENT_GLOBAL_FLAG, + sizeof(NVM_ISOLATION_PAYLOAD_B), NVM_ISOLATION_PAYLOAD_B, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + /* Iterate own namespace (no flag on startId). Expect ids in own_ids. + * Cap iterations to defend against an unintentional infinite loop. */ + cur = 0; + for (iters = 0; iters < 16; iters++) { + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + if (count == 0) { + break; + } + /* Must not carry the GLOBAL flag */ + WH_TEST_ASSERT_RETURN((cur & WH_KEYID_CLIENT_GLOBAL_FLAG) == 0); + WH_TEST_ASSERT_RETURN((cur & WH_KEYID_MASK) <= WH_KEYID_IDMAX); + seen_own[cur & WH_KEYID_MASK] = 1; + if (count == 1) { + break; + } + } + WH_TEST_ASSERT_RETURN(iters < 16); + + /* Iterate global namespace (GLOBAL flag on startId). Expect ids in + * global_ids, all returned with GLOBAL flag set. */ + cur = WH_KEYID_CLIENT_GLOBAL_FLAG; + for (iters = 0; iters < 16; iters++) { + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + if (count == 0) { + break; + } + WH_TEST_ASSERT_RETURN((cur & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0); + seen_global[cur & WH_KEYID_MASK] = 1; + if (count == 1) { + break; + } + } + WH_TEST_ASSERT_RETURN(iters < 16); + + for (i = 0; i < 2; i++) { + WH_TEST_ASSERT_RETURN(seen_own[own_ids[i]] == 1); + WH_TEST_ASSERT_RETURN(seen_global[global_ids[i]] == 1); + } + /* Crosscheck: globals never appear in the own scan and vice-versa */ + WH_TEST_ASSERT_RETURN(seen_own[global_ids[0]] == 0); + WH_TEST_ASSERT_RETURN(seen_own[global_ids[1]] == 0); + WH_TEST_ASSERT_RETURN(seen_global[own_ids[0]] == 0); + WH_TEST_ASSERT_RETURN(seen_global[own_ids[1]] == 0); + + /* Cleanup */ + for (i = 0; i < 2; i++) { + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, own_ids[i], &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL(_nvmDestroyViaServer( + client1, server1, global_ids[i] | WH_KEYID_CLIENT_GLOBAL_FLAG, + &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + } + + WH_TEST_PRINT(" NVM global namespace list: PASS\n"); + return WH_ERROR_OK; +} + +#else /* !WOLFHSM_CFG_GLOBAL_KEYS */ + +/* + * Without global keys there is no global namespace: AddObject must reject + * the GLOBAL flag and the other verbs must ignore it, resolving to the + * caller's own namespace. USER=0 objects (e.g. provisioned by whnvmtool) + * must stay unreachable, including via List. + */ +static int _testNvmGlobalFlagDisabled(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId own_id = 3; + const whNvmId planted_id = 6; + whNvmId planted_nvm_id; + whNvmMetadata meta = {0}; + int32_t out_rc = 0; + whNvmId count = 0; + whNvmId cur = 0; + whNvmSize out_len = 0; + uint8_t buf[64] = {0}; + + (void)client2; + (void)server2; + + WH_TEST_PRINT("Testing NVM GLOBAL flag with global keys disabled...\n"); + + /* Plant a USER=0 object directly, as provisioning would */ + planted_nvm_id = + WH_MAKE_KEYID(WH_KEYTYPE_NVM, WH_KEYUSER_GLOBAL, planted_id); + meta.id = planted_nvm_id; + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = sizeof(NVM_ISOLATION_PAYLOAD_B); + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, sizeof(NVM_ISOLATION_PAYLOAD_B), + NVM_ISOLATION_PAYLOAD_B) == WH_ERROR_OK); + + /* AddObject with the GLOBAL flag fails loudly */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer( + client1, server1, 5 | WH_KEYID_CLIENT_GLOBAL_FLAG, + sizeof(NVM_ISOLATION_PAYLOAD_A), NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + +#ifdef WOLFHSM_CFG_DMA + /* The DMA add path shares the same id validation */ + { + whNvmMetadata dma_meta = {0}; + dma_meta.id = 5 | WH_KEYID_CLIENT_GLOBAL_FLAG; + dma_meta.access = WH_NVM_ACCESS_ANY; + dma_meta.flags = WH_NVM_FLAGS_NONE; + dma_meta.len = sizeof(NVM_ISOLATION_PAYLOAD_A); + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObjectDmaRequest( + client1, &dma_meta, sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_NvmAddObjectDmaResponse(client1, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_BADARGS); + } +#endif + + /* Add an own object without the flag */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, own_id, + sizeof(NVM_ISOLATION_PAYLOAD_A), + NVM_ISOLATION_PAYLOAD_A, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* A GLOBAL-flagged List ignores the flag: it walks the caller's own + * namespace and never surfaces the planted USER=0 object */ + cur = WH_KEYID_CLIENT_GLOBAL_FLAG; + WH_TEST_RETURN_ON_FAIL( + _nvmListViaServer(client1, server1, cur, &out_rc, &count, &cur)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(count == 1); + WH_TEST_ASSERT_RETURN(cur == own_id); + + /* Read ignores the flag the same way: it resolves to the own object */ + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, own_id | WH_KEYID_CLIENT_GLOBAL_FLAG, sizeof(buf), + &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == sizeof(NVM_ISOLATION_PAYLOAD_A)); + WH_TEST_ASSERT_RETURN(memcmp(buf, NVM_ISOLATION_PAYLOAD_A, out_len) == 0); + + /* The planted USER=0 object is not reachable via the flag */ + WH_TEST_RETURN_ON_FAIL(_nvmGetMetadataViaServer( + client1, server1, planted_id | WH_KEYID_CLIENT_GLOBAL_FLAG, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* Cleanup */ + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, own_id, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN( + wh_Nvm_DestroyObjects(server1->nvm, 1, &planted_nvm_id) == WH_ERROR_OK); + + WH_TEST_PRINT(" NVM GLOBAL flag disabled semantics: PASS\n"); + return WH_ERROR_OK; +} +#endif /* WOLFHSM_CFG_GLOBAL_KEYS */ + +/* + * Reject malformed AddObject requests: + * - id portion == 0 (erased sentinel) + * - wrapped flag set + */ +static int _testNvmAddObjectRejections(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int32_t out_rc = 0; + (void)server2; + (void)client2; + + WH_TEST_PRINT("Testing NVM AddObject bad-id rejections...\n"); + + /* id=0 with own scope */ + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, 0, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* id=0 with global scope */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, + WH_KEYID_CLIENT_GLOBAL_FLAG, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* wrapped flag set */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmAddViaServer(client1, server1, + 5 | WH_KEYID_CLIENT_WRAPPED_FLAG, 4, + (const uint8_t*)"data", &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + WH_TEST_PRINT(" NVM AddObject rejections: PASS\n"); + return WH_ERROR_OK; +} + +/* + * The WRAPPED and HW client flags must not let the NVM API reach a + * differently-typed object. Plant a WRAPPED-typed and an HW-typed object in + * client1's own namespace at numeric id 9, then confirm the client NVM + * read/getmetadata/destroy verbs cannot touch them via those flags. + */ +static int _testNvmWrappedHwFlagIsolation(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + const whNvmId planted_id = 9; + const uint8_t secret[] = "planted-non-nvm-secret"; + const whNvmSize secretSz = (whNvmSize)sizeof(secret); + const whKeyId clientFlags[2] = {WH_KEYID_CLIENT_WRAPPED_FLAG, + WH_KEYID_CLIENT_HW_FLAG}; + int32_t out_rc = 0; + whNvmSize out_len = 0; + uint8_t buf[64] = {0}; + whNvmMetadata meta = {0}; + whKeyId wrappedId; + whKeyId hwId; + int i; + + (void)client2; + (void)server2; + + WH_TEST_PRINT("Testing NVM WRAPPED/HW flag type isolation...\n"); + + wrappedId = + WH_MAKE_KEYID(WH_KEYTYPE_WRAPPED, server1->comm->client_id, planted_id); + hwId = WH_MAKE_KEYID(WH_KEYTYPE_HW, server1->comm->client_id, planted_id); + + /* Plant the two forbidden objects directly in the shared store. */ + meta.access = WH_NVM_ACCESS_ANY; + meta.flags = WH_NVM_FLAGS_NONE; + meta.len = secretSz; + meta.id = wrappedId; + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, secretSz, secret) == WH_ERROR_OK); + meta.id = hwId; + WH_TEST_ASSERT_RETURN( + wh_Nvm_AddObject(server1->nvm, &meta, secretSz, secret) == WH_ERROR_OK); + + for (i = 0; i < 2; i++) { + whNvmId flagged = (whNvmId)(planted_id | clientFlags[i]); + + /* Read: must not return the planted bytes. With the flag stripped this + * resolves to a nonexistent NVM object 9 and fails. */ + memset(buf, 0, sizeof(buf)); + out_rc = 0; + WH_TEST_RETURN_ON_FAIL(_nvmReadViaServer( + client1, server1, flagged, secretSz, &out_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(memcmp(buf, secret, secretSz) != 0); + + /* GetMetadata: must not surface the planted object. */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmGetMetadataViaServer(client1, server1, flagged, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc != WH_ERROR_OK); + + /* Destroy: must not reach the planted object. */ + out_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _nvmDestroyViaServer(client1, server1, flagged, &out_rc)); + } + + /* Both planted objects must still exist after the flagged destroys. */ + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server1->nvm, wrappedId, &meta) == + WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(wh_Nvm_GetMetadata(server1->nvm, hwId, &meta) == + WH_ERROR_OK); + + /* Clean up the planted objects. */ + (void)wh_Nvm_DestroyObjects(server1->nvm, 1, &wrappedId); + (void)wh_Nvm_DestroyObjects(server1->nvm, 1, &hwId); + + WH_TEST_PRINT(" NVM WRAPPED/HW flag isolation: PASS\n"); + return WH_ERROR_OK; +} + +static int _runNvmIdTranslationTests(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + WH_TEST_PRINT("=== NVM Id Translation Tests Begin ===\n"); + WH_TEST_RETURN_ON_FAIL( + _testNvmClientIsolation(client1, server1, client2, server2)); +#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( + _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 */ + /* ============================================================================ * MULTI-CLIENT SEQUENTIAL TEST FRAMEWORK * ========================================================================== */ @@ -1636,6 +2152,11 @@ static int whTest_MultiClientSequential(void) _runGlobalKeysTests(client1, server1, client2, server2)); #endif +#ifndef WOLFHSM_CFG_LEGACY_CLIENT_NVM + WH_TEST_RETURN_ON_FAIL( + _runNvmIdTranslationTests(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..20dedcb4d 100644 --- a/test/wh_test_posix_threadsafe_stress.c +++ b/test/wh_test_posix_threadsafe_stress.c @@ -28,10 +28,21 @@ * Architecture: * - 1 shared NVM context with lock * - 4 server contexts sharing the NVM - * - 4 client threads (all doing both NVM and keystore ops) + * - 4 client threads (all doing both NVM and keystore ops), each with a + * distinct comm client_id so the server scopes ids per client * - Different contention phases to stress test contention patterns across * various different APIs * + * Every phase runs once per namespace variant: + * - global: all ids carry WH_KEYID_CLIENT_GLOBAL_FLAG, so every client + * operates on the same shared objects (cross-client contention). + * - local: bare ids resolve into each client's private namespace. Nobody + * else can touch a client's objects, so acceptable-error sets are strict + * and reads verify per-client data patterns. + * - mixed: clients 0-1 churn the shared global objects while clients 2-3 + * run deterministic operations on private objects. Any perturbation of + * the private objects is a hard failure. + * * NOTE: Uses PTHREAD_MUTEX_ERRORCHECK attribute to trap undefined behavior * errors (EDEADLK for deadlock, EPERM for non-owner unlock) which indicate * bugs in the locking implementation. @@ -211,11 +222,44 @@ static const whTransportServerCb serverTransportCb = WH_TRANSPORT_MEM_SERVER_CB; #define REVOKE_EXPORT_KEY_LOCAL 13 /* ID=13, local */ #define FRESHEN_KEY_GLOBAL WH_CLIENT_KEYID_MAKE_GLOBAL(14) /* ID=14, global */ #define FRESHEN_KEY_LOCAL 15 /* ID=15, local */ + +/* The mixed variant needs its own revoke keys: the keys revoked during the + * global and local runs are permanently NONMODIFIABLE and can't be reused */ +#define REVOKE_CACHE_KEY_MIXED_GLOBAL WH_CLIENT_KEYID_MAKE_GLOBAL(16) +#define REVOKE_CACHE_KEY_MIXED_LOCAL 17 +#define REVOKE_EXPORT_KEY_MIXED_GLOBAL WH_CLIENT_KEYID_MAKE_GLOBAL(18) +#define REVOKE_EXPORT_KEY_MIXED_LOCAL 19 + #define HOT_NVM_ID ((whNvmId)100) #define HOT_NVM_ID_2 ((whNvmId)101) #define HOT_NVM_ID_3 ((whNvmId)102) #define HOT_COUNTER_ID ((whNvmId)200) +/* Id ranges for the Add With Reclaim phase. Client NVM ids are limited to + * 1-255 (bits 8-10 are namespace/type flags), so the ids cycle within a + * bounded range. Re-adding an existing id retires the old copy as + * reclaimable, which keeps compaction busy for the whole phase. Ranges stay + * below HOT_NVM_ID so they never collide with the hot objects. */ +#define RECLAIM_ID_BASE ((whNvmId)10) +#define RECLAIM_IDS_PER_CLIENT 20 /* global: disjoint range per client */ +#define RECLAIM_IDS_LOCAL 40 /* local: cycle length per namespace */ +#define RECLAIM_SETUP_COUNT 10 /* objects pre-filled by setup */ + +/* Data-fill patterns. Writers fill objects with a single byte that encodes + * the writer and namespace side; readers verify content so torn reads and + * cross-namespace data bleed become hard failures. */ +#define GLOBAL_PATTERN_BASE 0x50 /* global-side writer: 0x50 | clientId */ +#define LOCAL_PATTERN_BASE 0xA0 /* local-side writer: 0xA0 | clientId */ + +/* Content expectation for doNvmRead/doNvmReadDma */ +#define NVM_EXPECT_NONE (-1) /* no content check */ +#define NVM_EXPECT_ANY_GLOBAL 0x100 /* uniform, from any global-side writer */ + +/* Test-local failure codes. Outside the wolfHSM error space so they are + * never in an acceptable-error set. */ +#define STRESS_ERR_DATA_MISMATCH (-77001) /* object content wrong/torn */ +#define STRESS_ERR_NAMESPACE_LEAK (-77002) /* list returned foreign-ns id */ + /* ============================================================================ * PHASE DEFINITIONS * ========================================================================== */ @@ -285,6 +329,16 @@ typedef enum { ROLE_OP_B, /* Perform operation B (e.g., Evict, Read) */ } ClientRole; +/* Namespace variant for a phase run. Selects which namespace each client's + * ids resolve into and how strictly per-operation results are judged. */ +typedef enum { + VARIANT_GLOBAL = 0, /* all clients share objects (global namespace) */ + VARIANT_LOCAL, /* each client uses its own private namespace */ + VARIANT_MIXED, /* clients 0-1 global, clients 2-3 private */ +} NamespaceVariant; + +#define VARIANT_COUNT 3 + /* Phase configuration */ typedef struct { ContentionPhase phase; @@ -495,6 +549,8 @@ typedef struct { volatile int stopFlag; volatile int errorCount; volatile int iterationCount; + volatile int successCount; + volatile int commInitFailed; /* Pointer back to shared context */ struct StressTestContext* sharedCtx; @@ -535,17 +591,91 @@ typedef struct StressTestContext { pthread_barrier_t setupCompleteBarrier; pthread_barrier_t streamStartBarrier; pthread_barrier_t streamEndBarrier; + pthread_barrier_t cleanupStartBarrier; /* Phase control */ - volatile int phaseRunning; - volatile ContentionPhase currentPhase; - volatile whKeyId currentKeyId; - volatile ClientRole clientRoles[NUM_CLIENTS]; + volatile int phaseRunning; + volatile ContentionPhase currentPhase; + volatile NamespaceVariant currentVariant; + volatile ClientRole clientRoles[NUM_CLIENTS]; } StressTestContext; /* Forward declarations */ static void* serverThread(void* arg); static void* contentionClientThread(void* arg); +static whKeyId selectKeyIdForPhase(ContentionPhase phase, + NamespaceVariant variant, int globalSide); + +/* ============================================================================ + * NAMESPACE VARIANT HELPERS + * ========================================================================== */ + +static const char* variantName(NamespaceVariant variant) +{ + switch (variant) { + case VARIANT_GLOBAL: + return "global"; + case VARIANT_LOCAL: + return "local"; + case VARIANT_MIXED: + return "mixed"; + default: + return "?"; + } +} + +/* True if this client targets the shared global namespace this run */ +static int isGlobalSide(const ClientServerPair* pair, NamespaceVariant variant) +{ + return (variant == VARIANT_GLOBAL) || + ((variant == VARIANT_MIXED) && (pair->clientId < (NUM_CLIENTS / 2))); +} + +/* NVM object id for this client under the current variant. Global-side ids + * carry WH_KEYID_CLIENT_GLOBAL_FLAG; local ids are bare and the server + * scopes them to this client's namespace. */ +static whNvmId nvmIdFor(const ClientServerPair* pair, NamespaceVariant variant, + whNvmId baseId) +{ + if (isGlobalSide(pair, variant)) { + return (whNvmId)(baseId | WH_KEYID_CLIENT_GLOBAL_FLAG); + } + return baseId; +} + +/* Data byte this client writes into NVM objects under the current variant */ +static uint8_t nvmPatternFor(const ClientServerPair* pair, + NamespaceVariant variant) +{ + if (isGlobalSide(pair, variant)) { + return (uint8_t)(GLOBAL_PATTERN_BASE | pair->clientId); + } + return (uint8_t)(LOCAL_PATTERN_BASE | pair->clientId); +} + +/* Content expectation for this client's reads under the current variant. + * Global-side objects may hold any global-side writer's pattern; a private + * object must hold exactly its owner's pattern. */ +static int nvmReadExpectFor(const ClientServerPair* pair, + NamespaceVariant variant) +{ + if (isGlobalSide(pair, variant)) { + return NVM_EXPECT_ANY_GLOBAL; + } + return (int)nvmPatternFor(pair, variant); +} + +/* Role for this client under the current variant. The mixed variant re-pairs + * roles by client parity so that both namespace sides get both operations + * (tables are laid out as {A,A,B,B} or all-A). */ +static ClientRole roleFor(const PhaseConfig* config, NamespaceVariant variant, + int clientId) +{ + if (variant == VARIANT_MIXED) { + return config->roles[(clientId % 2 == 0) ? 0 : (NUM_CLIENTS - 1)]; + } + return config->roles[clientId]; +} /* ============================================================================ * INITIALIZATION HELPERS @@ -706,9 +836,12 @@ static void* serverThread(void* arg) /* Wait for all threads to start */ pthread_barrier_wait(&ctx->startBarrier); - /* Process requests until stopped */ - while (!ATOMIC_LOAD_INT(&pair->stopFlag) && - !ATOMIC_LOAD_INT(&ctx->globalStopFlag)) { + /* Process requests until this pair is explicitly stopped. Do NOT exit + * on globalStopFlag: clients still send per-phase cleanup requests + * after the final phase, and a server that exits early leaves its + * client spinning forever for a response. join_threads sets stopFlag + * once the clients are done. */ + while (!ATOMIC_LOAD_INT(&pair->stopFlag)) { rc = wh_Server_HandleRequestMessage(&pair->server); if (rc == WH_ERROR_NOTREADY) { @@ -728,14 +861,63 @@ static void* serverThread(void* arg) * CLIENT OPERATIONS * ========================================================================== */ -static int doNvmAddObject(whClientContext* client, whNvmId id, int iteration) +/* Register this client's id with its server. Without this the server treats + * every request as USER=0 (the shared global namespace) and per-client + * isolation is never engaged. */ +static int doCommInit(ClientServerPair* pair) +{ + uint32_t outClientId = 0; + uint32_t outServerId = 0; + int rc; + + rc = wh_Client_CommInitRequest(&pair->client); + if (rc != WH_ERROR_OK) { + return rc; + } + + do { + rc = wh_Client_CommInitResponse(&pair->client, &outClientId, + &outServerId); + if (rc == WH_ERROR_NOTREADY) { + sched_yield(); + } + } while (rc == WH_ERROR_NOTREADY); + + return rc; +} + +/* Verify read-back object content. Data must be uniform (a torn read mixing + * two writers' patterns fails) and match the expectation: an exact owner + * pattern, or any global-side writer's pattern. */ +static int checkNvmData(const uint8_t* data, whNvmSize len, int expect) +{ + whNvmSize i; + + if (expect == NVM_EXPECT_NONE || len == 0) { + return WH_ERROR_OK; + } + for (i = 1; i < len; i++) { + if (data[i] != data[0]) { + return STRESS_ERR_DATA_MISMATCH; + } + } + if (expect == NVM_EXPECT_ANY_GLOBAL) { + return ((data[0] & 0xF0) == GLOBAL_PATTERN_BASE) + ? WH_ERROR_OK + : STRESS_ERR_DATA_MISMATCH; + } + return (data[0] == (uint8_t)expect) ? WH_ERROR_OK + : STRESS_ERR_DATA_MISMATCH; +} + +static int doNvmAddObject(whClientContext* client, whNvmId id, uint8_t pattern) { uint8_t data[NVM_OBJECT_DATA_SIZE]; int32_t out_rc; int rc; - /* Fill data with pattern */ - memset(data, (uint8_t)(iteration & 0xFF), sizeof(data)); + /* Fill data with this writer's pattern so readers can verify content */ + memset(data, pattern, sizeof(data)); /* Send request */ rc = wh_Client_NvmAddObjectRequest(client, id, WH_NVM_ACCESS_ANY, @@ -760,7 +942,9 @@ static int doNvmAddObject(whClientContext* client, whNvmId id, int iteration) return out_rc; } -static int doNvmRead(whClientContext* client, whNvmId id) +/* Read an object and, when expect is not NVM_EXPECT_NONE, verify its + * content (see checkNvmData) */ +static int doNvmRead(whClientContext* client, whNvmId id, int expect) { uint8_t data[NVM_OBJECT_DATA_SIZE]; whNvmSize outSz = sizeof(data); @@ -784,12 +968,19 @@ static int doNvmRead(whClientContext* client, whNvmId id) if (rc != WH_ERROR_OK) { return rc; } + if (out_rc != WH_ERROR_OK) { + return out_rc; + } - return out_rc; + return checkNvmData(data, outSz, expect); } -static int doNvmList(whClientContext* client) +/* List one namespace (listGlobal selects the shared global namespace via + * the GLOBAL flag on startId, else this client's own namespace) and verify + * any returned id belongs to the namespace that was asked for. */ +static int doNvmList(whClientContext* client, int listGlobal) { + whNvmId startId = listGlobal ? (whNvmId)WH_KEYID_CLIENT_GLOBAL_FLAG : 0; whNvmId outId; whNvmSize outCount; int32_t out_rc; @@ -797,7 +988,7 @@ static int doNvmList(whClientContext* client) /* Send request */ rc = wh_Client_NvmListRequest(client, WH_NVM_ACCESS_ANY, - WH_NVM_FLAGS_USAGE_ANY, 0); + WH_NVM_FLAGS_USAGE_ANY, startId); if (rc != WH_ERROR_OK) { return rc; } @@ -813,8 +1004,19 @@ static int doNvmList(whClientContext* client) if (rc != WH_ERROR_OK) { return rc; } + if (out_rc != WH_ERROR_OK) { + return out_rc; + } - return out_rc; + /* A local list must never surface a global id and vice versa */ + if (outCount > 0) { + int idIsGlobal = (outId & WH_KEYID_CLIENT_GLOBAL_FLAG) != 0; + if (idIsGlobal != (listGlobal != 0)) { + return STRESS_ERR_NAMESPACE_LEAK; + } + } + + return WH_ERROR_OK; } static int doNvmDestroy(whClientContext* client, whNvmId id) @@ -1192,15 +1394,15 @@ static int doKeyExportDma(ClientServerPair* pair, whKeyId keyId) return rc; } -static int doNvmAddObjectDma(ClientServerPair* pair, whNvmId id, int iteration) +static int doNvmAddObjectDma(ClientServerPair* pair, whNvmId id, + uint8_t pattern) { whNvmMetadata meta; int32_t out_rc; int rc; - /* Fill DMA buffer with pattern */ - memset(pair->dmaNvmBuffer, (uint8_t)(iteration & 0xFF), - sizeof(pair->dmaNvmBuffer)); + /* Fill DMA buffer with this writer's pattern */ + memset(pair->dmaNvmBuffer, pattern, sizeof(pair->dmaNvmBuffer)); /* Set up metadata */ memset(&meta, 0, sizeof(meta)); @@ -1231,7 +1433,10 @@ static int doNvmAddObjectDma(ClientServerPair* pair, whNvmId id, int iteration) return out_rc; } -static int doNvmReadDma(ClientServerPair* pair, whNvmId id) +/* DMA read with optional content verification. The DMA response carries no + * length, so pass NVM_EXPECT_NONE for objects whose size can change (resize + * phases) since stale tail bytes can't be told apart from real data. */ +static int doNvmReadDma(ClientServerPair* pair, whNvmId id, int expect) { int32_t out_rc; int rc; @@ -1254,8 +1459,11 @@ static int doNvmReadDma(ClientServerPair* pair, whNvmId id) if (rc != WH_ERROR_OK) { return rc; } + if (out_rc != WH_ERROR_OK) { + return out_rc; + } - return out_rc; + return checkNvmData(pair->dmaNvmBuffer, sizeof(pair->dmaNvmBuffer), expect); } #endif /* WOLFHSM_CFG_DMA */ @@ -1264,11 +1472,48 @@ 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); + uint8_t pattern = nvmPatternFor(pair, variant); + int provision; int rc; + /* Counters are per-client in every variant (the counter message path + * has no global-namespace support), so every client provisions its own + * counter regardless of the provisioning rule below. */ + if (phase == PHASE_COUNTER_CONCURRENT_INCREMENT || + phase == 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); + if (rc == WH_ERROR_NOSPACE) + rc = WH_ERROR_OK; + return 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) { /* Keystore phases that need clean state (evict first) */ case PHASE_KS_CONCURRENT_CACHE: @@ -1290,7 +1535,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 +1547,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 +1571,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 +1588,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 +1648,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 +1661,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 +1679,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 +1700,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 +1721,161 @@ static int doPhaseSetup(ClientServerPair* pair, ContentionPhase phase, } } +/* ============================================================================ + * PHASE CLEANUP + * ========================================================================== */ + +/* Best-effort teardown after each phase run so namespaces don't accumulate + * objects across the many phase/variant runs (a full object table would + * starve later phases). Return codes are intentionally ignored: revoked + * keys can't be erased and objects may already be gone. */ +static void doPhaseCleanup(ClientServerPair* pair, ContentionPhase phase, + NamespaceVariant variant) +{ + whClientContext* client = &pair->client; + int globalSide = isGlobalSide(pair, variant); + whKeyId keyId = selectKeyIdForPhase(phase, variant, globalSide); + whNvmId hotId = nvmIdFor(pair, variant, HOT_NVM_ID); + int owner; + int i; + + /* Same ownership rule as setup: one owner per shared resource, every + * client for its own */ + switch (variant) { + case VARIANT_GLOBAL: + owner = (pair->clientId == 0); + break; + case VARIANT_LOCAL: + owner = 1; + break; + case VARIANT_MIXED: + default: + owner = (pair->clientId == 0) || !globalSide; + break; + } + + switch (phase) { + /* Counters are per-client in every variant */ + case PHASE_COUNTER_CONCURRENT_INCREMENT: + case PHASE_COUNTER_INCREMENT_VS_READ: + (void)doCounterDestroy(client, 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); + uint8_t pattern = nvmPatternFor(pair, variant); + int readExpect = nvmReadExpectFor(pair, variant); switch (phase) { /* Keystore phases */ @@ -1535,56 +1920,58 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, /* NVM phases */ case PHASE_NVM_CONCURRENT_ADD: - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); case PHASE_NVM_ADD_VS_READ: if (role == ROLE_OP_A) - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); else - return doNvmRead(client, HOT_NVM_ID); + return doNvmRead(client, hotId, readExpect); case PHASE_NVM_ADD_VS_DESTROY: if (role == ROLE_OP_A) - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); case PHASE_NVM_READ_VS_DESTROY: if (role == ROLE_OP_A) - return doNvmRead(client, HOT_NVM_ID); + return doNvmRead(client, hotId, readExpect); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); case PHASE_NVM_CONCURRENT_READ: - return doNvmRead(client, HOT_NVM_ID); + return doNvmRead(client, hotId, readExpect); case PHASE_NVM_LIST_DURING_MODIFY: if (role == ROLE_OP_A) { /* Alternate between add and destroy */ if (iteration % 2 == 0) - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); } else { - return doNvmList(client); + /* Alternate between this client's namespace and the global + * one; both must only ever return their own ids */ + return doNvmList(client, iteration % 2); } case PHASE_NVM_CONCURRENT_DESTROY: - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); /* Cross-subsystem phases */ case PHASE_CROSS_COMMIT_VS_ADD: if (role == ROLE_OP_A) return doKeyCommit(client, keyId); else - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); case PHASE_CROSS_COMMIT_VS_DESTROY: if (role == ROLE_OP_A) return doKeyCommit(client, keyId); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); case PHASE_CROSS_FRESHEN_VS_MODIFY: if (role == ROLE_OP_A) { @@ -1594,9 +1981,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,29 +2023,39 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, case PHASE_KS_EXPLICIT_FRESHEN: return doKeyExport(client, keyId); - /* Add With Reclaim: all threads add unique objects to trigger reclaim - */ - case PHASE_NVM_ADD_WITH_RECLAIM: - /* Use client ID and iteration to create unique object IDs */ - return doNvmAddObject(client, - (whNvmId)(HOT_NVM_ID + 10 + - (pair->clientId * 1000) + - iteration), - iteration); + /* Add With Reclaim: all threads add objects from bounded id ranges. + * Client ids are limited to 1-255, so global-side clients cycle a + * disjoint per-client range in the shared namespace and local-side + * clients cycle a range in their own namespace. Re-adding an id + * retires the old copy as reclaimable, keeping compaction busy. */ + case PHASE_NVM_ADD_WITH_RECLAIM: { + whNvmId base; + if (globalSide) { + base = (whNvmId)(RECLAIM_ID_BASE + + pair->clientId * RECLAIM_IDS_PER_CLIENT + + (iteration % RECLAIM_IDS_PER_CLIENT)); + } + else { + base = (whNvmId)(RECLAIM_ID_BASE + + (iteration % RECLAIM_IDS_LOCAL)); + } + return doNvmAddObject(client, nvmIdFor(pair, variant, base), + pattern); + } /* GetAvailable vs Add */ case PHASE_NVM_GETAVAILABLE_VS_ADD: if (role == ROLE_OP_A) return doNvmGetAvailable(client); else - return doNvmAddObject(client, HOT_NVM_ID, iteration); + return doNvmAddObject(client, hotId, pattern); /* GetMetadata vs Destroy */ case PHASE_NVM_GETMETADATA_VS_DESTROY: if (role == ROLE_OP_A) - return doNvmGetMetadata(client, HOT_NVM_ID); + return doNvmGetMetadata(client, hotId); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); /* Counter Concurrent Increment */ case PHASE_COUNTER_CONCURRENT_INCREMENT: { @@ -1679,7 +2076,7 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, 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 +2089,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 +2121,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 +2160,42 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, /* DMA: Add DMA vs Read */ case PHASE_NVM_ADD_DMA_VS_READ: if (role == ROLE_OP_A) - return doNvmAddObjectDma(pair, HOT_NVM_ID, iteration); + return doNvmAddObjectDma(pair, hotId, pattern); else - return doNvmRead(client, HOT_NVM_ID); + return doNvmRead(client, hotId, readExpect); /* DMA: Read DMA vs Destroy */ case PHASE_NVM_READ_DMA_VS_DESTROY: if (role == ROLE_OP_A) - return doNvmReadDma(pair, HOT_NVM_ID); + return doNvmReadDma(pair, hotId, readExpect); else - return doNvmDestroy(client, HOT_NVM_ID); + return doNvmDestroy(client, hotId); /* NVM Read DMA vs Resize */ case PHASE_NVM_READ_DMA_VS_RESIZE: if (role == ROLE_OP_A) { - /* DMA Read operation */ - return doNvmReadDma(pair, HOT_NVM_ID); + /* DMA Read operation. No content check: the object's size + * changes under us and the DMA response carries no length, + * so stale tail bytes can't be told apart from data. */ + return doNvmReadDma(pair, hotId, NVM_EXPECT_NONE); } else { /* Resize operation: destroy and re-add with different size */ - int rc; - uint8_t data[NVM_OBJECT_DATA_SIZE]; - int32_t out_rc; - whNvmSize newSize = (iteration % 2 == 0) - ? NVM_OBJECT_DATA_SIZE - : (NVM_OBJECT_DATA_SIZE / 2); - whNvmMetadata meta; + int rc; + uint8_t data[NVM_OBJECT_DATA_SIZE]; + int32_t out_rc; + whNvmSize newSize = (iteration % 2 == 0) + ? NVM_OBJECT_DATA_SIZE + : (NVM_OBJECT_DATA_SIZE / 2); /* Destroy existing object */ - (void)doNvmDestroy(client, HOT_NVM_ID); + (void)doNvmDestroy(client, hotId); /* Re-add with new size */ - memset(data, (uint8_t)(iteration & 0xFF), newSize); - memset(&meta, 0, sizeof(meta)); - meta.id = HOT_NVM_ID; - meta.access = WH_NVM_ACCESS_ANY; - meta.flags = WH_NVM_FLAGS_USAGE_ANY; - meta.len = newSize; - + memset(data, pattern, newSize); rc = wh_Client_NvmAddObjectRequest( - client, HOT_NVM_ID, WH_NVM_ACCESS_ANY, - WH_NVM_FLAGS_USAGE_ANY, 0, NULL, newSize, data); + client, hotId, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_USAGE_ANY, 0, + NULL, newSize, data); if (rc != WH_ERROR_OK) { return rc; } @@ -1828,12 +2220,98 @@ static int executePhaseOperation(ClientServerPair* pair, ContentionPhase phase, * RESULT VALIDATION * ========================================================================== */ -static int isAcceptableResult(ContentionPhase phase, int rc) +static int isAcceptableResult(ContentionPhase phase, ClientRole role, + int globalSide, int rc) { /* Always acceptable */ if (rc == WH_ERROR_OK) return 1; + /* Private-namespace (local-side) NVM expectations are strict: nobody + * else can touch this client's objects, so cross-client noise codes are + * real failures. What remains acceptable is self-inflicted (a client + * destroying its own object each iteration with nothing re-adding it) + * and NOSPACE (namespaces isolate ids, not capacity). Keystore and + * cross-subsystem phases keep the shared sets below: cache-slot + * dynamics make their self-inflicted effects role-independent. */ + if (!globalSide) { + switch (phase) { + case PHASE_NVM_CONCURRENT_ADD: + return (rc == WH_ERROR_NOSPACE); + + /* A adds its own object, B reads its own object */ + case PHASE_NVM_ADD_VS_READ: + return (role == ROLE_OP_A) && (rc == WH_ERROR_NOSPACE); + + /* B destroys its own object once, then sees NOTFOUND */ + case PHASE_NVM_ADD_VS_DESTROY: + return (role == ROLE_OP_A) ? (rc == WH_ERROR_NOSPACE) + : (rc == WH_ERROR_NOTFOUND); + + /* A's object is never destroyed by anyone: reads must succeed */ + case PHASE_NVM_READ_VS_DESTROY: + return (role == ROLE_OP_B) && (rc == WH_ERROR_NOTFOUND); + + case PHASE_NVM_CONCURRENT_READ: + return 0; + + /* A alternates add/destroy of its own object; B's lists must + * always succeed */ + case PHASE_NVM_LIST_DURING_MODIFY: + return (role == ROLE_OP_A) && + (rc == WH_ERROR_NOSPACE || rc == WH_ERROR_NOTFOUND); + + case PHASE_NVM_CONCURRENT_DESTROY: + return (rc == WH_ERROR_NOTFOUND); + + /* Readers are strict; resizers may see their own destroy fail + * after a NOSPACE'd re-add */ + case PHASE_NVM_READ_VS_RESIZE: + return (role == ROLE_OP_B) && + (rc == WH_ERROR_NOTFOUND || rc == WH_ERROR_NOSPACE); + + case PHASE_NVM_CONCURRENT_RESIZE: + return (rc == WH_ERROR_NOTFOUND || rc == WH_ERROR_NOSPACE); + + case PHASE_NVM_ADD_WITH_RECLAIM: + return (rc == WH_ERROR_NOSPACE || rc == WH_ERROR_ACCESS); + + case PHASE_NVM_GETAVAILABLE_VS_ADD: + return (role == ROLE_OP_B) && (rc == WH_ERROR_NOSPACE); + + /* A queries its own object, which nobody destroys */ + case PHASE_NVM_GETMETADATA_VS_DESTROY: + return (role == ROLE_OP_B) && (rc == WH_ERROR_NOTFOUND); + +#ifdef WOLFHSM_CFG_DMA + case PHASE_NVM_ADD_DMA_VS_READ: + return (role == ROLE_OP_A) && (rc == WH_ERROR_NOSPACE); + + case PHASE_NVM_READ_DMA_VS_DESTROY: + return (role == ROLE_OP_B) && (rc == WH_ERROR_NOTFOUND); + + case PHASE_NVM_READ_DMA_VS_RESIZE: + return (role == ROLE_OP_B) && + (rc == WH_ERROR_NOTFOUND || rc == WH_ERROR_NOSPACE); +#endif + + default: + break; /* keystore/cross/counter: shared sets below */ + } + } + + /* Counters are per-client in every variant: after per-client setup the + * counter always exists, so only NOSPACE (shared capacity) is tolerated + * for increments and reads must succeed. */ + switch (phase) { + case PHASE_COUNTER_CONCURRENT_INCREMENT: + return (rc == WH_ERROR_NOSPACE); + case PHASE_COUNTER_INCREMENT_VS_READ: + return (role == ROLE_OP_A) && (rc == WH_ERROR_NOSPACE); + default: + break; + } + switch (phase) { /* Cache operations: NOSPACE acceptable (cache full) */ case PHASE_KS_CONCURRENT_CACHE: @@ -1901,13 +2379,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 +2419,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 +2446,19 @@ static void* contentionClientThread(void* arg) pthread_barrier_wait(&ctx->setupCompleteBarrier); pthread_barrier_wait(&ctx->streamStartBarrier); pthread_barrier_wait(&ctx->streamEndBarrier); + pthread_barrier_wait(&ctx->cleanupStartBarrier); break; } - /* Only client 0 does setup */ - if (pair->clientId == 0) { - rc = doPhaseSetup(pair, ctx->currentPhase, ctx->currentKeyId); - if (rc != WH_ERROR_OK) { - WH_ERROR_PRINT("Setup failed for phase %d: %d\n", - ctx->currentPhase, rc); - } + phase = ctx->currentPhase; + variant = ctx->currentVariant; + + /* Every client runs setup; doPhaseSetup decides internally which + * resources this client is responsible for provisioning */ + rc = doPhaseSetup(pair, phase, variant); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("Client %d setup failed for phase %d (%s): %d\n", + pair->clientId, phase, variantName(variant), rc); } pthread_barrier_wait(&ctx->setupCompleteBarrier); @@ -1978,22 +2466,32 @@ static void* contentionClientThread(void* arg) /* ===== STREAMING PHASE (tight loop, no barriers) ===== */ pthread_barrier_wait(&ctx->streamStartBarrier); - ContentionPhase phase = ctx->currentPhase; - whKeyId keyId = ctx->currentKeyId; - ClientRole role = ctx->clientRoles[pair->clientId]; - localIteration = 0; + role = ctx->clientRoles[pair->clientId]; + globalSide = isGlobalSide(pair, variant); + keyId = selectKeyIdForPhase(phase, variant, globalSide); + localIteration = 0; /* Stream requests until phaseRunning becomes 0 */ while (ATOMIC_LOAD_INT(&ctx->phaseRunning)) { - rc = - executePhaseOperation(pair, phase, role, localIteration, keyId); + if (pair->commInitFailed) { + /* Poison the phase: without a registered client id the + * namespace machinery under test is not engaged */ + rc = WH_ERROR_ABORTED; + } + else { + rc = executePhaseOperation(pair, phase, role, localIteration, + keyId, variant); + } /* Count iteration */ localIteration++; ATOMIC_ADD_INT(&pair->iterationCount, 1); - /* Track unexpected errors */ - if (!isAcceptableResult(phase, rc)) { + /* Track successes (counter validation) and unexpected errors */ + if (rc == WH_ERROR_OK) { + ATOMIC_ADD_INT(&pair->successCount, 1); + } + else if (!isAcceptableResult(phase, role, globalSide, rc)) { ATOMIC_ADD_INT(&pair->errorCount, 1); } @@ -2002,6 +2500,11 @@ static void* contentionClientThread(void* arg) /* Wait for all clients to finish streaming */ pthread_barrier_wait(&ctx->streamEndBarrier); + + /* Main validates results between these barriers using our client + * context, so hold off cleanup until it signals completion */ + pthread_barrier_wait(&ctx->cleanupStartBarrier); + doPhaseCleanup(pair, phase, variant); } return NULL; @@ -2022,51 +2525,50 @@ static int allClientsReachedIterations(StressTestContext* ctx, int target) return 1; } -/* Post-phase validation for applicable tests - * Returns WH_ERROR_OK if validation passes, error code otherwise */ -static int validatePhaseResult(StressTestContext* ctx, ContentionPhase phase, - int totalIterations, int totalErrors) +/* Post-phase validation for applicable tests. Runs on the main thread while + * all clients are parked at cleanupStartBarrier, so it may safely use their + * client contexts. Returns WH_ERROR_OK if validation passes. */ +static int validatePhaseResult(StressTestContext* ctx, ContentionPhase phase) { - int rc; - switch (phase) { - case PHASE_COUNTER_CONCURRENT_INCREMENT: { - /* Validate counter value matches expected increments */ - uint32_t counter = 0; + case PHASE_COUNTER_CONCURRENT_INCREMENT: + case PHASE_COUNTER_INCREMENT_VS_READ: { + /* Counters are always per-client (the counter message path has + * no global-namespace support), so each incrementing client's + * counter must exactly match its successful increments: less + * means a lost update, more means a phantom increment. */ + int failed = 0; + int i; - /* 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; - } + for (i = 0; i < NUM_CLIENTS; i++) { + uint32_t counter = 0; + int expected; + 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, HOT_COUNTER_ID, + &counter); + expected = 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; + 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 +2577,54 @@ static int validatePhaseResult(StressTestContext* ctx, ContentionPhase phase, } } -/* Select the appropriate keyId for a phase. - * Revoke-related phases need unique key IDs because revoked keys can't be - * erased or re-cached. Each phase type that might leave a key revoked needs - * its own key ID to avoid conflicts. - */ -static whKeyId selectKeyIdForPhase(ContentionPhase phase, int isGlobal) +/* Select the appropriate keyId for a phase, based on which namespace side + * this client is on. Revoke-related phases need unique key IDs per variant + * because revoked keys are permanently NONMODIFIABLE and can't be erased or + * re-cached by a later run. */ +static whKeyId selectKeyIdForPhase(ContentionPhase phase, + NamespaceVariant variant, int globalSide) { switch (phase) { case PHASE_KS_REVOKE_VS_CACHE: - return isGlobal ? REVOKE_CACHE_KEY_GLOBAL : REVOKE_CACHE_KEY_LOCAL; + if (variant == VARIANT_MIXED) { + return globalSide ? REVOKE_CACHE_KEY_MIXED_GLOBAL + : REVOKE_CACHE_KEY_MIXED_LOCAL; + } + return globalSide ? REVOKE_CACHE_KEY_GLOBAL + : REVOKE_CACHE_KEY_LOCAL; case PHASE_KS_REVOKE_VS_EXPORT: - return isGlobal ? REVOKE_EXPORT_KEY_GLOBAL - : REVOKE_EXPORT_KEY_LOCAL; + if (variant == VARIANT_MIXED) { + return globalSide ? REVOKE_EXPORT_KEY_MIXED_GLOBAL + : REVOKE_EXPORT_KEY_MIXED_LOCAL; + } + return globalSide ? REVOKE_EXPORT_KEY_GLOBAL + : REVOKE_EXPORT_KEY_LOCAL; /* Freshen uses HOT_KEY_ID (not unique IDs) so it can reuse keys that * are already committed by earlier phases - avoids NVM space issues */ default: - return isGlobal ? HOT_KEY_ID_GLOBAL : HOT_KEY_ID_LOCAL; + return globalSide ? HOT_KEY_ID_GLOBAL : HOT_KEY_ID_LOCAL; } } static int runPhase(StressTestContext* ctx, const PhaseConfig* config, - whKeyId keyId) + NamespaceVariant variant) { int i; int rc; #ifdef WOLFHSM_CFG_TEST_STRESS_PHASE_TIMEOUT_SEC time_t phaseStart; #endif - int totalIterations = 0; - int totalErrors = 0; - int timedOut = 0; - const char* keyScope = - (keyId & WH_KEYID_CLIENT_GLOBAL_FLAG) ? "global" : "local"; + int totalIterations = 0; + int totalErrors = 0; + int timedOut = 0; - WH_TEST_PRINT(" Phase: %s (%s key)\n", config->name, keyScope); + WH_TEST_PRINT(" Phase: %s (%s)\n", config->name, variantName(variant)); /* 1. Set phase info for all clients */ - ctx->currentPhase = config->phase; - ctx->currentKeyId = keyId; + ctx->currentPhase = config->phase; + ctx->currentVariant = variant; for (i = 0; i < NUM_CLIENTS; i++) { - ctx->clientRoles[i] = config->roles[i]; + ctx->clientRoles[i] = roleFor(config, variant, i); } /* 2. Signal clients to run setup */ @@ -2128,6 +2637,7 @@ static int runPhase(StressTestContext* ctx, const PhaseConfig* config, for (i = 0; i < NUM_CLIENTS; i++) { ATOMIC_STORE_INT(&ctx->pairs[i].iterationCount, 0); ATOMIC_STORE_INT(&ctx->pairs[i].errorCount, 0); + ATOMIC_STORE_INT(&ctx->pairs[i].successCount, 0); } /* 5. Signal clients to start streaming */ @@ -2170,8 +2680,14 @@ static int runPhase(StressTestContext* ctx, const PhaseConfig* config, WH_TEST_PRINT(" Total: %d iterations, %d errors\n", totalIterations, totalErrors); - /* 10. Run phase-specific validation */ - rc = validatePhaseResult(ctx, config->phase, totalIterations, totalErrors); + /* 10. Run phase-specific validation. Clients are parked at + * cleanupStartBarrier here, so their contexts are safe to use. */ + rc = validatePhaseResult(ctx, config->phase); + + /* 11. Release clients to run per-phase cleanup. They finish before the + * next phase's setup barrier. */ + pthread_barrier_wait(&ctx->cleanupStartBarrier); + if (rc != WH_ERROR_OK) { return rc; } @@ -2209,6 +2725,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 +2736,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 +2802,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 +2840,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 +2867,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 +2897,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 de647235b..468117da7 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_MAKE_KEYID(WH_KEYTYPE_SHE, 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) @@ -248,12 +238,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; } @@ -282,12 +275,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; } @@ -385,9 +380,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; } @@ -501,9 +496,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. */ @@ -639,16 +632,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_MAKE_KEYID(WH_KEYTYPE_SHE, - 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; @@ -910,12 +895,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); @@ -1069,16 +1054,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); @@ -1102,19 +1087,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); @@ -2219,12 +2202,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; } @@ -2288,8 +2271,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; } diff --git a/wolfhsm/wh_client.h b/wolfhsm/wh_client.h index 82bbf96fc..659d04bcb 100644 --- a/wolfhsm/wh_client.h +++ b/wolfhsm/wh_client.h @@ -1658,6 +1658,48 @@ int wh_Client_CounterDestroyResponse(whClientContext* c); */ int wh_Client_CounterDestroy(whClientContext* c, whNvmId counterId); +/** + * @section client_nvm_id_namespace Client NVM id semantics + * + * The `id` parameter passed to these NVM functions is a client-facing + * identifier in the same encoding used for keys (`wolfhsm/wh_keyid.h`): + * + * - Bits 0..7 (`WH_KEYID_MASK`) : numeric id, 1..255. Zero is + * reserved as the erased sentinel + * and is rejected by AddObject. + * - Bit 8 (`WH_KEYID_CLIENT_GLOBAL_FLAG`): when set, the request + * targets the shared global NVM + * namespace. When clear, it targets + * the calling client's own + * namespace. Requires + * WOLFHSM_CFG_GLOBAL_KEYS; without + * it AddObject rejects the flag and + * the other verbs ignore it. + * - Bit 9 (`WH_KEYID_CLIENT_WRAPPED_FLAG`): reserved; NVM AddObject + * rejects ids with this flag set. + * - Bit 10 (`WH_KEYID_CLIENT_HW_FLAG`): reserved; NVM AddObject + * rejects ids with this flag set. + * + * The server translates each request id into a TYPE/USER/ID server-internal + * encoding via `wh_KeyId_TranslateFromClient(WH_KEYTYPE_NVM, client_id, id)` + * before reaching the underlying NVM layer. This gives every client a private + * 1..255 namespace (plus a shared 1..255 global namespace when + * WOLFHSM_CFG_GLOBAL_KEYS is defined) and prevents the client NVM API from + * being used to reach keys, counters, SHE objects, or other clients' data. + * Without WOLFHSM_CFG_GLOBAL_KEYS, USER=0 objects such as factory-provisioned + * ones are unreachable through this API. + * + * With WOLFHSM_CFG_GLOBAL_KEYS, `wh_Client_NvmList` honors the GLOBAL flag on + * `startId` as a namespace selector: pass 0 to iterate the calling client's + * own objects, or `WH_KEYID_CLIENT_GLOBAL_FLAG` to iterate the global + * namespace. Returned ids carry the appropriate flag so the caller can chain + * calls. Without it, the flag is ignored and List walks the caller's own + * namespace. + * + * Define `WOLFHSM_CFG_LEGACY_CLIENT_NVM` to disable translation entirely and + * fall back to the legacy global-flat 16-bit id space. + */ + /** NVM functions */ /** * @brief Sends a non-volatile memory (NVM) initialization request to the diff --git a/wolfhsm/wh_client_she.h b/wolfhsm/wh_client_she.h index 778bfb47a..8a97243c0 100644 --- a/wolfhsm/wh_client_she.h +++ b/wolfhsm/wh_client_she.h @@ -69,13 +69,19 @@ /** SHE provisioning and identity functions */ +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT +/* Test/provisioning-only key management. These bypass the SHE M1-M5 + * authenticated key update and debug-authorization protocols and must NOT be + * exposed in production builds. Gated by WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT. + */ + /** * @brief Pre-programs a SHE key directly into NVM, bypassing the key update * protocol. * * This is a wolfHSM-specific provisioning helper that has no equivalent in the * AUTOSAR SHE command set. It writes @p key straight into the SHE NVM slot - * @p keyId with an update counter of zero, skipping the encrypted M1-M5 + * @p keyId with the update counter @p count, skipping the encrypted M1-M5 * CMD_LOAD_KEY protocol. It is intended for the initial provisioning of a * blank device (for example installing the MASTER_ECU_KEY or BOOT_MAC at * production) before any key-update authorization key exists; subsequent @@ -84,6 +90,8 @@ * * @param[in] c Pointer to the client context. * @param[in] keyId SHE key slot to write (0-15, e.g. WH_SHE_MASTER_ECU_KEY_ID). + * @param[in] count Initial SHE key counter to store with the key (0 for a + * factory-fresh slot). * @param[in] flags SHE key protection flags to store with the key * (WH_SHE_FLAG_WRITE_PROTECT, WH_SHE_FLAG_BOOT_PROTECT, etc.). * @param[in] key Pointer to the key material to store. @@ -91,7 +99,23 @@ * @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 (wolfHSM-specific). + * + * Removes the SHE key in slot @p keyId from the calling client's NVM + * namespace. Like wh_Client_ShePreProgramKey(), this is a provisioning helper + * with no AUTOSAR SHE equivalent, since the spec treats SHE keys as fixed + * hardware slots. + * + * @param[in] c Pointer to the client context. + * @param[in] keyId SHE key slot to destroy (0-15). + * @return int Returns 0 on success, or a negative error code on failure. + */ +int wh_Client_SheDestroyKey(whClientContext* c, whNvmId keyId); +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ /** * @brief Sends a request to set the ECU UID (wolfHSM-specific). diff --git a/wolfhsm/wh_message.h b/wolfhsm/wh_message.h index 1c343aa57..a23f26283 100644 --- a/wolfhsm/wh_message.h +++ b/wolfhsm/wh_message.h @@ -95,6 +95,8 @@ enum WH_SHE_ENUM { WH_SHE_DEC_CBC, WH_SHE_GEN_MAC, WH_SHE_VERIFY_MAC, + WH_SHE_PRE_PROGRAM_KEY, + WH_SHE_DESTROY_KEY, }; /* counter actions */ diff --git a/wolfhsm/wh_message_she.h b/wolfhsm/wh_message_she.h index 0e28ee78e..94efb84c0 100644 --- a/wolfhsm/wh_message_she.h +++ b/wolfhsm/wh_message_she.h @@ -389,6 +389,61 @@ int wh_MessageShe_TranslateVerifyMacResponse( uint16_t magic, const whMessageShe_VerifyMacResponse* src, whMessageShe_VerifyMacResponse* dest); +#ifdef WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT +/* Pre-program Key Request. Persists a SHE-typed NVM entry under the calling + * client's USER namespace. Carried on the SHE message group so that NVM + * client-id translation does not need to special-case typed adds. + * + * NOTE: Bypasses the SHE M1-M5 authenticated key-update protocol. Not part of + * the SHE specification; gated behind WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT for + * test/provisioning use only. */ +typedef struct { + uint32_t keyId; + uint32_t count; + uint32_t flags; + uint32_t keySz; + /* Data follows: + * uint8_t key[keySz] + */ +} whMessageShe_PreProgramKeyRequest; + +typedef struct { + int32_t rc; + uint8_t WH_PAD[4]; +} whMessageShe_PreProgramKeyResponse; + +int wh_MessageShe_TranslatePreProgramKeyRequest( + uint16_t magic, const whMessageShe_PreProgramKeyRequest* src, + whMessageShe_PreProgramKeyRequest* dest); + +int wh_MessageShe_TranslatePreProgramKeyResponse( + uint16_t magic, const whMessageShe_PreProgramKeyResponse* src, + whMessageShe_PreProgramKeyResponse* dest); + +/* Destroy Key Request. Removes a SHE-typed NVM entry from the calling + * client's USER namespace. + * + * NOTE: Bypasses the SHE debug-authorization protocol. Test/provisioning use + * only; see WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT. */ +typedef struct { + uint32_t keyId; + uint8_t WH_PAD[4]; +} whMessageShe_DestroyKeyRequest; + +typedef struct { + int32_t rc; + uint8_t WH_PAD[4]; +} whMessageShe_DestroyKeyResponse; + +int wh_MessageShe_TranslateDestroyKeyRequest( + uint16_t magic, const whMessageShe_DestroyKeyRequest* src, + whMessageShe_DestroyKeyRequest* dest); + +int wh_MessageShe_TranslateDestroyKeyResponse( + uint16_t magic, const whMessageShe_DestroyKeyResponse* src, + whMessageShe_DestroyKeyResponse* dest); +#endif /* WOLFHSM_CFG_SHE_ENABLE_TEST_KEY_MGMT */ + #endif /* WOLFHSM_CFG_SHE_EXTENSION */ #endif /* !WOLFHSM_WH_MESSAGE_SHE_H_ */ diff --git a/wolfhsm/wh_settings.h b/wolfhsm/wh_settings.h index 1139fee9c..7e4c19bce 100644 --- a/wolfhsm/wh_settings.h +++ b/wolfhsm/wh_settings.h @@ -50,6 +50,17 @@ * WOLFHSM_CFG_KEYWRAP - If defined, include the key wrap functionality * Default: Not defined * + * WOLFHSM_CFG_LEGACY_CLIENT_NVM - If defined, the client-facing NVM message + * handlers in wh_server_nvm.c pass IDs to the underlying NVM layer verbatim + * (legacy raw, globally-flat ID space). When NOT defined (the default), + * every client-supplied NVM id is translated via + * wh_KeyId_TranslateFromClient(WH_KEYTYPE_NVM, client_id, id) so each client + * has its own private 1..255 id namespace plus a shared 1..255 global + * namespace (selected with WH_KEYID_CLIENT_GLOBAL_FLAG). This closes the + * loophole where a client could reach another client's keys / objects via + * the raw NVM API. + * Default: Not defined + * * WOLFHSM_CFG_KEYWRAP_MAX_KEY_SIZE - The maximum size (in bytes) of a key that * can be wrapped * Default: 512