diff --git a/docs/src/5-Features.md b/docs/src/5-Features.md
index 3a24cac1e..f7d16a48d 100644
--- a/docs/src/5-Features.md
+++ b/docs/src/5-Features.md
@@ -21,6 +21,8 @@ This chapter provides a detailed overview of the high level features that wolfHS
- [Key Cache, Key IDs, and NVM Backing Store](#key-cache-key-ids-and-nvm-backing-store)
- [Global Keys](#global-keys)
- [Wrapped Keys](#wrapped-keys)
+ - [Hardware-Only Keys](#hardware-only-keys)
+ - [Supported Components](#supported-components)
- [Key Usage Policies](#key-usage-policies)
- [Certificate Management](#certificate-management)
- [Trusted Root Storage](#trusted-root-storage)
@@ -289,7 +291,7 @@ The keystore is two-tier: a fixed-size **key cache** in server RAM holds the wor
Keys are named by a 16-bit identifier (`whKeyId`), which has two forms — a simple one the client uses and a fuller one the server uses internally:
-- **Client-side**: Each client gets a dedicated namespace of 255 key identifiers that are specific to and only accessible by that client. These IDs range from `[1, 255]`, where `0` is the reserved sentinel value `WH_KEYID_ERASED` used internally to mark empty key slots (this sentinel value is also used to request a dynamically assigned ID for a key cache operation — see the keystore API documentation for more information). The client can also set a flag bit in the keyId top byte to ask for special handling — bit 8 for a [global key](#global-keys), bit 9 for a [wrapped key](#wrapped-keys). That is all a client ever deals with, and the `WH_CLIENT_KEYID_MAKE_*` macros in `wolfhsm/wh_client.h` set those flags for it.
+- **Client-side**: Each client gets a dedicated namespace of 255 key identifiers that are specific to and only accessible by that client. These IDs range from `[1, 255]`, where `0` is the reserved sentinel value `WH_KEYID_ERASED` used internally to mark empty key slots (this sentinel value is also used to request a dynamically assigned ID for a key cache operation — see the keystore API documentation for more information). The client can also set a flag bit in the keyId top byte to ask for special handling — bit 8 for a [global key](#global-keys), bit 9 for a [wrapped key](#wrapped-keys), and bit 10 for a [hardware-only key](#hardware-only-keys). That is all a client ever deals with, and the `WH_CLIENT_KEYID_MAKE_*` macros in `wolfhsm/wh_client.h` set those flags for it.
- **Server-side**: internally every key has a globally unique id that also encodes *what* the key is and *who* owns it. When a request arrives, the server expands the client's provided keyId number into this full form, and collapses it back on the way out (`wh_KeyId_TranslateFromClient()` and its inverse). Client code never touches the internal fields.
The server-side `whKeyId` packs three fields into its 16 bits:
@@ -356,18 +358,56 @@ The wrap format used by wolfHSM is a length-prefixed, authenticated encryption b
The metadata is bound into the authenticated plaintext so that the wrapped blob carries not only the key bytes but also its policy, label, and identifier — a recipient cannot strip or substitute metadata without invalidating the authentication tag. The on-wire constants `WH_KEYWRAP_AES_GCM_IV_SIZE`, `WH_KEYWRAP_AES_GCM_TAG_SIZE`, and `WH_KEYWRAP_AES_GCM_HEADER_SIZE` are defined in `wolfhsm/wh_common.h` and may be used by callers to size wrap output buffers. The maximum wrappable key size is controlled by `WOLFHSM_CFG_KEYWRAP_MAX_KEY_SIZE`.
-The lifecycle exposed to clients consists of three primary operations:
+The lifecycle exposed to clients consists of four primary operations:
- **Wrap**: the client supplies plaintext key bytes, a metadata template, and the keyId of a server-resident KEK; the server encrypts the (metadata || key) blob with the KEK and returns the wrapped blob to the client. The plaintext is not written to NVM as part of this operation.
+- **Wrap-export**: the client names a key *already resident in the server* by its keyId (never presenting plaintext), plus a KEK keyId; the server reads the target key — enforcing its export policy, so a `NONEXPORTABLE` key is refused — and returns it wrapped under the KEK. This backs up or migrates a key the server already holds without ever exposing its plaintext to the client. Because a server-held secret is moved across the trust boundary, the KEK must be a **trusted KEK** (see below).
- **Unwrap-and-export**: the client supplies a wrapped blob and the KEK's keyId; the server decrypts the blob, authenticates the tag, and returns the recovered metadata and key bytes to the client. This is the operation used by host-side workflows that need to consume the key off-device, for example to inject it into a non-HSM peer.
-- **Unwrap-and-cache**: the client supplies a wrapped blob and the KEK's keyId; the server decrypts the blob and installs the recovered key directly into the keystore cache as if `wh_Client_KeyCache` had been called locally with the recovered bytes. This is the more common operation in production deployments, since it lets a key live on disk in encrypted form and be hydrated into the HSM at runtime without the plaintext ever transiting the client. Clients can then commit the unwrapped key to NVM if they wish.
+- **Unwrap-and-cache**: the client supplies a wrapped blob and the KEK's keyId; the server decrypts the blob and installs the recovered key directly into the keystore cache as if `wh_Client_KeyCache` had been called locally with the recovered bytes. This is the more common operation in production deployments, since it lets a key live on disk in encrypted form and be hydrated into the HSM at runtime without the plaintext ever transiting the client. Clients can then commit the unwrapped key to NVM if they wish. Because this injects a key into the server keystore, the KEK must be a **trusted KEK** (see below).
-In all three operations the KEK is identified by its existing keyId in the keystore, must carry the `WH_NVM_FLAGS_USAGE_WRAP` usage flag, and is enforced server-side by the keystore policy machinery. A key without the `WRAP` usage flag cannot be used to wrap or unwrap regardless of any client request.
+In every operation the KEK is identified by its existing keyId in the keystore. A software (keystore-resident) KEK must carry the `WH_NVM_FLAGS_USAGE_WRAP` usage flag, enforced server-side by the keystore policy machinery: a key without the `WRAP` usage flag cannot be used to wrap or unwrap regardless of any client request. This usage-flag check does not apply to [hardware-only KEKs](#hardware-only-keys), which carry no `whNvmMetadata`; for them the hardware keystore backend is the policy authority instead.
A parallel pair of APIs — `wh_Client_DataWrap` and `wh_Client_DataUnwrap` — applies the same construction to arbitrary application data rather than key material. These are useful when a client needs the same authenticated-encryption guarantee for non-key payloads using a key resident in the HSM.
> **Note**: Wrapped key identifiers are signaled on the wire by setting `WH_KEYID_CLIENT_WRAPPED_FLAG` (bit 9) in the request keyId, which the server translates internally to `WH_KEYTYPE_WRAPPED`. Clients construct wrapped-key identifiers using `WH_CLIENT_KEYID_MAKE_WRAPPED()`, and the combined wrapped-and-global form using `WH_CLIENT_KEYID_MAKE_WRAPPED_GLOBAL()`; both are defined in `wolfhsm/wh_client.h`.
+#### Trusted KEKs
+
+Two of the operations — **wrap-export** and **unwrap-and-cache** — additionally require the KEK to be a **trusted KEK**, because each moves a *server-held* secret across the client trust boundary: wrap-export extracts one, and unwrap-and-cache injects one. If a client could name a KEK whose plaintext it knows (for example an AES key it cached itself), it could decrypt an extracted blob to recover the target key, or forge a blob to inject a key of its choosing, defeating the wrapped-key confidentiality guarantee. A trusted KEK is one that the client can neither read nor set, and is one of the following:
+
+- a **[hardware-only key](#hardware-only-keys)**: A key stored in hardware in a port-specific manner (see next section)
+- a software key carrying the server-only **`WH_NVM_FLAGS_KEK`** flag.
+
+For a software KEK, the `WH_NVM_FLAGS_KEK` flag will be stripped by the server from *every* client request that supplies key or object metadata such that a client can never cause a key or object to carry it. The strip lives in the policy-checked entry points that all client requests funnel through. It is able to be set only by by calling the unchecked keystore/NVM API directly, which is not exposed to via the client API but remains useable for server-side code or offline provisioning via `whnvmtool`. A key carrying the flag is treated as unreadable, immutable, non-evictable, and non-exportable through the client API, and is the only cache/NVM key permitted to act as a KEK for wrap-export and unwrap-and-cache.
+
+The ordinary **wrap**, **unwrap-and-export**, and data wrap/unwrap operations are not affected by trusted KEKs, as no server secret crosses the boundary in those, so they continue to accept any client-chosen KEK that carries `WH_NVM_FLAGS_USAGE_WRAP`.
+
+For deployment guidance, an NVM-backed system that wishes to use software KEKs for keywrap export should provision the software KEK in an offline manner (e.g. using `whnvmtool` or building the NVM image manually). An NVM-less system has no secure persistent source for software-KEK bytes other than the firmware image, so a **hardware KEK is required** in this case.
+
+### Hardware-Only Keys
+
+Some platforms provide key material that the HSM core can read but that never lives in the wolfHSM keystore at all — KEKs burned into OTP or fuses, keys held by an SoC key-management block, or other hardware-provisioned secrets. The optional **hardware keystore front-end** (`WOLFHSM_CFG_HWKEYSTORE`) lets clients reference such keys as KEKs in [wrapped-key](#wrapped-keys) operations while guaranteeing the material never enters the key cache, never touches NVM, and is never returned to a client.
+
+The front-end (`wolfhsm/wh_hwkeystore.h`) is a platform-agnostic, fully configurable abstraction over the hardware keystore, following the same backend-callback-table paradigm as other elements of the core library. A backend provides a `whHwKeystoreCb` callback table whose required `GetKey` callback copies a requested key's bytes into a caller-provided buffer on demand, plus optional `Init` and `Cleanup` callbacks for backend setup/teardown. The callback table, an opaque backend context and config (for backend-specific state), and a lock (serializing callback invocations when the backing hardware is shared across server threads under `WOLFHSM_CFG_THREADSAFE`) are described by a `whHwKeystoreConfig` and held in a `whHwKeystoreContext`. The server application initializes the context once with `wh_HwKeystore_Init()` — which binds the callback table and invokes the backend `Init` callback if present — and binds it to one or more server contexts through the optional `hwKeystore` member of `whServerConfig`.
+
+Clients designate a hardware-only key by setting `WH_KEYID_CLIENT_HW_FLAG` (bit 10) in the request keyId, normally via the `WH_CLIENT_KEYID_MAKE_HW()` macro in `wolfhsm/wh_client.h`. The server translates the flag to the internal key type `WH_KEYTYPE_HW` and routes accordingly:
+
+- In the keywrap operations (key wrap, unwrap-and-export, unwrap-and-cache, and data wrap/unwrap), a hardware-only KEK id causes the server to fetch the KEK from the hardware keystore into a local stack buffer via `wh_HwKeystore_GetKey()`, perform the AES-GCM operation, and zeroize the buffer before returning. The KEK is never cached and never appears in any response. An unwrap-and-cache under a hardware KEK still caches the unwrapped payload since the payload is an ordinary key; only the KEK is hardware-resident.
+- Hardware-only keyIds are rejected in every other operation with `WH_ERROR_ACCESS`.
+
+Because hardware-only keys carry no `whNvmMetadata`, the [usage-flag policy machinery](#key-usage-policies) does not apply to them. The largest key the keywrap path will fetch from the backend is bounded by `WOLFHSM_CFG_HWKEYSTORE_MAX_KEY_SIZE` (default 32 bytes, sized for an AES-256 KEK), which should be respected (or enforced), by the port layer.
+
+#### Supported Components
+
+Hardware-only keys can be consumed by exactly one wolfHSM component today — the **[keywrap](#wrapped-keys) API**, where a `WH_CLIENT_KEYID_MAKE_HW()` id names the KEK. The supported operations are:
+
+- **Key wrap** (`wh_Client_KeyWrap`) — wrap a client-supplied key under the hardware-resident KEK.
+- **Key unwrap-and-export** (`wh_Client_KeyUnwrapAndExport`) — unwrap a blob and return the recovered key to the client.
+- **Key unwrap-and-cache** (`wh_Client_KeyUnwrapAndCache`) — unwrap a blob into the key cache; only the KEK is hardware-resident, while the unwrapped payload becomes an ordinary cached key.
+- **Data wrap and unwrap** (`wh_Client_DataWrap`, `wh_Client_DataUnwrap`) — apply the same authenticated encryption to arbitrary application data.
+
+No other component can name a hardware-only key: a direct wolfCrypt operation through the crypto callback, or any keystore lifecycle operation (cache, export, commit, evict, erase, revoke), is rejected with `WH_ERROR_ACCESS`. **Support is limited to keywrap KEK usage for now** — broader use, such as referencing a hardware-only key directly in a crypto operation, may be added in a future release.
+
### Key Usage Policies
A key's `whNvmMetadata` carries a `flags` field that the keystore checks on every server-side operation to constrain how the key may be used. The flags fall into two groups: **lifecycle flags** that govern whether a key may be modified, destroyed, exported, or cached at all, and **usage flags** that govern which cryptographic operations it may take part in.
diff --git a/docs/src/9-Configuration.md b/docs/src/9-Configuration.md
index 136017494..1c79c3687 100644
--- a/docs/src/9-Configuration.md
+++ b/docs/src/9-Configuration.md
@@ -75,10 +75,21 @@ These macros enable or tune optional cryptographic subsystems built on top of wo
| `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. |
+| `WOLFHSM_CFG_HWKEYSTORE` | Undefined | If defined, compile the hardware keystore front-end (`wh_HwKeystore_*`) and hardware-only key support (`WH_KEYTYPE_HW`, `WH_CLIENT_KEYID_MAKE_HW()`). Hardware-only keys are served on demand by a user-supplied callback and are usable only as keywrap KEKs; they never enter the key cache or NVM and are never exported. See [Hardware-Only Keys](5-Features.md#hardware-only-keys). |
+| `WOLFHSM_CFG_HWKEYSTORE_MAX_KEY_SIZE` | `32` | Maximum size, in bytes, of a key served by the hardware keystore backend; sizes the local buffer that holds a hardware KEK for the duration of a keywrap operation. Only consulted when `WOLFHSM_CFG_HWKEYSTORE` is defined. |
| `WOLFHSM_CFG_GLOBAL_KEYS` | Undefined | If defined, enable the global-keys feature, allowing keys to be cached so that they are visible to every client rather than scoped to the caching client. See [Global Keys](5-Features.md#global-keys) for a full discussion of the API and security implications. |
| `WH_DEV_ID` | `0x5748534D` (`"WHSM"`) | Value of the process-global crypto device ID registered by every `wh_Client_Init()` with the unified client crypto callback. Also the device ID bound to a client whose `whClientConfig.devId` is left `0`. Override it if the default collides with another crypto-callback device ID in the application. See [Transparent Offload via Crypto Callbacks](5-Features.md#transparent-offload-via-crypto-callbacks) for registration lifetime, multi-client rules, and wolfCrypt callback-table sizing (`MAX_CRYPTO_DEVID_CALLBACKS`, default 8). |
| `WH_DEV_ID_DMA` | `0x57444D41` (`"WDMA"`) | Value of the process-global DMA-only crypto device ID, registered by every `wh_Client_Init()` when `WOLFHSM_CFG_DMA` is defined. Reserved: not valid as a `whClientConfig.devId`. Override it if the default collides with another crypto-callback device ID in the application. |
+### Provisioning a Trusted Software KEK
+
+The wrap-export and unwrap-and-cache operations require a [trusted KEK](5-Features.md#trusted-keks) — a key the client can neither read nor set. On a system without a hardware keystore, that KEK is a software key carrying the server-only `WH_NVM_FLAGS_KEK` flag (bit 12 of `whNvmFlags`). The server strips this flag from every client-supplied metadata path, so it can be set only by trusted provisioning that bypasses the request handlers:
+
+- **NVM-backed systems**: provision the KEK in an offline NVM image with `whnvmtool` (see the tool's README). The recommended flag value is `WH_NVM_FLAGS_KEK | WH_NVM_FLAGS_NONEXPORTABLE | WH_NVM_FLAGS_NONMODIFIABLE | WH_NVM_FLAGS_USAGE_WRAP`, i.e. `0x1000 | 0x0004 | 0x0001 | 0x0200 = 0x1205`. (`WH_NVM_FLAGS_KEK` already makes the key unreadable, immutable, and KEK-only through the client API; setting the other bits as well keeps the intent explicit.)
+- **NVM-less systems**: there is no secure persistent source for the KEK bytes, so use a hardware KEK (`WOLFHSM_CFG_HWKEYSTORE`) instead. Server-internal boot code *can* cache a `WH_NVM_FLAGS_KEK` key directly, but it would still have to obtain the plaintext from the firmware image, which is not a confidential store.
+
+Because the flag is a provenance bit rather than a request a client can make, this also closes the empty-slot hole on shared key ids: even with `WOLFHSM_CFG_GLOBAL_KEYS` enabled — where a client can reach the global (`USER=0`) namespace — a client that caches a key at the KEK's id does not get the flag, so that key is not KEK-eligible and wrap-export/unwrap-and-cache reject it.
+
## Keystore and Key Cache
These macros size the server-side key cache. The cache is split into "regular" slots (sized for common symmetric and EC keys) and "big" slots (sized for RSA-class keys); both are statically allocated.
diff --git a/src/wh_client_keywrap.c b/src/wh_client_keywrap.c
index 0bb1e15f0..c38a871d7 100644
--- a/src/wh_client_keywrap.c
+++ b/src/wh_client_keywrap.c
@@ -127,6 +127,112 @@ int wh_Client_KeyWrap(whClientContext* ctx, enum wc_CipherType cipherType,
return ret;
}
+int wh_Client_KeyWrapExportRequest(whClientContext* ctx,
+ enum wc_CipherType cipherType,
+ uint16_t keyId, uint16_t keyType,
+ uint16_t serverKeyId)
+{
+ uint16_t group = WH_MESSAGE_GROUP_KEY;
+ uint16_t action = WH_KEY_KEYWRAPEXPORT;
+ whMessageKeystore_KeyWrapExportRequest* req = NULL;
+
+ if (ctx == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Set the request pointer to the shared comm data memory region */
+ req = (whMessageKeystore_KeyWrapExportRequest*)wh_CommClient_GetDataPtr(
+ ctx->comm);
+ if (req == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Initialize the request. There is no trailing payload: the server already
+ * holds the key identified by keyId. */
+ req->keyId = keyId;
+ req->keyType = keyType;
+ req->serverKeyId = serverKeyId;
+ req->cipherType = cipherType;
+
+ return wh_Client_SendRequest(ctx, group, action, sizeof(*req),
+ (uint8_t*)req);
+}
+
+int wh_Client_KeyWrapExportResponse(whClientContext* ctx,
+ enum wc_CipherType cipherType,
+ void* wrappedKeyOut,
+ uint16_t* wrappedKeyInOutSz)
+{
+ int ret;
+ uint16_t group;
+ uint16_t action;
+ uint16_t size;
+ whMessageKeystore_KeyWrapExportResponse* resp = NULL;
+ uint8_t* respData;
+
+ if (ctx == NULL || wrappedKeyOut == NULL || wrappedKeyInOutSz == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Set the response pointer to the shared comm data memory region */
+ resp = (whMessageKeystore_KeyWrapExportResponse*)wh_CommClient_GetDataPtr(
+ ctx->comm);
+ if (resp == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Receive the response */
+ ret = wh_Client_RecvResponse(ctx, &group, &action, &size, (uint8_t*)resp);
+ if (ret != WH_ERROR_OK) {
+ return ret;
+ }
+
+ if (group != WH_MESSAGE_GROUP_KEY || action != WH_KEY_KEYWRAPEXPORT ||
+ size < sizeof(*resp) || size < sizeof(*resp) + resp->wrappedKeySz ||
+ resp->cipherType != cipherType) {
+ return WH_ERROR_ABORTED;
+ }
+
+ if (resp->rc != 0) {
+ return resp->rc;
+ }
+ else if (resp->wrappedKeySz > *wrappedKeyInOutSz) {
+ return WH_ERROR_BUFFER_SIZE;
+ }
+
+ /* Copy the wrapped key from the response data into wrappedKeyOut */
+ respData = (uint8_t*)(resp + 1);
+ memcpy(wrappedKeyOut, respData, resp->wrappedKeySz);
+ *wrappedKeyInOutSz = resp->wrappedKeySz;
+
+ return WH_ERROR_OK;
+}
+
+int wh_Client_KeyWrapExport(whClientContext* ctx, enum wc_CipherType cipherType,
+ uint16_t keyId, uint16_t keyType,
+ uint16_t serverKeyId, void* wrappedKeyOut,
+ uint16_t* wrappedKeyInOutSz)
+{
+ int ret = WH_ERROR_OK;
+
+ if (ctx == NULL || wrappedKeyOut == NULL || wrappedKeyInOutSz == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ ret = wh_Client_KeyWrapExportRequest(ctx, cipherType, keyId, keyType,
+ serverKeyId);
+ if (ret != WH_ERROR_OK) {
+ return ret;
+ }
+
+ do {
+ ret = wh_Client_KeyWrapExportResponse(ctx, cipherType, wrappedKeyOut,
+ wrappedKeyInOutSz);
+ } while (ret == WH_ERROR_NOTREADY);
+
+ return ret;
+}
+
int wh_Client_KeyUnwrapAndExportRequest(whClientContext* ctx,
enum wc_CipherType cipherType,
uint16_t serverKeyId,
diff --git a/src/wh_hwkeystore.c b/src/wh_hwkeystore.c
new file mode 100644
index 000000000..69f1b04ae
--- /dev/null
+++ b/src/wh_hwkeystore.c
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of wolfHSM.
+ *
+ * wolfHSM is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfHSM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with wolfHSM. If not, see .
+ */
+/*
+ * src/wh_hwkeystore.c
+ *
+ * Hardware keystore front-end implementation. Dispatches to a backend callback
+ * table (whHwKeystoreCb) with argument validation and lock serialization. See
+ * wolfhsm/wh_hwkeystore.h for the module description.
+ */
+
+/* Pick up compile-time configuration */
+#include "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+
+#include
+#include
+#include
+
+#include "wolfhsm/wh_error.h"
+#include "wolfhsm/wh_hwkeystore.h"
+
+int wh_HwKeystore_Init(whHwKeystoreContext* context,
+ const whHwKeystoreConfig* config)
+{
+ if ((context == NULL) || (config == NULL) || (config->cb == NULL) ||
+ (config->cb->GetKey == NULL)) {
+ return WH_ERROR_BADARGS;
+ }
+
+ memset(context, 0, sizeof(*context));
+ context->cb = config->cb;
+ context->context = config->context;
+
+ /* Initialize the lock before the backend so that, on a backend Init
+ * failure, teardown is a simple lock cleanup with no backend state to
+ * undo. This keeps init and cleanup symmetric: wh_HwKeystore_Cleanup tears
+ * down the backend then the lock, the reverse of the order here */
+#ifdef WOLFHSM_CFG_THREADSAFE
+ {
+ int rc = wh_Lock_Init(&context->lock, config->lockConfig);
+ if (rc != WH_ERROR_OK) {
+ memset(context, 0, sizeof(*context));
+ return rc;
+ }
+ }
+#endif /* WOLFHSM_CFG_THREADSAFE */
+
+ /* Initialize the backend if it provides an Init callback. Done last so it
+ * is the only fallible step after the lock: on failure, undo the lock so a
+ * successful backend Init is never left without a paired Cleanup */
+ if (context->cb->Init != NULL) {
+ int rc = context->cb->Init(context->context, config->config);
+ if (rc != WH_ERROR_OK) {
+#ifdef WOLFHSM_CFG_THREADSAFE
+ (void)wh_Lock_Cleanup(&context->lock);
+#endif /* WOLFHSM_CFG_THREADSAFE */
+ memset(context, 0, sizeof(*context));
+ return rc;
+ }
+ }
+
+ context->initialized = 1;
+ return WH_ERROR_OK;
+}
+
+int wh_HwKeystore_Cleanup(whHwKeystoreContext* context)
+{
+ int rc = WH_ERROR_OK;
+
+ if (context == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Capture the backend teardown result but always finish releasing the lock
+ * and zeroing the context, then surface the backend error. */
+ if ((context->cb != NULL) && (context->cb->Cleanup != NULL)) {
+ rc = context->cb->Cleanup(context->context);
+ }
+
+#ifdef WOLFHSM_CFG_THREADSAFE
+ {
+ /* Only let a lock-release failure surface when the backend cleanup
+ * itself succeeded, so a real backend error is not hidden */
+ int relRc = wh_Lock_Cleanup(&context->lock);
+ if (rc == WH_ERROR_OK) {
+ rc = relRc;
+ }
+ }
+#endif /* WOLFHSM_CFG_THREADSAFE */
+
+ memset(context, 0, sizeof(*context));
+ return rc;
+}
+
+int wh_HwKeystore_GetKey(whHwKeystoreContext* context, whKeyId keyId,
+ uint8_t* out, uint16_t* inout_len)
+{
+ int rc;
+
+ if ((context == NULL) || (context->initialized == 0) ||
+ (context->cb == NULL) || (context->cb->GetKey == NULL) ||
+ (out == NULL) || (inout_len == NULL)) {
+ return WH_ERROR_BADARGS;
+ }
+
+#ifdef WOLFHSM_CFG_THREADSAFE
+ rc = wh_Lock_Acquire(&context->lock);
+ if (rc != WH_ERROR_OK) {
+ return rc;
+ }
+#endif /* WOLFHSM_CFG_THREADSAFE */
+
+ rc = context->cb->GetKey(context->context, keyId, out, inout_len);
+
+#ifdef WOLFHSM_CFG_THREADSAFE
+ {
+ /* Preserve the backend result; only surface a release failure when the
+ * GetKey call itself succeeded so a real lock error is not hidden */
+ int relRc = wh_Lock_Release(&context->lock);
+ if (rc == WH_ERROR_OK) {
+ rc = relRc;
+ }
+ }
+#endif /* WOLFHSM_CFG_THREADSAFE */
+
+ return rc;
+}
+
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
diff --git a/src/wh_keyid.c b/src/wh_keyid.c
index 239d615a7..185ce8ec4 100644
--- a/src/wh_keyid.c
+++ b/src/wh_keyid.c
@@ -44,6 +44,14 @@ whKeyId wh_KeyId_TranslateFromClient(uint16_t type, uint16_t clientId,
}
#endif
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Convert hardware-only flag to TYPE=WH_KEYTYPE_HW. Checked after
+ * the wrapped flag so HW wins if a client sets both */
+ if ((reqId & WH_KEYID_CLIENT_HW_FLAG) != 0) {
+ type = WH_KEYTYPE_HW;
+ }
+#endif
+
return WH_MAKE_KEYID(type, user, id);
}
@@ -65,5 +73,12 @@ whKeyId wh_KeyId_TranslateToClient(whKeyId serverId)
}
#endif
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Convert TYPE=HW to hardware-only flag */
+ if (WH_KEYID_TYPE(serverId) == WH_KEYTYPE_HW) {
+ clientId |= WH_KEYID_CLIENT_HW_FLAG;
+ }
+#endif
+
return clientId;
}
diff --git a/src/wh_message_keystore.c b/src/wh_message_keystore.c
index 209ca5483..63823c0a9 100644
--- a/src/wh_message_keystore.c
+++ b/src/wh_message_keystore.c
@@ -354,6 +354,35 @@ int wh_MessageKeystore_TranslateKeyWrapResponse(
return 0;
}
+/* Wrap-and-export (by id) Request translation */
+int wh_MessageKeystore_TranslateKeyWrapExportRequest(
+ uint16_t magic, const whMessageKeystore_KeyWrapExportRequest* src,
+ whMessageKeystore_KeyWrapExportRequest* dest)
+{
+ if ((src == NULL) || (dest == NULL)) {
+ return WH_ERROR_BADARGS;
+ }
+ WH_T16(magic, dest, src, keyId);
+ WH_T16(magic, dest, src, keyType);
+ WH_T16(magic, dest, src, serverKeyId);
+ WH_T16(magic, dest, src, cipherType);
+ return 0;
+}
+
+/* Wrap-and-export (by id) Response translation */
+int wh_MessageKeystore_TranslateKeyWrapExportResponse(
+ uint16_t magic, const whMessageKeystore_KeyWrapExportResponse* src,
+ whMessageKeystore_KeyWrapExportResponse* dest)
+{
+ if ((src == NULL) || (dest == NULL)) {
+ return WH_ERROR_BADARGS;
+ }
+ WH_T32(magic, dest, src, rc);
+ WH_T16(magic, dest, src, wrappedKeySz);
+ WH_T16(magic, dest, src, cipherType);
+ return 0;
+}
+
/* Key Unwrap Request translation */
int wh_MessageKeystore_TranslateKeyUnwrapAndExportRequest(
uint16_t magic, const whMessageKeystore_KeyUnwrapAndExportRequest* src,
diff --git a/src/wh_nvm.c b/src/wh_nvm.c
index 8e24cf05a..0f7e7d686 100644
--- a/src/wh_nvm.c
+++ b/src/wh_nvm.c
@@ -60,6 +60,12 @@ static int wh_Nvm_CheckPolicy(whNvmContext* context, whNvmOp op, whNvmId id,
*existing_meta = meta;
}
+ /* A server-only key (e.g. a trusted KEK, WH_NVM_FLAGS_KEK) must be
+ * immutable through the client NVM API regardless of its other flags. */
+ if (meta.flags & WH_NVM_FLAGS_KEK) {
+ return WH_ERROR_ACCESS;
+ }
+
switch (op) {
case WH_NVM_OP_ADD:
if (meta.flags & WH_NVM_FLAGS_NONMODIFIABLE) {
@@ -263,17 +269,24 @@ int wh_Nvm_AddObject(whNvmContext* context, whNvmMetadata *meta,
return context->cb->AddObject(context->context, meta, data_len, data);
}
-int wh_Nvm_AddObjectChecked(whNvmContext* context, whNvmMetadata* meta,
+int wh_Nvm_AddObjectChecked(whNvmContext* context, const whNvmMetadata* meta,
whNvmSize data_len, const uint8_t* data)
{
- int ret;
+ int ret;
+ whNvmMetadata sanitized;
ret = wh_Nvm_CheckPolicy(context, WH_NVM_OP_ADD, meta->id, NULL);
if (ret != WH_ERROR_OK && ret != WH_ERROR_NOTFOUND) {
return ret;
}
- return wh_Nvm_AddObject(context, meta, data_len, data);
+ /* Copy before sanitizing: meta may point at a read-only client DMA mapping,
+ * so we must not write through it. Strip server-only flags a client may
+ * never set. */
+ sanitized = *meta;
+ sanitized.flags &= ~WH_NVM_FLAGS_SERVER_ONLY;
+
+ return wh_Nvm_AddObject(context, &sanitized, data_len, data);
}
int wh_Nvm_List(whNvmContext* context,
diff --git a/src/wh_server.c b/src/wh_server.c
index 694b2be57..e9796d633 100644
--- a/src/wh_server.c
+++ b/src/wh_server.c
@@ -88,6 +88,9 @@ int wh_Server_Init(whServerContext* server, whServerConfig* config)
#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION
server->auth = config->auth;
#endif /* WOLFHSM_CFG_ENABLE_AUTHENTICATION */
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ server->hwKeystore = config->hwKeystore;
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
#ifndef WOLFHSM_CFG_NO_CRYPTO
server->crypto = config->crypto;
diff --git a/src/wh_server_crypto.c b/src/wh_server_crypto.c
index 788f91f53..4de19704f 100644
--- a/src/wh_server_crypto.c
+++ b/src/wh_server_crypto.c
@@ -260,7 +260,8 @@ int wh_Server_CacheImportRsaKey(whServerContext* ctx, RsaKey* key,
/* set meta */
cacheMeta->id = keyId;
cacheMeta->len = der_size;
- cacheMeta->flags = flags;
+ /* clients can't set server-only flags (e.g. trusted KEK) */
+ cacheMeta->flags = flags & ~WH_NVM_FLAGS_SERVER_ONLY;
cacheMeta->access = WH_NVM_ACCESS_ANY;
if ( (label != NULL) &&
@@ -604,7 +605,8 @@ int wh_Server_EccKeyCacheImport(whServerContext* ctx, ecc_key* key,
/* set meta */
cacheMeta->id = keyId;
cacheMeta->len = der_size;
- cacheMeta->flags = flags;
+ /* clients can't set server-only flags (e.g. trusted KEK) */
+ cacheMeta->flags = flags & ~WH_NVM_FLAGS_SERVER_ONLY;
cacheMeta->access = WH_NVM_ACCESS_ANY;
if ( (label != NULL) &&
@@ -664,7 +666,8 @@ int wh_Server_CacheImportEd25519Key(whServerContext* ctx, ed25519_key* key,
if (ret == WH_ERROR_OK) {
cacheMeta->id = keyId;
cacheMeta->len = der_size;
- cacheMeta->flags = flags;
+ /* clients can't set server-only flags (e.g. trusted KEK) */
+ cacheMeta->flags = flags & ~WH_NVM_FLAGS_SERVER_ONLY;
cacheMeta->access = WH_NVM_ACCESS_ANY;
if ((label != NULL) && (label_len > 0)) {
@@ -723,7 +726,8 @@ int wh_Server_CacheImportCurve25519Key(whServerContext* server,
/* Update metadata to cache the key */
cacheMeta->id = keyId;
cacheMeta->len = keySz;
- cacheMeta->flags = flags;
+ /* clients can't set server-only flags (e.g. trusted KEK) */
+ cacheMeta->flags = flags & ~WH_NVM_FLAGS_SERVER_ONLY;
cacheMeta->access = WH_NVM_ACCESS_ANY;
if ((label != NULL) && (label_len > 0)) {
memcpy(cacheMeta->label, label, label_len);
@@ -793,7 +797,8 @@ int wh_Server_MlDsaKeyCacheImport(whServerContext* ctx, wc_MlDsaKey* key,
if (ret == WH_ERROR_OK) {
cacheMeta->id = keyId;
cacheMeta->len = der_size;
- cacheMeta->flags = flags;
+ /* clients can't set server-only flags (e.g. trusted KEK) */
+ cacheMeta->flags = flags & ~WH_NVM_FLAGS_SERVER_ONLY;
cacheMeta->access = WH_NVM_ACCESS_ANY;
if ((label != NULL) && (label_len > 0)) {
@@ -849,7 +854,8 @@ int wh_Server_MlKemKeyCacheImport(whServerContext* ctx, MlKemKey* key,
if (ret == WH_ERROR_OK) {
cacheMeta->id = keyId;
cacheMeta->len = keySize;
- cacheMeta->flags = flags;
+ /* clients can't set server-only flags (e.g. trusted KEK) */
+ cacheMeta->flags = flags & ~WH_NVM_FLAGS_SERVER_ONLY;
cacheMeta->access = WH_NVM_ACCESS_ANY;
if ((label != NULL) && (label_len > 0)) {
memcpy(cacheMeta->label, label, label_len);
@@ -1393,7 +1399,8 @@ int wh_Server_KeyCacheImportRaw(whServerContext* ctx, const uint8_t* keyData,
cacheMeta->id = keyId;
cacheMeta->len = keySize;
- cacheMeta->flags = flags;
+ /* clients can't set server-only flags (e.g. trusted KEK) */
+ cacheMeta->flags = flags & ~WH_NVM_FLAGS_SERVER_ONLY;
cacheMeta->access = WH_NVM_ACCESS_ANY;
if ((label != NULL) && (label_len > 0)) {
@@ -1440,7 +1447,8 @@ int wh_Server_CmacKdfKeyCacheImport(whServerContext* ctx,
if (ret == WH_ERROR_OK) {
cacheMeta->id = keyId;
cacheMeta->len = keySize;
- cacheMeta->flags = flags;
+ /* clients can't set server-only flags (e.g. trusted KEK) */
+ cacheMeta->flags = flags & ~WH_NVM_FLAGS_SERVER_ONLY;
cacheMeta->access = WH_NVM_ACCESS_ANY;
if ((label != NULL) && (label_len > 0)) {
diff --git a/src/wh_server_keystore.c b/src/wh_server_keystore.c
index e7529ab3d..1b86d4dee 100644
--- a/src/wh_server_keystore.c
+++ b/src/wh_server_keystore.c
@@ -46,6 +46,7 @@
#ifdef WOLFHSM_CFG_SHE_EXTENSION
#include "wolfhsm/wh_server_she.h"
+#include "wolfhsm/wh_she_common.h" /* For wh_She_Label2Meta (counter guard) */
#endif
#include "wolfhsm/wh_server_keystore.h"
@@ -150,10 +151,26 @@ static int _KeystoreCheckPolicy(whServerContext* server, whKsOp op,
int foundInCache = 0;
int foundInNvm = 0;
- if ((server == NULL) || WH_KEYID_ISERASED(keyId)) {
+ /* Use WH_KEYID_IS_UNASSIGNED (not WH_KEYID_ISERASED) so SHE slot 0
+ * (WH_SHE_SECRET_KEY_ID, ID field == 0) is treated as an explicit key id
+ * rather than the dynamic-assignment sentinel. This keeps the policy gate
+ * consistent with the SHE-aware read path it guards
+ * (wh_Server_KeystoreReadKey) so a SHE slot-0 key can be
+ * wrap-exported/evicted/etc. */
+ if ((server == NULL) || WH_KEYID_IS_UNASSIGNED(keyId)) {
return WH_ERROR_BADARGS;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Hardware-only keys are not keystore-managed: no keystore operation is
+ * permitted on them. Must be checked before the existence lookup below,
+ * since these keys are never in cache or NVM and some callers (e.g.
+ * GetCacheSlotChecked) deliberately tolerate WH_ERROR_NOTFOUND */
+ if (WH_KEYID_ISHW(keyId)) {
+ return WH_ERROR_ACCESS;
+ }
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
/* Check cache first */
ret = _FindInCache(server, keyId, NULL, NULL, NULL, &cacheMeta);
if (ret == WH_ERROR_OK && cacheMeta != NULL) {
@@ -182,6 +199,16 @@ static int _KeystoreCheckPolicy(whServerContext* server, whKsOp op,
/* Get flags from the appropriate source */
flags = (foundInCache) ? cacheMeta->flags : nvmMeta.flags;
+ /* A trusted KEK is frozen against all client keystore ops: it can only be
+ * *used* as a KEK by the keywrap path, which freshens it via the unchecked
+ * cache-slot path and so bypasses this gate. Mirrors the WH_KEYID_ISHW gate
+ * above but flag-based, so the flag is self-protecting regardless of the
+ * key's other bits, and blocks a client re-caching over the KEK id to drop
+ * the flag. */
+ if (flags & WH_NVM_FLAGS_KEK) {
+ return WH_ERROR_ACCESS;
+ }
+
switch (op) {
case WH_KS_OP_CACHE:
if (flags & WH_NVM_FLAGS_NONMODIFIABLE) {
@@ -222,6 +249,16 @@ static int _KeystoreCheckPolicy(whServerContext* server, whKsOp op,
return WH_ERROR_OK;
}
+
+/* Clear flags a client may never set. Called at every point where
+ * client-supplied metadata becomes a whNvmMetadata, so the only way a key can
+ * carry a server-only flag is via trusted provisioning (whnvmtool image or
+ * server-internal boot code), never through the request handlers. */
+static void _SanitizeClientFlags(whNvmMetadata* meta)
+{
+ meta->flags &= ~WH_NVM_FLAGS_SERVER_ONLY;
+}
+
/**
* @brief Find a key in the specified cache context
*/
@@ -606,6 +643,13 @@ int wh_Server_KeystoreGetUniqueId(whServerContext* server, whNvmId* inout_id)
return WH_ERROR_BADARGS;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Hardware-only key ids are assigned by the hardware backend */
+ if (type == WH_KEYTYPE_HW) {
+ return WH_ERROR_BADARGS;
+ }
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
/* try every index until we find a unique one, don't worry about capacity */
for (id = WH_KEYID_IDMAX; id > WH_KEYID_ERASED; id--) {
/* id loop var is not an input client ID so we don't need to handle the
@@ -666,6 +710,14 @@ int wh_Server_KeystoreGetCacheSlot(whServerContext* server, whKeyId keyId,
return WH_ERROR_BADARGS;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Hardware-only keys must never occupy a cache slot. This core check is
+ * the sole protection for the DMA cache path, which allocates its slot
+ * here without going through _KeystoreCacheKey */
+ if (WH_KEYID_ISHW(keyId)) {
+ return WH_ERROR_ACCESS;
+ }
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
ret = _FindInCache(server, keyId, &idx, &isBig, &buf, &foundMeta);
if (ret == WH_ERROR_OK) {
@@ -706,12 +758,19 @@ static int _KeystoreCacheKey(whServerContext* server, whNvmMetadata* meta,
/* make sure id is valid */
if ((server == NULL) || (meta == NULL) || (in == NULL) ||
- WH_KEYID_ISERASED(meta->id) ||
+ WH_KEYID_IS_UNASSIGNED(meta->id) ||
((meta->len > WOLFHSM_CFG_SERVER_KEYCACHE_BUFSIZE) &&
(meta->len > WOLFHSM_CFG_SERVER_KEYCACHE_BIG_BUFSIZE))) {
return WH_ERROR_BADARGS;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Hardware-only keys must never enter the key cache */
+ if (WH_KEYID_ISHW(meta->id)) {
+ return WH_ERROR_ACCESS;
+ }
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
if (checked) {
ret = wh_Server_KeystoreGetCacheSlotChecked(server, meta->id, meta->len,
&slotBuf, &slotMeta);
@@ -791,10 +850,19 @@ int wh_Server_KeystoreFreshenKey(whServerContext* server, whKeyId keyId,
whNvmMetadata** cacheMetaOut;
whNvmMetadata tmpMeta[1];
- if ((server == NULL) || WH_KEYID_ISERASED(keyId)) {
+ if ((server == NULL) || WH_KEYID_IS_UNASSIGNED(keyId)) {
return WH_ERROR_BADARGS;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Hardware-only keys are never cached and are not usable through the
+ * keystore. The keywrap KEK path fetches them directly from the hardware
+ * keystore instead */
+ if (WH_KEYID_ISHW(keyId)) {
+ return WH_ERROR_ACCESS;
+ }
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
/* Use local buffers to allow for optional (NULL) output parameters */
cacheBufOut = (outBuf != NULL) ? outBuf : (uint8_t**)&cacheBufLocal;
cacheMetaOut = (outMeta != NULL) ? outMeta : &cacheMetaLocal;
@@ -853,12 +921,17 @@ int wh_Server_KeystoreReadKey(whServerContext* server, whKeyId keyId,
whNvmMetadata* cacheMeta = NULL;
uint8_t* cacheBuffer = NULL;
- if ((server == NULL) || (outSz == NULL) ||
- (WH_KEYID_ISERASED(keyId) &&
- (WH_KEYID_TYPE(keyId) != WH_KEYTYPE_SHE))) {
+ if ((server == NULL) || (outSz == NULL) || WH_KEYID_IS_UNASSIGNED(keyId)) {
return WH_ERROR_BADARGS;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Hardware-only key material must never be read out of the server */
+ if (WH_KEYID_ISHW(keyId)) {
+ return WH_ERROR_ACCESS;
+ }
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
/* Check the cache using unified function */
ret = _FindInCache(server, keyId, NULL, NULL, &cacheBuffer, &cacheMeta);
if (ret == WH_ERROR_OK) {
@@ -903,8 +976,15 @@ int wh_Server_KeystoreReadKey(whServerContext* server, whKeyId keyId,
if (out != NULL)
ret = wh_Nvm_Read(server->nvm, keyId, 0, *outSz, out);
}
- /* cache key if free slot, will only kick out other committed keys */
- if (ret == 0 && out != NULL) {
+ /* cache key if free slot, will only kick out other committed keys.
+ * Skip SHE SECRET_KEY (slot 0): _KeystoreCacheKey now accepts an id with a
+ * zero ID field for SHE keys (so SECRET_KEY can be primed via
+ * unwrap-and-cache on a NVM-less server), but auto-caching it here would
+ * block a later prime of the same slot (unwrap-and-cache rejects ids
+ * already in cache). Keep reading it straight from NVM each time. */
+ if (ret == 0 && out != NULL &&
+ !((WH_KEYID_TYPE(meta->id) == WH_KEYTYPE_SHE) &&
+ (WH_KEYID_ID(meta->id) == WH_KEYID_ERASED))) {
if (wh_Server_KeystoreCacheKey(server, meta, out) == WH_ERROR_OK) {
/* Cached key found in NVM. Mark it committed so it can be
evicted later. */
@@ -949,10 +1029,17 @@ int wh_Server_KeystoreEvictKey(whServerContext* server, whNvmId keyId)
int ret = 0;
whKeyCacheContext* ctx;
- if ((server == NULL) || WH_KEYID_ISERASED(keyId)) {
+ if ((server == NULL) || WH_KEYID_IS_UNASSIGNED(keyId)) {
return WH_ERROR_BADARGS;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Hardware-only keys are never cached, so there is nothing to evict */
+ if (WH_KEYID_ISHW(keyId)) {
+ return WH_ERROR_ACCESS;
+ }
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
/* Get the appropriate cache context for this key */
ctx = _GetCacheContext(server, keyId);
@@ -986,7 +1073,7 @@ int wh_Server_KeystoreCommitKey(whServerContext* server, whNvmId keyId)
int ret;
whKeyCacheContext* ctx;
- if ((server == NULL) || WH_KEYID_ISERASED(keyId)) {
+ if ((server == NULL) || WH_KEYID_IS_UNASSIGNED(keyId)) {
return WH_ERROR_BADARGS;
}
@@ -994,6 +1081,13 @@ int wh_Server_KeystoreCommitKey(whServerContext* server, whNvmId keyId)
return WH_ERROR_ABORTED;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Hardware-only keys must never be persisted to NVM */
+ if (WH_KEYID_ISHW(keyId)) {
+ return WH_ERROR_ACCESS;
+ }
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
/* Get the appropriate cache context for this key */
ctx = _GetCacheContext(server, keyId);
@@ -1025,7 +1119,7 @@ int wh_Server_KeystoreCommitKeyChecked(whServerContext* server, whNvmId keyId)
int wh_Server_KeystoreEraseKey(whServerContext* server, whNvmId keyId)
{
- if ((server == NULL) || (WH_KEYID_ISERASED(keyId))) {
+ if ((server == NULL) || (WH_KEYID_IS_UNASSIGNED(keyId))) {
return WH_ERROR_BADARGS;
}
@@ -1033,6 +1127,13 @@ int wh_Server_KeystoreEraseKey(whServerContext* server, whNvmId keyId)
return WH_ERROR_ABORTED;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Hardware-only keys are not keystore-managed and cannot be erased */
+ if (WH_KEYID_ISHW(keyId)) {
+ return WH_ERROR_ACCESS;
+ }
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
/* remove the key from the cache if present */
(void)wh_Server_KeystoreEvictKey(server, keyId);
@@ -1050,7 +1151,7 @@ int wh_Server_KeystoreEraseKeyChecked(whServerContext* server, whNvmId keyId)
{
int ret;
- if ((server == NULL) || (WH_KEYID_ISERASED(keyId))) {
+ if ((server == NULL) || (WH_KEYID_IS_UNASSIGNED(keyId))) {
return WH_ERROR_BADARGS;
}
@@ -1058,6 +1159,13 @@ int wh_Server_KeystoreEraseKeyChecked(whServerContext* server, whNvmId keyId)
return WH_ERROR_ABORTED;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ /* Hardware-only keys are not keystore-managed and cannot be erased */
+ if (WH_KEYID_ISHW(keyId)) {
+ return WH_ERROR_ACCESS;
+ }
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
/* remove the key from the cache if present, enforcing policy */
ret = wh_Server_KeystoreEvictKeyChecked(server, keyId);
@@ -1096,7 +1204,7 @@ int wh_Server_KeystoreRevokeKey(whServerContext* server, whNvmId keyId)
uint8_t* cacheBuf = NULL;
whNvmMetadata* cacheMeta = NULL;
- if ((server == NULL) || WH_KEYID_ISERASED(keyId)) {
+ if ((server == NULL) || WH_KEYID_IS_UNASSIGNED(keyId)) {
return WH_ERROR_BADARGS;
}
@@ -1146,18 +1254,93 @@ int wh_Server_KeystoreRevokeKey(whServerContext* server, whNvmId keyId)
#ifndef NO_AES
#ifdef HAVE_AESGCM
-static int _AesGcmKeyWrap(whServerContext* server, whKeyId serverKeyId,
- uint8_t* keyIn, uint16_t keySz,
- whNvmMetadata* metadataIn, uint8_t* wrappedKeyOut,
- uint16_t wrappedKeySz)
+/* Resolve the KEK for a keywrap operation. Hardware-only KEKs (TYPE=HW)
+ * are fetched from the server's hardware keystore into hwKekBuf, which the
+ * caller must keep local and zeroize after use; they carry no NVM metadata,
+ * so usage policy is delegated to the hardware keystore backend. All other
+ * KEKs are freshened into the key cache and, when enforceUsage is nonzero,
+ * must carry WH_NVM_FLAGS_USAGE_WRAP.
+ *
+ * When enforceTrustedKek is nonzero the KEK must be one the client cannot know
+ * or set: a hardware key (returned above) or a software key carrying
+ * WH_NVM_FLAGS_KEK. This is the unified eligibility predicate
+ * isTrustedKek = WH_KEYID_ISHW(id) || (flags & WH_NVM_FLAGS_KEK), required by
+ * the ops that move a server secret across the client boundary (KeyWrapExport,
+ * KeyUnwrapAndCache). */
+static int _KeywrapResolveKek(whServerContext* server, whKeyId serverKeyId,
+ int enforceUsage, int enforceTrustedKek,
+ uint8_t* hwKekBuf, uint16_t hwKekBufSz,
+ const uint8_t** outKek, uint32_t* outKekSz)
+{
+ int ret;
+ whNvmMetadata* kekMeta = NULL;
+ uint8_t* kek = NULL;
+
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ if (WH_KEYID_ISHW(serverKeyId)) {
+ uint16_t hwKekSz = hwKekBufSz;
+ if (server->hwKeystore == NULL) {
+ /* No hardware keystore bound to this server */
+ return WH_ERROR_NOTFOUND;
+ }
+ ret = wh_HwKeystore_GetKey(server->hwKeystore, serverKeyId, hwKekBuf,
+ &hwKekSz);
+ if (ret == WH_ERROR_OK) {
+ *outKek = hwKekBuf;
+ *outKekSz = hwKekSz;
+ }
+ return ret;
+ }
+#else
+ (void)hwKekBuf;
+ (void)hwKekBufSz;
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
+ ret = wh_Server_KeystoreFreshenKey(server, serverKeyId, &kek, &kekMeta);
+ if (ret != WH_ERROR_OK) {
+ return ret;
+ }
+
+ if (enforceUsage) {
+ /* Validate key usage policy for wrapping (KEK) */
+ ret =
+ wh_Server_KeystoreEnforceKeyUsage(kekMeta, WH_NVM_FLAGS_USAGE_WRAP);
+ if (ret != WH_ERROR_OK) {
+ return ret;
+ }
+ }
+
+ /* A hardware KEK already returned above and is inherently trusted; a
+ * software KEK qualifies only if it was provisioned with WH_NVM_FLAGS_KEK
+ * (which a client can never set). */
+ if (enforceTrustedKek && !(kekMeta->flags & WH_NVM_FLAGS_KEK)) {
+ return WH_ERROR_ACCESS;
+ }
+
+ *outKek = kek;
+ *outKekSz = kekMeta->len;
+ return WH_ERROR_OK;
+}
+
+/* Size of the local storage each keywrap helper provides for a hardware-only
+ * KEK. Minimal when no hardware keystore is configured, since the resolver
+ * then never writes to it */
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+#define WH_KEYWRAP_HWKEK_BUF_SIZE WOLFHSM_CFG_HWKEYSTORE_MAX_KEY_SIZE
+#else
+#define WH_KEYWRAP_HWKEK_BUF_SIZE 1
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
+static int _AesGcmKeyWrapWithKek(whServerContext* server,
+ const uint8_t* serverKey, uint32_t serverKeySz,
+ uint8_t* keyIn, uint16_t keySz,
+ whNvmMetadata* metadataIn,
+ uint8_t* wrappedKeyOut, uint16_t wrappedKeySz)
{
int ret = 0;
Aes aes[1];
uint8_t authTag[WH_KEYWRAP_AES_GCM_TAG_SIZE];
uint8_t iv[WH_KEYWRAP_AES_GCM_IV_SIZE];
- uint8_t* serverKey;
- uint32_t serverKeySz;
- whNvmMetadata* serverKeyMetadata;
uint8_t plainBlob[sizeof(*metadataIn) + WOLFHSM_CFG_KEYWRAP_MAX_KEY_SIZE];
uint32_t plainBlobSz = sizeof(*metadataIn) + keySz;
uint8_t* encBlob;
@@ -1173,21 +1356,6 @@ static int _AesGcmKeyWrap(whServerContext* server, whKeyId serverKeyId,
return WH_ERROR_BUFFER_SIZE;
}
- /* Get the server side key */
- ret = wh_Server_KeystoreFreshenKey(server, serverKeyId,
- &serverKey, &serverKeyMetadata);
- if (ret != WH_ERROR_OK) {
- return ret;
- }
- serverKeySz = serverKeyMetadata->len;
-
- /* Validate key usage policy for wrapping (KEK) */
- ret = wh_Server_KeystoreEnforceKeyUsage(serverKeyMetadata,
- WH_NVM_FLAGS_USAGE_WRAP);
- if (ret != WH_ERROR_OK) {
- return ret;
- }
-
/* Initialize AES context and set it to use the server side key */
ret = wc_AesInit(aes, NULL, server->devId);
if (ret != 0) {
@@ -1217,32 +1385,60 @@ static int _AesGcmKeyWrap(whServerContext* server, whKeyId serverKeyId,
/* Encrypt the blob */
ret = wc_AesGcmEncrypt(aes, encBlob, plainBlob, plainBlobSz, iv,
sizeof(iv), authTag, sizeof(authTag), NULL, 0);
- if (ret != 0) {
- wc_AesFree(aes);
- return ret;
+ if (ret == 0) {
+ /* Prepend IV + authTag to encrypted blob */
+ memcpy(wrappedKeyOut, iv, sizeof(iv));
+ memcpy(wrappedKeyOut + sizeof(iv), authTag, sizeof(authTag));
}
- /* Prepend IV + authTag to encrypted blob */
- memcpy(wrappedKeyOut, iv, sizeof(iv));
- memcpy(wrappedKeyOut + sizeof(iv), authTag, sizeof(authTag));
-
wc_AesFree(aes);
- return WH_ERROR_OK;
+ /* plainBlob held the cleartext metadata+key; wipe the stack copy */
+ wh_Utils_ForceZero(plainBlob, sizeof(plainBlob));
+
+ return (ret == 0) ? WH_ERROR_OK : ret;
}
-static int _AesGcmKeyUnwrap(whServerContext* server, uint16_t serverKeyId,
- void* wrappedKeyIn, uint16_t wrappedKeySz,
- whNvmMetadata* metadataOut, void* keyOut,
- uint16_t keySz)
+static int _AesGcmKeyWrap(whServerContext* server, whKeyId serverKeyId,
+ int requireTrustedKek, uint8_t* keyIn, uint16_t keySz,
+ whNvmMetadata* metadataIn, uint8_t* wrappedKeyOut,
+ uint16_t wrappedKeySz)
+{
+ int ret;
+ const uint8_t* serverKey = NULL;
+ uint32_t serverKeySz = 0;
+ uint8_t hwKek[WH_KEYWRAP_HWKEK_BUF_SIZE];
+
+ if (server == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Get the server side key (KEK) */
+ ret = _KeywrapResolveKek(server, serverKeyId, 1, requireTrustedKek, hwKek,
+ (uint16_t)sizeof(hwKek), &serverKey, &serverKeySz);
+ if (ret == WH_ERROR_OK) {
+ ret =
+ _AesGcmKeyWrapWithKek(server, serverKey, serverKeySz, keyIn, keySz,
+ metadataIn, wrappedKeyOut, wrappedKeySz);
+ }
+
+ /* Wipe any hardware KEK material from local storage */
+ wh_Utils_ForceZero(hwKek, sizeof(hwKek));
+
+ return ret;
+}
+
+static int _AesGcmKeyUnwrapWithKek(whServerContext* server,
+ const uint8_t* serverKey,
+ uint32_t serverKeySz, void* wrappedKeyIn,
+ uint16_t wrappedKeySz,
+ whNvmMetadata* metadataOut, void* keyOut,
+ uint16_t keySz)
{
int ret = 0;
Aes aes[1];
uint8_t authTag[WH_KEYWRAP_AES_GCM_TAG_SIZE];
uint8_t iv[WH_KEYWRAP_AES_GCM_IV_SIZE];
- uint8_t* serverKey;
- uint32_t serverKeySz;
- whNvmMetadata* serverKeyMetadata;
uint8_t* encBlob;
uint16_t encBlobSz;
uint8_t plainBlob[sizeof(*metadataOut) + WOLFHSM_CFG_KEYWRAP_MAX_KEY_SIZE];
@@ -1259,22 +1455,6 @@ static int _AesGcmKeyUnwrap(whServerContext* server, uint16_t serverKeyId,
encBlob = (uint8_t*)wrappedKeyIn + sizeof(iv) + sizeof(authTag);
encBlobSz = wrappedKeySz - sizeof(iv) - sizeof(authTag);
-
- /* Get the server side key */
- ret = wh_Server_KeystoreFreshenKey(server, serverKeyId,
- &serverKey, &serverKeyMetadata);
- if (ret != WH_ERROR_OK) {
- return ret;
- }
- serverKeySz = serverKeyMetadata->len;
-
- /* Validate key usage policy for unwrapping (KEK) */
- ret = wh_Server_KeystoreEnforceKeyUsage(serverKeyMetadata,
- WH_NVM_FLAGS_USAGE_WRAP);
- if (ret != WH_ERROR_OK) {
- return ret;
- }
-
/* Initialize AES context and set it to use the server side key */
ret = wc_AesInit(aes, NULL, server->devId);
if (ret != 0) {
@@ -1294,30 +1474,59 @@ static int _AesGcmKeyUnwrap(whServerContext* server, uint16_t serverKeyId,
/* Decrypt the encrypted blob */
ret = wc_AesGcmDecrypt(aes, plainBlob, encBlob, encBlobSz, iv, sizeof(iv),
authTag, sizeof(authTag), NULL, 0);
- if (ret != 0) {
- wc_AesFree(aes);
- return ret;
+ if (ret == 0) {
+ /* Extract metadata and key from the decrypted blob */
+ memcpy(metadataOut, plainBlob, sizeof(*metadataOut));
+ memcpy(keyOut, plainBlob + sizeof(*metadataOut), keySz);
}
- /* Extract metadata and key from the decrypted blob */
- memcpy(metadataOut, plainBlob, sizeof(*metadataOut));
- memcpy(keyOut, plainBlob + sizeof(*metadataOut), keySz);
-
wc_AesFree(aes);
- return WH_ERROR_OK;
+
+ /* plainBlob held the decrypted metadata+key; wipe the stack copy */
+ wh_Utils_ForceZero(plainBlob, sizeof(plainBlob));
+
+ return ret;
}
-static int _AesGcmDataWrap(whServerContext* server, whKeyId serverKeyId,
- uint8_t* dataIn, uint16_t dataSz,
- uint8_t* wrappedDataOut, uint16_t wrappedDataSz)
+static int _AesGcmKeyUnwrap(whServerContext* server, uint16_t serverKeyId,
+ int requireTrustedKek, void* wrappedKeyIn,
+ uint16_t wrappedKeySz, whNvmMetadata* metadataOut,
+ void* keyOut, uint16_t keySz)
+{
+ int ret;
+ const uint8_t* serverKey = NULL;
+ uint32_t serverKeySz = 0;
+ uint8_t hwKek[WH_KEYWRAP_HWKEK_BUF_SIZE];
+
+ if (server == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Get the server side key (KEK) */
+ ret = _KeywrapResolveKek(server, serverKeyId, 1, requireTrustedKek, hwKek,
+ (uint16_t)sizeof(hwKek), &serverKey, &serverKeySz);
+ if (ret == WH_ERROR_OK) {
+ ret = _AesGcmKeyUnwrapWithKek(server, serverKey, serverKeySz,
+ wrappedKeyIn, wrappedKeySz, metadataOut,
+ keyOut, keySz);
+ }
+
+ /* Wipe any hardware KEK material from local storage */
+ wh_Utils_ForceZero(hwKek, sizeof(hwKek));
+
+ return ret;
+}
+
+static int _AesGcmDataWrapWithKek(whServerContext* server,
+ const uint8_t* serverKey,
+ uint32_t serverKeySz, uint8_t* dataIn,
+ uint16_t dataSz, uint8_t* wrappedDataOut,
+ uint16_t wrappedDataSz)
{
int ret = 0;
Aes aes[1];
uint8_t authTag[WH_KEYWRAP_AES_GCM_TAG_SIZE];
uint8_t iv[WH_KEYWRAP_AES_GCM_IV_SIZE];
- uint8_t* serverKey;
- uint32_t serverKeySz;
- whNvmMetadata* serverKeyMetadata;
uint8_t* encBlob;
if (server == NULL || dataIn == NULL || wrappedDataOut == NULL) {
@@ -1329,14 +1538,6 @@ static int _AesGcmDataWrap(whServerContext* server, whKeyId serverKeyId,
return WH_ERROR_BUFFER_SIZE;
}
- /* Get the server side key */
- ret = wh_Server_KeystoreFreshenKey(server, serverKeyId,
- &serverKey, &serverKeyMetadata);
- if (ret != WH_ERROR_OK) {
- return ret;
- }
- serverKeySz = serverKeyMetadata->len;
-
/* Initialize AES context and set it to use the server side key */
ret = wc_AesInit(aes, NULL, server->devId);
if (ret != 0) {
@@ -1376,17 +1577,44 @@ static int _AesGcmDataWrap(whServerContext* server, whKeyId serverKeyId,
return WH_ERROR_OK;
}
-static int _AesGcmDataUnwrap(whServerContext* server, uint16_t serverKeyId,
- void* wrappedDataIn, uint16_t wrappedDataSz,
- void* dataOut, uint16_t dataSz)
+static int _AesGcmDataWrap(whServerContext* server, whKeyId serverKeyId,
+ uint8_t* dataIn, uint16_t dataSz,
+ uint8_t* wrappedDataOut, uint16_t wrappedDataSz)
+{
+ int ret;
+ const uint8_t* serverKey = NULL;
+ uint32_t serverKeySz = 0;
+ uint8_t hwKek[WH_KEYWRAP_HWKEK_BUF_SIZE];
+
+ if (server == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Get the server side key (KEK). Data wrap does not enforce usage flags
+ * on the KEK (existing behavior) */
+ ret = _KeywrapResolveKek(server, serverKeyId, 0, 0, hwKek,
+ (uint16_t)sizeof(hwKek), &serverKey, &serverKeySz);
+ if (ret == WH_ERROR_OK) {
+ ret = _AesGcmDataWrapWithKek(server, serverKey, serverKeySz, dataIn,
+ dataSz, wrappedDataOut, wrappedDataSz);
+ }
+
+ /* Wipe any hardware KEK material from local storage */
+ wh_Utils_ForceZero(hwKek, sizeof(hwKek));
+
+ return ret;
+}
+
+static int _AesGcmDataUnwrapWithKek(whServerContext* server,
+ const uint8_t* serverKey,
+ uint32_t serverKeySz, void* wrappedDataIn,
+ uint16_t wrappedDataSz, void* dataOut,
+ uint16_t dataSz)
{
int ret = 0;
Aes aes[1];
uint8_t authTag[WH_KEYWRAP_AES_GCM_TAG_SIZE];
uint8_t iv[WH_KEYWRAP_AES_GCM_IV_SIZE];
- uint8_t* serverKey;
- uint32_t serverKeySz;
- whNvmMetadata* serverKeyMetadata;
uint8_t* encBlob;
uint16_t encBlobSz;
@@ -1402,14 +1630,6 @@ static int _AesGcmDataUnwrap(whServerContext* server, uint16_t serverKeyId,
encBlob = (uint8_t*)wrappedDataIn + sizeof(iv) + sizeof(authTag);
encBlobSz = wrappedDataSz - sizeof(iv) - sizeof(authTag);
- /* Get the server side key */
- ret = wh_Server_KeystoreFreshenKey(server, serverKeyId,
- &serverKey, &serverKeyMetadata);
- if (ret != WH_ERROR_OK) {
- return ret;
- }
- serverKeySz = serverKeyMetadata->len;
-
/* Initialize AES context and set it to use the server side key */
ret = wc_AesInit(aes, NULL, server->devId);
if (ret != 0) {
@@ -1438,6 +1658,35 @@ static int _AesGcmDataUnwrap(whServerContext* server, uint16_t serverKeyId,
return WH_ERROR_OK;
}
+static int _AesGcmDataUnwrap(whServerContext* server, uint16_t serverKeyId,
+ void* wrappedDataIn, uint16_t wrappedDataSz,
+ void* dataOut, uint16_t dataSz)
+{
+ int ret;
+ const uint8_t* serverKey = NULL;
+ uint32_t serverKeySz = 0;
+ uint8_t hwKek[WH_KEYWRAP_HWKEK_BUF_SIZE];
+
+ if (server == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Get the server side key (KEK). Data unwrap does not enforce usage
+ * flags on the KEK (existing behavior) */
+ ret = _KeywrapResolveKek(server, serverKeyId, 0, 0, hwKek,
+ (uint16_t)sizeof(hwKek), &serverKey, &serverKeySz);
+ if (ret == WH_ERROR_OK) {
+ ret = _AesGcmDataUnwrapWithKek(server, serverKey, serverKeySz,
+ wrappedDataIn, wrappedDataSz, dataOut,
+ dataSz);
+ }
+
+ /* Wipe any hardware KEK material from local storage */
+ wh_Utils_ForceZero(hwKek, sizeof(hwKek));
+
+ return ret;
+}
+
#endif /* HAVE_AESGCM */
#endif /* !NO_AES */
@@ -1503,9 +1752,11 @@ static int _HandleKeyWrapRequest(whServerContext* server,
return WH_ERROR_BUFFER_SIZE;
}
- /* Wrap the key */
- ret = _AesGcmKeyWrap(server, serverKeyId, key, req->keySz,
- &metadata, wrappedKey, wrappedKeySz);
+ /* Wrap the key. The client supplies the plaintext, so no server
+ * secret crosses the boundary; the KEK may be any client key. */
+ ret = _AesGcmKeyWrap(server, serverKeyId, /*requireTrustedKek=*/0,
+ key, req->keySz, &metadata, wrappedKey,
+ wrappedKeySz);
if (ret != WH_ERROR_OK) {
return ret;
}
@@ -1524,6 +1775,128 @@ static int _HandleKeyWrapRequest(whServerContext* server,
return WH_ERROR_OK;
}
+/* Wrap a key the server already holds (identified by id) and return the wrapped
+ * blob. The client presents only an id; never plaintext. The blob carries the
+ * key's real metadata so it round-trips through unwrap-and-cache. */
+static int
+_HandleKeyWrapExportRequest(whServerContext* server,
+ whMessageKeystore_KeyWrapExportRequest* req,
+ uint8_t* reqData, uint32_t reqDataSz,
+ whMessageKeystore_KeyWrapExportResponse* resp,
+ uint8_t* respData, uint32_t respDataSz)
+{
+ int ret;
+ uint8_t* wrappedKey;
+ whNvmMetadata metadata = {0};
+ uint8_t key[WOLFHSM_CFG_KEYWRAP_MAX_KEY_SIZE];
+ uint32_t keySz = sizeof(key);
+ whKeyId targetKeyId;
+ whKeyId serverKeyId;
+ uint16_t targetKeyType;
+
+ /* reqData/reqDataSz are unused: the key to wrap already lives in the
+ * keystore, so there is no inline key payload in the request. */
+ (void)reqData;
+ (void)reqDataSz;
+
+ if (server == NULL || req == NULL || resp == NULL || respData == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Ensure the cipher type in the response matches the request */
+ resp->cipherType = req->cipherType;
+ /* Wrapped key size is only passed back to the client on success */
+ resp->wrappedKeySz = 0;
+
+ /* Translate the client-supplied ids. The KEK is always a crypto key; the
+ * target key type comes from req->keyType because there is no client flag
+ * for SHE keys. */
+ targetKeyId = wh_KeyId_TranslateFromClient(
+ req->keyType, server->comm->client_id, req->keyId);
+ serverKeyId = wh_KeyId_TranslateFromClient(
+ WH_KEYTYPE_CRYPTO, server->comm->client_id, req->serverKeyId);
+
+ /* Validate the *translated* target key type against the allow-list. Using
+ * the translated type also closes the gap where a client sets the WRAPPED
+ * flag in keyId to override req->keyType. */
+ targetKeyType = WH_KEYID_TYPE(targetKeyId);
+ switch (targetKeyType) {
+ case WH_KEYTYPE_CRYPTO:
+ case WH_KEYTYPE_WRAPPED:
+#ifdef WOLFHSM_CFG_SHE_EXTENSION
+ case WH_KEYTYPE_SHE:
+#endif
+ break;
+ default:
+ return WH_ERROR_BADARGS;
+ }
+
+ /* Read the key and its real metadata, enforcing export policy
+ * (NONEXPORTABLE). For wrapped keys this is a cache-only probe. */
+ ret = wh_Server_KeystoreReadKeyChecked(server, targetKeyId, &metadata, key,
+ &keySz);
+ if (ret != WH_ERROR_OK) {
+ return ret;
+ }
+
+ /* Normalize non-SHE keys into the WRAPPED namespace so the blob round-trips
+ * through unwrap-and-cache without colliding with server-managed keyIds.
+ * SHE keys must keep TYPE=SHE so the SHE API can find them after caching.
+ * USER is preserved for the unwrap-side ownership check. */
+ if (WH_KEYID_TYPE(metadata.id) != WH_KEYTYPE_SHE) {
+ metadata.id =
+ WH_MAKE_KEYID(WH_KEYTYPE_WRAPPED, WH_KEYID_USER(metadata.id),
+ WH_KEYID_ID(metadata.id));
+ }
+
+ /* Store the wrapped key in the response data */
+ wrappedKey = respData;
+
+ switch (req->cipherType) {
+
+#ifndef NO_AES
+#ifdef HAVE_AESGCM
+ case WC_CIPHER_AES_GCM: {
+ uint16_t wrappedKeySz = WH_KEYWRAP_AES_GCM_HEADER_SIZE +
+ sizeof(metadata) + (uint16_t)keySz;
+
+ /* Check if the response data can fit the wrapped key */
+ if (respDataSz < wrappedKeySz) {
+ ret = WH_ERROR_BUFFER_SIZE;
+ goto out;
+ }
+
+ /* Wrap the key with its real metadata. This extracts a server-held
+ * secret to the client, so the KEK must be a trusted (HW or
+ * WH_NVM_FLAGS_KEK) key the client cannot know. */
+ ret = _AesGcmKeyWrap(server, serverKeyId, /*requireTrustedKek=*/1,
+ key, (uint16_t)keySz, &metadata, wrappedKey,
+ wrappedKeySz);
+ if (ret != WH_ERROR_OK) {
+ goto out;
+ }
+
+ /* Tell the client how big the wrapped key is */
+ resp->wrappedKeySz = wrappedKeySz;
+
+ } break;
+#endif /* HAVE_AESGCM */
+#endif /* !NO_AES */
+
+ default:
+ ret = WH_ERROR_BADARGS;
+ goto out;
+ }
+
+ ret = WH_ERROR_OK;
+
+out:
+ /* key[] held a cleartext server secret; wipe the stack copy on every exit
+ */
+ wh_Utils_ForceZero(key, sizeof(key));
+ return ret;
+}
+
static int _HandleKeyUnwrapAndExportRequest(
whServerContext* server, whMessageKeystore_KeyUnwrapAndExportRequest* req,
uint8_t* reqData, uint32_t reqDataSz,
@@ -1583,15 +1956,17 @@ static int _HandleKeyUnwrapAndExportRequest(
return WH_ERROR_BUFFER_SIZE;
}
- /* Unwrap the key */
- ret = _AesGcmKeyUnwrap(server, serverKeyId, wrappedKey,
+ /* Unwrap the key. The plaintext is handed back to the client, not
+ * injected into the server, so the KEK may be any client key. */
+ ret = _AesGcmKeyUnwrap(server, serverKeyId,
+ /*requireTrustedKek=*/0, wrappedKey,
req->wrappedKeySz, metadata, key, keySz);
if (ret != WH_ERROR_OK) {
return ret;
}
/* Dynamic keyId generation for wrapped keys is not allowed */
- if (WH_KEYID_ISERASED(metadata->id)) {
+ if (WH_KEYID_IS_UNASSIGNED(metadata->id)) {
/* Wrapped keys must use explicit identifiers */
return WH_ERROR_BADARGS;
}
@@ -1660,6 +2035,8 @@ static int _HandleKeyUnwrapAndCacheRequest(
uint16_t keySz = 0;
uint8_t key[WOLFHSM_CFG_KEYWRAP_MAX_KEY_SIZE];
whKeyId serverKeyId;
+ uint16_t wrappedKeyUser;
+ uint16_t wrappedKeyType;
/* Check if the reqData is big enough to hold the wrapped key */
if (reqDataSz < req->wrappedKeySz) {
@@ -1693,7 +2070,11 @@ static int _HandleKeyUnwrapAndCacheRequest(
sizeof(metadata);
resp->cipherType = WC_CIPHER_AES_GCM;
- ret = _AesGcmKeyUnwrap(server, serverKeyId, wrappedKey,
+ /* Unwrap-and-cache injects a key into the server keystore, so the
+ * KEK must be a trusted (HW or WH_NVM_FLAGS_KEK) key, else a
+ * client could forge a blob under a KEK it knows. */
+ ret = _AesGcmKeyUnwrap(server, serverKeyId,
+ /*requireTrustedKek=*/1, wrappedKey,
req->wrappedKeySz, &metadata, key, keySz);
if (ret != WH_ERROR_OK) {
return ret;
@@ -1707,24 +2088,40 @@ static int _HandleKeyUnwrapAndCacheRequest(
return WH_ERROR_BADARGS;
}
+ /* Strip server-only flags decoded from the blob. A legitimate blob never
+ * carries WH_NVM_FLAGS_KEK (a KEK is rejected as a wrap-export target, so
+ * one is never produced), and a forged blob must not be able to mint a KEK,
+ * so dropping it here is always safe. */
+ _SanitizeClientFlags(&metadata);
+
/* Verify the key size argument and key size from the the metadata match */
if (keySz != metadata.len) {
- return WH_ERROR_BADARGS;
+ ret = WH_ERROR_BADARGS;
+ goto out;
}
- /* Dynamic keyId generation for wrapped keys is not allowed */
- if (WH_KEYID_ISERASED(metadata.id)) {
- /* Wrapped keys must use explicit identifiers */
- return WH_ERROR_BADARGS;
+ /* Dynamic keyId generation for wrapped keys is not allowed; they must use
+ * explicit identifiers. SHE keys are exempt - their ids are fixed slots
+ * (slot 0 == SECRET_KEY is a valid explicit id), so they can be primed via
+ * unwrap-and-cache on a NVM-less server. */
+ if (WH_KEYID_IS_UNASSIGNED(metadata.id)) {
+ ret = WH_ERROR_BADARGS;
+ goto out;
}
/* Extract ownership from unwrapped metadata (preserves original owner) */
- uint16_t wrappedKeyUser = WH_KEYID_USER(metadata.id);
- uint16_t wrappedKeyType = WH_KEYID_TYPE(metadata.id);
+ wrappedKeyUser = WH_KEYID_USER(metadata.id);
+ wrappedKeyType = WH_KEYID_TYPE(metadata.id);
- /* Require explicit wrapped-key encoding */
- if (wrappedKeyType != WH_KEYTYPE_WRAPPED) {
- return WH_ERROR_ABORTED;
+ /* Require explicit wrapped-key encoding. SHE keys are also permitted so a
+ * SHE key blob can be primed into the cache and used via the SHE API. */
+ if (wrappedKeyType != WH_KEYTYPE_WRAPPED
+#ifdef WOLFHSM_CFG_SHE_EXTENSION
+ && wrappedKeyType != WH_KEYTYPE_SHE
+#endif
+ ) {
+ ret = WH_ERROR_ABORTED;
+ goto out;
}
/* Validate ownership: USER field must match requesting client.
@@ -1734,25 +2131,60 @@ static int _HandleKeyUnwrapAndCacheRequest(
* Local keys (USER!=0): only owning client can unwrap and cache */
if (wrappedKeyUser != WH_KEYUSER_GLOBAL &&
wrappedKeyUser != server->comm->client_id) {
- return WH_ERROR_ACCESS;
+ ret = WH_ERROR_ACCESS;
+ goto out;
}
#else
/* Without global keys, USER must match requesting client */
if (wrappedKeyUser != server->comm->client_id) {
- return WH_ERROR_ACCESS;
+ ret = WH_ERROR_ACCESS;
+ goto out;
}
#endif /* WOLFHSM_CFG_GLOBAL_KEYS */
/* Ensure a key with the unwrapped ID does not already exist in cache */
if (_ExistsInCache(server, metadata.id)) {
- return WH_ERROR_ABORTED;
+ ret = WH_ERROR_ABORTED;
+ goto out;
+ }
+
+#ifdef WOLFHSM_CFG_SHE_EXTENSION
+ /* For SHE keys, enforce counter monotonicity (allow-equal) against any
+ * committed key in NVM, so a primed blob cannot roll a slot's counter back
+ * and shadow the committed key. The slot is known not to be in cache here
+ * (checked above), so this consults NVM. A first prime after a cold boot
+ * (no stored key) establishes the baseline. With no NVM there is no
+ * committed counter to roll back against, so the guard is skipped and the
+ * cached blob establishes the baseline. */
+ if (wrappedKeyType == WH_KEYTYPE_SHE && server->nvm != NULL) {
+ whNvmMetadata storedMeta;
+ ret = wh_Nvm_GetMetadata(server->nvm, metadata.id, &storedMeta);
+ if (ret == WH_ERROR_OK) {
+ uint32_t blobCount = 0;
+ uint32_t storedCount = 0;
+ (void)wh_She_Label2Meta(metadata.label, &blobCount, NULL);
+ (void)wh_She_Label2Meta(storedMeta.label, &storedCount, NULL);
+ if (blobCount < storedCount) {
+ ret = WH_ERROR_ACCESS;
+ goto out;
+ }
+ }
+ else if (ret != WH_ERROR_NOTFOUND) {
+ goto out;
+ }
}
+#endif /* WOLFHSM_CFG_SHE_EXTENSION */
/* Store the assigned key ID in the response, preserving client flags */
resp->keyId = wh_KeyId_TranslateToClient(metadata.id);
/* Cache the key */
- return wh_Server_KeystoreCacheKey(server, &metadata, key);
+ ret = wh_Server_KeystoreCacheKey(server, &metadata, key);
+
+out:
+ /* key[] held decrypted key material; wipe the stack copy on every exit */
+ wh_Utils_ForceZero(key, sizeof(key));
+ return ret;
}
static int _HandleDataWrapRequest(whServerContext* server,
@@ -1952,7 +2384,9 @@ int wh_Server_HandleKeyRequest(whServerContext* server, uint16_t magic,
WH_KEYTYPE_CRYPTO, server->comm->client_id, req.id);
meta->access = WH_NVM_ACCESS_ANY;
meta->flags = req.flags;
- meta->len = req.sz;
+ /* clients can't set server-only flags */
+ _SanitizeClientFlags(meta);
+ meta->len = req.sz;
/* truncate label if it's too large */
if (req.labelSz > WH_NVM_LABEL_LEN) {
req.labelSz = WH_NVM_LABEL_LEN;
@@ -1964,7 +2398,7 @@ int wh_Server_HandleKeyRequest(whServerContext* server, uint16_t magic,
ret = WH_SERVER_NVM_LOCK(server);
if (ret == WH_ERROR_OK) {
/* get a new id if one wasn't provided */
- if (WH_KEYID_ISERASED(meta->id)) {
+ if (WH_KEYID_IS_UNASSIGNED(meta->id)) {
ret =
wh_Server_KeystoreGetUniqueId(server, &meta->id);
}
@@ -2011,7 +2445,9 @@ int wh_Server_HandleKeyRequest(whServerContext* server, uint16_t magic,
WH_KEYTYPE_CRYPTO, server->comm->client_id, req.id);
meta->access = WH_NVM_ACCESS_ANY;
meta->flags = req.flags;
- meta->len = req.key.sz;
+ /* clients can't set server-only flags */
+ _SanitizeClientFlags(meta);
+ meta->len = req.key.sz;
/* truncate label if it's too large */
if (req.labelSz > WH_NVM_LABEL_LEN) {
req.labelSz = WH_NVM_LABEL_LEN;
@@ -2023,7 +2459,7 @@ int wh_Server_HandleKeyRequest(whServerContext* server, uint16_t magic,
ret = WH_SERVER_NVM_LOCK(server);
if (ret == WH_ERROR_OK) {
/* get a new id if one wasn't provided */
- if (WH_KEYID_ISERASED(meta->id)) {
+ if (WH_KEYID_IS_UNASSIGNED(meta->id)) {
ret =
wh_Server_KeystoreGetUniqueId(server, &meta->id);
}
@@ -2568,6 +3004,54 @@ int wh_Server_HandleKeyRequest(whServerContext* server, uint16_t magic,
} break;
+ case WH_KEY_KEYWRAPEXPORT: {
+ whMessageKeystore_KeyWrapExportRequest wrapReq = {0};
+ whMessageKeystore_KeyWrapExportResponse wrapResp = {0};
+ uint8_t* reqData;
+ uint8_t* respData;
+ uint32_t respDataSz = WOLFHSM_CFG_COMM_DATA_LEN - sizeof(wrapResp);
+ uint32_t reqDataSz;
+
+ /* Validate req_size can hold the fixed request struct */
+ if (req_size < sizeof(wrapReq)) {
+ ret = WH_ERROR_BADARGS;
+ }
+
+ if (ret == WH_ERROR_OK) {
+ /* Compute actual variable data size from the received packet */
+ reqDataSz = req_size - sizeof(wrapReq);
+
+ /* Translate request */
+ (void)wh_MessageKeystore_TranslateKeyWrapExportRequest(
+ magic, req_packet, &wrapReq);
+
+ /* Set the request data pointer directly after the request */
+ reqData = (uint8_t*)req_packet +
+ sizeof(whMessageKeystore_KeyWrapExportRequest);
+
+ /* Set the response data pointer directly after the response */
+ respData = (uint8_t*)resp_packet +
+ sizeof(whMessageKeystore_KeyWrapExportResponse);
+ }
+
+ if (ret == WH_ERROR_OK) {
+ ret = WH_SERVER_NVM_LOCK(server);
+ if (ret == WH_ERROR_OK) {
+ ret = _HandleKeyWrapExportRequest(server, &wrapReq, reqData,
+ reqDataSz, &wrapResp,
+ respData, respDataSz);
+
+ (void)WH_SERVER_NVM_UNLOCK(server);
+ } /* WH_SERVER_NVM_LOCK() */
+ }
+ wrapResp.rc = ret;
+
+ (void)wh_MessageKeystore_TranslateKeyWrapExportResponse(
+ magic, &wrapResp, resp_packet);
+ *out_resp_size = sizeof(wrapResp) + wrapResp.wrappedKeySz;
+
+ } break;
+
case WH_KEY_KEYUNWRAPEXPORT: {
whMessageKeystore_KeyUnwrapAndExportRequest unwrapReq = {0};
whMessageKeystore_KeyUnwrapAndExportResponse unwrapResp = {0};
diff --git a/src/wh_server_she.c b/src/wh_server_she.c
index 3615dce16..c6d9e1c55 100644
--- a/src/wh_server_she.c
+++ b/src/wh_server_she.c
@@ -611,8 +611,16 @@ static int _LoadKey(whServerContext* server, uint16_t magic, uint16_t req_size,
/* Update the meta label with new values */
wh_She_Meta2Label(she_meta_count, she_meta_flags, meta->label);
meta->len = WH_SHE_KEY_SZ;
- /* cache if ram key, overwrite otherwise */
- if (WH_KEYID_ID(meta->id) == WH_SHE_RAM_KEY_ID) {
+ /* Cache the key when it is the RAM key, or when there is no NVM to
+ * persist to (e.g. a key primed via unwrap-and-cache on a no-NVM
+ * platform). In both cases the cache is the source of truth and there
+ * is no NVM slot to update; wh_Server_KeystoreCacheKey evicts any
+ * existing entry for this id first, so reads stay fresh without an
+ * explicit evict. Otherwise persist to NVM as before so the SHE key and
+ * its monotonic counter survive cache eviction and reboot; a
+ * cache-resident copy from the read above must not divert the update
+ * away from NVM. */
+ if (WH_KEYID_ID(meta->id) == WH_SHE_RAM_KEY_ID || server->nvm == NULL) {
ret = wh_Server_KeystoreCacheKey(server, meta,
req.messageTwo + WH_SHE_KEY_SZ);
}
@@ -987,7 +995,15 @@ static int _InitRnd(whServerContext* server, uint16_t magic, uint16_t req_size,
meta->id = WH_MAKE_KEYID(WH_KEYTYPE_SHE, server->comm->client_id,
WH_SHE_PRNG_SEED_ID);
meta->len = WH_SHE_KEY_SZ;
- ret = wh_Nvm_AddObject(server->nvm, meta, meta->len, cmacOutput);
+ /* Persist the PRNG seed to NVM, or cache it when there is no NVM to
+ * persist to (mirrors the LOAD_KEY nvm==NULL path). wh_Server_Keystore-
+ * CacheKey evicts any existing entry for this id first. */
+ if (server->nvm == NULL) {
+ ret = wh_Server_KeystoreCacheKey(server, meta, cmacOutput);
+ }
+ else {
+ ret = wh_Nvm_AddObject(server->nvm, meta, meta->len, cmacOutput);
+ }
if (ret != 0) {
ret = WH_SHE_ERC_KEY_UPDATE_ERROR;
}
@@ -1124,7 +1140,14 @@ static int _ExtendSeed(whServerContext* server, uint16_t magic,
meta->id = WH_MAKE_KEYID(WH_KEYTYPE_SHE, server->comm->client_id,
WH_SHE_PRNG_SEED_ID);
meta->len = WH_SHE_KEY_SZ;
- ret = wh_Nvm_AddObject(server->nvm, meta, meta->len, kdfInput);
+ /* Persist to NVM, or cache when there is no NVM (mirrors LOAD_KEY).
+ * wh_Server_KeystoreCacheKey evicts any existing entry for this id. */
+ if (server->nvm == NULL) {
+ ret = wh_Server_KeystoreCacheKey(server, meta, kdfInput);
+ }
+ else {
+ ret = wh_Nvm_AddObject(server->nvm, meta, meta->len, kdfInput);
+ }
if (ret != 0) {
ret = WH_SHE_ERC_KEY_UPDATE_ERROR;
}
diff --git a/test-refactor/config/wolfhsm_cfg.h b/test-refactor/config/wolfhsm_cfg.h
index 620758c68..884facad2 100644
--- a/test-refactor/config/wolfhsm_cfg.h
+++ b/test-refactor/config/wolfhsm_cfg.h
@@ -49,6 +49,7 @@
#ifndef WOLFHSM_CFG_NO_CRYPTO
#define WOLFHSM_CFG_KEYWRAP
+#define WOLFHSM_CFG_HWKEYSTORE
#endif
#define WOLFHSM_CFG_SERVER_NVM_FLASH_LOG
diff --git a/test-refactor/misc/wh_test_hwkeystore.c b/test-refactor/misc/wh_test_hwkeystore.c
new file mode 100644
index 000000000..fd59ff5e4
--- /dev/null
+++ b/test-refactor/misc/wh_test_hwkeystore.c
@@ -0,0 +1,664 @@
+/*
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of wolfHSM.
+ *
+ * wolfHSM is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfHSM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with wolfHSM. If not, see .
+ */
+/*
+ * test-refactor/misc/wh_test_hwkeystore.c
+ *
+ * End-to-end hardware-only key (WH_KEYTYPE_HW) coverage. Lives in the
+ * misc group because it requires a server with a known hardware keystore
+ * backend bound, which the port's shared server does not guarantee: this
+ * test spins up its own client/server pair over the mem transport, binds an
+ * emulated hardware keystore serving a single AES-256 KEK, and pumps the
+ * server inline between the split (non-blocking) client request/response
+ * calls so no threading is needed.
+ *
+ * _whTest_HwKeystoreKeyWrap - key wrap / unwrap-export / unwrap-and-cache
+ * with a hardware-only KEK; wrapped+hwonly
+ * flag precedence; unserved-id and wrong-KEK
+ * negative paths
+ * _whTest_HwKeystoreDataWrap - data wrap/unwrap roundtrip with a
+ * hardware-only KEK
+ * _whTest_HwKeystoreRejections - keystore operations on a hardware-only id
+ * must fail with WH_ERROR_ACCESS
+ *
+ * Crypto key use of a hardware-only id (rejected at the keystore freshen
+ * choke point) is covered server-side in server/wh_test_hwkeystore_server.c,
+ * since the blocking wolfCrypt callback cannot be pumped single-threaded.
+ */
+
+#include "wolfhsm/wh_settings.h"
+
+#if defined(WOLFHSM_CFG_ENABLE_CLIENT) && \
+ defined(WOLFHSM_CFG_ENABLE_SERVER) && !defined(WOLFHSM_CFG_NO_CRYPTO) && \
+ defined(WOLFHSM_CFG_KEYWRAP) && defined(WOLFHSM_CFG_HWKEYSTORE) && \
+ !defined(NO_AES) && defined(HAVE_AESGCM)
+
+#include
+#include
+
+#include "wolfssl/wolfcrypt/settings.h"
+#include "wolfssl/wolfcrypt/types.h"
+
+#include "wolfhsm/wh_error.h"
+#include "wolfhsm/wh_common.h"
+#include "wolfhsm/wh_comm.h"
+#include "wolfhsm/wh_keyid.h"
+#include "wolfhsm/wh_hwkeystore.h"
+#include "wolfhsm/wh_nvm.h"
+#include "wolfhsm/wh_nvm_flash.h"
+#include "wolfhsm/wh_flash_ramsim.h"
+#include "wolfhsm/wh_transport_mem.h"
+#include "wolfhsm/wh_client.h"
+#include "wolfhsm/wh_server.h"
+
+#include "wh_test_common.h"
+#include "wh_test_list.h"
+
+#define BUFFER_SIZE 4096
+#define FLASH_RAM_SIZE (1024 * 1024)
+#define FLASH_SECTOR_SIZE (128 * 1024)
+#define FLASH_PAGE_SIZE 8
+
+/* Id and material of the only KEK served by the emulated hardware keystore */
+#define WH_TEST_HWKEK_ID 3
+#define WH_TEST_HWKEK_SIZE 32
+
+#define WH_TEST_AES_KEYSIZE 32
+#define WH_TEST_WRAPPED_KEYID 20
+#define WH_TEST_WRAPPED_KEYSIZE \
+ (WH_KEYWRAP_AES_GCM_HEADER_SIZE + WH_TEST_AES_KEYSIZE + \
+ sizeof(whNvmMetadata))
+
+static const uint8_t _hwKekMaterial[WH_TEST_HWKEK_SIZE] = {
+ 0x9a, 0x4e, 0x21, 0xc7, 0x5d, 0x10, 0xfb, 0x33, 0x6f, 0x82, 0xd4,
+ 0x59, 0xee, 0x07, 0xb1, 0x2c, 0x48, 0x95, 0x3a, 0xc6, 0x71, 0x0d,
+ 0xb8, 0xe5, 0x12, 0x6a, 0xf9, 0x84, 0x2f, 0xd0, 0x5b, 0xa7};
+
+static int _HwKeystoreGetKey(void* context, whKeyId keyId, uint8_t* out,
+ uint16_t* inout_len)
+{
+ (void)context;
+
+ /* Only hardware-only keyIds should ever reach a hardware keystore */
+ if (WH_KEYID_TYPE(keyId) != WH_KEYTYPE_HW) {
+ return WH_ERROR_ACCESS;
+ }
+
+ /* Serve only the known test KEK id, refuse everything else */
+ if (WH_KEYID_ID(keyId) != WH_TEST_HWKEK_ID) {
+ return WH_ERROR_NOTFOUND;
+ }
+
+ if ((out == NULL) || (inout_len == NULL) ||
+ (*inout_len < sizeof(_hwKekMaterial))) {
+ return WH_ERROR_BUFFER_SIZE;
+ }
+
+ memcpy(out, _hwKekMaterial, sizeof(_hwKekMaterial));
+ *inout_len = sizeof(_hwKekMaterial);
+ return WH_ERROR_OK;
+}
+
+/* clang-format off */
+static const whHwKeystoreCb _hwKeystoreCb = {
+ .Init = NULL,
+ .Cleanup = NULL,
+ .GetKey = _HwKeystoreGetKey,
+};
+/* clang-format on */
+
+/* Self-contained client/server pair over the mem transport. The server is
+ * pumped inline (wh_Server_HandleRequestMessage) between each split client
+ * request/response pair */
+typedef struct {
+ whServerContext server[1];
+ whClientContext client[1];
+ whNvmContext nvm[1];
+ whHwKeystoreContext hwKeystore[1];
+#ifndef WOLFHSM_CFG_NO_CRYPTO
+ whServerCryptoContext crypto[1];
+#endif
+ /* Transport */
+ uint8_t reqBuf[BUFFER_SIZE];
+ uint8_t respBuf[BUFFER_SIZE];
+ whTransportMemConfig tmcf[1];
+ whTransportServerCb tscb[1];
+ whTransportMemServerContext tmsc[1];
+ whCommServerConfig cs_conf[1];
+ whTransportClientCb tccb[1];
+ whTransportMemClientContext tmcc[1];
+ whCommClientConfig cc_conf[1];
+ whClientConfig c_conf[1];
+ /* Flash / NVM */
+ whFlashRamsimCtx fc[1];
+ whFlashRamsimCfg fc_conf[1];
+ whFlashCb fcb[1];
+ whNvmFlashConfig nf_conf[1];
+ whNvmFlashContext nfc[1];
+ whNvmCb nfcb[1];
+ whNvmConfig n_conf[1];
+ /* Hardware keystore */
+ whHwKeystoreConfig hwks_conf[1];
+ whServerConfig s_conf[1];
+} TestCtx;
+
+/* Static to keep the misc group's stack footprint small */
+static TestCtx _testCtx;
+static uint8_t _flashMemory[FLASH_RAM_SIZE];
+
+static int _SetupClientServer(TestCtx* t)
+{
+ uint32_t client_id = 0;
+ uint32_t server_id = 0;
+
+ memset(t, 0, sizeof(*t));
+ memset(_flashMemory, 0, sizeof(_flashMemory));
+
+ /* Transport */
+ t->tmcf[0] = (whTransportMemConfig){
+ .req = (whTransportMemCsr*)t->reqBuf,
+ .req_size = sizeof(t->reqBuf),
+ .resp = (whTransportMemCsr*)t->respBuf,
+ .resp_size = sizeof(t->respBuf),
+ };
+ t->tscb[0] = (whTransportServerCb)WH_TRANSPORT_MEM_SERVER_CB;
+ t->cs_conf[0] = (whCommServerConfig){
+ .transport_cb = t->tscb,
+ .transport_context = (void*)t->tmsc,
+ .transport_config = (void*)t->tmcf,
+ .server_id = 124,
+ };
+ t->tccb[0] = (whTransportClientCb)WH_TRANSPORT_MEM_CLIENT_CB;
+ t->cc_conf[0] = (whCommClientConfig){
+ .transport_cb = t->tccb,
+ .transport_context = (void*)t->tmcc,
+ .transport_config = (void*)t->tmcf,
+ .client_id = WH_TEST_DEFAULT_CLIENT_ID,
+ };
+ t->c_conf[0] = (whClientConfig){
+ .comm = t->cc_conf,
+ };
+
+ /* Flash / NVM */
+ t->fc_conf[0] = (whFlashRamsimCfg){
+ .size = FLASH_RAM_SIZE,
+ .sectorSize = FLASH_SECTOR_SIZE,
+ .pageSize = FLASH_PAGE_SIZE,
+ .erasedByte = ~(uint8_t)0,
+ .memory = _flashMemory,
+ };
+ t->fcb[0] = (whFlashCb)WH_FLASH_RAMSIM_CB;
+ t->nf_conf[0] = (whNvmFlashConfig){
+ .cb = t->fcb,
+ .context = t->fc,
+ .config = t->fc_conf,
+ };
+ t->nfcb[0] = (whNvmCb)WH_NVM_FLASH_CB;
+ t->n_conf[0] = (whNvmConfig){
+ .cb = t->nfcb,
+ .context = t->nfc,
+ .config = t->nf_conf,
+ };
+
+ /* Hardware keystore front-end backed by the test getKey callback */
+ t->hwks_conf[0] = (whHwKeystoreConfig){
+ .cb = &_hwKeystoreCb,
+ .context = NULL,
+ };
+
+ /* Server config */
+ t->s_conf[0] = (whServerConfig){
+ .comm_config = t->cs_conf,
+ .nvm = t->nvm,
+#ifndef WOLFHSM_CFG_NO_CRYPTO
+ .crypto = t->crypto,
+ .devId = INVALID_DEVID,
+#endif
+ .hwKeystore = t->hwKeystore,
+ };
+
+ WH_TEST_RETURN_ON_FAIL(wh_Nvm_Init(t->nvm, t->n_conf));
+ WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Init(t->hwKeystore, t->hwks_conf));
+ WH_TEST_RETURN_ON_FAIL(wolfCrypt_Init());
+ WH_TEST_RETURN_ON_FAIL(wc_InitRng_ex(t->crypto->rng, NULL, INVALID_DEVID));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_Init(t->server, t->s_conf));
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Server_SetConnected(t->server, WH_COMM_CONNECTED));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Init(t->client, t->c_conf));
+
+ /* Comm init so the server learns the client id */
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CommInitRequest(t->client));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Client_CommInitResponse(t->client, &client_id, &server_id));
+ WH_TEST_ASSERT_RETURN(client_id == t->client->comm->client_id);
+
+ return WH_ERROR_OK;
+}
+
+static void _CleanupClientServer(TestCtx* t)
+{
+ (void)wh_Client_Cleanup(t->client);
+ (void)wh_Server_Cleanup(t->server);
+ (void)wh_Nvm_Cleanup(t->nvm);
+ (void)wh_HwKeystore_Cleanup(t->hwKeystore);
+ (void)wc_FreeRng(t->crypto->rng);
+ (void)wolfCrypt_Cleanup();
+}
+
+/* Sequential wrappers: send the request, pump the server once, then collect
+ * the response. The server handler reports operation errors in the response
+ * rc, which the client response call returns */
+
+static int _KeyWrap(TestCtx* t, whKeyId kekId, uint8_t* keyIn, uint16_t keySz,
+ whNvmMetadata* meta, uint8_t* wrappedOut,
+ uint16_t* wrappedSz)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(
+ t->client, WC_CIPHER_AES_GCM, kekId, keyIn, keySz, meta));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_KeyWrapResponse(t->client, WC_CIPHER_AES_GCM, wrappedOut,
+ wrappedSz);
+}
+
+static int _KeyUnwrapAndExport(TestCtx* t, whKeyId kekId, uint8_t* wrappedIn,
+ uint16_t wrappedSz, whNvmMetadata* metaOut,
+ uint8_t* keyOut, uint16_t* keySz)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportRequest(
+ t->client, WC_CIPHER_AES_GCM, kekId, wrappedIn, wrappedSz));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_KeyUnwrapAndExportResponse(t->client, WC_CIPHER_AES_GCM,
+ metaOut, keyOut, keySz);
+}
+
+static int _KeyUnwrapAndCache(TestCtx* t, whKeyId kekId, uint8_t* wrappedIn,
+ uint16_t wrappedSz, uint16_t* keyIdOut)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndCacheRequest(
+ t->client, WC_CIPHER_AES_GCM, kekId, wrappedIn, wrappedSz));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_KeyUnwrapAndCacheResponse(t->client, WC_CIPHER_AES_GCM,
+ keyIdOut);
+}
+
+static int _DataWrap(TestCtx* t, whKeyId kekId, uint8_t* dataIn,
+ uint32_t dataSz, uint8_t* wrappedOut, uint32_t* wrappedSz)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_DataWrapRequest(
+ t->client, WC_CIPHER_AES_GCM, kekId, dataIn, dataSz));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_DataWrapResponse(t->client, WC_CIPHER_AES_GCM, wrappedOut,
+ wrappedSz);
+}
+
+static int _DataUnwrap(TestCtx* t, whKeyId kekId, uint8_t* wrappedIn,
+ uint32_t wrappedSz, uint8_t* dataOut, uint32_t* dataSz)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_DataUnwrapRequest(
+ t->client, WC_CIPHER_AES_GCM, kekId, wrappedIn, wrappedSz));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_DataUnwrapResponse(t->client, WC_CIPHER_AES_GCM, dataOut,
+ dataSz);
+}
+
+static int _KeyCache(TestCtx* t, uint32_t flags, uint8_t* label,
+ uint16_t labelSz, uint8_t* keyIn, uint16_t keySz,
+ uint16_t* keyIdInOut)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex(
+ t->client, flags, label, labelSz, keyIn, keySz, *keyIdInOut));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_KeyCacheResponse(t->client, keyIdInOut);
+}
+
+static int _KeyEvict(TestCtx* t, uint16_t keyId)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(t->client, keyId));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_KeyEvictResponse(t->client);
+}
+
+static int _KeyExport(TestCtx* t, uint16_t keyId, uint8_t* label,
+ uint16_t labelSz, uint8_t* keyOut, uint16_t* keySz)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(t->client, keyId));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_KeyExportResponse(t->client, label, labelSz, keyOut,
+ keySz);
+}
+
+static int _KeyCommit(TestCtx* t, uint16_t keyId)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCommitRequest(t->client, keyId));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_KeyCommitResponse(t->client);
+}
+
+static int _KeyErase(TestCtx* t, uint16_t keyId)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEraseRequest(t->client, keyId));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_KeyEraseResponse(t->client);
+}
+
+static int _KeyRevoke(TestCtx* t, uint16_t keyId)
+{
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyRevokeRequest(t->client, keyId));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ return wh_Client_KeyRevokeResponse(t->client);
+}
+
+static int _whTest_HwKeystoreKeyWrap(TestCtx* t)
+{
+ int ret;
+ whKeyId hwKekId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID);
+ uint8_t plainKey[WH_TEST_AES_KEYSIZE];
+ uint8_t tmpPlainKey[WH_TEST_AES_KEYSIZE];
+ uint16_t tmpPlainKeySz = sizeof(tmpPlainKey);
+ uint8_t wrappedKey[WH_TEST_WRAPPED_KEYSIZE];
+ uint16_t wrappedKeySz = sizeof(wrappedKey);
+ uint16_t unwrappedKeyId = WH_KEYID_ERASED;
+ whNvmMetadata metadata = {0};
+ whNvmMetadata tmpMetadata = {0};
+ size_t i;
+
+ metadata.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(t->client->comm->client_id,
+ WH_TEST_WRAPPED_KEYID);
+ metadata.len = WH_TEST_AES_KEYSIZE;
+ metadata.flags = WH_NVM_FLAGS_USAGE_ANY;
+ memcpy(metadata.label, "HW KEK wrapped key", sizeof("HW KEK wrapped key"));
+
+ /* Fixed pattern; distinct from the KEK material */
+ for (i = 0; i < sizeof(plainKey); i++) {
+ plainKey[i] = (uint8_t)(0xA0 ^ i);
+ }
+
+ /* Wrap a key using the hardware-only KEK */
+ ret = _KeyWrap(t, hwKekId, plainKey, sizeof(plainKey), &metadata,
+ wrappedKey, &wrappedKeySz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to KeyWrap with HW KEK %d\n", ret);
+ return ret;
+ }
+
+ /* Unwrap and export with the hardware-only KEK, check the roundtrip */
+ ret = _KeyUnwrapAndExport(t, hwKekId, wrappedKey, wrappedKeySz,
+ &tmpMetadata, tmpPlainKey, &tmpPlainKeySz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to KeyUnwrapAndExport with HW KEK %d\n", ret);
+ return ret;
+ }
+
+ if (memcmp(plainKey, tmpPlainKey, sizeof(plainKey)) != 0) {
+ WH_ERROR_PRINT("HW KEK wrap/unwrap key failed to match\n");
+ return WH_ERROR_ABORTED;
+ }
+
+ if (memcmp(&metadata, &tmpMetadata, sizeof(metadata)) != 0) {
+ WH_ERROR_PRINT("HW KEK wrap/unwrap metadata failed to match\n");
+ return WH_ERROR_ABORTED;
+ }
+
+ /* Unwrap-and-cache with the hardware-only KEK: the wrapped payload is an
+ * ordinary key and may enter the cache; only the KEK itself is
+ * hardware-resident */
+ ret = _KeyUnwrapAndCache(t, hwKekId, wrappedKey, wrappedKeySz,
+ &unwrappedKeyId);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to KeyUnwrapAndCache with HW KEK %d\n", ret);
+ return ret;
+ }
+ WH_TEST_RETURN_ON_FAIL(_KeyEvict(t, unwrappedKeyId));
+
+ /* A wrapped+hardware-only KEK id must behave as hardware-only (the
+ * hardware-only flag takes precedence) */
+ wrappedKeySz = sizeof(wrappedKey);
+ ret = _KeyWrap(t, hwKekId | WH_KEYID_CLIENT_WRAPPED_FLAG, plainKey,
+ sizeof(plainKey), &metadata, wrappedKey, &wrappedKeySz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to KeyWrap with wrapped+HW KEK %d\n", ret);
+ return ret;
+ }
+
+ /* Unwrapping a hardware-KEK-wrapped blob with a different, cached KEK
+ * must fail authentication, proving distinct key material was used */
+ {
+ uint16_t cachedKekId = WH_KEYID_ERASED;
+ uint8_t cachedKek[WH_TEST_HWKEK_SIZE];
+ uint8_t kekLabel[] = "cached-kek";
+
+ for (i = 0; i < sizeof(cachedKek); i++) {
+ cachedKek[i] = (uint8_t)(0x5C ^ i);
+ }
+
+ WH_TEST_RETURN_ON_FAIL(_KeyCache(
+ t, WH_NVM_FLAGS_NONEXPORTABLE | WH_NVM_FLAGS_USAGE_WRAP, kekLabel,
+ sizeof(kekLabel), cachedKek, sizeof(cachedKek), &cachedKekId));
+
+ tmpPlainKeySz = sizeof(tmpPlainKey);
+ ret = _KeyUnwrapAndExport(t, cachedKekId, wrappedKey, wrappedKeySz,
+ &tmpMetadata, tmpPlainKey, &tmpPlainKeySz);
+ WH_TEST_RETURN_ON_FAIL(_KeyEvict(t, cachedKekId));
+ if (ret == WH_ERROR_OK) {
+ WH_ERROR_PRINT("Unwrap with wrong KEK unexpectedly succeeded\n");
+ return WH_ERROR_ABORTED;
+ }
+ }
+
+ /* A hardware KEK id the backend does not serve must fail */
+ wrappedKeySz = sizeof(wrappedKey);
+ ret = _KeyWrap(t, WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID + 1), plainKey,
+ sizeof(plainKey), &metadata, wrappedKey, &wrappedKeySz);
+ if (ret != WH_ERROR_NOTFOUND) {
+ WH_ERROR_PRINT("KeyWrap with unserved HW KEK expected NOTFOUND, "
+ "got %d\n",
+ ret);
+ return WH_ERROR_ABORTED;
+ }
+
+ return WH_ERROR_OK;
+}
+
+static int _whTest_HwKeystoreDataWrap(TestCtx* t)
+{
+ int ret;
+ whKeyId hwKekId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID);
+ uint8_t data[] = "Example data!";
+ uint8_t unwrappedData[sizeof(data)] = {0};
+ uint32_t unwrappedDataSz = sizeof(unwrappedData);
+ uint8_t wrappedData[sizeof(data) + WH_KEYWRAP_AES_GCM_HEADER_SIZE] = {0};
+ uint32_t wrappedDataSz = sizeof(wrappedData);
+
+ ret =
+ _DataWrap(t, hwKekId, data, sizeof(data), wrappedData, &wrappedDataSz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to DataWrap with HW KEK %d\n", ret);
+ return ret;
+ }
+
+ ret = _DataUnwrap(t, hwKekId, wrappedData, sizeof(wrappedData),
+ unwrappedData, &unwrappedDataSz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to DataUnwrap with HW KEK %d\n", ret);
+ return ret;
+ }
+
+ if (memcmp(data, unwrappedData, sizeof(data)) != 0) {
+ WH_ERROR_PRINT("HW KEK unwrapped data failed to match input data\n");
+ return WH_ERROR_ABORTED;
+ }
+
+ return WH_ERROR_OK;
+}
+
+/* Hardware-only keys must be rejected by every keystore operation; only the
+ * keywrap KEK paths may resolve them. Crypto key use is rejected at the same
+ * keystore choke points, covered in server/wh_test_hwkeystore_server.c */
+static int _whTest_HwKeystoreRejections(TestCtx* t)
+{
+ int ret;
+ whKeyId hwKeyId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID);
+ uint8_t buf[WH_TEST_HWKEK_SIZE] = {0};
+ uint16_t bufSz = sizeof(buf);
+ uint8_t label[WH_NVM_LABEL_LEN] = "hwonly reject";
+ uint16_t cacheKeyId = hwKeyId;
+
+ /* Caching key material under a hardware-only id must be rejected */
+ ret = _KeyCache(t, WH_NVM_FLAGS_NONE, label, sizeof(label), buf,
+ sizeof(buf), &cacheKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyCache of HW-only id expected ACCESS, got %d\n", ret);
+ return WH_ERROR_ABORTED;
+ }
+
+#ifdef WOLFHSM_CFG_DMA
+ /* The DMA cache path must reject hardware-only ids as well */
+ cacheKeyId = hwKeyId;
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheDmaRequest(
+ t->client, WH_NVM_FLAGS_NONE, label, sizeof(label), buf, sizeof(buf),
+ cacheKeyId));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server));
+ ret = wh_Client_KeyCacheDmaResponse(t->client, &cacheKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyCacheDma of HW-only id expected ACCESS, got %d\n",
+ ret);
+ return WH_ERROR_ABORTED;
+ }
+#endif
+
+ /* Exporting a hardware-only key must be rejected */
+ ret = _KeyExport(t, hwKeyId, label, sizeof(label), buf, &bufSz);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyExport of HW-only id expected ACCESS, got %d\n",
+ ret);
+ return WH_ERROR_ABORTED;
+ }
+
+ /* Commit/evict/erase/revoke of a hardware-only key must be rejected */
+ ret = _KeyCommit(t, hwKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyCommit of HW-only id expected ACCESS, got %d\n",
+ ret);
+ return WH_ERROR_ABORTED;
+ }
+
+ ret = _KeyEvict(t, hwKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyEvict of HW-only id expected ACCESS, got %d\n", ret);
+ return WH_ERROR_ABORTED;
+ }
+
+ ret = _KeyErase(t, hwKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyErase of HW-only id expected ACCESS, got %d\n", ret);
+ return WH_ERROR_ABORTED;
+ }
+
+ ret = _KeyRevoke(t, hwKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyRevoke of HW-only id expected ACCESS, got %d\n",
+ ret);
+ return WH_ERROR_ABORTED;
+ }
+
+ return WH_ERROR_OK;
+}
+
+/* A hardware-only KEK requested against a server that has no hardware keystore
+ * bound (HWKEYSTORE compiled in, whServerConfig.hwKeystore left NULL) must fail
+ * cleanly with NOTFOUND from the keywrap KEK resolver. Simulate the unbound
+ * configuration by detaching the server's keystore for the duration of the
+ * checks, then restore it for teardown */
+static int _whTest_HwKeystoreUnbound(TestCtx* t)
+{
+ int ret;
+ whKeyId hwKekId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID);
+ uint8_t plainKey[WH_TEST_AES_KEYSIZE] = {0};
+ uint8_t wrappedKey[WH_TEST_WRAPPED_KEYSIZE];
+ uint16_t wrappedKeySz = sizeof(wrappedKey);
+ uint8_t data[] = "Example data!";
+ uint8_t wrappedData[sizeof(data) + WH_KEYWRAP_AES_GCM_HEADER_SIZE];
+ uint32_t wrappedDataSz = sizeof(wrappedData);
+ whNvmMetadata metadata = {0};
+ whHwKeystoreContext* saved = t->server->hwKeystore;
+
+ metadata.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(t->client->comm->client_id,
+ WH_TEST_WRAPPED_KEYID);
+ metadata.len = WH_TEST_AES_KEYSIZE;
+ metadata.flags = WH_NVM_FLAGS_USAGE_ANY;
+
+ /* Detach the hardware keystore to mimic a server without one bound */
+ t->server->hwKeystore = NULL;
+
+ /* KeyWrap (usage-enforcing KEK path) must report NOTFOUND */
+ ret = _KeyWrap(t, hwKekId, plainKey, sizeof(plainKey), &metadata,
+ wrappedKey, &wrappedKeySz);
+ if (ret != WH_ERROR_NOTFOUND) {
+ t->server->hwKeystore = saved;
+ WH_ERROR_PRINT("KeyWrap with HW KEK and no bound keystore expected "
+ "NOTFOUND, got %d\n",
+ ret);
+ return WH_ERROR_ABORTED;
+ }
+
+ /* DataWrap (non-enforcing KEK path) must report NOTFOUND as well */
+ ret =
+ _DataWrap(t, hwKekId, data, sizeof(data), wrappedData, &wrappedDataSz);
+ if (ret != WH_ERROR_NOTFOUND) {
+ t->server->hwKeystore = saved;
+ WH_ERROR_PRINT("DataWrap with HW KEK and no bound keystore expected "
+ "NOTFOUND, got %d\n",
+ ret);
+ return WH_ERROR_ABORTED;
+ }
+
+ /* Restore the keystore for teardown */
+ t->server->hwKeystore = saved;
+
+ return WH_ERROR_OK;
+}
+
+int whTest_HwKeystore(void* ctx)
+{
+ int ret;
+ TestCtx* t = &_testCtx;
+
+ (void)ctx;
+
+ WH_TEST_RETURN_ON_FAIL(_SetupClientServer(t));
+
+ ret = _whTest_HwKeystoreKeyWrap(t);
+ if (ret == WH_ERROR_OK) {
+ ret = _whTest_HwKeystoreDataWrap(t);
+ }
+ if (ret == WH_ERROR_OK) {
+ ret = _whTest_HwKeystoreRejections(t);
+ }
+ if (ret == WH_ERROR_OK) {
+ ret = _whTest_HwKeystoreUnbound(t);
+ }
+
+ _CleanupClientServer(t);
+
+ return ret;
+}
+
+#endif /* WOLFHSM_CFG_ENABLE_CLIENT && WOLFHSM_CFG_ENABLE_SERVER && \
+ !WOLFHSM_CFG_NO_CRYPTO && WOLFHSM_CFG_KEYWRAP && \
+ WOLFHSM_CFG_HWKEYSTORE && !NO_AES && HAVE_AESGCM */
diff --git a/test-refactor/server/wh_test_hwkeystore_server.c b/test-refactor/server/wh_test_hwkeystore_server.c
new file mode 100644
index 000000000..5a642fd03
--- /dev/null
+++ b/test-refactor/server/wh_test_hwkeystore_server.c
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of wolfHSM.
+ *
+ * wolfHSM is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfHSM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with wolfHSM. If not, see .
+ */
+/*
+ * test-refactor/server/wh_test_hwkeystore_server.c
+ *
+ * Server-side hardware keystore coverage that needs no client:
+ *
+ * _whTest_HwKeystoreModule - wh_HwKeystore_Init/GetKey/Cleanup argument
+ * validation and GetKey dispatch, using a
+ * local context (the shared server context
+ * is not touched)
+ * _whTest_HwKeystoreLifecycle - optional backend Init/Cleanup callback
+ * dispatch and the Init-failure return path,
+ * using a backend that records its calls
+ * _whTest_KeystoreHwOnlyReject - every public keystore entry point must
+ * reject a hardware-only keyId with
+ * WH_ERROR_ACCESS before reaching the cache,
+ * NVM, or any hardware backend (none is
+ * bound to the shared server). FreshenKey is
+ * also the choke point through which all
+ * crypto handlers resolve keys, so this
+ * covers crypto key use of hardware-only ids
+ *
+ * End-to-end keywrap use of hardware-only KEKs is covered by the misc group
+ * test misc/wh_test_hwkeystore.c, which binds a hardware keystore to its own
+ * private client/server pair.
+ */
+
+#include "wolfhsm/wh_settings.h"
+
+#if defined(WOLFHSM_CFG_ENABLE_SERVER) && defined(WOLFHSM_CFG_HWKEYSTORE)
+
+#include
+#include
+
+#include "wolfhsm/wh_error.h"
+#include "wolfhsm/wh_common.h"
+#include "wolfhsm/wh_keyid.h"
+#include "wolfhsm/wh_hwkeystore.h"
+#include "wolfhsm/wh_nvm.h"
+#include "wolfhsm/wh_server.h"
+#include "wolfhsm/wh_server_keystore.h"
+
+#include "wh_test_common.h"
+#include "wh_test_list.h"
+
+/* Id and material of the only key served by the local test backend */
+#define WH_TEST_HWKEK_ID 3
+#define WH_TEST_HWKEK_SIZE 32
+
+/* USER field value for constructing server-internal hardware-only keyIds */
+#define WH_TEST_HW_USER 1
+
+static const uint8_t _hwKekMaterial[WH_TEST_HWKEK_SIZE] = {
+ 0x9a, 0x4e, 0x21, 0xc7, 0x5d, 0x10, 0xfb, 0x33, 0x6f, 0x82, 0xd4,
+ 0x59, 0xee, 0x07, 0xb1, 0x2c, 0x48, 0x95, 0x3a, 0xc6, 0x71, 0x0d,
+ 0xb8, 0xe5, 0x12, 0x6a, 0xf9, 0x84, 0x2f, 0xd0, 0x5b, 0xa7};
+
+static int _HwKeystoreGetKey(void* context, whKeyId keyId, uint8_t* out,
+ uint16_t* inout_len)
+{
+ (void)context;
+
+ /* Only hardware-only keyIds should ever reach a hardware keystore */
+ if (WH_KEYID_TYPE(keyId) != WH_KEYTYPE_HW) {
+ return WH_ERROR_ACCESS;
+ }
+
+ /* Serve only the known test KEK id, refuse everything else */
+ if (WH_KEYID_ID(keyId) != WH_TEST_HWKEK_ID) {
+ return WH_ERROR_NOTFOUND;
+ }
+
+ if ((out == NULL) || (inout_len == NULL) ||
+ (*inout_len < sizeof(_hwKekMaterial))) {
+ return WH_ERROR_BUFFER_SIZE;
+ }
+
+ memcpy(out, _hwKekMaterial, sizeof(_hwKekMaterial));
+ *inout_len = sizeof(_hwKekMaterial);
+ return WH_ERROR_OK;
+}
+
+/* clang-format off */
+static const whHwKeystoreCb _hwKeystoreCb = {
+ .Init = NULL,
+ .Cleanup = NULL,
+ .GetKey = _HwKeystoreGetKey,
+};
+/* clang-format on */
+
+/* Backend context recording optional Init/Cleanup callback dispatch. initRet
+ * and cleanupRet let a test force the Init/Cleanup failure paths; initConfig
+ * captures the config pointer forwarded to Init to prove it is plumbed
+ * through */
+typedef struct {
+ int initCalls;
+ int cleanupCalls;
+ int initRet;
+ int cleanupRet;
+ const void* initConfig;
+} HwKeystoreLifecycle;
+
+static int _HwKeystoreInit(void* context, const void* config)
+{
+ HwKeystoreLifecycle* lc = (HwKeystoreLifecycle*)context;
+ if (lc != NULL) {
+ lc->initCalls++;
+ lc->initConfig = config;
+ return lc->initRet;
+ }
+ return WH_ERROR_OK;
+}
+
+static int _HwKeystoreCleanup(void* context)
+{
+ HwKeystoreLifecycle* lc = (HwKeystoreLifecycle*)context;
+ if (lc != NULL) {
+ lc->cleanupCalls++;
+ return lc->cleanupRet;
+ }
+ return WH_ERROR_OK;
+}
+
+/* clang-format off */
+static const whHwKeystoreCb _hwKeystoreLifecycleCb = {
+ .Init = _HwKeystoreInit,
+ .Cleanup = _HwKeystoreCleanup,
+ .GetKey = _HwKeystoreGetKey,
+};
+/* clang-format on */
+
+/* wh_HwKeystore module front-end in isolation: argument validation, callback
+ * dispatch, and lifecycle */
+static int _whTest_HwKeystoreModule(void)
+{
+ int ret;
+ whHwKeystoreContext hwks[1] = {{0}};
+ whHwKeystoreConfig conf[1] = {{0}};
+ whHwKeystoreConfig badConf[1] = {{0}};
+ whKeyId servedId =
+ WH_MAKE_KEYID(WH_KEYTYPE_HW, WH_TEST_HW_USER, WH_TEST_HWKEK_ID);
+ uint8_t out[WH_TEST_HWKEK_SIZE] = {0};
+ uint16_t outLen = sizeof(out);
+
+ conf->cb = &_hwKeystoreCb;
+
+ /* Init argument validation: NULL context/config/getKey are rejected */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == wh_HwKeystore_Init(NULL, conf));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == wh_HwKeystore_Init(hwks, NULL));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_HwKeystore_Init(hwks, badConf));
+
+ /* GetKey must reject NULL and uninitialized contexts */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_HwKeystore_GetKey(NULL, servedId, out, &outLen));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_HwKeystore_GetKey(hwks, servedId, out, &outLen));
+
+ WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Init(hwks, conf));
+
+ /* GetKey argument validation on an initialized context */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_HwKeystore_GetKey(hwks, servedId, NULL, &outLen));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_HwKeystore_GetKey(hwks, servedId, out, NULL));
+
+ /* Served id: material and reported length must match the backend's */
+ ret = wh_HwKeystore_GetKey(hwks, servedId, out, &outLen);
+ WH_TEST_ASSERT_RETURN(WH_ERROR_OK == ret);
+ WH_TEST_ASSERT_RETURN(outLen == sizeof(_hwKekMaterial));
+ WH_TEST_ASSERT_RETURN(0 ==
+ memcmp(out, _hwKekMaterial, sizeof(_hwKekMaterial)));
+
+ /* Backend policy: unserved id, undersized buffer, non-HW type */
+ outLen = sizeof(out);
+ WH_TEST_ASSERT_RETURN(
+ WH_ERROR_NOTFOUND ==
+ wh_HwKeystore_GetKey(
+ hwks,
+ WH_MAKE_KEYID(WH_KEYTYPE_HW, WH_TEST_HW_USER, WH_TEST_HWKEK_ID + 1),
+ out, &outLen));
+ outLen = WH_TEST_HWKEK_SIZE - 1;
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BUFFER_SIZE ==
+ wh_HwKeystore_GetKey(hwks, servedId, out, &outLen));
+ outLen = sizeof(out);
+ WH_TEST_ASSERT_RETURN(
+ WH_ERROR_ACCESS ==
+ wh_HwKeystore_GetKey(
+ hwks,
+ WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, WH_TEST_HW_USER, WH_TEST_HWKEK_ID),
+ out, &outLen));
+
+ /* Cleanup zeroizes the context, after which GetKey must reject it */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == wh_HwKeystore_Cleanup(NULL));
+ WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Cleanup(hwks));
+ outLen = sizeof(out);
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_HwKeystore_GetKey(hwks, servedId, out, &outLen));
+
+ return WH_ERROR_OK;
+}
+
+/* Optional backend Init/Cleanup dispatch and the Init-failure return path,
+ * using a backend that records its callback invocations */
+static int _whTest_HwKeystoreLifecycle(void)
+{
+ HwKeystoreLifecycle lc = {0};
+ whHwKeystoreContext hwks[1] = {{0}};
+ whHwKeystoreConfig conf[1] = {{0}};
+ const int backendConfig = 0; /* sentinel forwarded to Init */
+ whKeyId servedId =
+ WH_MAKE_KEYID(WH_KEYTYPE_HW, WH_TEST_HW_USER, WH_TEST_HWKEK_ID);
+ uint8_t out[WH_TEST_HWKEK_SIZE] = {0};
+ uint16_t outLen = sizeof(out);
+
+ conf->cb = &_hwKeystoreLifecycleCb;
+ conf->context = &lc;
+ conf->config = &backendConfig;
+
+ /* Init-failure path: the backend Init rc is propagated, the config pointer
+ * was forwarded, and the context is left uninitialized (GetKey rejects it).
+ * Cleanup must NOT have run for a backend whose Init never succeeded */
+ lc.initRet = WH_ERROR_ABORTED;
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED == wh_HwKeystore_Init(hwks, conf));
+ WH_TEST_ASSERT_RETURN(lc.initCalls == 1);
+ WH_TEST_ASSERT_RETURN(lc.initConfig == &backendConfig);
+ WH_TEST_ASSERT_RETURN(lc.cleanupCalls == 0);
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_HwKeystore_GetKey(hwks, servedId, out, &outLen));
+
+ /* Init-success path: the backend Init is dispatched exactly once and the
+ * context becomes usable */
+ lc.initRet = WH_ERROR_OK;
+ lc.initCalls = 0;
+ WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Init(hwks, conf));
+ WH_TEST_ASSERT_RETURN(lc.initCalls == 1);
+
+ outLen = sizeof(out);
+ WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_GetKey(hwks, servedId, out, &outLen));
+ WH_TEST_ASSERT_RETURN(outLen == sizeof(_hwKekMaterial));
+
+ /* Cleanup dispatches the backend Cleanup exactly once */
+ WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Cleanup(hwks));
+ WH_TEST_ASSERT_RETURN(lc.cleanupCalls == 1);
+
+ /* Cleanup-failure path: a backend Cleanup error is surfaced to the caller,
+ * yet the context is still torn down (a subsequent GetKey rejects it) */
+ lc.initRet = WH_ERROR_OK;
+ lc.initCalls = 0;
+ lc.cleanupCalls = 0;
+ lc.cleanupRet = WH_ERROR_ABORTED;
+ WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Init(hwks, conf));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED == wh_HwKeystore_Cleanup(hwks));
+ WH_TEST_ASSERT_RETURN(lc.cleanupCalls == 1);
+ outLen = sizeof(out);
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_HwKeystore_GetKey(hwks, servedId, out, &outLen));
+
+ return WH_ERROR_OK;
+}
+
+/* Hardware-only keyIds must be rejected by every public keystore entry point
+ * with WH_ERROR_ACCESS (or WH_ERROR_BADARGS for id generation). These checks
+ * are pure keyId-type checks: they fire whether or not a hardware keystore
+ * is bound to the server, and before the cache or NVM is consulted */
+static int _whTest_KeystoreHwOnlyReject(whServerContext* server)
+{
+ whKeyId hwId =
+ WH_MAKE_KEYID(WH_KEYTYPE_HW, WH_TEST_HW_USER, WH_TEST_HWKEK_ID);
+ uint8_t keyBuf[WH_TEST_HWKEK_SIZE] = {0};
+ uint8_t* outBuf = NULL;
+ whNvmMetadata* outMeta = NULL;
+ whNvmMetadata meta = {0};
+ uint32_t outSz = sizeof(keyBuf);
+ whNvmId uniqueId;
+
+ /* Id generation: hardware-only ids are assigned by the backend, never by
+ * the keystore */
+ uniqueId = WH_MAKE_KEYID(WH_KEYTYPE_HW, WH_TEST_HW_USER, 0);
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_Server_KeystoreGetUniqueId(server, &uniqueId));
+
+ /* Cache slot allocation (also the sole guard on the DMA cache path) */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS ==
+ wh_Server_KeystoreGetCacheSlot(
+ server, hwId, sizeof(keyBuf), &outBuf, &outMeta));
+
+ /* Caching key material under a hardware-only id */
+ meta.id = hwId;
+ meta.len = sizeof(keyBuf);
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS ==
+ wh_Server_KeystoreCacheKey(server, &meta, keyBuf));
+
+ /* Freshen: the resolution path used by all crypto handlers, so this also
+ * proves crypto operations cannot use hardware-only keys */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == wh_Server_KeystoreFreshenKey(
+ server, hwId, NULL, NULL));
+
+ /* Reading hardware-only key material out of the keystore */
+ WH_TEST_ASSERT_RETURN(
+ WH_ERROR_ACCESS ==
+ wh_Server_KeystoreReadKey(server, hwId, NULL, keyBuf, &outSz));
+
+ /* Lifecycle operations */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS ==
+ wh_Server_KeystoreEvictKey(server, hwId));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS ==
+ wh_Server_KeystoreCommitKey(server, hwId));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS ==
+ wh_Server_KeystoreEraseKey(server, hwId));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS ==
+ wh_Server_KeystoreEraseKeyChecked(server, hwId));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS ==
+ wh_Server_KeystoreRevokeKey(server, hwId));
+
+ return WH_ERROR_OK;
+}
+
+int whTest_HwKeystoreServer(whServerContext* ctx)
+{
+ WH_TEST_RETURN_ON_FAIL(_whTest_HwKeystoreModule());
+ WH_TEST_RETURN_ON_FAIL(_whTest_HwKeystoreLifecycle());
+ WH_TEST_RETURN_ON_FAIL(_whTest_KeystoreHwOnlyReject(ctx));
+
+ return WH_ERROR_OK;
+}
+
+#endif /* WOLFHSM_CFG_ENABLE_SERVER && WOLFHSM_CFG_HWKEYSTORE */
diff --git a/test-refactor/wh_test_list.c b/test-refactor/wh_test_list.c
index a7ff07128..0484ac829 100644
--- a/test-refactor/wh_test_list.c
+++ b/test-refactor/wh_test_list.c
@@ -39,8 +39,10 @@
WH_TEST_DECL(whTest_ClientDevId);
WH_TEST_DECL(whTest_Comm);
WH_TEST_DECL(whTest_Dma);
+WH_TEST_DECL(whTest_HwKeystore);
WH_TEST_DECL(whTest_KeystoreReqSize);
WH_TEST_DECL(whTest_CertVerify);
+WH_TEST_DECL(whTest_HwKeystoreServer);
WH_TEST_DECL(whTest_NvmOptional);
WH_TEST_DECL(whTest_ClientCerts);
WH_TEST_DECL(whTest_Crypto_Aes);
@@ -71,12 +73,14 @@ const whTestCase whTestsMisc[] = {
{"whTest_ClientDevId", whTest_ClientDevId},
{"whTest_Comm", whTest_Comm},
{"whTest_Dma", whTest_Dma},
+ {"whTest_HwKeystore", whTest_HwKeystore},
{"whTest_KeystoreReqSize", whTest_KeystoreReqSize},
};
const size_t whTestsMiscCount = sizeof(whTestsMisc) / sizeof(whTestsMisc[0]);
const whTestCase whTestsServer[] = {
{"whTest_CertVerify", whTest_CertVerify},
+ {"whTest_HwKeystoreServer", whTest_HwKeystoreServer},
{"whTest_NvmOptional", whTest_NvmOptional},
};
const size_t whTestsServerCount = sizeof(whTestsServer) / sizeof(whTestsServer[0]);
diff --git a/test/config/wolfhsm_cfg.h b/test/config/wolfhsm_cfg.h
index e48753ccf..0ce3317dd 100644
--- a/test/config/wolfhsm_cfg.h
+++ b/test/config/wolfhsm_cfg.h
@@ -55,6 +55,7 @@
#ifndef WOLFHSM_CFG_NO_CRYPTO
#define WOLFHSM_CFG_KEYWRAP
+#define WOLFHSM_CFG_HWKEYSTORE
#endif
/* Test log-based NVM flash backend */
diff --git a/test/wh_test_crypto.c b/test/wh_test_crypto.c
index 0ea823e67..0e514213b 100644
--- a/test/wh_test_crypto.c
+++ b/test/wh_test_crypto.c
@@ -14554,6 +14554,13 @@ int whTest_CryptoClientConfig(whClientConfig* config)
if (ret == 0) {
ret = whTest_Client_DataWrap(client);
}
+#if defined(WOLFHSM_CFG_HWKEYSTORE) && defined(WOLFHSM_CFG_ENABLE_SERVER)
+ /* Hardware-only KEK tests need the in-process test server, which binds
+ * the test hardware keystore; external (client-only) servers may not */
+ if (ret == 0) {
+ ret = whTest_Client_HwKeystore(client);
+ }
+#endif
#endif
#ifndef NO_AES
@@ -15196,6 +15203,17 @@ static int wh_ClientServer_MemThreadTest(whTestNvmBackendType nvmType)
.cb = whTestDma_BounceServerCb,
};
#endif
+#if defined(WOLFHSM_CFG_HWKEYSTORE) && defined(WOLFHSM_CFG_KEYWRAP)
+ /* Hardware keystore front-end backed by the test getKey callback. The
+ * callback is defined in wh_test_keywrap.c, so only bind it when the
+ * keywrap suite (its sole consumer) is compiled in */
+ static const whHwKeystoreCb hwksCb = WH_TEST_HWKEYSTORE_CB;
+ whHwKeystoreContext hwKeystore[1] = {{0}};
+ whHwKeystoreConfig hwksConf[1] = {{
+ .cb = &hwksCb,
+ .context = NULL,
+ }};
+#endif
whServerConfig s_conf[1] = {{
.comm_config = cs_conf,
@@ -15204,10 +15222,16 @@ static int wh_ClientServer_MemThreadTest(whTestNvmBackendType nvmType)
.devId = INVALID_DEVID,
#ifdef WOLFHSM_CFG_DMA
.dmaConfig = &serverDmaConfig,
+#endif
+#if defined(WOLFHSM_CFG_HWKEYSTORE) && defined(WOLFHSM_CFG_KEYWRAP)
+ .hwKeystore = hwKeystore,
#endif
}};
WH_TEST_RETURN_ON_FAIL(wh_Nvm_Init(nvm, n_conf));
+#if defined(WOLFHSM_CFG_HWKEYSTORE) && defined(WOLFHSM_CFG_KEYWRAP)
+ WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Init(hwKeystore, hwksConf));
+#endif
#ifdef WOLFHSM_CFG_DMA
whTestDma_BounceReset();
@@ -15244,6 +15268,9 @@ static int wh_ClientServer_MemThreadTest(whTestNvmBackendType nvmType)
}
wh_Nvm_Cleanup(nvm);
+#if defined(WOLFHSM_CFG_HWKEYSTORE) && defined(WOLFHSM_CFG_KEYWRAP)
+ (void)wh_HwKeystore_Cleanup(hwKeystore);
+#endif
wc_FreeRng(crypto->rng);
wolfCrypt_Cleanup();
diff --git a/test/wh_test_keywrap.c b/test/wh_test_keywrap.c
index fdbbed684..559fbd33a 100644
--- a/test/wh_test_keywrap.c
+++ b/test/wh_test_keywrap.c
@@ -26,6 +26,7 @@
#if !defined(WOLFHSM_CFG_NO_CRYPTO)
#include "wolfssl/wolfcrypt/settings.h"
#include "wolfssl/wolfcrypt/types.h"
+#include "wolfssl/wolfcrypt/kdf.h" /* for HKDF and WC_SHA256 in trusted-KEK test */
#endif /* !WOLFHSM_CFG_NO_CRYPTO */
#include "wolfhsm/wh_error.h"
@@ -34,6 +35,46 @@
#include "wh_test_common.h"
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+#include "wolfhsm/wh_keyid.h"
+
+/* Hardware keystore test fixture: the id and material of the only KEK served
+ * by whTest_HwKeystoreGetKeyCb, which emulates a hardware keystore backend
+ * for the in-process test servers */
+#define WH_TEST_HWKEK_ID 3
+#define WH_TEST_HWKEK_SIZE 32
+
+static const uint8_t _hwKekMaterial[WH_TEST_HWKEK_SIZE] = {
+ 0x9a, 0x4e, 0x21, 0xc7, 0x5d, 0x10, 0xfb, 0x33, 0x6f, 0x82, 0xd4,
+ 0x59, 0xee, 0x07, 0xb1, 0x2c, 0x48, 0x95, 0x3a, 0xc6, 0x71, 0x0d,
+ 0xb8, 0xe5, 0x12, 0x6a, 0xf9, 0x84, 0x2f, 0xd0, 0x5b, 0xa7};
+
+int whTest_HwKeystoreGetKeyCb(void* context, whKeyId keyId, uint8_t* out,
+ uint16_t* inout_len)
+{
+ (void)context;
+
+ /* Only hardware-only keyIds should ever reach a hardware keystore */
+ if (WH_KEYID_TYPE(keyId) != WH_KEYTYPE_HW) {
+ return WH_ERROR_ACCESS;
+ }
+
+ /* Serve only the known test KEK id, refuse everything else */
+ if (WH_KEYID_ID(keyId) != WH_TEST_HWKEK_ID) {
+ return WH_ERROR_NOTFOUND;
+ }
+
+ if ((out == NULL) || (inout_len == NULL) ||
+ (*inout_len < sizeof(_hwKekMaterial))) {
+ return WH_ERROR_BUFFER_SIZE;
+ }
+
+ memcpy(out, _hwKekMaterial, sizeof(_hwKekMaterial));
+ *inout_len = sizeof(_hwKekMaterial);
+ return WH_ERROR_OK;
+}
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
#ifdef WOLFHSM_CFG_ENABLE_CLIENT
#include "wolfhsm/wh_client.h"
#include "wolfhsm/wh_client_crypto.h"
@@ -46,6 +87,8 @@
#define WH_TEST_AESGCM_KEY_OFFSET 0x1000
#define WH_TEST_AESGCM_KEYID 20
+#define WH_TEST_WRAPEXPORT_KEYID 21
+#define WH_TEST_WRAPEXPORT_NE_KEYID 22
#define WH_TEST_AES_KEYSIZE 32
#define WH_TEST_AES_TEXTSIZE 16
#define WH_TEST_AES_WRAPPED_KEYSIZE \
@@ -85,7 +128,17 @@ static int _CleanupServerKek(whClientContext* client)
#ifdef HAVE_AESGCM
-static int _AesGcm_TestKeyWrap(whClientContext* client, WC_RNG* rng)
+/* The wrap-export and unwrap-and-cache round trips below require a trusted KEK
+ * (HW or WH_NVM_FLAGS_KEK). They use the hardware KEK fixture, which is bound
+ * only by the in-process test server, so compile them only when the server is
+ * in this binary. */
+#if defined(WOLFHSM_CFG_HWKEYSTORE) && defined(WOLFHSM_CFG_ENABLE_SERVER)
+
+/* kekId names the KEK used for every wrap/unwrap step. The unwrap-and-cache
+ * step requires a trusted KEK (HW or WH_NVM_FLAGS_KEK), so callers pass the
+ * hardware KEK fixture id here. */
+static int _AesGcm_TestKeyWrap(whClientContext* client, WC_RNG* rng,
+ whKeyId kekId)
{
int ret = 0;
@@ -122,7 +175,7 @@ static int _AesGcm_TestKeyWrap(whClientContext* client, WC_RNG* rng)
return ret;
}
- ret = wh_Client_KeyWrap(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID, plainKey,
+ ret = wh_Client_KeyWrap(client, WC_CIPHER_AES_GCM, kekId, plainKey,
sizeof(plainKey), &metadata, wrappedKey,
&wrappedKeySz);
if (ret != 0) {
@@ -130,7 +183,7 @@ static int _AesGcm_TestKeyWrap(whClientContext* client, WC_RNG* rng)
return ret;
}
- ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, kekId,
wrappedKey, wrappedKeySz, &wrappedKeyId);
if (ret != 0) {
WH_ERROR_PRINT("Failed to wh_Client_AesGcmKeyWrapCache %d\n", ret);
@@ -186,7 +239,7 @@ static int _AesGcm_TestKeyWrap(whClientContext* client, WC_RNG* rng)
return -1;
}
- ret = wh_Client_KeyUnwrapAndExport(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ ret = wh_Client_KeyUnwrapAndExport(client, WC_CIPHER_AES_GCM, kekId,
wrappedKey, wrappedKeySz, &tmpMetadata,
tmpPlainKey, &tmpPlainKeySz);
if (ret != 0) {
@@ -232,6 +285,330 @@ static int _AesGcm_TestKeyWrap(whClientContext* client, WC_RNG* rng)
return ret;
}
+/* Exercises wh_Client_KeyWrapExport: wrap a key the server already holds (by
+ * id, never presenting plaintext), round-trip it through unwrap-and-cache, use
+ * it, confirm the exported material matches, and confirm NONEXPORTABLE is
+ * honored. */
+static int _AesGcm_TestKeyWrapExport(whClientContext* client, WC_RNG* rng,
+ whKeyId kekId)
+{
+ int ret;
+ uint8_t srcKey[WH_TEST_AES_KEYSIZE];
+ whKeyId srcKeyId = WH_TEST_WRAPEXPORT_KEYID;
+ uint8_t label[WH_NVM_LABEL_LEN] = "WrapExport Src";
+ uint8_t wrappedKey[WH_TEST_AES_WRAPPED_KEYSIZE];
+ uint16_t wrappedKeySz = sizeof(wrappedKey);
+ uint16_t wrappedKeyId = WH_KEYID_ERASED;
+ uint8_t tmpKey[WH_TEST_AES_KEYSIZE];
+ uint16_t tmpKeySz = sizeof(tmpKey);
+ whNvmMetadata tmpMeta = {0};
+
+ Aes aes[1];
+ const uint8_t plaintext[] = "wrap-export by id!";
+ uint8_t ciphertext[sizeof(plaintext)];
+ uint8_t decrypted[sizeof(plaintext)];
+ uint8_t tag[WH_KEYWRAP_AES_GCM_TAG_SIZE];
+ uint8_t iv[WH_KEYWRAP_AES_GCM_IV_SIZE];
+
+ /* 1. Generate and cache a source crypto key (exportable, full usage). */
+ ret = wc_RNG_GenerateBlock(rng, srcKey, sizeof(srcKey));
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to generate src key %d\n", ret);
+ return ret;
+ }
+ ret = wh_Client_KeyCache(client, WH_NVM_FLAGS_USAGE_ANY, label,
+ (uint16_t)sizeof(label), srcKey, sizeof(srcKey),
+ &srcKeyId);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to cache src key %d\n", ret);
+ return ret;
+ }
+
+ /* 2. Wrap-and-export the cached key by id; the client never sees plaintext.
+ */
+ ret = wh_Client_KeyWrapExport(client, WC_CIPHER_AES_GCM, srcKeyId,
+ WH_KEYTYPE_CRYPTO, kekId, wrappedKey,
+ &wrappedKeySz);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wh_Client_KeyWrapExport %d\n", ret);
+ return ret;
+ }
+
+ /* 3. Round-trip: unwrap-and-cache the blob (comes back as a wrapped key).
+ */
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, kekId,
+ wrappedKey, wrappedKeySz, &wrappedKeyId);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to unwrap-and-cache wrap-exported key %d\n",
+ ret);
+ return ret;
+ }
+
+ /* 4. Use the round-tripped key for AES-GCM via its wrapped key id. */
+ ret = wc_AesInit(aes, NULL, WH_DEV_ID);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_AesInit %d\n", ret);
+ return ret;
+ }
+ ret =
+ wh_Client_AesSetKeyId(aes, WH_CLIENT_KEYID_MAKE_WRAPPED(wrappedKeyId));
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wh_Client_AesSetKeyId %d\n", ret);
+ wc_AesFree(aes);
+ return ret;
+ }
+ ret = wc_RNG_GenerateBlock(rng, iv, sizeof(iv));
+ if (ret != 0) {
+ wc_AesFree(aes);
+ return ret;
+ }
+ ret = wc_AesGcmEncrypt(aes, ciphertext, plaintext, sizeof(plaintext), iv,
+ sizeof(iv), tag, sizeof(tag), NULL, 0);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_AesGcmEncrypt %d\n", ret);
+ wc_AesFree(aes);
+ return ret;
+ }
+ ret = wc_AesGcmDecrypt(aes, decrypted, ciphertext, sizeof(ciphertext), iv,
+ sizeof(iv), tag, sizeof(tag), NULL, 0);
+ wc_AesFree(aes);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_AesGcmDecrypt %d\n", ret);
+ return ret;
+ }
+ if (memcmp(decrypted, plaintext, sizeof(plaintext)) != 0) {
+ WH_ERROR_PRINT("wrap-export round-trip decrypt mismatch\n");
+ return WH_ERROR_ABORTED;
+ }
+
+ /* 5. The exported material must match the original, and the embedded id
+ * must have been normalized to the wrapped-key namespace. */
+ ret = wh_Client_KeyUnwrapAndExport(client, WC_CIPHER_AES_GCM, kekId,
+ wrappedKey, wrappedKeySz, &tmpMeta,
+ tmpKey, &tmpKeySz);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to unwrap-and-export wrap-exported key %d\n",
+ ret);
+ return ret;
+ }
+ if (tmpKeySz != sizeof(srcKey) ||
+ memcmp(tmpKey, srcKey, sizeof(srcKey)) != 0) {
+ WH_ERROR_PRINT("wrap-export key material mismatch\n");
+ return WH_ERROR_ABORTED;
+ }
+ if (WH_KEYID_TYPE(tmpMeta.id) != WH_KEYTYPE_WRAPPED) {
+ WH_ERROR_PRINT("wrap-export did not normalize to wrapped type\n");
+ return WH_ERROR_ABORTED;
+ }
+
+ (void)wh_Client_KeyEvict(client,
+ WH_CLIENT_KEYID_MAKE_WRAPPED(wrappedKeyId));
+ (void)wh_Client_KeyEvict(client, srcKeyId);
+
+ /* 6. A NONEXPORTABLE key must be refused by wrap-export. */
+ {
+ whKeyId neKeyId = WH_TEST_WRAPEXPORT_NE_KEYID;
+ uint8_t neLabel[WH_NVM_LABEL_LEN] = "WrapExport NoExp";
+ uint16_t neWrappedSz = sizeof(wrappedKey);
+
+ ret = wh_Client_KeyCache(
+ client, WH_NVM_FLAGS_USAGE_ANY | WH_NVM_FLAGS_NONEXPORTABLE,
+ neLabel, (uint16_t)sizeof(neLabel), srcKey, sizeof(srcKey),
+ &neKeyId);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to cache nonexportable key %d\n", ret);
+ return ret;
+ }
+ ret = wh_Client_KeyWrapExport(client, WC_CIPHER_AES_GCM, neKeyId,
+ WH_KEYTYPE_CRYPTO, kekId, wrappedKey,
+ &neWrappedSz);
+ (void)wh_Client_KeyEvict(client, neKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT(
+ "wrap-export of nonexportable key expected ACCESS, got %d\n",
+ ret);
+ return WH_ERROR_ABORTED;
+ }
+ }
+
+ return WH_ERROR_OK;
+}
+
+#endif /* WOLFHSM_CFG_HWKEYSTORE && WOLFHSM_CFG_ENABLE_SERVER */
+
+/* The wrap-export and unwrap-and-cache operations require a trusted KEK. Prove
+ * that (a) a plain client-cached USAGE_WRAP key (WH_TEST_KEKID, provisioned by
+ * _InitServerKek) is refused as their KEK, and (b) a client cannot forge a
+ * trusted KEK by setting WH_NVM_FLAGS_KEK itself -- the server strips the flag,
+ * so the key is still refused. Needs no hardware keystore, so it runs against
+ * any server. */
+static int _AesGcm_TestTrustedKekPolicy(whClientContext* client, WC_RNG* rng)
+{
+ int ret;
+ uint8_t srcKey[WH_TEST_AES_KEYSIZE];
+ whKeyId srcKeyId = WH_TEST_WRAPEXPORT_KEYID;
+ whKeyId forgeId = WH_TEST_KEKID + 1;
+ uint8_t label[WH_NVM_LABEL_LEN] = "TrustedKek key";
+ uint8_t wrappedKey[WH_TEST_AES_WRAPPED_KEYSIZE];
+ uint16_t wrappedKeySz = sizeof(wrappedKey);
+ whKeyId wrappedKeyId = WH_KEYID_ERASED;
+ whNvmMetadata wrapMeta = {
+ .id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(client->comm->client_id,
+ WH_TEST_AESGCM_KEYID),
+ .label = "TrustedKek blob",
+ .len = WH_TEST_AES_KEYSIZE,
+ .flags = WH_NVM_FLAGS_USAGE_ANY,
+ };
+
+ ret = wc_RNG_GenerateBlock(rng, srcKey, sizeof(srcKey));
+ if (ret != 0) {
+ return ret;
+ }
+
+ /* Cache an ordinary, exportable source key to try to wrap-export. */
+ ret = wh_Client_KeyCache(client, WH_NVM_FLAGS_USAGE_ANY, label,
+ (uint16_t)sizeof(label), srcKey, sizeof(srcKey),
+ &srcKeyId);
+ if (ret != 0) {
+ WH_ERROR_PRINT("trusted-kek: cache src failed %d\n", ret);
+ return ret;
+ }
+
+ /* (a) wrap-export under a plain client KEK must be refused. */
+ ret = wh_Client_KeyWrapExport(client, WC_CIPHER_AES_GCM, srcKeyId,
+ WH_KEYTYPE_CRYPTO, WH_TEST_KEKID, wrappedKey,
+ &wrappedKeySz);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("trusted-kek: wrap-export with plain KEK expected "
+ "ACCESS, got %d\n",
+ ret);
+ (void)wh_Client_KeyEvict(client, srcKeyId);
+ return WH_TEST_FAIL;
+ }
+
+ /* (a) unwrap-and-cache under a plain client KEK must be refused. Build a
+ * correctly sized blob under the same plain KEK (KeyWrap itself does not
+ * require a trusted KEK); the trusted-KEK check rejects the cache attempt
+ * before authentication. */
+ wrappedKeySz = sizeof(wrappedKey);
+ ret =
+ wh_Client_KeyWrap(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID, srcKey,
+ sizeof(srcKey), &wrapMeta, wrappedKey, &wrappedKeySz);
+ if (ret != 0) {
+ WH_ERROR_PRINT("trusted-kek: KeyWrap (plain KEK) failed %d\n", ret);
+ (void)wh_Client_KeyEvict(client, srcKeyId);
+ return ret;
+ }
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ wrappedKey, wrappedKeySz, &wrappedKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("trusted-kek: unwrap-and-cache with plain KEK expected "
+ "ACCESS, got %d\n",
+ ret);
+ (void)wh_Client_KeyEvict(client, srcKeyId);
+ return WH_TEST_FAIL;
+ }
+
+ /* (b) A client that provisions an NVM object carrying WH_NVM_FLAGS_KEK 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. */
+ {
+ whKeyId nvmForgeId = WH_TEST_KEKID + 2;
+ whNvmId nvmObjId = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO,
+ client->comm->client_id, nvmForgeId);
+ int32_t nvmRc = 0;
+
+ ret = wh_Client_NvmAddObject(client, nvmObjId, WH_NVM_ACCESS_ANY,
+ WH_NVM_FLAGS_KEK | 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);
+ (void)wh_Client_KeyEvict(client, srcKeyId);
+ return (ret != 0) ? ret : (int)nvmRc;
+ }
+ wrappedKeySz = sizeof(wrappedKey);
+ ret = wh_Client_KeyWrapExport(client, WC_CIPHER_AES_GCM, srcKeyId,
+ WH_KEYTYPE_CRYPTO, nvmForgeId, wrappedKey,
+ &wrappedKeySz);
+ {
+ int32_t destroyRc = 0;
+ (void)wh_Client_NvmDestroyObjects(client, 1, &nvmObjId, &destroyRc);
+ }
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("trusted-kek: wrap-export with NVM-forged KEK "
+ "expected ACCESS, got %d\n",
+ ret);
+ (void)wh_Client_KeyEvict(client, srcKeyId);
+ return WH_TEST_FAIL;
+ }
+ }
+
+#ifdef HAVE_HKDF
+ /* (c) A client that derives a key with HKDF and asks for it to be cached
+ * carrying WH_NVM_FLAGS_KEK must not obtain a trusted KEK: the crypto
+ * cache-import path strips the flag too. HKDF is deterministic over the
+ * client-supplied inputs, so the client knows the derived bytes; without
+ * the strip it could wrap-export a server secret under a KEK it can
+ * reproduce locally. */
+ {
+ whKeyId hkdfKekId = WH_KEYID_ERASED;
+ const uint8_t hkdfIkm[] = "trusted-kek hkdf ikm";
+ uint8_t hkdfLabel[WH_NVM_LABEL_LEN] = "TrustedKek hkdf";
+
+ ret = wh_Client_HkdfMakeCacheKey(
+ client, WC_SHA256, WH_KEYID_ERASED, hkdfIkm,
+ (uint32_t)sizeof(hkdfIkm), NULL, 0, NULL, 0, &hkdfKekId,
+ WH_NVM_FLAGS_KEK | WH_NVM_FLAGS_USAGE_WRAP, hkdfLabel,
+ (uint32_t)sizeof(hkdfLabel), WH_TEST_AES_KEYSIZE);
+ if (ret != 0) {
+ WH_ERROR_PRINT("trusted-kek: HKDF cache-key make failed %d\n", ret);
+ (void)wh_Client_KeyEvict(client, srcKeyId);
+ return ret;
+ }
+ wrappedKeySz = sizeof(wrappedKey);
+ ret = wh_Client_KeyWrapExport(client, WC_CIPHER_AES_GCM, srcKeyId,
+ WH_KEYTYPE_CRYPTO, hkdfKekId, wrappedKey,
+ &wrappedKeySz);
+ (void)wh_Client_KeyEvict(client, hkdfKekId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("trusted-kek: wrap-export with HKDF-derived KEK "
+ "expected ACCESS, got %d\n",
+ ret);
+ (void)wh_Client_KeyEvict(client, srcKeyId);
+ return WH_TEST_FAIL;
+ }
+ }
+#endif /* HAVE_HKDF */
+
+ /* (d) A client that sets WH_NVM_FLAGS_KEK in its own cache request must not
+ * obtain a trusted KEK: the server strips the flag, so using the key as a
+ * KEK for wrap-export is still refused. */
+ ret = wh_Client_KeyCache(client, WH_NVM_FLAGS_KEK | WH_NVM_FLAGS_USAGE_WRAP,
+ label, (uint16_t)sizeof(label), srcKey,
+ sizeof(srcKey), &forgeId);
+ if (ret != 0) {
+ WH_ERROR_PRINT("trusted-kek: cache forged KEK failed %d\n", ret);
+ (void)wh_Client_KeyEvict(client, srcKeyId);
+ return ret;
+ }
+ wrappedKeySz = sizeof(wrappedKey);
+ ret = wh_Client_KeyWrapExport(client, WC_CIPHER_AES_GCM, srcKeyId,
+ WH_KEYTYPE_CRYPTO, forgeId, wrappedKey,
+ &wrappedKeySz);
+ (void)wh_Client_KeyEvict(client, forgeId);
+ (void)wh_Client_KeyEvict(client, srcKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("trusted-kek: wrap-export with client-flagged KEK "
+ "expected ACCESS, got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+
+ return WH_ERROR_OK;
+}
+
static int _AesGcm_TestDataWrap(whClientContext* client)
{
int ret = 0;
@@ -342,6 +719,243 @@ static int _AesGcm_TestDataUnwrapUnderflow(whClientContext* client)
return WH_ERROR_OK;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+
+static int _AesGcm_TestHwKeystoreKeyWrap(whClientContext* client, WC_RNG* rng)
+{
+ int ret;
+ whKeyId hwKekId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID);
+ uint8_t plainKey[WH_TEST_AES_KEYSIZE];
+ uint8_t tmpPlainKey[WH_TEST_AES_KEYSIZE];
+ uint16_t tmpPlainKeySz = sizeof(tmpPlainKey);
+ uint8_t wrappedKey[WH_TEST_AES_WRAPPED_KEYSIZE];
+ uint16_t wrappedKeySz = sizeof(wrappedKey);
+ whKeyId unwrappedKeyId = WH_KEYID_ERASED;
+ whNvmMetadata metadata = {
+ .id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(client->comm->client_id,
+ WH_TEST_AESGCM_KEYID),
+ .label = "HW KEK wrapped key",
+ .len = WH_TEST_AES_KEYSIZE,
+ .flags = WH_NVM_FLAGS_USAGE_ANY,
+ };
+ whNvmMetadata tmpMetadata = {0};
+
+ ret = wc_RNG_GenerateBlock(rng, plainKey, sizeof(plainKey));
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_RNG_GenerateBlock for key data %d\n", ret);
+ return ret;
+ }
+
+ /* Wrap a key using the hardware-only KEK */
+ ret = wh_Client_KeyWrap(client, WC_CIPHER_AES_GCM, hwKekId, plainKey,
+ sizeof(plainKey), &metadata, wrappedKey,
+ &wrappedKeySz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to wh_Client_KeyWrap with HW KEK %d\n", ret);
+ return ret;
+ }
+
+ /* Unwrap and export with the hardware-only KEK, check the roundtrip */
+ ret = wh_Client_KeyUnwrapAndExport(client, WC_CIPHER_AES_GCM, hwKekId,
+ wrappedKey, wrappedKeySz, &tmpMetadata,
+ tmpPlainKey, &tmpPlainKeySz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT(
+ "Failed to wh_Client_KeyUnwrapAndExport with HW KEK %d\n", ret);
+ return ret;
+ }
+
+ if (memcmp(plainKey, tmpPlainKey, sizeof(plainKey)) != 0) {
+ WH_ERROR_PRINT("HW KEK wrap/unwrap key failed to match\n");
+ return WH_TEST_FAIL;
+ }
+
+ if (memcmp(&metadata, &tmpMetadata, sizeof(metadata)) != 0) {
+ WH_ERROR_PRINT("HW KEK wrap/unwrap metadata failed to match\n");
+ return WH_TEST_FAIL;
+ }
+
+ /* Unwrap-and-cache with the hardware-only KEK: the wrapped payload is an
+ * ordinary key and may enter the cache; only the KEK itself is
+ * hardware-resident */
+ ret =
+ wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, hwKekId,
+ wrappedKey, wrappedKeySz, &unwrappedKeyId);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to wh_Client_KeyUnwrapAndCache with HW KEK %d\n",
+ ret);
+ return ret;
+ }
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvict(client, unwrappedKeyId));
+
+ /* A wrapped+hardware-only KEK id must behave as hardware-only (the
+ * hardware-only flag takes precedence) */
+ wrappedKeySz = sizeof(wrappedKey);
+ ret = wh_Client_KeyWrap(
+ client, WC_CIPHER_AES_GCM, hwKekId | WH_KEYID_CLIENT_WRAPPED_FLAG,
+ plainKey, sizeof(plainKey), &metadata, wrappedKey, &wrappedKeySz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to wh_Client_KeyWrap with wrapped+HW KEK %d\n",
+ ret);
+ return ret;
+ }
+
+ /* Unwrapping a hardware-KEK-wrapped blob with a different, cached KEK
+ * must fail authentication, proving distinct key material was used */
+ WH_TEST_RETURN_ON_FAIL(_InitServerKek(client));
+ tmpPlainKeySz = sizeof(tmpPlainKey);
+ ret = wh_Client_KeyUnwrapAndExport(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ wrappedKey, wrappedKeySz, &tmpMetadata,
+ tmpPlainKey, &tmpPlainKeySz);
+ WH_TEST_RETURN_ON_FAIL(_CleanupServerKek(client));
+ if (ret == WH_ERROR_OK) {
+ WH_ERROR_PRINT("Unwrap with wrong KEK unexpectedly succeeded\n");
+ return WH_TEST_FAIL;
+ }
+
+ /* A hardware KEK id the backend does not serve must fail */
+ wrappedKeySz = sizeof(wrappedKey);
+ ret = wh_Client_KeyWrap(client, WC_CIPHER_AES_GCM,
+ WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID + 1),
+ plainKey, sizeof(plainKey), &metadata, wrappedKey,
+ &wrappedKeySz);
+ if (ret != WH_ERROR_NOTFOUND) {
+ WH_ERROR_PRINT("KeyWrap with unserved HW KEK expected NOTFOUND, "
+ "got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+
+ return WH_ERROR_OK;
+}
+
+static int _AesGcm_TestHwKeystoreDataWrap(whClientContext* client)
+{
+ int ret;
+ whKeyId hwKekId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID);
+ uint8_t data[] = "Example data!";
+ uint8_t unwrappedData[sizeof(data)] = {0};
+ uint32_t unwrappedDataSz = sizeof(unwrappedData);
+ uint8_t wrappedData[sizeof(data) + WH_KEYWRAP_AES_GCM_HEADER_SIZE] = {0};
+ uint32_t wrappedDataSz = sizeof(wrappedData);
+
+ ret = wh_Client_DataWrap(client, WC_CIPHER_AES_GCM, hwKekId, data,
+ sizeof(data), wrappedData, &wrappedDataSz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to wh_Client_DataWrap with HW KEK %d\n", ret);
+ return ret;
+ }
+
+ ret = wh_Client_DataUnwrap(client, WC_CIPHER_AES_GCM, hwKekId, wrappedData,
+ sizeof(wrappedData), unwrappedData,
+ &unwrappedDataSz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to wh_Client_DataUnwrap with HW KEK %d\n", ret);
+ return ret;
+ }
+
+ if (memcmp(data, unwrappedData, sizeof(data)) != 0) {
+ WH_ERROR_PRINT("HW KEK unwrapped data failed to match input data\n");
+ return WH_TEST_FAIL;
+ }
+
+ return WH_ERROR_OK;
+}
+
+/* Hardware-only keys must be rejected by every keystore operation and by
+ * crypto key use; only the keywrap KEK paths may resolve them */
+static int _TestHwKeystoreKeystoreRejections(whClientContext* client)
+{
+ int ret;
+ whKeyId hwKeyId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID);
+ uint8_t buf[WH_TEST_HWKEK_SIZE] = {0};
+ uint16_t bufSz = sizeof(buf);
+ uint8_t label[WH_NVM_LABEL_LEN] = "hwonly reject";
+ whKeyId cacheKeyId = hwKeyId;
+
+ /* Caching key material under a hardware-only id must be rejected */
+ ret = wh_Client_KeyCache(client, WH_NVM_FLAGS_NONE, label, sizeof(label),
+ buf, sizeof(buf), &cacheKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyCache of HW-only id expected ACCESS, got %d\n", ret);
+ return WH_TEST_FAIL;
+ }
+
+#ifdef WOLFHSM_CFG_DMA
+ /* The DMA cache path must reject hardware-only ids as well */
+ cacheKeyId = hwKeyId;
+ ret = wh_Client_KeyCacheDma(client, WH_NVM_FLAGS_NONE, label, sizeof(label),
+ buf, sizeof(buf), &cacheKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyCacheDma of HW-only id expected ACCESS, got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+#endif
+
+ /* Exporting a hardware-only key must be rejected */
+ ret =
+ wh_Client_KeyExport(client, hwKeyId, label, sizeof(label), buf, &bufSz);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyExport of HW-only id expected ACCESS, got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+
+ /* Commit/evict/erase/revoke of a hardware-only key must be rejected */
+ ret = wh_Client_KeyCommit(client, hwKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyCommit of HW-only id expected ACCESS, got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+
+ ret = wh_Client_KeyEvict(client, hwKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyEvict of HW-only id expected ACCESS, got %d\n", ret);
+ return WH_TEST_FAIL;
+ }
+
+ ret = wh_Client_KeyErase(client, hwKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyErase of HW-only id expected ACCESS, got %d\n", ret);
+ return WH_TEST_FAIL;
+ }
+
+ ret = wh_Client_KeyRevoke(client, hwKeyId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("KeyRevoke of HW-only id expected ACCESS, got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+
+ /* Crypto operations must not be able to use hardware-only keys */
+ {
+ Aes aes[1];
+ const uint8_t plaintext[16] = {0};
+ uint8_t ciphertext[sizeof(plaintext)];
+ uint8_t iv[WH_KEYWRAP_AES_GCM_IV_SIZE] = {0};
+ uint8_t tag[WH_KEYWRAP_AES_GCM_TAG_SIZE] = {0};
+
+ WH_TEST_RETURN_ON_FAIL(wc_AesInit(aes, NULL, WH_DEV_ID));
+ WH_TEST_RETURN_ON_FAIL(wh_Client_AesSetKeyId(aes, hwKeyId));
+
+ ret = wc_AesGcmEncrypt(aes, ciphertext, plaintext, sizeof(plaintext),
+ iv, sizeof(iv), tag, sizeof(tag), NULL, 0);
+ wc_AesFree(aes);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("AES-GCM with HW-only key expected ACCESS, "
+ "got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+ }
+
+ return WH_ERROR_OK;
+}
+
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
#endif /* HAVE_AESGCM */
int whTest_Client_KeyWrap(whClientContext* client)
@@ -362,10 +976,25 @@ int whTest_Client_KeyWrap(whClientContext* client)
}
#ifdef HAVE_AESGCM
- ret = _AesGcm_TestKeyWrap(client, rng);
- if (ret != WH_ERROR_OK) {
- WH_ERROR_PRINT("Failed to _AesGcm_TestKeyWrap %d\n", ret);
+#if defined(WOLFHSM_CFG_HWKEYSTORE) && defined(WOLFHSM_CFG_ENABLE_SERVER)
+ /* Wrap-export and unwrap-and-cache need a trusted KEK; use the hardware KEK
+ * fixture bound by the in-process test server. */
+ {
+ whKeyId trustedKek = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID);
+
+ ret = _AesGcm_TestKeyWrap(client, rng, trustedKek);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to _AesGcm_TestKeyWrap %d\n", ret);
+ }
+
+ if (ret == WH_ERROR_OK) {
+ ret = _AesGcm_TestKeyWrapExport(client, rng, trustedKek);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to _AesGcm_TestKeyWrapExport %d\n", ret);
+ }
+ }
}
+#endif /* WOLFHSM_CFG_HWKEYSTORE && WOLFHSM_CFG_ENABLE_SERVER */
if (ret == WH_ERROR_OK) {
ret = _AesGcm_TestKeyUnwrapUnderflow(client);
@@ -374,6 +1003,13 @@ int whTest_Client_KeyWrap(whClientContext* client)
ret);
}
}
+
+ if (ret == WH_ERROR_OK) {
+ ret = _AesGcm_TestTrustedKekPolicy(client, rng);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to _AesGcm_TestTrustedKekPolicy %d\n", ret);
+ }
+ }
#endif
_CleanupServerKek(client);
@@ -412,6 +1048,52 @@ int whTest_Client_DataWrap(whClientContext* client)
return ret;
}
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+/* Requires a server with a hardware keystore backed by
+ * whTest_HwKeystoreGetKeyCb */
+int whTest_Client_HwKeystore(whClientContext* client)
+{
+ int ret = 0;
+ WC_RNG rng[1];
+
+ ret = wc_InitRng_ex(rng, NULL, WH_DEV_ID);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_InitRng_ex %d\n", ret);
+ return ret;
+ }
+
+#ifdef HAVE_AESGCM
+ ret = _AesGcm_TestHwKeystoreKeyWrap(client, rng);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to _AesGcm_TestHwKeystoreKeyWrap %d\n", ret);
+ }
+
+ if (ret == WH_ERROR_OK) {
+ ret = _AesGcm_TestHwKeystoreDataWrap(client);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to _AesGcm_TestHwKeystoreDataWrap %d\n",
+ ret);
+ }
+ }
+
+ if (ret == WH_ERROR_OK) {
+ ret = _TestHwKeystoreKeystoreRejections(client);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to _TestHwKeystoreKeystoreRejections %d\n",
+ ret);
+ }
+ }
+#endif
+
+ if (ret == WH_ERROR_OK) {
+ WH_TEST_PRINT("HW KEYSTORE KEYWRAP TESTS SUCCESS\n");
+ }
+
+ (void)wc_FreeRng(rng);
+ return ret;
+}
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
int whTest_KeyWrapClientConfig(whClientConfig* clientCfg)
{
int ret = 0;
diff --git a/test/wh_test_keywrap.h b/test/wh_test_keywrap.h
index 955fe8e15..92d91245e 100644
--- a/test/wh_test_keywrap.h
+++ b/test/wh_test_keywrap.h
@@ -26,4 +26,26 @@ int whTest_Client_KeyWrap(whClientContext* ctx);
int whTest_Client_DataWrap(whClientContext* ctx);
int whTest_KeyWrapClientConfig(whClientConfig* cf);
-#endif /* WH_TEST_COMM_H_ */
+#if defined(WOLFHSM_CFG_HWKEYSTORE) && defined(WOLFHSM_CFG_KEYWRAP)
+#include "wolfhsm/wh_hwkeystore.h"
+
+/* Test getKey callback emulating a hardware keystore backend; bind this to
+ * the server's whHwKeystoreContext when running whTest_Client_HwKeystore.
+ * Defined in wh_test_keywrap.c, so only available when keywrap is enabled */
+int whTest_HwKeystoreGetKeyCb(void* context, whKeyId keyId, uint8_t* out,
+ uint16_t* inout_len);
+int whTest_Client_HwKeystore(whClientContext* ctx);
+
+/* Convenience callback-table initializer for the emulated test backend. The
+ * backend needs no setup/teardown, so Init and Cleanup are NULL */
+/* clang-format off */
+#define WH_TEST_HWKEYSTORE_CB \
+ { \
+ .Init = NULL, \
+ .Cleanup = NULL, \
+ .GetKey = whTest_HwKeystoreGetKeyCb, \
+ }
+/* clang-format on */
+#endif /* WOLFHSM_CFG_HWKEYSTORE && WOLFHSM_CFG_KEYWRAP */
+
+#endif /* WH_TEST_KEYWRAP_H_ */
diff --git a/test/wh_test_multiclient.c b/test/wh_test_multiclient.c
index 76d3815f4..820da2892 100644
--- a/test/wh_test_multiclient.c
+++ b/test/wh_test_multiclient.c
@@ -79,6 +79,18 @@ static const uint8_t TEST_KEY_DATA_3[] = "TestGlobalKey3DataLonger";
#define DUMMY_KEYID_1 1
#define DUMMY_KEYID_2 2
+#ifdef WOLFHSM_CFG_KEYWRAP
+/* Trusted KEK for unwrap-and-cache. The test setup provisions it in the shared
+ * NVM with WH_NVM_FLAGS_KEK (the way whnvmtool would), since unwrap-and-cache
+ * now requires a trusted KEK a client can never upload. Distinct global id, so
+ * it does not collide with the DUMMY_KEYID_* keys the other tests use. */
+#define WH_TEST_MC_WRAP_KEK_ID 0x30
+static const uint8_t s_mcWrapKek[32] = {
+ 0x03, 0x03, 0x0d, 0xd9, 0xeb, 0x18, 0x17, 0x2e, 0x06, 0x6e, 0x19,
+ 0xce, 0x98, 0x44, 0x54, 0x0d, 0x78, 0xa0, 0xbe, 0xe7, 0x35, 0x43,
+ 0x40, 0xa4, 0x22, 0x8a, 0xd1, 0x0e, 0xa3, 0x63, 0x1c, 0x0b};
+#endif /* WOLFHSM_CFG_KEYWRAP */
+
/* ============================================================================
* MULTI-CLIENT TEST FRAMEWORK INFRASTRUCTURE
* ========================================================================== */
@@ -599,9 +611,8 @@ static int _testGlobalKeyUnwrapCache(whClientContext* client1,
whServerContext* server2)
{
int ret;
- whKeyId serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1);
- whKeyId cachedKeyId = 0;
- uint8_t wrapKey[AES_256_KEY_SIZE] = "GlobalUnwrapKey123456789012!";
+ whKeyId serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(WH_TEST_MC_WRAP_KEK_ID);
+ whKeyId cachedKeyId = 0;
uint8_t plainKey[AES_256_KEY_SIZE] = "KeyToCacheViaUnwrap123456!!";
#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata))
uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0};
@@ -614,15 +625,9 @@ static int _testGlobalKeyUnwrapCache(whClientContext* client1,
WH_TEST_PRINT("Test: Key unwrap and cache with global server key\n");
- /* Client 1 caches a global wrapping key */
- WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex(
- client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"UnwrapKey50",
- sizeof("UnwrapKey50"), wrapKey, sizeof(wrapKey), serverKeyId));
- WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1));
- WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId));
-
- /* Client 1 wraps a global key */
- serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1);
+ /* The trusted KEK is provisioned in NVM by the test setup; client 1 wraps a
+ * global key under it. */
+ serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(WH_TEST_MC_WRAP_KEK_ID);
meta.id =
WH_CLIENT_KEYID_MAKE_WRAPPED_META(WH_KEYUSER_GLOBAL, DUMMY_KEYID_2);
meta.len = sizeof(plainKey);
@@ -634,8 +639,8 @@ static int _testGlobalKeyUnwrapCache(whClientContext* client1,
WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse(
client1, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz));
- /* Client 2 unwraps and caches the key using the global server key */
- serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1);
+ /* Client 2 unwraps and caches the key using the trusted KEK */
+ serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(WH_TEST_MC_WRAP_KEK_ID);
ret = wh_Client_KeyUnwrapAndCacheRequest(client2, WC_CIPHER_AES_GCM,
serverKeyId, wrappedKey,
sizeof(wrappedKey));
@@ -664,10 +669,8 @@ static int _testGlobalKeyUnwrapCache(whClientContext* client1,
WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2));
WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client2));
- serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1);
- WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId));
- WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1));
- WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1));
+ /* The KEK is server-owned in NVM (carries WH_NVM_FLAGS_KEK) and is not
+ * client-evictable, so there is nothing to clean up for it here. */
WH_TEST_PRINT(" PASS: Key unwrap and cache with global server key\n");
@@ -1104,8 +1107,7 @@ static int _testWrappedKey_LocalWrap_GlobalKey_AnyCacheGlobal(
whClientContext* client2, whServerContext* server2)
{
int ret;
- whKeyId serverKeyId = DUMMY_KEYID_1; /* Local wrapping key */
- uint8_t wrapKey[AES_256_KEY_SIZE] = "LocalWrapKey2Test10aXXXXXXXXX!";
+ whKeyId serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(WH_TEST_MC_WRAP_KEK_ID);
uint8_t plainKey[AES_256_KEY_SIZE] = "GlobalPlainKey2Test10aXXXXXXX!";
#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata))
uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0};
@@ -1119,15 +1121,9 @@ static int _testWrappedKey_LocalWrap_GlobalKey_AnyCacheGlobal(
WH_TEST_DEBUG_PRINT("Test 10a: Local wrap key + Global wrapped key (Cache global)\n");
- /* Client 1 caches a LOCAL wrapping key */
- WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex(
- client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey_10a",
- sizeof("WrapKey_10a"), wrapKey, sizeof(wrapKey), serverKeyId));
- WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1));
- WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId));
-
- /* Client 1 wraps a GLOBAL key (USER=0) */
- serverKeyId = DUMMY_KEYID_1; /* Use local wrapping key */
+ /* The trusted KEK is provisioned in NVM by the test setup; client 1 wraps a
+ * GLOBAL key (USER=0) under it. */
+ serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(WH_TEST_MC_WRAP_KEK_ID);
meta.id =
WH_CLIENT_KEYID_MAKE_WRAPPED_META(WH_KEYUSER_GLOBAL, DUMMY_KEYID_2);
meta.len = sizeof(plainKey);
@@ -1162,10 +1158,8 @@ static int _testWrappedKey_LocalWrap_GlobalKey_AnyCacheGlobal(
WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2));
WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client2));
- serverKeyId = DUMMY_KEYID_1;
- WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId));
- WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1));
- WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1));
+ /* The KEK is server-owned in NVM (WH_NVM_FLAGS_KEK) and not
+ * client-evictable, so there is nothing to clean up for it here. */
WH_TEST_PRINT(" PASS: Local wrap key + Global wrapped key (Cache global)\n");
@@ -1565,6 +1559,25 @@ static int whTest_MultiClientSequential(void)
if (ret != 0)
return ret;
+#ifdef WOLFHSM_CFG_KEYWRAP
+ /* Provision the trusted KEK into the shared NVM before any client runs, the
+ * way whnvmtool would. WH_NVM_FLAGS_KEK makes it the trusted KEK that
+ * unwrap-and-cache requires; both servers freshen it from this NVM. */
+ {
+ whNvmMetadata kekMeta = {0};
+ kekMeta.id = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, WH_KEYUSER_GLOBAL,
+ WH_TEST_MC_WRAP_KEK_ID);
+ kekMeta.access = WH_NVM_ACCESS_ANY;
+ kekMeta.flags = WH_NVM_FLAGS_KEK | WH_NVM_FLAGS_USAGE_WRAP |
+ WH_NVM_FLAGS_NONEXPORTABLE | WH_NVM_FLAGS_NONMODIFIABLE;
+ kekMeta.len = (whNvmSize)sizeof(s_mcWrapKek);
+ memcpy(kekMeta.label, "MC wrap KEK", sizeof("MC wrap KEK"));
+ ret = wh_Nvm_AddObject(nvm, &kekMeta, kekMeta.len, s_mcWrapKek);
+ if (ret != 0)
+ return ret;
+ }
+#endif /* WOLFHSM_CFG_KEYWRAP */
+
#if !defined(WOLFHSM_CFG_NO_CRYPTO)
/* Initialize RNGs */
ret = wc_InitRng_ex(crypto1->rng, NULL, INVALID_DEVID);
diff --git a/test/wh_test_she.c b/test/wh_test_she.c
index b15deddd4..34e27a049 100644
--- a/test/wh_test_she.c
+++ b/test/wh_test_she.c
@@ -66,6 +66,7 @@
#include "wolfssl/wolfcrypt/cmac.h"
#include "wh_test_common.h"
+#include "wh_test_she_no_nvm.h"
#if defined(WOLFHSM_CFG_TEST_POSIX)
#include /* For sleep */
@@ -92,6 +93,20 @@ enum {
#define TEST_ADMIN_PIN "1234"
#endif
+#if defined(WOLFHSM_CFG_KEYWRAP) && defined(HAVE_AESGCM)
+/* The SHE<->keywrap interop tests use a single trusted KEK that the server
+ * provisions in NVM with WH_NVM_FLAGS_KEK (see _ProvisionSheServerKek), the way
+ * whnvmtool would on an NVM-backed device. A client can never set that flag, so
+ * the KEK cannot be uploaded from the client: the client only names it by id,
+ * and where it builds blobs itself it wraps under the same known bytes. Defined
+ * outside WOLFHSM_CFG_ENABLE_CLIENT so the server task can provision it too. */
+#define WH_SHE_INTEROP_KEK_ID 0x20
+static const uint8_t s_sheKek[32] = {
+ 0x03, 0x03, 0x0d, 0xd9, 0xeb, 0x18, 0x17, 0x2e, 0x06, 0x6e, 0x19,
+ 0xce, 0x98, 0x44, 0x54, 0x0d, 0x78, 0xa0, 0xbe, 0xe7, 0x35, 0x43,
+ 0x40, 0xa4, 0x22, 0x8a, 0xd1, 0x0e, 0xa3, 0x63, 0x1c, 0x0b};
+#endif /* WOLFHSM_CFG_KEYWRAP && HAVE_AESGCM */
+
#ifdef WOLFHSM_CFG_ENABLE_CLIENT
/* Helper function to destroy a SHE key so the unit tests don't
* leak NVM objects across invocations. Necessary, as SHE doesn't expose a
@@ -111,6 +126,61 @@ static int _destroySheKey(whClientContext* client, whNvmId clientSheKeyId)
return rc;
}
+#if defined(WOLFHSM_CFG_KEYWRAP) && defined(HAVE_AESGCM)
+/* Build an AES-GCM wrapped-key blob for a SHE key the same way the server's KEK
+ * would, so a test can drive unwrap-and-cache without first having to cache the
+ * target slot. Uses software AES with the known KEK bytes. Blob layout matches
+ * the server: [IV(12) || authTag(16) || GCM(metadata || key)]. */
+static int _BuildSheKeyBlob(const uint8_t* kek, word32 kekSz, whKeyId sheKeyId,
+ uint32_t counter, uint32_t sheFlags,
+ const uint8_t* keyBytes, uint8_t* blobOut,
+ uint16_t* blobInOutSz)
+{
+ int ret;
+ Aes aes[1];
+ whNvmMetadata meta;
+ uint8_t plain[sizeof(whNvmMetadata) + WH_SHE_KEY_SZ];
+ uint8_t iv[WH_KEYWRAP_AES_GCM_IV_SIZE];
+ uint8_t tag[WH_KEYWRAP_AES_GCM_TAG_SIZE];
+ uint16_t need = (uint16_t)(WH_KEYWRAP_AES_GCM_HEADER_SIZE + sizeof(meta) +
+ WH_SHE_KEY_SZ);
+
+ if (*blobInOutSz < need) {
+ return WH_ERROR_BUFFER_SIZE;
+ }
+
+ memset(&meta, 0, sizeof(meta));
+ meta.id = sheKeyId;
+ meta.len = WH_SHE_KEY_SZ;
+ wh_She_Meta2Label(counter, sheFlags, meta.label);
+
+ memcpy(plain, &meta, sizeof(meta));
+ memcpy(plain + sizeof(meta), keyBytes, WH_SHE_KEY_SZ);
+
+ memset(iv, 0x24, sizeof(iv)); /* a fixed IV is fine for a test blob */
+
+ ret = wc_AesInit(aes, NULL, INVALID_DEVID);
+ if (ret != 0) {
+ return ret;
+ }
+ ret = wc_AesGcmSetKey(aes, kek, kekSz);
+ if (ret == 0) {
+ ret = wc_AesGcmEncrypt(aes, blobOut + WH_KEYWRAP_AES_GCM_HEADER_SIZE,
+ plain, sizeof(plain), iv, sizeof(iv), tag,
+ sizeof(tag), NULL, 0);
+ }
+ wc_AesFree(aes);
+ if (ret != 0) {
+ return ret;
+ }
+
+ memcpy(blobOut, iv, sizeof(iv));
+ memcpy(blobOut + sizeof(iv), tag, sizeof(tag));
+ *blobInOutSz = need;
+ return 0;
+}
+#endif /* WOLFHSM_CFG_KEYWRAP && HAVE_AESGCM */
+
int whTest_SheClientConfig(whClientConfig* config)
{
int ret = 0;
@@ -472,6 +542,173 @@ int whTest_SheClientConfig(whClientConfig* config)
goto exit;
}
+#if defined(WOLFHSM_CFG_KEYWRAP) && defined(HAVE_AESGCM)
+ /* SHE <-> keywrap interop: wrap-export a SHE key, prime an unused SHE slot
+ * via unwrap-and-cache and use it, and verify the SHE counter rollback
+ * guard on unwrap-and-cache. */
+ {
+ /* The KEK is the trusted, server-resident WH_NVM_FLAGS_KEK key the
+ * server task provisioned in NVM; the client only names it by id. */
+ whKeyId kekId = WH_SHE_INTEROP_KEK_ID;
+ uint8_t blob[128];
+ uint16_t blobSz;
+ uint16_t expSz = (uint16_t)(WH_KEYWRAP_AES_GCM_HEADER_SIZE +
+ sizeof(whNvmMetadata) + WH_SHE_KEY_SZ);
+ const whNvmId SHE_PRIME_SLOT = 6;
+ const whNvmId SHE_CTR_SLOT = 7;
+ uint8_t sheKey[WH_SHE_KEY_SZ];
+ 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];
+
+ /* The server freshens the NVM-resident KEK on demand into an evictable
+ * cache slot, so it does not pin a slot the way a client-cached KEK
+ * would -- no KeyCommit workaround is needed for the second
+ * unwrap-and-cache below. */
+
+ /* (1) Wrap-and-export the cached RAM key (slot 14) by id; the blob must
+ * keep TYPE=SHE and be the expected size. */
+ blobSz = sizeof(blob);
+ ret = wh_Client_KeyWrapExport(client, WC_CIPHER_AES_GCM,
+ WH_SHE_RAM_KEY_ID, WH_KEYTYPE_SHE, kekId,
+ blob, &blobSz);
+ if (ret != 0 || blobSz != expSz) {
+ WH_ERROR_PRINT("SHE wrap-export failed ret=%d sz=%u exp=%u\n", ret,
+ blobSz, expSz);
+ ret = (ret != 0) ? ret : WH_ERROR_ABORTED;
+ goto exit;
+ }
+
+ /* (1b) Regression for the slot-0 wrap-export gate: WH_SHE_SECRET_KEY_ID
+ * has ID field == 0, which the keystore policy gate
+ * (_KeystoreCheckPolicy) previously rejected as an unassigned id, so a
+ * SHE SECRET_KEY could be primed and used but never wrap-exported. The
+ * pre-programmed SECRET_KEY (slot 0, loaded earlier in this test) must
+ * wrap-export like any other SHE slot. */
+ blobSz = sizeof(blob);
+ ret = wh_Client_KeyWrapExport(client, WC_CIPHER_AES_GCM,
+ WH_SHE_SECRET_KEY_ID, WH_KEYTYPE_SHE,
+ kekId, blob, &blobSz);
+ if (ret != 0 || blobSz != expSz) {
+ WH_ERROR_PRINT(
+ "SHE slot-0 wrap-export failed ret=%d sz=%u exp=%u\n", ret,
+ blobSz, expSz);
+ ret = (ret != 0) ? ret : WH_ERROR_ABORTED;
+ goto exit;
+ }
+
+ /* (2) Prime an unused SHE slot via unwrap-and-cache, then use it. */
+ memset(sheKey, 0x5a, sizeof(sheKey));
+ blobSz = sizeof(blob);
+ ret = _BuildSheKeyBlob(s_sheKek, sizeof(s_sheKek),
+ WH_MAKE_KEYID(WH_KEYTYPE_SHE,
+ client->comm->client_id,
+ SHE_PRIME_SLOT),
+ 1, 0, sheKey, blob, &blobSz);
+ if (ret != 0) {
+ WH_ERROR_PRINT("SHE interop: build prime blob failed %d\n", ret);
+ goto exit;
+ }
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, kekId,
+ blob, blobSz, &outId);
+ if (ret != 0) {
+ WH_ERROR_PRINT("SHE unwrap-and-cache failed %d\n", ret);
+ goto exit;
+ }
+ memset(ecbIn, 0x11, sizeof(ecbIn));
+ ret = wh_Client_SheEncEcb(client, SHE_PRIME_SLOT, ecbIn, ecbOut,
+ sizeof(ecbIn));
+ if (ret == 0) {
+ ret = wh_Client_SheDecEcb(client, SHE_PRIME_SLOT, ecbOut, ecbBack,
+ sizeof(ecbOut));
+ }
+ if (ret != 0 || memcmp(ecbIn, ecbBack, sizeof(ecbIn)) != 0) {
+ WH_ERROR_PRINT("SHE primed-key ECB round trip failed %d\n", ret);
+ ret = (ret != 0) ? ret : WH_ERROR_ABORTED;
+ goto exit;
+ }
+
+ /* (3) Counter guard (anti-rollback) on the SHE unwrap-and-cache path
+ * (_HandleKeyUnwrapAndCacheRequest): seed an NVM SHE slot with
+ * counter=5, then exercise both branches of the guard:
+ * - a lower-counter (3) prime is rejected with WH_ERROR_ACCESS;
+ * - an equal-counter (5) prime is accepted (the guard is
+ * allow-equal). The baseline prime in (2) targets an empty slot, so its
+ * counter comparison is skipped (no stored counter in NVM) rather than
+ * taking the accept branch. */
+ 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;
+ }
+ if (ret != 0) {
+ WH_ERROR_PRINT("SHE interop: seed counter slot failed %d\n", ret);
+ goto exit;
+ }
+ /* lower counter -> rejected */
+ blobSz = sizeof(blob);
+ ret = _BuildSheKeyBlob(s_sheKek, sizeof(s_sheKek),
+ WH_MAKE_KEYID(WH_KEYTYPE_SHE,
+ client->comm->client_id,
+ SHE_CTR_SLOT),
+ 3, 0, sheKey, blob, &blobSz);
+ if (ret != 0) {
+ goto exit;
+ }
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, kekId,
+ blob, blobSz, &outId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("SHE counter rollback expected ACCESS, got %d\n",
+ ret);
+ ret = WH_ERROR_ABORTED;
+ goto exit;
+ }
+
+ /* equal counter (== stored 5) -> accepted (allow-equal). This is the
+ * accept branch the baseline prime in (2) never reached, because
+ * SHE_CTR_SLOT already has a committed counter of 5 in NVM. */
+ blobSz = sizeof(blob);
+ ret = _BuildSheKeyBlob(s_sheKek, sizeof(s_sheKek),
+ WH_MAKE_KEYID(WH_KEYTYPE_SHE,
+ client->comm->client_id,
+ SHE_CTR_SLOT),
+ 5, 0, sheKey, blob, &blobSz);
+ if (ret != 0) {
+ goto exit;
+ }
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, kekId,
+ blob, blobSz, &outId);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("SHE counter equal expected OK, got %d\n", ret);
+ ret = (ret != 0) ? ret : WH_ERROR_ABORTED;
+ goto exit;
+ }
+
+ /* cleanup: destroy the NVM counter slot. The KEK is server-owned; the
+ * client must not be able to evict it (it carries WH_NVM_FLAGS_KEK),
+ * which doubles as an immutability check. */
+ (void)_destroySheKey(client, SHE_CTR_SLOT);
+ ret = wh_Client_KeyEvict(client, kekId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("SHE interop: KEK evict expected ACCESS, got %d\n",
+ ret);
+ ret = (ret == 0) ? WH_ERROR_ABORTED : ret;
+ goto exit;
+ }
+ ret = 0;
+
+ WH_TEST_PRINT("SHE <-> keywrap interop SUCCESS\n");
+ }
+#endif /* WOLFHSM_CFG_KEYWRAP && HAVE_AESGCM */
+
/* destroy "pre-programmed" keys so we don't leak NVM */
if ((ret = _destroySheKey(client, WH_SHE_BOOT_MAC_KEY_ID)) != 0) {
WH_ERROR_PRINT("Failed to _destroySheKey, ret=%d\n", ret);
@@ -900,6 +1137,29 @@ static int whTest_SheWriteProtect(whClientConfig* config)
#endif /* WOLFHSM_CFG_ENABLE_CLIENT */
#ifdef WOLFHSM_CFG_ENABLE_SERVER
+#if defined(WOLFHSM_CFG_KEYWRAP) && defined(HAVE_AESGCM)
+/* Provision the trusted keywrap KEK in NVM with WH_NVM_FLAGS_KEK, the way
+ * whnvmtool would on an NVM-backed device. The flag (which clients can never
+ * set) makes it the trusted KEK that wrap-export and unwrap-and-cache require.
+ * Provisioning it in NVM (not the cache) means it is freshened on demand and
+ * does not pin a cache slot. The id matches what clients name: plain
+ * WH_SHE_INTEROP_KEK_ID translated against WH_TEST_DEFAULT_CLIENT_ID. */
+static int _ProvisionSheServerKek(whServerContext* server)
+{
+ whNvmMetadata meta = {0};
+
+ meta.id = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, WH_TEST_DEFAULT_CLIENT_ID,
+ WH_SHE_INTEROP_KEK_ID);
+ meta.access = WH_NVM_ACCESS_ANY;
+ meta.flags = WH_NVM_FLAGS_KEK | WH_NVM_FLAGS_USAGE_WRAP |
+ WH_NVM_FLAGS_NONEXPORTABLE | WH_NVM_FLAGS_NONMODIFIABLE;
+ meta.len = (whNvmSize)sizeof(s_sheKek);
+ memcpy(meta.label, "SHE interop KEK", sizeof("SHE interop KEK"));
+
+ return wh_Nvm_AddObject(server->nvm, &meta, meta.len, s_sheKek);
+}
+#endif /* WOLFHSM_CFG_KEYWRAP && HAVE_AESGCM */
+
int whTest_SheServerConfig(whServerConfig* config)
{
whServerContext server[1] = {0};
@@ -911,6 +1171,10 @@ int whTest_SheServerConfig(whServerConfig* config)
}
WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, config));
+#if defined(WOLFHSM_CFG_KEYWRAP) && defined(HAVE_AESGCM)
+ /* Trusted keywrap KEK is provisioned before the server accepts requests. */
+ WH_TEST_RETURN_ON_FAIL(_ProvisionSheServerKek(server));
+#endif
WH_TEST_RETURN_ON_FAIL(wh_Server_SetConnected(server, am_connected));
while(am_connected == WH_COMM_CONNECTED) {
@@ -1649,6 +1913,275 @@ static int wh_She_TestReqSizeChecking(void)
#if defined(WOLFHSM_CFG_TEST_POSIX) && defined(WOLFHSM_CFG_ENABLE_CLIENT) && \
defined(WOLFHSM_CFG_ENABLE_SERVER)
+
+#if defined(WOLFHSM_CFG_KEYWRAP) && defined(HAVE_AESGCM)
+/*
+ * End-to-end SHE <-> keywrap reboot interop, run across two server sessions to
+ * model a real power cycle: each wh_ClientServer_MemThreadTest() call builds a
+ * fresh server (empty cache) and fresh RAM-NVM. The client carries only the
+ * wrapped blob across the "reset" (the no-NVM premise).
+ *
+ * Session 1 (provision): secure boot -> offline M1/M2/M3 -> SheLoadKey a
+ * target key -> SheEncEcb (capture ciphertext) -> KeyWrapExport the key by
+ * id -> save the blob.
+ *
+ * Session 2 (restore): secure boot -> KeyUnwrapAndCache the saved blob (NVM
+ * has no target key, so it is restored purely from the client's blob) ->
+ * SheEncEcb -> must match session 1.
+ *
+ * The KEK itself is the trusted WH_NVM_FLAGS_KEK key the server task provisions
+ * in NVM in both sessions (s_sheKek); the client never uploads it.
+ */
+
+/* Shared inputs that must match across the two sessions. */
+static uint8_t s_interopUid[WH_SHE_UID_SZ] = {0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01};
+static uint8_t s_interopPlain[WH_SHE_KEY_SZ] = {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
+ 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00};
+/* Carried from the provision session to the restore session. */
+static uint8_t s_interopBlob[256];
+static uint16_t s_interopBlobSz;
+static uint8_t s_interopCipher[WH_SHE_KEY_SZ];
+
+#define WH_SHE_INTEROP_TARGET_SLOT 4
+
+/* Establish secure-boot state so SHE key operations are permitted. Both
+ * sessions must do this (a fresh server starts un-booted). */
+static int _SheInteropSecureBoot(whClientContext* client)
+{
+ int ret;
+ Cmac cmac[1];
+ uint8_t bootMacKey[WH_SHE_KEY_SZ] = {0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6,
+ 0x07, 0x18, 0x29, 0x3a, 0x4b, 0x5c,
+ 0x6d, 0x7e, 0x8f, 0x90};
+ uint8_t bootloader[64];
+ uint32_t bootloaderSz = sizeof(bootloader);
+ uint8_t zeros[WH_SHE_BOOT_MAC_PREFIX_LEN] = {0};
+ uint8_t digest[WH_SHE_KEY_SZ] = {0};
+ uint32_t digestSz = sizeof(digest);
+ uint8_t sreg = 0;
+
+ memset(bootloader, 0xB7, sizeof(bootloader));
+
+ /* boot MAC digest = CMAC_bootMacKey(zeros || size || bootloader) */
+ if ((ret = wc_InitCmac(cmac, bootMacKey, sizeof(bootMacKey), WC_CMAC_AES,
+ NULL)) != 0) {
+ return ret;
+ }
+ if ((ret = wc_CmacUpdate(cmac, zeros, sizeof(zeros))) != 0) {
+ return ret;
+ }
+ if ((ret = wc_CmacUpdate(cmac, (uint8_t*)&bootloaderSz,
+ sizeof(bootloaderSz))) != 0) {
+ return ret;
+ }
+ if ((ret = wc_CmacUpdate(cmac, bootloader, sizeof(bootloader))) != 0) {
+ return ret;
+ }
+ digestSz = AES_BLOCK_SIZE;
+ if ((ret = wc_CmacFinal(cmac, digest, (word32*)&digestSz)) != 0) {
+ return ret;
+ }
+
+ if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0,
+ bootMacKey, sizeof(bootMacKey))) !=
+ 0) {
+ return ret;
+ }
+ if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, digest,
+ sizeof(digest))) != 0) {
+ return ret;
+ }
+ if ((ret = wh_Client_SheSetUid(client, s_interopUid,
+ sizeof(s_interopUid))) != 0) {
+ return ret;
+ }
+ if ((ret = wh_Client_SheSecureBoot(client, bootloader, bootloaderSz)) !=
+ 0) {
+ return ret;
+ }
+ if ((ret = wh_Client_SheGetStatus(client, &sreg)) != 0) {
+ return ret;
+ }
+ if ((sreg & WH_SHE_SREG_BOOT_OK) == 0 ||
+ (sreg & WH_SHE_SREG_BOOT_FINISHED) == 0 ||
+ (sreg & WH_SHE_SREG_SECURE_BOOT) == 0) {
+ return WH_ERROR_ABORTED;
+ }
+ return 0;
+}
+
+static int _SheInteropProvision(whClientConfig* config)
+{
+ int ret;
+ whClientContext client[1] = {0};
+ uint32_t outClientId = 0;
+ uint32_t outServerId = 0;
+ uint8_t secretKey[WH_SHE_KEY_SZ] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae,
+ 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88,
+ 0x09, 0xcf, 0x4f, 0x3c};
+ uint8_t masterEcuKey[WH_SHE_KEY_SZ] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
+ 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
+ 0x0c, 0x0d, 0x0e, 0x0f};
+ uint8_t targetKey[WH_SHE_KEY_SZ] = {0xde, 0xad, 0xbe, 0xef, 0x01, 0x23,
+ 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
+ 0xfe, 0xdc, 0xba, 0x98};
+ uint8_t m1[WH_SHE_M1_SZ];
+ uint8_t m2[WH_SHE_M2_SZ];
+ uint8_t m3[WH_SHE_M3_SZ];
+ uint8_t m4[WH_SHE_M4_SZ];
+ uint8_t m5[WH_SHE_M5_SZ];
+ uint8_t o4[WH_SHE_M4_SZ];
+ uint8_t o5[WH_SHE_M5_SZ];
+ uint16_t blobSz = sizeof(s_interopBlob);
+
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client, config));
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Client_CommInit(client, &outClientId, &outServerId));
+#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION
+ WH_TEST_RETURN_ON_FAIL(wh_Client_AuthLogin(
+ client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, TEST_ADMIN_PIN,
+ strlen(TEST_ADMIN_PIN), &ret, NULL));
+#endif
+
+ ret = _SheInteropSecureBoot(client);
+ if (ret != 0) {
+ WH_ERROR_PRINT("interop provision: secure boot failed %d\n", ret);
+ goto exit;
+ }
+
+ /* 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));
+ if (ret != 0) {
+ goto exit;
+ }
+ ret = wh_She_GenerateLoadableKey(
+ WH_SHE_MASTER_ECU_KEY_ID, WH_SHE_SECRET_KEY_ID, 1, 0, s_interopUid,
+ masterEcuKey, secretKey, m1, m2, m3, m4, m5);
+ if (ret != 0) {
+ goto exit;
+ }
+ ret = wh_Client_SheLoadKey(client, m1, m2, m3, o4, o5);
+ if (ret != 0) {
+ WH_ERROR_PRINT("interop provision: load master ECU failed %d\n", ret);
+ goto exit;
+ }
+ ret = wh_She_GenerateLoadableKey(
+ WH_SHE_INTEROP_TARGET_SLOT, WH_SHE_MASTER_ECU_KEY_ID, 1, 0,
+ s_interopUid, targetKey, masterEcuKey, m1, m2, m3, m4, m5);
+ if (ret != 0) {
+ goto exit;
+ }
+ ret = wh_Client_SheLoadKey(client, m1, m2, m3, o4, o5);
+ if (ret != 0) {
+ WH_ERROR_PRINT(
+ "interop provision: load target via M1/M2/M3 failed %d\n", ret);
+ goto exit;
+ }
+
+ /* Capture the target key's ECB output for cross-session comparison. */
+ ret =
+ wh_Client_SheEncEcb(client, WH_SHE_INTEROP_TARGET_SLOT, s_interopPlain,
+ s_interopCipher, sizeof(s_interopPlain));
+ if (ret != 0) {
+ goto exit;
+ }
+
+ /* Wrap-export the target key by id under the server's trusted KEK. */
+ ret = wh_Client_KeyWrapExport(
+ client, WC_CIPHER_AES_GCM, WH_SHE_INTEROP_TARGET_SLOT, WH_KEYTYPE_SHE,
+ WH_SHE_INTEROP_KEK_ID, s_interopBlob, &blobSz);
+ if (ret != 0) {
+ WH_ERROR_PRINT("interop provision: wrap-export failed %d\n", ret);
+ goto exit;
+ }
+ s_interopBlobSz = blobSz;
+
+exit:
+ (void)wh_Client_CommClose(client);
+ (void)wh_Client_Cleanup(client);
+ return ret;
+}
+
+static int _SheInteropRestore(whClientConfig* config)
+{
+ int ret;
+ whClientContext client[1] = {0};
+ uint32_t outClientId = 0;
+ uint32_t outServerId = 0;
+ uint8_t cipher[WH_SHE_KEY_SZ] = {0};
+ uint16_t outId = 0;
+
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client, config));
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Client_CommInit(client, &outClientId, &outServerId));
+#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION
+ WH_TEST_RETURN_ON_FAIL(wh_Client_AuthLogin(
+ client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, TEST_ADMIN_PIN,
+ strlen(TEST_ADMIN_PIN), &ret, NULL));
+#endif
+
+ /* Fresh boot: re-establish secure-boot state. The target SHE key is NOT in
+ * NVM (this server's NVM is empty); the trusted KEK was provisioned in NVM
+ * by the server task, so the client just names it. */
+ ret = _SheInteropSecureBoot(client);
+ if (ret != 0) {
+ WH_ERROR_PRINT("interop restore: secure boot failed %d\n", ret);
+ goto exit;
+ }
+
+ /* Prime the SHE key purely from the client-held wrapped blob. */
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM,
+ WH_SHE_INTEROP_KEK_ID, s_interopBlob,
+ s_interopBlobSz, &outId);
+ if (ret != 0) {
+ WH_ERROR_PRINT("interop restore: unwrap-and-cache failed %d\n", ret);
+ goto exit;
+ }
+
+ /* Use the restored key via the SHE API; it must reproduce the provision
+ * session's ciphertext, proving the exact key round-tripped. */
+ ret = wh_Client_SheEncEcb(client, WH_SHE_INTEROP_TARGET_SLOT,
+ s_interopPlain, cipher, sizeof(s_interopPlain));
+ if (ret != 0) {
+ WH_ERROR_PRINT("interop restore: SheEncEcb failed %d\n", ret);
+ goto exit;
+ }
+ if (memcmp(cipher, s_interopCipher, sizeof(cipher)) != 0) {
+ WH_ERROR_PRINT("interop restore: restored key does not match\n");
+ ret = WH_ERROR_ABORTED;
+ goto exit;
+ }
+ WH_TEST_PRINT("SHE wrapped-key reboot interop SUCCESS\n");
+
+exit:
+ (void)wh_Client_CommClose(client);
+ (void)wh_Client_Cleanup(client);
+ return ret;
+}
+
+/* Drive the two sessions back-to-back. Each MemThreadTest call uses a fresh
+ * server + NVM, modeling the power cycle between provision and restore. */
+static int wh_She_TestWrappedInterop(void)
+{
+ int ret;
+
+ s_interopBlobSz = 0;
+ memset(s_interopBlob, 0, sizeof(s_interopBlob));
+ memset(s_interopCipher, 0, sizeof(s_interopCipher));
+
+ ret = wh_ClientServer_MemThreadTest(_SheInteropProvision);
+ if (ret != 0) {
+ return ret;
+ }
+ return wh_ClientServer_MemThreadTest(_SheInteropRestore);
+}
+#endif /* WOLFHSM_CFG_KEYWRAP && HAVE_AESGCM */
+
int whTest_She(void)
{
WH_TEST_PRINT("Testing SHE: master ECU key fallback...\n");
@@ -1665,6 +2198,12 @@ int whTest_She(void)
WH_TEST_PRINT("Testing SHE: (pthread) mem write protect...\n");
WH_TEST_RETURN_ON_FAIL(
wh_ClientServer_MemThreadTest(whTest_SheWriteProtect));
+#if defined(WOLFHSM_CFG_KEYWRAP) && defined(HAVE_AESGCM)
+ WH_TEST_PRINT("Testing SHE: (pthread) wrapped-key reboot interop...\n");
+ WH_TEST_RETURN_ON_FAIL(wh_She_TestWrappedInterop());
+ WH_TEST_PRINT("Testing SHE: (pthread) NVM-less wrapped-key flow...\n");
+ WH_TEST_RETURN_ON_FAIL(whTest_SheNoNvm());
+#endif
return 0;
}
#endif
diff --git a/test/wh_test_she_no_nvm.c b/test/wh_test_she_no_nvm.c
new file mode 100644
index 000000000..11c51c148
--- /dev/null
+++ b/test/wh_test_she_no_nvm.c
@@ -0,0 +1,732 @@
+/*
+ * Copyright (C) 2024 wolfSSL Inc.
+ *
+ * This file is part of wolfHSM.
+ *
+ * wolfHSM is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfHSM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with wolfHSM. If not, see .
+ */
+/*
+ * test/wh_test_she_no_nvm.c
+ *
+ * NVM-less SHE test. A server is started with a NULL NVM context
+ * (whServerConfig.nvm == NULL) and a client provisions every SHE key it needs
+ * purely from client-held AES-GCM wrapped blobs: it caches an initial KEK,
+ * wraps the plaintext SHE keys into blobs, then primes them into the server's
+ * volatile key cache on demand via unwrap-and-cache. The client then drives the
+ * client-facing SHE surface (secure boot, LoadKey, LoadPlainKey, ECB/CBC,
+ * CMAC, ExportRamKey, and the PRNG) to prove it all works without any NVM
+ * backing.
+ *
+ * This exercises NVM-less SHE end to end, including paths that depend on
+ * server-side fixes landed alongside this test:
+ * - SHE SECRET_KEY (slot 0) is provisioned via unwrap-and-cache. The keystore
+ * now exempts SHE keys from the "id 0 == unassigned" rejection
+ * (WH_KEYID_IS_UNASSIGNED), since SHE slots are fixed, explicit ids.
+ * - InitRnd/ExtendSeed cache the PRNG seed when there is no NVM to persist
+ * it to (mirroring the LOAD_KEY nvm==NULL path).
+ */
+
+#include
+#include /* For printf */
+#include /* For memset, memcpy, strlen */
+
+#include "wolfhsm/wh_settings.h"
+#include "wolfhsm/wh_common.h"
+#include "wolfhsm/wh_error.h"
+
+#if defined(WOLFHSM_CFG_SHE_EXTENSION) && !defined(WOLFHSM_CFG_NO_CRYPTO) && \
+ defined(WOLFHSM_CFG_KEYWRAP)
+
+#include "wolfssl/wolfcrypt/settings.h"
+#include "wolfssl/wolfcrypt/types.h"
+#include "wolfssl/wolfcrypt/aes.h"
+#include "wolfssl/wolfcrypt/cmac.h"
+
+/* The threaded harness with a NULL-NVM server needs POSIX threads plus both a
+ * client and an in-process server, and AES-GCM for the wrapped-key blobs. */
+#if defined(HAVE_AESGCM) && defined(WOLFHSM_CFG_TEST_POSIX) && \
+ defined(WOLFHSM_CFG_ENABLE_CLIENT) && defined(WOLFHSM_CFG_ENABLE_SERVER)
+
+#include
+
+#include "wolfhsm/wh_nvm.h"
+#include "wolfhsm/wh_keyid.h"
+#include "wolfhsm/wh_comm.h"
+#include "wolfhsm/wh_transport_mem.h"
+#include "wolfhsm/wh_server.h"
+#include "wolfhsm/wh_server_keystore.h"
+#include "wolfhsm/wh_server_she.h"
+#include "wolfhsm/wh_client.h"
+#include "wolfhsm/wh_client_she.h"
+#include "wolfhsm/wh_she_common.h"
+#include "wolfhsm/wh_she_crypto.h"
+
+#include "wh_test_common.h"
+#include "wh_test_she_no_nvm.h"
+
+enum {
+ BUFFER_SIZE = sizeof(whTransportMemCsr) + sizeof(whCommHeader) +
+ WOLFHSM_CFG_COMM_DATA_LEN,
+};
+
+#ifndef TEST_ADMIN_USERNAME
+#define TEST_ADMIN_USERNAME "admin"
+#endif
+#ifndef TEST_ADMIN_PIN
+#define TEST_ADMIN_PIN "1234"
+#endif
+
+/* Test KEK cache id (intrinsic in production; uploaded here for test only). */
+#define SHE_NONVM_KEK_ID 0x20
+/* SHE slot provisioned via unwrap-and-cache and used directly. */
+#define SHE_NONVM_WORKING_SLOT 4
+/* SHE slot loaded through the SheLoadKey protocol (cache path). */
+#define SHE_NONVM_USER_SLOT 5
+/* Wrapped-blob size for one 16-byte SHE key (matches the server's KEK). */
+#define SHE_NONVM_BLOB_SZ \
+ (WH_KEYWRAP_AES_GCM_HEADER_SIZE + sizeof(whNvmMetadata) + WH_SHE_KEY_SZ)
+
+/* ---- Hardcoded plaintext test material (per the test spec) -------------- */
+
+/* Test KEK. In production the KEK is intrinsic; uploading it from the client is
+ * for test purposes only. */
+static const uint8_t s_kek[32] = {
+ 0x03, 0x03, 0x0d, 0xd9, 0xeb, 0x18, 0x17, 0x2e, 0x06, 0x6e, 0x19,
+ 0xce, 0x98, 0x44, 0x54, 0x0d, 0x78, 0xa0, 0xbe, 0xe7, 0x35, 0x43,
+ 0x40, 0xa4, 0x22, 0x8a, 0xd1, 0x0e, 0xa3, 0x63, 0x1c, 0x0b};
+
+static const uint8_t s_uid[WH_SHE_UID_SZ] = {0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x01};
+
+/* Reused from the SHE test vectors. */
+static const uint8_t s_secretKey[WH_SHE_KEY_SZ] = {
+ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
+ 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c};
+static const uint8_t s_masterEcuKey[WH_SHE_KEY_SZ] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
+static const uint8_t s_bootMacKey[WH_SHE_KEY_SZ] = {
+ 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0x07, 0x18,
+ 0x29, 0x3a, 0x4b, 0x5c, 0x6d, 0x7e, 0x8f, 0x90};
+static const uint8_t s_workingKey[WH_SHE_KEY_SZ] = {
+ 0xde, 0xad, 0xbe, 0xef, 0x01, 0x23, 0x45, 0x67,
+ 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98};
+static const uint8_t s_userKey[WH_SHE_KEY_SZ] = {
+ 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08,
+ 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00};
+static const uint8_t s_prngSeed[WH_SHE_KEY_SZ] = {
+ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96,
+ 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a};
+static const uint8_t s_entropy[WH_SHE_KEY_SZ] = {
+ 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c,
+ 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51};
+
+/* A SHE key the test holds as a client-side wrapped blob, plus its plaintext
+ * and SHE label fields. (struct suggested by the test spec) */
+typedef struct {
+ whKeyId slot; /* SHE slot id (WH_SHE_*_ID) */
+ uint32_t counter; /* SHE label counter */
+ uint32_t flags; /* SHE label flags */
+ uint8_t plain[WH_SHE_KEY_SZ];
+ uint8_t blob[SHE_NONVM_BLOB_SZ];
+ uint16_t blobSz;
+} SheNoNvmKey;
+
+/* Build an AES-GCM wrapped-key blob for a SHE key the same way the server's KEK
+ * would, so the test can drive unwrap-and-cache without first having to get the
+ * key into the server. Uses software AES with the known KEK bytes. Blob layout
+ * matches the server: [IV(12) || authTag(16) || GCM(metadata || key)].
+ *
+ * Note: wh_Client_KeyWrap cannot be used for SHE keys because its server
+ * handler requires a TYPE=WRAPPED key id (WH_KEYID_ISWRAPPED), whereas a SHE
+ * key must keep TYPE=WH_KEYTYPE_SHE so the SHE API can find it after caching.
+ */
+static int _BuildSheKeyBlob(const uint8_t* kek, word32 kekSz, whKeyId sheKeyId,
+ uint32_t counter, uint32_t sheFlags,
+ const uint8_t* keyBytes, uint8_t* blobOut,
+ uint16_t* blobInOutSz)
+{
+ int ret;
+ Aes aes[1];
+ whNvmMetadata meta;
+ uint8_t plain[sizeof(whNvmMetadata) + WH_SHE_KEY_SZ];
+ uint8_t iv[WH_KEYWRAP_AES_GCM_IV_SIZE];
+ uint8_t tag[WH_KEYWRAP_AES_GCM_TAG_SIZE];
+ uint16_t need = (uint16_t)(WH_KEYWRAP_AES_GCM_HEADER_SIZE + sizeof(meta) +
+ WH_SHE_KEY_SZ);
+
+ if (*blobInOutSz < need) {
+ return WH_ERROR_BUFFER_SIZE;
+ }
+
+ memset(&meta, 0, sizeof(meta));
+ meta.id = sheKeyId;
+ meta.len = WH_SHE_KEY_SZ;
+ wh_She_Meta2Label(counter, sheFlags, meta.label);
+
+ memcpy(plain, &meta, sizeof(meta));
+ memcpy(plain + sizeof(meta), keyBytes, WH_SHE_KEY_SZ);
+
+ memset(iv, 0x24, sizeof(iv)); /* a fixed IV is fine for a test blob */
+
+ ret = wc_AesInit(aes, NULL, INVALID_DEVID);
+ if (ret != 0) {
+ return ret;
+ }
+ ret = wc_AesGcmSetKey(aes, kek, kekSz);
+ if (ret == 0) {
+ ret = wc_AesGcmEncrypt(aes, blobOut + WH_KEYWRAP_AES_GCM_HEADER_SIZE,
+ plain, sizeof(plain), iv, sizeof(iv), tag,
+ sizeof(tag), NULL, 0);
+ }
+ wc_AesFree(aes);
+ if (ret != 0) {
+ return ret;
+ }
+
+ memcpy(blobOut, iv, sizeof(iv));
+ memcpy(blobOut + sizeof(iv), tag, sizeof(tag));
+ *blobInOutSz = need;
+ return 0;
+}
+
+/* boot MAC digest = CMAC_bootMacKey(zeros || size || bootloader) */
+static int _ComputeBootMac(const uint8_t* bootloader, uint32_t bootloaderSz,
+ const uint8_t* bootMacKey, uint8_t* digestOut)
+{
+ int ret;
+ Cmac cmac[1];
+ uint8_t zeros[WH_SHE_BOOT_MAC_PREFIX_LEN] = {0};
+ word32 digestSz = WH_SHE_KEY_SZ;
+
+ if ((ret = wc_InitCmac(cmac, bootMacKey, WH_SHE_KEY_SZ, WC_CMAC_AES,
+ NULL)) != 0) {
+ return ret;
+ }
+ if ((ret = wc_CmacUpdate(cmac, zeros, sizeof(zeros))) != 0) {
+ return ret;
+ }
+ if ((ret = wc_CmacUpdate(cmac, (const uint8_t*)&bootloaderSz,
+ sizeof(bootloaderSz))) != 0) {
+ return ret;
+ }
+ if ((ret = wc_CmacUpdate(cmac, bootloader, bootloaderSz)) != 0) {
+ return ret;
+ }
+ digestSz = AES_BLOCK_SIZE;
+ return wc_CmacFinal(cmac, digestOut, &digestSz);
+}
+
+/* (1) Wrap every SHE key the test uses into a client-held blob, under the
+ * trusted KEK that boot code provisioned in the server cache
+ * (_ProvisionServerKek). Unwrap-and-cache now requires a trusted KEK
+ * (WH_NVM_FLAGS_KEK), which a client can never set, so the client cannot upload
+ * the KEK; it only names it by id and wraps under the same known bytes. */
+static int _InitKekAndWrapSHEKeys(whClientContext* client, SheNoNvmKey* keys,
+ int n, whKeyId* outKekId)
+{
+ int ret;
+ int i;
+ whKeyId kekId = SHE_NONVM_KEK_ID;
+
+ for (i = 0; i < n; i++) {
+ keys[i].blobSz = (uint16_t)sizeof(keys[i].blob);
+ ret = _BuildSheKeyBlob(s_kek, (word32)sizeof(s_kek),
+ WH_MAKE_KEYID(WH_KEYTYPE_SHE,
+ client->comm->client_id,
+ keys[i].slot),
+ keys[i].counter, keys[i].flags, keys[i].plain,
+ keys[i].blob, &keys[i].blobSz);
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: wrap SHE slot %u failed %d\n",
+ (unsigned)keys[i].slot, ret);
+ return ret;
+ }
+ }
+
+ *outKekId = kekId;
+ return 0;
+}
+
+/* (2) Unwrap and cache every SHE key blob so the keys are resident in the
+ * server's volatile cache, ready for the SHE API to use. */
+static int _UnwrapAndCacheSHEKeys(whClientContext* client, SheNoNvmKey* keys,
+ int n, whKeyId kekId)
+{
+ int ret;
+ int i;
+ uint16_t outId;
+
+ for (i = 0; i < n; i++) {
+ outId = 0;
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, kekId,
+ keys[i].blob, keys[i].blobSz, &outId);
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: unwrap-and-cache SHE slot %u failed %d\n",
+ (unsigned)keys[i].slot, ret);
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static int _SheNoNvmClientConfig(whClientConfig* config)
+{
+ int ret = 0;
+ whClientContext client[1] = {0};
+ uint32_t outClientId = 0;
+ uint32_t outServerId = 0;
+ whKeyId kekId = SHE_NONVM_KEK_ID;
+
+ uint8_t bootloader[64];
+ uint32_t bootloaderSz = sizeof(bootloader);
+ uint8_t sreg = 0;
+
+ uint8_t m1[WH_SHE_M1_SZ];
+ uint8_t m2[WH_SHE_M2_SZ];
+ uint8_t m3[WH_SHE_M3_SZ];
+ uint8_t m4[WH_SHE_M4_SZ];
+ uint8_t m5[WH_SHE_M5_SZ];
+ uint8_t o4[WH_SHE_M4_SZ];
+ uint8_t o5[WH_SHE_M5_SZ];
+
+ uint8_t plain[WH_SHE_KEY_SZ];
+ uint8_t cipher[WH_SHE_KEY_SZ];
+ uint8_t back[WH_SHE_KEY_SZ];
+ uint8_t mac[WH_SHE_KEY_SZ];
+ uint8_t iv[WH_SHE_KEY_SZ] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
+ 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
+ 0x0c, 0x0d, 0x0e, 0x0f};
+
+ /* SHE keys provisioned purely from client-held wrapped blobs:
+ * SECRET_KEY - auth for ExportRamKey and PRNG derivation
+ * MASTER_ECU_KEY - auth for the user-slot LoadKey below
+ * BOOT_MAC_KEY - secure-boot CMAC key
+ * BOOT_MAC - expected bootloader CMAC (computed below)
+ * working slot - used directly via the SHE cipher API
+ * PRNG_SEED - seed state for InitRnd/ExtendSeed
+ * SECRET_KEY is SHE slot 0; priming it via unwrap-and-cache relies on the
+ * keystore exempting SHE keys from the "id 0 == unassigned" check. */
+ SheNoNvmKey keys[6];
+
+ if (config == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client, config));
+ WH_TEST_RETURN_ON_FAIL(
+ wh_Client_CommInit(client, &outClientId, &outServerId));
+#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION
+ WH_TEST_RETURN_ON_FAIL(wh_Client_AuthLogin(
+ client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, TEST_ADMIN_PIN,
+ strlen(TEST_ADMIN_PIN), &ret, NULL));
+#endif
+
+ /* Build the key table. Plaintext is hardcoded; BOOT_MAC is the CMAC of the
+ * (fixed) bootloader so secure boot will accept it. */
+ memset(bootloader, 0xB7, sizeof(bootloader));
+ memset(keys, 0, sizeof(keys));
+
+ keys[0].slot = WH_SHE_SECRET_KEY_ID;
+ keys[0].counter = 1;
+ memcpy(keys[0].plain, s_secretKey, WH_SHE_KEY_SZ);
+
+ keys[1].slot = WH_SHE_MASTER_ECU_KEY_ID;
+ keys[1].counter = 1;
+ memcpy(keys[1].plain, s_masterEcuKey, WH_SHE_KEY_SZ);
+
+ keys[2].slot = WH_SHE_BOOT_MAC_KEY_ID;
+ keys[2].counter = 1;
+ memcpy(keys[2].plain, s_bootMacKey, WH_SHE_KEY_SZ);
+
+ keys[3].slot = WH_SHE_BOOT_MAC;
+ keys[3].counter = 1;
+ ret =
+ _ComputeBootMac(bootloader, bootloaderSz, s_bootMacKey, keys[3].plain);
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: compute BOOT_MAC failed %d\n", ret);
+ goto exit;
+ }
+
+ keys[4].slot = SHE_NONVM_WORKING_SLOT;
+ keys[4].counter = 1;
+ memcpy(keys[4].plain, s_workingKey, WH_SHE_KEY_SZ);
+
+ keys[5].slot = WH_SHE_PRNG_SEED_ID;
+ keys[5].counter = 1;
+ memcpy(keys[5].plain, s_prngSeed, WH_SHE_KEY_SZ);
+
+ /* (1) cache the KEK and wrap every SHE key into a client-held blob */
+ ret = _InitKekAndWrapSHEKeys(client, keys, 6, &kekId);
+ if (ret != 0) {
+ goto exit;
+ }
+
+ /* (2) prime them into the NULL-NVM server's volatile cache */
+ ret = _UnwrapAndCacheSHEKeys(client, keys, 6, kekId);
+ if (ret != 0) {
+ goto exit;
+ }
+ WH_TEST_PRINT("SHE no-nvm: unwrap-and-cache provisioning SUCCESS\n");
+
+ /* Secure boot using the cached BOOT_MAC_KEY + BOOT_MAC. */
+ ret = wh_Client_SheSetUid(client, (uint8_t*)s_uid, sizeof(s_uid));
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: SheSetUid failed %d\n", ret);
+ goto exit;
+ }
+ ret = wh_Client_SheSecureBoot(client, bootloader, bootloaderSz);
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: SheSecureBoot failed %d\n", ret);
+ goto exit;
+ }
+ ret = wh_Client_SheGetStatus(client, &sreg);
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: SheGetStatus failed %d\n", ret);
+ goto exit;
+ }
+ if ((sreg & WH_SHE_SREG_BOOT_OK) == 0 ||
+ (sreg & WH_SHE_SREG_BOOT_FINISHED) == 0 ||
+ (sreg & WH_SHE_SREG_SECURE_BOOT) == 0) {
+ WH_ERROR_PRINT("no-nvm: secure boot status 0x%02x\n", sreg);
+ ret = WH_ERROR_ABORTED;
+ goto exit;
+ }
+ WH_TEST_PRINT("SHE no-nvm: secure boot SUCCESS\n");
+
+ /* LoadKey cache path: load a user key (auth = master ECU key, primed via
+ * unwrap-and-cache above). The loaded key lands in the cache because the
+ * server has no NVM (src/wh_server_she.c LOAD_KEY nvm==NULL guard). */
+ ret = wh_She_GenerateLoadableKey(
+ SHE_NONVM_USER_SLOT, WH_SHE_MASTER_ECU_KEY_ID, 1, 0, (uint8_t*)s_uid,
+ (uint8_t*)s_userKey, (uint8_t*)s_masterEcuKey, m1, m2, m3, m4, m5);
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: generate user-slot M1/M2/M3 failed %d\n", ret);
+ goto exit;
+ }
+ ret = wh_Client_SheLoadKey(client, m1, m2, m3, o4, o5);
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: LoadKey user slot failed %d\n", ret);
+ goto exit;
+ }
+ WH_TEST_PRINT("SHE no-nvm: LoadKey (cache path) SUCCESS\n");
+
+ /* ECB round trip on the LoadKey-provisioned user slot. */
+ memset(plain, 0x11, sizeof(plain));
+ ret = wh_Client_SheEncEcb(client, SHE_NONVM_USER_SLOT, plain, cipher,
+ sizeof(plain));
+ if (ret == 0) {
+ ret = wh_Client_SheDecEcb(client, SHE_NONVM_USER_SLOT, cipher, back,
+ sizeof(cipher));
+ }
+ if (ret != 0 || memcmp(plain, back, sizeof(plain)) != 0) {
+ WH_ERROR_PRINT("no-nvm: user-slot ECB round trip failed %d\n", ret);
+ ret = (ret != 0) ? ret : WH_ERROR_ABORTED;
+ goto exit;
+ }
+
+ /* ECB round trip on the unwrap-and-cache-provisioned working slot. */
+ memset(plain, 0x22, sizeof(plain));
+ ret = wh_Client_SheEncEcb(client, SHE_NONVM_WORKING_SLOT, plain, cipher,
+ sizeof(plain));
+ if (ret == 0) {
+ ret = wh_Client_SheDecEcb(client, SHE_NONVM_WORKING_SLOT, cipher, back,
+ sizeof(cipher));
+ }
+ if (ret != 0 || memcmp(plain, back, sizeof(plain)) != 0) {
+ WH_ERROR_PRINT("no-nvm: working-slot ECB round trip failed %d\n", ret);
+ ret = (ret != 0) ? ret : WH_ERROR_ABORTED;
+ goto exit;
+ }
+ WH_TEST_PRINT("SHE no-nvm: ECB round trips SUCCESS\n");
+
+ /* RAM key: plain load, ECB, then export + re-import round trip. The
+ * exported M1..M5 authenticate with SECRET_KEY (slot 0); re-importing
+ * reproduces the same RAM key, so decrypting the earlier ciphertext must
+ * round-trip back to the plaintext. */
+ ret = wh_Client_SheLoadPlainKey(client, (uint8_t*)s_workingKey,
+ WH_SHE_KEY_SZ);
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: SheLoadPlainKey failed %d\n", ret);
+ goto exit;
+ }
+ memset(plain, 0x33, sizeof(plain));
+ ret = wh_Client_SheEncEcb(client, WH_SHE_RAM_KEY_ID, plain, cipher,
+ sizeof(plain));
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: RAM EncEcb failed %d\n", ret);
+ goto exit;
+ }
+ ret = wh_Client_SheExportRamKey(client, m1, m2, m3, m4, m5);
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: SheExportRamKey failed %d\n", ret);
+ goto exit;
+ }
+ ret = wh_Client_SheLoadKey(client, m1, m2, m3, o4, o5);
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: re-import exported RAM key failed %d\n", ret);
+ goto exit;
+ }
+ ret = wh_Client_SheDecEcb(client, WH_SHE_RAM_KEY_ID, cipher, back,
+ sizeof(cipher));
+ if (ret != 0 || memcmp(plain, back, sizeof(plain)) != 0) {
+ WH_ERROR_PRINT("no-nvm: RAM ECB export round trip failed %d\n", ret);
+ ret = (ret != 0) ? ret : WH_ERROR_ABORTED;
+ goto exit;
+ }
+ WH_TEST_PRINT("SHE no-nvm: ExportRamKey round trip SUCCESS\n");
+
+ memset(plain, 0x44, sizeof(plain));
+ ret = wh_Client_SheEncCbc(client, WH_SHE_RAM_KEY_ID, iv, sizeof(iv), plain,
+ cipher, sizeof(plain));
+ if (ret == 0) {
+ ret = wh_Client_SheDecCbc(client, WH_SHE_RAM_KEY_ID, iv, sizeof(iv),
+ cipher, back, sizeof(cipher));
+ }
+ if (ret != 0 || memcmp(plain, back, sizeof(plain)) != 0) {
+ WH_ERROR_PRINT("no-nvm: RAM CBC round trip failed %d\n", ret);
+ ret = (ret != 0) ? ret : WH_ERROR_ABORTED;
+ goto exit;
+ }
+
+ ret = wh_Client_SheGenerateMac(client, WH_SHE_RAM_KEY_ID, plain,
+ sizeof(plain), mac, sizeof(mac));
+ if (ret == 0) {
+ ret = wh_Client_SheVerifyMac(client, WH_SHE_RAM_KEY_ID, plain,
+ sizeof(plain), mac, sizeof(mac), &sreg);
+ }
+ if (ret != 0 || sreg != 0) {
+ WH_ERROR_PRINT("no-nvm: RAM CMAC failed ret=%d status=%d\n", ret, sreg);
+ ret = (ret != 0) ? ret : WH_ERROR_ABORTED;
+ goto exit;
+ }
+ WH_TEST_PRINT("SHE no-nvm: RAM key ECB/CBC/CMAC SUCCESS\n");
+
+ /* PRNG: init from SECRET_KEY + PRNG_SEED, draw a block, then extend the
+ * seed. InitRnd/ExtendSeed cache the updated PRNG seed since there is no
+ * NVM to persist it to. */
+ {
+ uint8_t rnd[WH_SHE_KEY_SZ];
+ uint32_t rndSz = sizeof(rnd);
+
+ ret = wh_Client_SheInitRnd(client);
+ if (ret == 0) {
+ ret = wh_Client_SheRnd(client, rnd, &rndSz);
+ }
+ if (ret == 0) {
+ ret = wh_Client_SheExtendSeed(client, (uint8_t*)s_entropy,
+ sizeof(s_entropy));
+ }
+ if (ret != 0) {
+ WH_ERROR_PRINT("no-nvm: PRNG init/rnd/extend failed %d\n", ret);
+ goto exit;
+ }
+ }
+ WH_TEST_PRINT("SHE no-nvm: PRNG (init/rnd/extend) SUCCESS\n");
+
+ /* The boot-provisioned KEK is a trusted WH_NVM_FLAGS_KEK key: the client
+ * must be able neither to read it nor to evict it. */
+ {
+ uint8_t kbuf[sizeof(s_kek)];
+ uint16_t kbufSz = (uint16_t)sizeof(kbuf);
+ uint8_t klabel[WH_NVM_LABEL_LEN];
+
+ ret = wh_Client_KeyExport(client, kekId, klabel, sizeof(klabel), kbuf,
+ &kbufSz);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("no-nvm: KEK export expected ACCESS, got %d\n", ret);
+ ret = (ret == 0) ? WH_ERROR_ABORTED : ret;
+ goto exit;
+ }
+ ret = wh_Client_KeyEvict(client, kekId);
+ if (ret != WH_ERROR_ACCESS) {
+ WH_ERROR_PRINT("no-nvm: KEK evict expected ACCESS, got %d\n", ret);
+ ret = (ret == 0) ? WH_ERROR_ABORTED : ret;
+ goto exit;
+ }
+ ret = 0;
+ }
+ WH_TEST_PRINT("SHE no-nvm: KEK unreadable and immutable SUCCESS\n");
+ WH_TEST_PRINT("SHE no-nvm flow SUCCESS\n");
+
+exit:
+ WH_TEST_RETURN_ON_FAIL(wh_Client_CommClose(client));
+ if (ret == 0) {
+ WH_TEST_RETURN_ON_FAIL(wh_Client_Cleanup(client));
+ }
+ else {
+ wh_Client_Cleanup(client);
+ }
+
+ return ret;
+}
+
+/* Provision the trusted KEK directly in the server cache, the way boot code
+ * would on an NVM-less device. It carries WH_NVM_FLAGS_KEK -- a flag the
+ * request handlers strip from every client path, so only server-internal
+ * provisioning like this can set it. That makes it the trusted KEK that
+ * unwrap-and-cache now requires. committed=0 keeps it pinned for the life of
+ * the (NVM-less) server. The id matches what the client names: plain
+ * SHE_NONVM_KEK_ID translated against WH_TEST_DEFAULT_CLIENT_ID. */
+static int _ProvisionServerKek(whServerContext* server)
+{
+ whNvmMetadata meta = {0};
+
+ meta.id = WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, WH_TEST_DEFAULT_CLIENT_ID,
+ SHE_NONVM_KEK_ID);
+ meta.access = WH_NVM_ACCESS_ANY;
+ meta.flags = WH_NVM_FLAGS_KEK | WH_NVM_FLAGS_USAGE_WRAP |
+ WH_NVM_FLAGS_NONEXPORTABLE | WH_NVM_FLAGS_NONMODIFIABLE;
+ meta.len = (whNvmSize)sizeof(s_kek);
+ memcpy(meta.label, "SHE no-nvm KEK", sizeof("SHE no-nvm KEK"));
+
+ return wh_Server_KeystoreCacheKey(server, &meta, (uint8_t*)s_kek);
+}
+
+/* Server event loop, started on its own thread with a NULL NVM context. */
+static int _SheNoNvmServerConfig(whServerConfig* config)
+{
+ whServerContext server[1] = {0};
+ whCommConnected am_connected = WH_COMM_CONNECTED;
+ int ret = 0;
+
+ if (config == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, config));
+ /* Boot-time KEK provisioning happens before the server accepts requests. */
+ WH_TEST_RETURN_ON_FAIL(_ProvisionServerKek(server));
+ WH_TEST_RETURN_ON_FAIL(wh_Server_SetConnected(server, am_connected));
+
+ while (am_connected == WH_COMM_CONNECTED) {
+ ret = wh_Server_HandleRequestMessage(server);
+ if ((ret != WH_ERROR_NOTREADY) && (ret != WH_ERROR_OK)) {
+ WH_ERROR_PRINT("no-nvm: HandleRequestMessage failed %d\n", ret);
+ break;
+ }
+ wh_Server_GetConnected(server, &am_connected);
+ }
+ if ((ret == 0) || (ret == WH_ERROR_NOTREADY)) {
+ WH_TEST_RETURN_ON_FAIL(wh_Server_Cleanup(server));
+ }
+ else {
+ ret = wh_Server_Cleanup(server);
+ }
+
+ return ret;
+}
+
+static void* _SheNoNvmServerTask(void* cf)
+{
+ WH_TEST_ASSERT(0 == _SheNoNvmServerConfig((whServerConfig*)cf));
+ return NULL;
+}
+
+static void* _SheNoNvmClientTask(void* cf)
+{
+ WH_TEST_ASSERT(0 == _SheNoNvmClientConfig((whClientConfig*)cf));
+ return NULL;
+}
+
+static void _SheNoNvmThreadTest(whClientConfig* c_conf, whServerConfig* s_conf)
+{
+ pthread_t cthread = {0};
+ pthread_t sthread = {0};
+ void* retval;
+ int rc = 0;
+
+ rc = pthread_create(&sthread, NULL, _SheNoNvmServerTask, s_conf);
+ if (rc == 0) {
+ rc = pthread_create(&cthread, NULL, _SheNoNvmClientTask, c_conf);
+ if (rc == 0) {
+ /* All good. Block on joining */
+ pthread_join(cthread, &retval);
+ pthread_join(sthread, &retval);
+ }
+ else {
+ /* Cancel the server thread */
+ pthread_cancel(sthread);
+ pthread_join(sthread, &retval);
+ }
+ }
+}
+
+int whTest_SheNoNvm(void)
+{
+ /* Memory transport shared between client and server threads. */
+ uint8_t req[BUFFER_SIZE] = {0};
+ uint8_t resp[BUFFER_SIZE] = {0};
+ whTransportMemConfig tmcf[1] = {{
+ .req = (whTransportMemCsr*)req,
+ .req_size = sizeof(req),
+ .resp = (whTransportMemCsr*)resp,
+ .resp_size = sizeof(resp),
+ }};
+
+ /* Client configuration/contexts */
+ whTransportClientCb tccb[1] = {WH_TRANSPORT_MEM_CLIENT_CB};
+ whTransportMemClientContext tmcc[1] = {0};
+ whCommClientConfig cc_conf[1] = {{
+ .transport_cb = tccb,
+ .transport_context = (void*)tmcc,
+ .transport_config = (void*)tmcf,
+ .client_id = WH_TEST_DEFAULT_CLIENT_ID,
+ }};
+ whClientConfig c_conf[1] = {{
+ .comm = cc_conf,
+ }};
+
+ /* Server configuration/contexts */
+ whTransportServerCb tscb[1] = {WH_TRANSPORT_MEM_SERVER_CB};
+ whTransportMemServerContext tmsc[1] = {0};
+ whCommServerConfig cs_conf[1] = {{
+ .transport_cb = tscb,
+ .transport_context = (void*)tmsc,
+ .transport_config = (void*)tmcf,
+ .server_id = 124,
+ }};
+
+ whServerCryptoContext crypto[1] = {0};
+ whServerSheContext she[1];
+
+ /* The whole point of this test: a server with NO NVM backing. */
+ whServerConfig s_conf[1] = {{
+ .comm_config = cs_conf,
+ .nvm = NULL,
+ .crypto = crypto,
+ .she = she,
+ .devId = INVALID_DEVID,
+ }};
+
+ memset(she, 0, sizeof(she));
+
+ WH_TEST_RETURN_ON_FAIL(wolfCrypt_Init());
+ WH_TEST_RETURN_ON_FAIL(wc_InitRng_ex(crypto->rng, NULL, INVALID_DEVID));
+
+ _SheNoNvmThreadTest(c_conf, s_conf);
+
+ wc_FreeRng(crypto->rng);
+ wolfCrypt_Cleanup();
+
+ return WH_ERROR_OK;
+}
+
+#endif /* HAVE_AESGCM && WOLFHSM_CFG_TEST_POSIX && WOLFHSM_CFG_ENABLE_CLIENT \
+ && WOLFHSM_CFG_ENABLE_SERVER */
+
+#endif /* WOLFHSM_CFG_SHE_EXTENSION && !WOLFHSM_CFG_NO_CRYPTO && \
+ WOLFHSM_CFG_KEYWRAP */
diff --git a/test/wh_test_she_no_nvm.h b/test/wh_test_she_no_nvm.h
new file mode 100644
index 000000000..ba705fff4
--- /dev/null
+++ b/test/wh_test_she_no_nvm.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 wolfSSL Inc.
+ *
+ * This file is part of wolfHSM.
+ *
+ * wolfHSM is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfHSM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with wolfHSM. If not, see .
+ */
+#ifndef WH_TEST_SHE_NO_NVM_H_
+#define WH_TEST_SHE_NO_NVM_H_
+
+int whTest_SheNoNvm(void);
+
+#endif /* WH_TEST_SHE_NO_NVM_H_ */
diff --git a/tools/whnvmtool/README.md b/tools/whnvmtool/README.md
index a53454e5b..36f4d7f72 100644
--- a/tools/whnvmtool/README.md
+++ b/tools/whnvmtool/README.md
@@ -59,6 +59,19 @@ where:
- ``: Valid file path to a file containing the key's data. Data will be read from this file and stored in the NVM key.
+### Provisioning a Trusted Key-Encryption Key (KEK)
+
+The keystore `wrap-export` and `unwrap-and-cache` operations require a **trusted KEK** — one the client can neither read nor set. On a system without a hardware keystore, this is a software key carrying the server-only `WH_NVM_FLAGS_KEK` flag (bit 12). The server strips that flag from every client request, so the only way to set it is to write it directly into an NVM image with this tool (or via trusted server-internal boot code).
+
+To provision such a KEK, give a `key` entry the flag value `0x1205`, which is `WH_NVM_FLAGS_KEK | WH_NVM_FLAGS_NONEXPORTABLE | WH_NVM_FLAGS_NONMODIFIABLE | WH_NVM_FLAGS_USAGE_WRAP` (`0x1000 | 0x0004 | 0x0001 | 0x0200`). The `WH_NVM_FLAGS_KEK` bit alone already makes the key unreadable, immutable, and KEK-only through the client API; the remaining bits make that intent explicit. Use `clientId` 0 to place the KEK in the global namespace if clients reference it with the global flag, or a specific `clientId` to scope it to one client.
+
+```
+# A trusted software KEK for wrap-export / unwrap-and-cache (global namespace)
+key 0 0x20 0xFFFF 0x1205 "Export KEK" "path/to/kek.bin"
+```
+
+The KEK material file must be exactly the key size used to wrap (e.g. 32 bytes for AES-256) and must be kept secret from clients.
+
### General Schema Rules and Restrictions
1. Each entry must be on a separate line.
diff --git a/wolfhsm/wh_client.h b/wolfhsm/wh_client.h
index 7c4e8c189..204506d7a 100644
--- a/wolfhsm/wh_client.h
+++ b/wolfhsm/wh_client.h
@@ -1200,6 +1200,64 @@ int wh_Client_KeyWrapResponse(whClientContext* ctx,
enum wc_CipherType cipherType,
void* wrappedKeyOut, uint16_t* wrappedKeyInOutSz);
+/**
+ * @brief Wraps a key the server already holds (by id) and exports the blob.
+ *
+ * Unlike wh_Client_KeyWrap, the client never presents plaintext key material:
+ * it names an existing keystore key by id (and type) and the server reads it,
+ * enforces export permissions (NONEXPORTABLE), wraps it with the KEK, and
+ * returns the wrapped blob. SHE keys are wrapped as TYPE=SHE; other keys are
+ * normalized to the wrapped-key namespace so the blob round-trips through
+ * wh_Client_KeyUnwrapAndCache. Blocks until the operation completes.
+ *
+ * @param[in] ctx Pointer to the client context.
+ * @param[in] cipherType Cipher used to wrap the key.
+ * @param[in] keyId Client-facing id (with optional GLOBAL/WRAPPED flags) of the
+ * keystore key to wrap.
+ * @param[in] keyType WH_KEYTYPE_* of the target key (e.g. WH_KEYTYPE_CRYPTO or
+ * WH_KEYTYPE_SHE).
+ * @param[in] serverKeyId Key ID of the key encryption key on the server.
+ * @param[out] wrappedKeyOut Pointer to store the wrapped key.
+ * @param[in,out] wrappedKeyInOutSz IN: size of wrappedKeyOut; OUT: size of the
+ * wrapped key object returned by the server.
+ * @return int Returns 0 on success, or a negative error code on failure.
+ */
+int wh_Client_KeyWrapExport(whClientContext* ctx, enum wc_CipherType cipherType,
+ uint16_t keyId, uint16_t keyType,
+ uint16_t serverKeyId, void* wrappedKeyOut,
+ uint16_t* wrappedKeyInOutSz);
+
+/**
+ * @brief Sends a wrap-and-export (by id) request to the server. Non-blocking.
+ *
+ * @param[in] ctx Pointer to the client context.
+ * @param[in] cipherType Cipher used to wrap the key.
+ * @param[in] keyId Client-facing id of the keystore key to wrap.
+ * @param[in] keyType WH_KEYTYPE_* of the target key.
+ * @param[in] serverKeyId Key ID of the key encryption key on the server.
+ * @return int Returns 0 on success, or a negative error code on failure.
+ */
+int wh_Client_KeyWrapExportRequest(whClientContext* ctx,
+ enum wc_CipherType cipherType,
+ uint16_t keyId, uint16_t keyType,
+ uint16_t serverKeyId);
+
+/**
+ * @brief Receives a wrap-and-export response from the server. Returns
+ * WH_ERROR_NOTREADY if a response has not been received.
+ *
+ * @param[in] ctx Pointer to the client context.
+ * @param[in] cipherType Cipher used to wrap the key.
+ * @param[out] wrappedKeyOut Pointer to store the wrapped key.
+ * @param[in,out] wrappedKeyInOutSz IN: size of wrappedKeyOut; OUT: size of the
+ * wrapped key object.
+ * @return int Returns 0 on success, or a negative error code on failure.
+ */
+int wh_Client_KeyWrapExportResponse(whClientContext* ctx,
+ enum wc_CipherType cipherType,
+ void* wrappedKeyOut,
+ uint16_t* wrappedKeyInOutSz);
+
/**
* @brief Requests the server to unwrap and export a wrapped key and receives
* the response
@@ -1355,6 +1413,47 @@ int wh_Client_DataWrap(whClientContext* ctx, enum wc_CipherType cipherType,
uint16_t serverKeyId, void* dataIn, uint32_t dataInSz,
void* wrappedDataOut, uint32_t* wrappedDataInOutSz);
+/**
+ * @brief Sends a data wrap request to the server
+ *
+ * This function prepares and sends a data wrap request to the server. The
+ * request data contains the plaintext data for the server to wrap. This
+ * function does not block; it returns immediately after sending the request.
+ *
+ * @param[in] ctx Pointer to the client context.
+ * @param[in] cipherType Cipher used when wrapping the data.
+ * @param[in] serverKeyId Key ID to be used for wrapping the data.
+ * @param[in] dataIn Pointer to the plaintext data you want to wrap.
+ * @param[in] dataInSz The size in bytes of the plaintext data.
+ * @return int Returns 0 on success, or a negative error code on failure.
+ */
+int wh_Client_DataWrapRequest(whClientContext* ctx,
+ enum wc_CipherType cipherType,
+ uint16_t serverKeyId, void* dataIn,
+ uint32_t dataInSz);
+
+/**
+ * @brief Receives a data wrap response from the server
+ *
+ * This function attempts to process a data wrap response message from the
+ * server. It will validate the response and extract the wrapped data from
+ * the response data. This function does not block; it returns
+ * WH_ERROR_NOTREADY if a response has not been received.
+ *
+ * @param[in] ctx Pointer to the client context.
+ * @param[in] cipherType Cipher used when wrapping the data.
+ * @param[out] wrappedDataOut The pointer to the buffer that stores the
+ * resulting wrapped data.
+ * @param[in/out] wrappedDataSz IN: The size in bytes of wrappedDataOut
+ * buffer. OUT: The size of the wrapped data object returned from the server.
+ * OUT may be less than IN.
+ * @return int Returns 0 on success, WH_ERROR_NOTREADY if no response is
+ * available, or a negative error code on failure.
+ */
+int wh_Client_DataWrapResponse(whClientContext* ctx,
+ enum wc_CipherType cipherType,
+ void* wrappedDataOut, uint32_t* wrappedDataSz);
+
/**
* @brief Helper function to unwrap a wrapped data object using a specified key
*
@@ -1379,6 +1478,48 @@ int wh_Client_DataUnwrap(whClientContext* ctx, enum wc_CipherType cipherType,
uint32_t wrappedDataInSz, void* dataOut,
uint32_t* dataInOutSz);
+/**
+ * @brief Sends a data unwrap request to the server
+ *
+ * This function prepares and sends a data unwrap request to the server. The
+ * request data contains the wrapped data object for the server to unwrap.
+ * This function does not block; it returns immediately after sending the
+ * request.
+ *
+ * @param[in] ctx Pointer to the client context.
+ * @param[in] cipherType Cipher used when unwrapping the data.
+ * @param[in] serverKeyId Key ID to be used for unwrapping the data.
+ * @param[in] wrappedDataIn Pointer to the wrapped data object you want to
+ * unwrap.
+ * @param[in] wrappedDataInSz The size in bytes of the wrapped data object.
+ * @return int Returns 0 on success, or a negative error code on failure.
+ */
+int wh_Client_DataUnwrapRequest(whClientContext* ctx,
+ enum wc_CipherType cipherType,
+ uint16_t serverKeyId, void* wrappedDataIn,
+ uint32_t wrappedDataInSz);
+
+/**
+ * @brief Receives a data unwrap response from the server
+ *
+ * This function attempts to process a data unwrap response message from the
+ * server. It will validate the response and extract the unwrapped data from
+ * the response data. This function does not block; it returns
+ * WH_ERROR_NOTREADY if a response has not been received.
+ *
+ * @param[in] ctx Pointer to the client context.
+ * @param[in] cipherType Cipher used when unwrapping the data.
+ * @param[out] dataOut The pointer to the buffer that stores the resulting
+ * unwrapped data.
+ * @param[in/out] dataSz IN: The size in bytes of dataOut. OUT: The size of
+ * the unwrapped data object returned by the server. OUT may be less than IN.
+ * @return int Returns 0 on success, WH_ERROR_NOTREADY if no response is
+ * available, or a negative error code on failure.
+ */
+int wh_Client_DataUnwrapResponse(whClientContext* ctx,
+ enum wc_CipherType cipherType, void* dataOut,
+ uint32_t* dataSz);
+
/* Counter functions */
int wh_Client_CounterInitRequest(whClientContext* c, whNvmId counterId,
uint32_t counter);
@@ -3578,6 +3719,35 @@ int wh_Client_CertVerifyAcertDma(whClientContext* c, const void* cert,
#define WH_CLIENT_KEYID_MAKE_WRAPPED_GLOBAL(_id) \
((_id) | WH_KEYID_CLIENT_GLOBAL_FLAG | WH_KEYID_CLIENT_WRAPPED_FLAG)
+/**
+ * @brief Mark a key ID as hardware-only
+ *
+ * Sets the hardware-only flag in a client keyId to indicate to the server
+ * that this key's material lives exclusively in a hardware keystore. The
+ * server will translate this to KEYTYPE=WH_KEYTYPE_HW and fetch the
+ * material from its hardware keystore backend on demand.
+ *
+ * Hardware-only keys never enter the server key cache or NVM and are never
+ * returned to a client. They are only usable as KEKs in the keywrap API
+ * (key/data wrap and unwrap); all other keystore and crypto operations
+ * reject them with WH_ERROR_ACCESS.
+ *
+ * @note Requires the server to be built with WOLFHSM_CFG_HWKEYSTORE. On a
+ * server without hardware keystore support the hardware-only flag is
+ * ignored and the request is treated as an ordinary key ID (looked up
+ * in the normal cache/NVM keystore), which typically fails with a
+ * key-not-found error.
+ *
+ * @param _id The key ID (0-255), as understood by the server's hardware
+ * keystore backend
+ * @return keyId with hardware-only flag set
+ *
+ * Example:
+ * whKeyId hwKek = WH_CLIENT_KEYID_MAKE_HW(2);
+ * wh_Client_KeyWrap(client, WC_CIPHER_AES_GCM, hwKek, ...);
+ */
+#define WH_CLIENT_KEYID_MAKE_HW(_id) ((_id) | WH_KEYID_CLIENT_HW_FLAG)
+
/**
* @brief Construct wrapped key metadata ID with explicit ownership
*
diff --git a/wolfhsm/wh_common.h b/wolfhsm/wh_common.h
index 73aa48c03..4f4bf014e 100644
--- a/wolfhsm/wh_common.h
+++ b/wolfhsm/wh_common.h
@@ -80,6 +80,16 @@ typedef uint16_t whNvmFlags;
#define WH_NVM_FLAGS_EPHEMERAL ((whNvmFlags)1 << 4)
/* Cannot be destroyed (but can be modified) */
#define WH_NVM_FLAGS_NONDESTROYABLE ((whNvmFlags)1 << 11)
+/* Trusted key-encryption key. Server-only: set exclusively by trusted
+ * provisioning (whnvmtool image or server-internal boot code); the server
+ * strips it from every client-supplied metadata path. A key carrying this flag
+ * is unreadable, immutable, non-evictable and non-exportable through the client
+ * API, and is the only kind of cache/NVM key permitted to act as a KEK for
+ * KeyWrapExport / KeyUnwrapAndCache. */
+#define WH_NVM_FLAGS_KEK ((whNvmFlags)1 << 12)
+
+/* Flags a client may never set; stripped from all client-supplied metadata. */
+#define WH_NVM_FLAGS_SERVER_ONLY (WH_NVM_FLAGS_KEK)
/* Key usage policy flags
*
diff --git a/wolfhsm/wh_hwkeystore.h b/wolfhsm/wh_hwkeystore.h
new file mode 100644
index 000000000..273d71f95
--- /dev/null
+++ b/wolfhsm/wh_hwkeystore.h
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2026 wolfSSL Inc.
+ *
+ * This file is part of wolfHSM.
+ *
+ * wolfHSM is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * wolfHSM is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with wolfHSM. If not, see .
+ */
+/*
+ * wolfhsm/wh_hwkeystore.h
+ *
+ * Hardware keystore front-end module. Provides a platform-agnostic, fully
+ * configurable abstraction over hardware-backed key storage (OTP, fuses,
+ * secure enclaves, SoC key managers, etc.) through a backend callback table
+ * (whHwKeystoreCb), mirroring the configurable-backend paradigm used by the
+ * wh_lock and wh_log modules.
+ *
+ * The server uses this module to fetch the material of "hardware-only" keys
+ * (WH_KEYTYPE_HW, requested by clients via WH_CLIENT_KEYID_MAKE_HW)
+ * on demand. Hardware-only key material never enters the server key cache or
+ * NVM and is never returned to a client; the server fetches it into a local
+ * (stack) buffer, uses it, and zeroizes it. Hardware-only keys are only
+ * usable as KEKs in the keywrap API.
+ *
+ * A backend provides a whHwKeystoreCb table (GetKey required; Init and Cleanup
+ * optional) plus optional opaque context/config pointers for backend-specific
+ * state. A whHwKeystoreContext is owned by the application, initialized once
+ * with wh_HwKeystore_Init(), and bound to one or more server contexts through
+ * the optional hwKeystore member of whServerConfig. The embedded lock
+ * serializes callback invocations when the backing hardware is shared across
+ * server threads (WOLFHSM_CFG_THREADSAFE).
+ */
+
+#ifndef WOLFHSM_WH_HWKEYSTORE_H_
+#define WOLFHSM_WH_HWKEYSTORE_H_
+
+/* Pick up compile-time configuration */
+#include "wolfhsm/wh_settings.h"
+
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+
+#include
+
+#include "wolfhsm/wh_keyid.h"
+
+#ifdef WOLFHSM_CFG_THREADSAFE
+#include "wolfhsm/wh_lock.h"
+#endif
+
+/**
+ * Hardware keystore backend callback signatures.
+ *
+ * All callbacks receive the backend's opaque context pointer (from
+ * whHwKeystoreConfig). GetKey is required; Init and Cleanup are optional and
+ * may be NULL in the callback table.
+ *
+ * Return: WH_ERROR_OK on success, negative error code on failure.
+ */
+
+/** Initialize the backend - called once from wh_HwKeystore_Init(). Optional. */
+typedef int (*whHwKeystoreInitCb)(void* context, const void* config);
+
+/** Cleanup the backend - called once from wh_HwKeystore_Cleanup(). Optional. */
+typedef int (*whHwKeystoreCleanupCb)(void* context);
+
+/**
+ * Hardware keystore getKey callback. Required.
+ *
+ * Copies the material of the requested key into the caller-provided buffer.
+ * The callback receives the full server-internal keyId (TYPE/USER/ID fields,
+ * see wolfhsm/wh_keyid.h); backends typically dispatch on WH_KEYID_ID() and
+ * may use WH_KEYID_USER() to partition keys between clients.
+ *
+ * The backend is the policy authority for hardware-only keys: it must return
+ * an error (e.g. WH_ERROR_ACCESS or WH_ERROR_NOTFOUND) for any keyId it does
+ * not serve. The core performs no usage-flag (WH_NVM_FLAGS_USAGE_WRAP) check on
+ * hardware keys, so the backend must also refuse any keyId not intended for
+ * keywrap KEK use.
+ *
+ * Output-buffer contract: on entry *inout_len is the capacity of out in bytes.
+ * The backend MUST NOT write more than that many bytes to out and, on success,
+ * MUST set *inout_len to the number of bytes actually written (which therefore
+ * must not exceed the input capacity). If the key does not fit, the backend
+ * must return WH_ERROR_BUFFER_SIZE and write nothing.
+ *
+ * @param[in] context Backend context pointer from whHwKeystoreConfig
+ * @param[in] keyId Server-internal keyId of the requested key
+ * @param[out] out Buffer to receive the key material
+ * @param[in,out] inout_len In: size of out in bytes. Out: actual key size
+ * @return WH_ERROR_OK on success, negative error code on failure
+ */
+typedef int (*whHwKeystoreGetKeyCb)(void* context, whKeyId keyId, uint8_t* out,
+ uint16_t* inout_len);
+
+/**
+ * Hardware keystore backend callback table.
+ *
+ * A backend provides implementations of these callbacks. GetKey is required.
+ * Init and Cleanup may be NULL if the backend needs no setup/teardown.
+ */
+typedef struct whHwKeystoreCb_t {
+ whHwKeystoreInitCb Init; /* Initialize backend (optional) */
+ whHwKeystoreCleanupCb Cleanup; /* Cleanup backend (optional) */
+ whHwKeystoreGetKeyCb GetKey; /* Fetch key material (required) */
+} whHwKeystoreCb;
+
+/* Context structure associated with a hardware keystore instance */
+typedef struct whHwKeystoreContext_t {
+ const whHwKeystoreCb* cb; /* Backend callback table */
+ void* context; /* Opaque backend context */
+#ifdef WOLFHSM_CFG_THREADSAFE
+ whLock lock; /* Lock serializing callback invocations */
+#endif
+ int initialized; /* 1 if initialized, 0 otherwise */
+ uint8_t WH_PAD[4];
+} whHwKeystoreContext;
+
+/* Configuration structure associated with a hardware keystore instance */
+typedef struct whHwKeystoreConfig_t {
+ const whHwKeystoreCb* cb; /* Backend callback table (GetKey required) */
+ void* context; /* Opaque backend context (passed to cbs) */
+ const void* config; /* Backend-specific config (passed to Init) */
+#ifdef WOLFHSM_CFG_THREADSAFE
+ whLockConfig* lockConfig; /* Lock configuration (NULL for no-op locking) */
+#endif
+} whHwKeystoreConfig;
+
+/**
+ * @brief Initialize a hardware keystore instance.
+ *
+ * Binds the backend callback table, initializes the embedded lock, and invokes
+ * the backend Init callback (if present). Must be called in a single-threaded
+ * context before the context is bound to any server.
+ *
+ * @param[in] context Pointer to the hardware keystore context
+ * @param[in] config Pointer to the configuration. config->cb and
+ * config->cb->GetKey are required
+ * @return int WH_ERROR_OK on success, WH_ERROR_BADARGS if context, config,
+ * config->cb, or config->cb->GetKey is NULL, or a negative error code
+ * from the backend Init callback or lock initialization
+ */
+int wh_HwKeystore_Init(whHwKeystoreContext* context,
+ const whHwKeystoreConfig* config);
+
+/**
+ * @brief Cleanup a hardware keystore instance.
+ *
+ * Cleans up the embedded lock and zeros the context. Must only be called
+ * when no servers are using the context.
+ *
+ * @param[in] context Pointer to the hardware keystore context
+ * @return int WH_ERROR_OK on success, WH_ERROR_BADARGS if context is NULL
+ */
+int wh_HwKeystore_Cleanup(whHwKeystoreContext* context);
+
+/**
+ * @brief Fetch key material from the hardware keystore backend.
+ *
+ * Acquires the lock, invokes the backend GetKey callback, and releases the
+ * lock.
+ *
+ * @param[in] context Pointer to an initialized hardware keystore context
+ * @param[in] keyId Server-internal keyId of the requested key
+ * @param[out] out Buffer to receive the key material
+ * @param[in,out] inout_len In: size of out in bytes. Out: actual key size
+ * @return int WH_ERROR_OK on success, WH_ERROR_BADARGS on invalid arguments
+ * or uninitialized context, or the error returned by the lock or the
+ * backend callback
+ */
+int wh_HwKeystore_GetKey(whHwKeystoreContext* context, whKeyId keyId,
+ uint8_t* out, uint16_t* inout_len);
+
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
+#endif /* !WOLFHSM_WH_HWKEYSTORE_H_ */
diff --git a/wolfhsm/wh_keyid.h b/wolfhsm/wh_keyid.h
index be1c91ebf..fe15742e4 100644
--- a/wolfhsm/wh_keyid.h
+++ b/wolfhsm/wh_keyid.h
@@ -68,6 +68,7 @@ typedef uint16_t whKeyId;
* - Regular keys: Simple numeric ID (e.g., 5)
* - Global keys: ID with WH_KEYID_CLIENT_GLOBAL_FLAG set
* - Wrapped keys: ID with WH_KEYID_CLIENT_WRAPPED_FLAG set
+ * - Hardware-only keys: ID with WH_KEYID_CLIENT_HW_FLAG set
* - Wrapped metadata: Must use full WH_MAKE_KEYID() construction including type
* and metadata when populating the ID field in metadata to be wrapped
*
@@ -78,9 +79,17 @@ typedef uint16_t whKeyId;
/* Bit 9: Client-to-server signal for wrapped key */
#define WH_KEYID_CLIENT_WRAPPED_FLAG ((whKeyId)0x0200)
+/* Bit 10: Client-to-server signal for a hardware-only key. The key material
+ * lives exclusively in a hardware keystore (see wolfhsm/wh_hwkeystore.h) and
+ * is fetched on demand by the server. It never enters the key cache or NVM
+ * and is never returned to a client. Such keys are only usable as KEKs in
+ * the keywrap API */
+#define WH_KEYID_CLIENT_HW_FLAG ((whKeyId)0x0400)
+
/* Combined mask of all client-facing flags */
-#define WH_CLIENT_KEYID_FLAGS_MASK \
- (WH_KEYID_CLIENT_GLOBAL_FLAG | WH_KEYID_CLIENT_WRAPPED_FLAG)
+#define WH_CLIENT_KEYID_FLAGS_MASK \
+ (WH_KEYID_CLIENT_GLOBAL_FLAG | WH_KEYID_CLIENT_WRAPPED_FLAG | \
+ WH_KEYID_CLIENT_HW_FLAG)
/* Macro to construct a server-unique keyid */
#define WH_MAKE_KEYID(_type, _user, _id) \
@@ -93,6 +102,7 @@ typedef uint16_t whKeyId;
#define WH_KEYID_ISERASED(_kid) (WH_KEYID_ID(_kid) == WH_KEYID_ERASED)
#define WH_KEYID_ISWRAPPED(_kid) (WH_KEYID_TYPE(_kid) == WH_KEYTYPE_WRAPPED)
+#define WH_KEYID_ISHW(_kid) (WH_KEYID_TYPE(_kid) == WH_KEYTYPE_HW)
/* Reserve USER=0 for global keys in the internal keyId encoding.
* This is server-internal; clients use WH_KEYID_CLIENT_GLOBAL_FLAG from
@@ -105,6 +115,15 @@ typedef uint16_t whKeyId;
#define WH_KEYTYPE_SHE 0x2 /* SKE keys are AES or CMAC binary arrays */
#define WH_KEYTYPE_COUNTER 0x3 /* Monotonic counter */
#define WH_KEYTYPE_WRAPPED 0x4 /* Wrapped key metadata */
+#define WH_KEYTYPE_HW 0x5 /* HW-only key. Port-specific */
+
+/* True when a key id carries no explicit identifier (ID field == 0) and so must
+ * not be accepted as one - it would collide with the "assign me one" sentinel
+ * used for dynamic id assignment. SHE keys are exempt: their ids are fixed by
+ * the SHE slot map, where slot 0 (SECRET_KEY) is a legitimate explicit id, not
+ * a request for dynamic assignment. */
+#define WH_KEYID_IS_UNASSIGNED(_kid) \
+ (WH_KEYID_ISERASED(_kid) && (WH_KEYID_TYPE(_kid) != WH_KEYTYPE_SHE))
/* Convert a keyId to a pointer to be stored in wolfcrypt devctx */
#define WH_KEYID_TO_DEVCTX(_k) ((void*)((intptr_t)(_k)))
@@ -117,10 +136,12 @@ typedef uint16_t whKeyId;
* (TYPE + USER + ID). Client flags are:
* - 0x0100 (bit 8): WH_KEYID_CLIENT_GLOBAL_FLAG → USER = 0
* - 0x0200 (bit 9): WH_KEYID_CLIENT_WRAPPED_FLAG → TYPE = WH_KEYTYPE_WRAPPED
+ * - 0x0400 (bit 10): WH_KEYID_CLIENT_HW_FLAG → TYPE = WH_KEYTYPE_HW
*
* @param type Key type to use as the TYPE field. Input value is ignored and
- * WH_KEYTYPE_WRAPPED is used if the input clientId has the
- * WH_CLIENT_KEYID_WRAPPED flag set.
+ * WH_KEYTYPE_WRAPPED or WH_KEYTYPE_HW is used if the input clientId has
+ * the corresponding flag set. If both flags are set, HW takes
+ * precedence.
* @param clientId Client identifier to use as USER field. Must be in
* [1, WH_CLIENT_ID_MAX] (0 is reserved for WH_KEYUSER_GLOBAL);
* wh_Client_Init() and the server's INIT handling enforce this so callers
@@ -138,6 +159,7 @@ whKeyId wh_KeyId_TranslateFromClient(uint16_t type, uint16_t clientId,
* client-facing format (ID + flags). Server encoding is converted to flags:
* - USER = 0 (WH_KEYUSER_GLOBAL) → 0x0100 (WH_KEYID_CLIENT_GLOBAL_FLAG)
* - TYPE = WH_KEYTYPE_WRAPPED → 0x0200 (WH_KEYID_CLIENT_WRAPPED_FLAG)
+ * - TYPE = WH_KEYTYPE_HW → 0x0400 (WH_KEYID_CLIENT_HW_FLAG)
*
* This ensures clients can identify global and wrapped keys after they are
* returned from server operations (cache, key generation, etc.).
diff --git a/wolfhsm/wh_message.h b/wolfhsm/wh_message.h
index c562c877f..e1e5f9fd4 100644
--- a/wolfhsm/wh_message.h
+++ b/wolfhsm/wh_message.h
@@ -72,6 +72,7 @@ enum WH_KEY_ENUM {
WH_KEY_DATAUNWRAP,
WH_KEY_EXPORT_PUBLIC,
WH_KEY_EXPORT_PUBLIC_DMA,
+ WH_KEY_KEYWRAPEXPORT,
};
/* SHE actions */
diff --git a/wolfhsm/wh_message_keystore.h b/wolfhsm/wh_message_keystore.h
index 95aea456f..d3b1610aa 100644
--- a/wolfhsm/wh_message_keystore.h
+++ b/wolfhsm/wh_message_keystore.h
@@ -336,6 +336,34 @@ int wh_MessageKeystore_TranslateKeyWrapResponse(
uint16_t magic, const whMessageKeystore_KeyWrapResponse* src,
whMessageKeystore_KeyWrapResponse* dest);
+/* Wrap-and-export (by id) Request: wrap a key the server already holds,
+ * identified by keyId, and return the wrapped blob. No data follows. */
+typedef struct {
+ uint16_t keyId; /* client-facing id (+GLOBAL/WRAPPED flags) to wrap */
+ uint16_t keyType; /* WH_KEYTYPE_* of the target key (CRYPTO or SHE) */
+ uint16_t serverKeyId; /* KEK (client-facing id) */
+ uint16_t cipherType; /* enum wc_CipherType */
+} whMessageKeystore_KeyWrapExportRequest;
+
+/* Wrap-and-export (by id) Response */
+typedef struct {
+ uint32_t rc;
+ uint16_t wrappedKeySz;
+ uint16_t cipherType;
+ /* Data follows:
+ * uint8_t wrappedKey[wrappedKeySz]
+ */
+} whMessageKeystore_KeyWrapExportResponse;
+
+/* Wrap-and-export translation functions */
+int wh_MessageKeystore_TranslateKeyWrapExportRequest(
+ uint16_t magic, const whMessageKeystore_KeyWrapExportRequest* src,
+ whMessageKeystore_KeyWrapExportRequest* dest);
+
+int wh_MessageKeystore_TranslateKeyWrapExportResponse(
+ uint16_t magic, const whMessageKeystore_KeyWrapExportResponse* src,
+ whMessageKeystore_KeyWrapExportResponse* dest);
+
/* Unwrap Key export Request */
typedef struct {
uint16_t wrappedKeySz;
diff --git a/wolfhsm/wh_nvm.h b/wolfhsm/wh_nvm.h
index 91506f148..c141ee23e 100644
--- a/wolfhsm/wh_nvm.h
+++ b/wolfhsm/wh_nvm.h
@@ -286,22 +286,31 @@ int wh_Nvm_AddObject(whNvmContext* context, whNvmMetadata* meta,
whNvmSize data_len, const uint8_t* data);
/**
- * @brief Adds an object to NVM with policy checking.
- *
- * Same as wh_Nvm_AddObject(), but first checks if an existing object with the
- * same ID has the WH_NVM_FLAGS_NONMODIFIABLE flag set. If so, returns an
- * access error.
+ * @brief Adds an object to NVM with policy checking. Use this for adds driven
+ * by client requests; trusted/internal provisioning should use
+ * wh_Nvm_AddObject().
+ *
+ * Same as wh_Nvm_AddObject(), but enforces client policy:
+ * - Rejects the add if an existing object with the same ID is non-modifiable
+ * (WH_NVM_FLAGS_NONMODIFIABLE) or is a server-only key (WH_NVM_FLAGS_KEK).
+ * - Strips the server-only flags (via WH_NVM_FLAGS_SERVER_ONLY bitmask) before
+ * storing, so a client request can never set them. Only the unchecked
+ * wh_Nvm_AddObject() can set those flags. The caller's metadata is copied
+ * internally and left unmodified (meta may point at read-only client DMA
+ * memory).
*
* @param[in] context Pointer to the NVM context. Must not be NULL.
- * @param[in,out] meta Pointer to the object metadata. Must not be NULL.
+ * @param[in] meta Pointer to the object metadata. Must not be NULL. Not
+ * modified; the server sanitizes an internal copy.
* @param[in] data_len Length of the object data in bytes.
* @param[in] data Pointer to the object data.
* @return int WH_ERROR_OK on success.
- * WH_ERROR_ACCESS if existing object is non-modifiable.
+ * WH_ERROR_ACCESS if the existing object is non-modifiable or a
+ * server-only key.
* WH_ERROR_BADARGS if context is NULL or not initialized.
* Other negative error codes on backend failure.
*/
-int wh_Nvm_AddObjectChecked(whNvmContext* context, whNvmMetadata* meta,
+int wh_Nvm_AddObjectChecked(whNvmContext* context, const whNvmMetadata* meta,
whNvmSize data_len, const uint8_t* data);
/**
@@ -377,15 +386,16 @@ int wh_Nvm_DestroyObjects(whNvmContext* context, whNvmId list_count,
* @brief Destroys a list of objects from NVM with policy checking.
*
* Same as wh_Nvm_DestroyObjects(), but first checks if any object in the list
- * has the WH_NVM_FLAGS_NONMODIFIABLE or WH_NVM_FLAGS_NONDESTROYABLE flags set.
- * If so, returns an access error without destroying any objects.
+ * has the WH_NVM_FLAGS_NONMODIFIABLE or WH_NVM_FLAGS_NONDESTROYABLE flags set,
+ * or is a server-only key (WH_NVM_FLAGS_KEK). If so, returns an access error
+ * without destroying any objects.
*
* @param[in] context Pointer to the NVM context. Must not be NULL.
* @param[in] list_count Number of IDs in the list.
* @param[in] id_list Array of object IDs to destroy.
* @return int WH_ERROR_OK on success.
- * WH_ERROR_ACCESS if any object is non-modifiable or
- * non-destroyable.
+ * WH_ERROR_ACCESS if any object is non-modifiable,
+ * non-destroyable, or a server-only key.
* WH_ERROR_BADARGS if context is NULL, not initialized, or
* id_list is NULL with non-zero list_count.
* Other negative error codes on backend failure.
@@ -416,7 +426,8 @@ int wh_Nvm_Read(whNvmContext* context, whNvmId id, whNvmSize offset,
* @brief Reads data from an NVM object with policy checking.
*
* Same as wh_Nvm_Read(), but first checks if the object has the
- * WH_NVM_FLAGS_NONEXPORTABLE flag set. If so, returns an access error.
+ * WH_NVM_FLAGS_NONEXPORTABLE flag set, or is a server-only key
+ * (WH_NVM_FLAGS_KEK). If so, returns an access error.
*
* @param[in] context Pointer to the NVM context. Must not be NULL.
* @param[in] id ID of the object to read from.
@@ -424,7 +435,7 @@ int wh_Nvm_Read(whNvmContext* context, whNvmId id, whNvmSize offset,
* @param[in] data_len Number of bytes to read.
* @param[out] data Buffer to store the read data.
* @return int WH_ERROR_OK on success.
- * WH_ERROR_ACCESS if object is non-exportable.
+ * WH_ERROR_ACCESS if object is non-exportable or a server-only key.
* WH_ERROR_BADARGS if context is NULL or not initialized.
* WH_ERROR_NOTFOUND if the object does not exist.
* Other negative error codes on backend failure.
diff --git a/wolfhsm/wh_server.h b/wolfhsm/wh_server.h
index 30eb56f09..4611d4440 100644
--- a/wolfhsm/wh_server.h
+++ b/wolfhsm/wh_server.h
@@ -44,6 +44,9 @@ typedef struct whServerContext_t whServerContext;
#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION
#include "wolfhsm/wh_auth.h"
#endif /* WOLFHSM_CFG_ENABLE_AUTHENTICATION */
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+#include "wolfhsm/wh_hwkeystore.h"
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
#include "wolfhsm/wh_message_customcb.h"
#include "wolfhsm/wh_log.h"
#ifdef WOLFHSM_CFG_DMA
@@ -145,6 +148,9 @@ typedef struct whServerConfig_t {
#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION
whAuthContext* auth;
#endif /* WOLFHSM_CFG_ENABLE_AUTHENTICATION */
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ whHwKeystoreContext* hwKeystore; /* optional; NULL = no HW keystore */
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
#ifndef WOLFHSM_CFG_NO_CRYPTO
whServerCryptoContext* crypto;
@@ -173,6 +179,9 @@ struct whServerContext_t {
#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION
whAuthContext* auth;
#endif /* WOLFHSM_CFG_ENABLE_AUTHENTICATION */
+#ifdef WOLFHSM_CFG_HWKEYSTORE
+ whHwKeystoreContext* hwKeystore;
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
whCommServer comm[1];
#ifndef WOLFHSM_CFG_NO_CRYPTO
whServerCryptoContext* crypto;
diff --git a/wolfhsm/wh_settings.h b/wolfhsm/wh_settings.h
index 5a1e3bce5..344fa06cd 100644
--- a/wolfhsm/wh_settings.h
+++ b/wolfhsm/wh_settings.h
@@ -47,6 +47,14 @@
* can be wrapped
* Default: 512
*
+ * WOLFHSM_CFG_HWKEYSTORE - If defined, include the hardware keystore
+ * front-end module and hardware-only key (WH_KEYTYPE_HW) support
+ * Default: Not defined
+ *
+ * WOLFHSM_CFG_HWKEYSTORE_MAX_KEY_SIZE - The maximum size (in bytes) of a key
+ * served by a hardware keystore backend
+ * Default: 32
+ *
* WOLFHSM_CFG_HEXDUMP - If defined, include wh_Utils_HexDump functionality
* using stdio.h
* Default: Not defined
@@ -464,6 +472,12 @@
#error "WOLFHSM_CFG_KEYWRAP is incompatible with WOLFHSM_CFG_NO_CRYPTO"
#endif
+#if defined(WOLFHSM_CFG_HWKEYSTORE)
+#ifndef WOLFHSM_CFG_HWKEYSTORE_MAX_KEY_SIZE
+#define WOLFHSM_CFG_HWKEYSTORE_MAX_KEY_SIZE 32
+#endif
+#endif /* WOLFHSM_CFG_HWKEYSTORE */
+
/* Trusted cert verify cache requires the certificate manager and crypto.
* Enforce here so downstream code can gate on
* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE alone instead of repeating the full