diff --git a/src/wh_message_nvm.c b/src/wh_message_nvm.c
index faaf3a0d5..304cdb18c 100644
--- a/src/wh_message_nvm.c
+++ b/src/wh_message_nvm.c
@@ -36,6 +36,24 @@
#include "wolfhsm/wh_message_nvm.h"
+/* Keep in step with wh_MessageNvm_TranslateAddObjectRequest */
+int wh_MessageNvm_TranslateMetadata(uint16_t magic, const whNvmMetadata* src,
+ whNvmMetadata* dest)
+{
+ if ((src == NULL) || (dest == NULL)) {
+ return WH_ERROR_BADARGS;
+ }
+ WH_T16(magic, dest, src, id);
+ WH_T16(magic, dest, src, access);
+ WH_T16(magic, dest, src, flags);
+ WH_T16(magic, dest, src, len);
+ /* Label is just a byte array, no translation needed */
+ if (src != dest) {
+ memcpy(dest->label, src->label, sizeof(dest->label));
+ }
+ return 0;
+}
+
int wh_MessageNvm_TranslateSimpleResponse(uint16_t magic,
const whMessageNvm_SimpleResponse* src,
whMessageNvm_SimpleResponse* dest)
@@ -141,6 +159,8 @@ int wh_MessageNvm_TranslateGetMetadataResponse(uint16_t magic,
return 0;
}
+/* The request mirrors whNvmMetadata: keep in step with
+ * wh_MessageNvm_TranslateMetadata */
int wh_MessageNvm_TranslateAddObjectRequest(uint16_t magic,
const whMessageNvm_AddObjectRequest* src,
whMessageNvm_AddObjectRequest* dest)
diff --git a/src/wh_server_keystore.c b/src/wh_server_keystore.c
index b08143c67..9a8c6627a 100644
--- a/src/wh_server_keystore.c
+++ b/src/wh_server_keystore.c
@@ -40,6 +40,7 @@
#include "wolfhsm/wh_error.h"
#include "wolfhsm/wh_message.h"
#include "wolfhsm/wh_message_keystore.h"
+#include "wolfhsm/wh_message_nvm.h" /* For wh_MessageNvm_TranslateMetadata */
#include "wolfhsm/wh_utils.h"
#include "wolfhsm/wh_server.h"
#include "wolfhsm/wh_log.h"
@@ -1805,6 +1806,7 @@ static int _AesGcmDataUnwrap(whServerContext* server, uint16_t serverKeyId,
#endif /* !NO_AES */
static int _HandleKeyWrapRequest(whServerContext* server,
+ uint16_t magic,
whMessageKeystore_KeyWrapRequest* req,
uint8_t* reqData, uint32_t reqDataSz,
whMessageKeystore_KeyWrapResponse* resp,
@@ -1824,20 +1826,24 @@ static int _HandleKeyWrapRequest(whServerContext* server,
return WH_ERROR_BADARGS;
}
+ /* Set before any failure exit: the client checks cipherType before rc */
+ resp->cipherType = req->cipherType;
+ resp->wrappedKeySz = 0;
+
/* Check if the reqData is big enough to hold the metadata and key */
if (reqDataSz < sizeof(metadata) + req->keySz) {
return WH_ERROR_BUFFER_SIZE;
}
- /* Extract the metadata and key from reqData */
+ /* Extract the metadata and key. The metadata trailer arrives in the
+ * client's byte order, so translate before any field is used */
memcpy(&metadata, reqData, sizeof(metadata));
+ ret = wh_MessageNvm_TranslateMetadata(magic, &metadata, &metadata);
+ if (ret != WH_ERROR_OK) {
+ return ret;
+ }
memcpy(key, reqData + sizeof(metadata), req->keySz);
- /* 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;
-
/* Ensure the keyId in the wrapped metadata has the wrapped flag set */
if (!WH_KEYID_ISWRAPPED(metadata.id)) {
WH_LOG_F(&server->log, WH_LOG_LEVEL_ERROR,
@@ -2012,16 +2018,18 @@ _HandleKeyWrapExportRequest(whServerContext* server,
}
static int _HandleKeyUnwrapAndExportRequest(
- whServerContext* server, whMessageKeystore_KeyUnwrapAndExportRequest* req,
- uint8_t* reqData, uint32_t reqDataSz,
- whMessageKeystore_KeyUnwrapAndExportResponse* resp, uint8_t* respData,
- uint32_t respDataSz)
+ whServerContext* server, uint16_t magic,
+ whMessageKeystore_KeyUnwrapAndExportRequest* req, uint8_t* reqData,
+ uint32_t reqDataSz, whMessageKeystore_KeyUnwrapAndExportResponse* resp,
+ uint8_t* respData, uint32_t respDataSz)
{
- int ret;
+ /* Defensive: a case that never assigns ret cannot report success */
+ int ret = WH_ERROR_BADARGS;
uint8_t* wrappedKey;
whNvmMetadata* metadata;
uint8_t* key;
whKeyId serverKeyId;
+ uint16_t keySz = 0;
if (server == NULL || req == NULL || reqData == NULL || resp == NULL ||
respData == NULL) {
@@ -2055,11 +2063,13 @@ static int _HandleKeyUnwrapAndExportRequest(
#ifndef NO_AES
#ifdef HAVE_AESGCM
case WC_CIPHER_AES_GCM: {
- uint16_t keySz;
+ uint16_t wrappedKeyUser = 0;
+ uint16_t wrappedKeyType = 0;
if (req->wrappedKeySz < WH_KEYWRAP_AES_GCM_HEADER_SIZE +
sizeof(*metadata)) {
- return WH_ERROR_BADARGS;
+ ret = WH_ERROR_BADARGS;
+ break;
}
keySz = req->wrappedKeySz -
@@ -2067,7 +2077,8 @@ static int _HandleKeyUnwrapAndExportRequest(
/* Check if the response data can fit the metadata + key */
if (respDataSz < sizeof(*metadata) + keySz) {
- return WH_ERROR_BUFFER_SIZE;
+ ret = WH_ERROR_BUFFER_SIZE;
+ break;
}
/* Unwrap the key. The plaintext is handed back to the client, not
@@ -2076,28 +2087,31 @@ static int _HandleKeyUnwrapAndExportRequest(
/*requireTrustedKek=*/0, wrappedKey,
req->wrappedKeySz, metadata, key, keySz);
if (ret != WH_ERROR_OK) {
- return ret;
+ break;
}
/* Dynamic keyId generation for wrapped keys is not allowed */
if (WH_KEYID_IS_UNASSIGNED(metadata->id)) {
/* Wrapped keys must use explicit identifiers */
- return WH_ERROR_BADARGS;
+ ret = WH_ERROR_BADARGS;
+ break;
}
/* 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;
+ ret = WH_ERROR_ABORTED;
+ break;
}
/* Check if the key is exportable */
if (metadata->flags & WH_NVM_FLAGS_NONEXPORTABLE) {
- return WH_ERROR_ACCESS;
+ ret = WH_ERROR_ACCESS;
+ break;
}
/* Validate ownership: USER field must match requesting client.
@@ -2106,12 +2120,14 @@ static int _HandleKeyUnwrapAndExportRequest(
/* Global keys (USER=0) can be exported by any client */
if (wrappedKeyUser != WH_KEYUSER_GLOBAL &&
wrappedKeyUser != server->comm->client_id) {
- return WH_ERROR_ACCESS;
+ ret = WH_ERROR_ACCESS;
+ break;
}
#else
/* Without global keys, USER must match requesting client */
if (wrappedKeyUser != server->comm->client_id) {
- return WH_ERROR_ACCESS;
+ ret = WH_ERROR_ACCESS;
+ break;
}
#endif /* WOLFHSM_CFG_GLOBAL_KEYS */
@@ -2122,7 +2138,26 @@ static int _HandleKeyUnwrapAndExportRequest(
#endif /* !NO_AES */
default:
- return WH_ERROR_BADARGS;
+ ret = WH_ERROR_BADARGS;
+ break;
+ }
+
+ if (ret == WH_ERROR_OK) {
+ /* The blob stores metadata in server order, so every check above ran
+ * on native values. Convert to client order only on the way out */
+ ret = wh_MessageNvm_TranslateMetadata(magic, metadata, metadata);
+ }
+
+ /* Keyed off the final ret so a failed translation scrubs too. respData is
+ * the long-lived comm buffer, so wipe the trailer and any decrypted key */
+ if (ret != WH_ERROR_OK && respDataSz >= sizeof(*metadata)) {
+ uint32_t scrubSz = sizeof(*metadata) + keySz;
+
+ if (scrubSz > respDataSz) {
+ scrubSz = respDataSz;
+ }
+ resp->keySz = 0;
+ wh_Utils_ForceZero(metadata, scrubSz);
}
return ret;
@@ -2472,7 +2507,8 @@ int wh_Server_HandleKeyRequest(whServerContext* server, uint16_t magic,
/* validate args, even though these functions are only supposed to be
* called by internal functions */
- if ((server == NULL) || (req_packet == NULL) || (out_resp_size == NULL)) {
+ if ((server == NULL) || (req_packet == NULL) || (resp_packet == NULL) ||
+ (out_resp_size == NULL)) {
return WH_ERROR_BADARGS;
}
@@ -3198,8 +3234,8 @@ int wh_Server_HandleKeyRequest(whServerContext* server, uint16_t magic,
if (ret == WH_ERROR_OK) {
ret = WH_SERVER_NVM_LOCK(server);
if (ret == WH_ERROR_OK) {
- ret = _HandleKeyWrapRequest(server, &wrapReq, reqData,
- reqDataSz, &wrapResp,
+ ret = _HandleKeyWrapRequest(server, magic, &wrapReq,
+ reqData, reqDataSz, &wrapResp,
respData, respDataSz);
(void)WH_SERVER_NVM_UNLOCK(server);
@@ -3300,14 +3336,22 @@ int wh_Server_HandleKeyRequest(whServerContext* server, uint16_t magic,
ret = WH_SERVER_NVM_LOCK(server);
if (ret == WH_ERROR_OK) {
ret = _HandleKeyUnwrapAndExportRequest(
- server, &unwrapReq, reqData, reqDataSz, &unwrapResp,
- respData, respDataSz);
+ server, magic, &unwrapReq, reqData, reqDataSz,
+ &unwrapResp, respData, respDataSz);
(void)WH_SERVER_NVM_UNLOCK(server);
} /* WH_SERVER_NVM_LOCK() */
}
unwrapResp.rc = ret;
+ /* The size below always counts a trailer, so clear what a failure
+ * would otherwise ship out of the shared buffer */
+ if (ret != WH_ERROR_OK && respDataSz >= sizeof(whNvmMetadata)) {
+ wh_Utils_ForceZero((uint8_t*)resp_packet +
+ sizeof(unwrapResp),
+ sizeof(whNvmMetadata));
+ }
+
(void)wh_MessageKeystore_TranslateKeyUnwrapAndExportResponse(
magic, &unwrapResp, resp_packet);
diff --git a/test-refactor/README.md b/test-refactor/README.md
index 979fba65e..db6022407 100644
--- a/test-refactor/README.md
+++ b/test-refactor/README.md
@@ -127,6 +127,8 @@ Not yet migrated (still live in `wolfHSM/test/`):
### Known coverage gaps
- FLASH_LOG backend for server/client-group tests. In `test/`, cert (`whTest_CertRamSim`), crypto (`wh_ClientServer_MemThreadTest`), image-manager (`whTest_ServerImgMgr`), and client/server were each run against both the plain flash and flash-log NVM backends. The refactored server/client-group tests consume a single server context (`wh_test_posix_server.c` hard-codes `WH_NVM_FLASH_CB`), so only the plain flash backend is exercised. Restoring parity means selecting the server backend via `whTest_NvmCfgBackend` and running the server + client groups once per backend (flash, then flash-log) from `wh_test_posix_main.c`. Tracked as a follow-up; the port-specific `whTest_NvmFlashLog` already covers the flash-log NVM object lifecycle directly.
+- Cross-endian metadata on the DMA NVM paths. `server/wh_test_keywrap_endian.c` and `client-server/wh_test_keywrap_endian_e2e.c` cover the `whNvmMetadata` trailer the key wrap messages carry, which the server translates via `wh_MessageNvm_TranslateMetadata()`. The DMA NVM paths (`WH_MESSAGE_NVM_ACTION_ADDOBJECTDMA` ingress and `WH_MESSAGE_NVM_ACTION_READDMA` write-back) hand the same struct across untranslated, so a big-endian client against a little-endian server writes byte-reversed `id`, `access`, `flags` and `len` into NVM. Pre-existing and untested in either harness. A fix must translate into a local copy: the struct lives in client DMA memory, so translating in place would write into the client's buffer.
+
### Other improvements
- Add callback from `wh_Server_HandleRequestMessage` to allow sleep and avoid a busy loop
- Add client-only harness to feed invalid server inputs from the test bench with the goal of expanding coverage.
diff --git a/test-refactor/client-server/wh_test_keywrap_endian_e2e.c b/test-refactor/client-server/wh_test_keywrap_endian_e2e.c
new file mode 100644
index 000000000..424768c54
--- /dev/null
+++ b/test-refactor/client-server/wh_test_keywrap_endian_e2e.c
@@ -0,0 +1,459 @@
+/*
+ * 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/client-server/wh_test_keywrap_endian_e2e.c
+ *
+ * End-to-end byte-order coverage for the key wrap metadata trailer, against a
+ * live server over the real transport:
+ *
+ * _whTest_KeyWrapE2ERoundTrip - wrap -> unwrap-and-export, and a denied
+ * export, whole packet in native and in
+ * swapped byte order
+ *
+ * Packets are hand built and pushed straight at the transport, because
+ * wh_CommClient always sends WH_COMM_MAGIC_NATIVE and rejects a non-native
+ * response. The hand-written swaps are also an oracle independent of the
+ * wh_Message*_Translate* code under test.
+ *
+ * Reaches what server/wh_test_keywrap_endian.c cannot: the comm layer accepting
+ * a non-native magic, and wh_Server_HandleRequestMessage passing one packet
+ * buffer as both req_packet and resp_packet, so the egress translation is an
+ * in-place swap inside the wrapped blob. The mem transport's own buffers are
+ * separate; that aliasing is above it.
+ */
+
+#include "wolfhsm/wh_settings.h"
+
+#if !defined(WOLFHSM_CFG_NO_CRYPTO) && defined(WOLFHSM_CFG_KEYWRAP) && \
+ !defined(NO_AES) && defined(HAVE_AESGCM) && \
+ defined(WOLFHSM_CFG_ENABLE_CLIENT)
+
+#include
+#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_message.h"
+#include "wolfhsm/wh_message_keystore.h"
+#include "wolfhsm/wh_client.h"
+
+#include "wh_test_common.h"
+#include "wh_test_list.h"
+
+/* Distinct id range so nothing collides with the other client suites */
+#define WH_TEST_KWE2E_KEK_ID 0x72
+#define WH_TEST_KWE2E_META_ID 0x26
+
+#define WH_TEST_KWE2E_KEYSIZE 32
+#define WH_TEST_KWE2E_WRAPPED_SIZE \
+ (WH_KEYWRAP_AES_GCM_HEADER_SIZE + sizeof(whNvmMetadata) + \
+ WH_TEST_KWE2E_KEYSIZE)
+
+/* Non-palindromic access mask: a missed translation changes its value */
+#define WH_TEST_KWE2E_ACCESS 0x1234
+
+/* Livelock backstop, not a timeout. Each spin costs a memory fence and two CSR
+ * loads; an observed exchange settles under 2000, so this caps a stuck loop
+ * below a second */
+#define WH_TEST_KWE2E_MAX_SPINS 10000000u
+
+/* The packet is cast to whCommHeader, so give it pointer-grade alignment.
+ * Static to keep it off the stack */
+static union {
+ uint64_t align[WH_COMM_MTU_U64_COUNT];
+ uint8_t bytes[WH_COMM_MTU];
+} _pkt;
+
+/* Byte order conversion is its own inverse, so one routine covers both
+ * encoding a request and decoding a response */
+static uint16_t _Swap16If(int swap, uint16_t val)
+{
+ if (swap == 0) {
+ return val;
+ }
+ return (uint16_t)(((val & 0xFF00u) >> 8) | ((val & 0x00FFu) << 8));
+}
+
+static uint32_t _Swap32If(int swap, uint32_t val)
+{
+ if (swap == 0) {
+ return val;
+ }
+ return ((val & 0xFF000000u) >> 24) | ((val & 0x00FF0000u) >> 8) |
+ ((val & 0x0000FF00u) << 8) | ((val & 0x000000FFu) << 24);
+}
+
+static void _SwapMetadataIf(int swap, const whNvmMetadata* src,
+ whNvmMetadata* dest)
+{
+ dest->id = _Swap16If(swap, src->id);
+ dest->access = _Swap16If(swap, src->access);
+ dest->flags = _Swap16If(swap, src->flags);
+ dest->len = _Swap16If(swap, src->len);
+ memcpy(dest->label, src->label, sizeof(dest->label));
+}
+
+static int _IsZeroed(const uint8_t* buf, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++) {
+ if (buf[i] != 0) {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+/* Every 16-bit field takes a value that differs from its byte-swapped self,
+ * so an untranslated field cannot pass unnoticed */
+static void _FillMetadata(whNvmMetadata* meta, whKeyId id)
+{
+ size_t i;
+
+ memset(meta, 0, sizeof(*meta));
+ meta->id = id;
+ meta->access = WH_TEST_KWE2E_ACCESS;
+ meta->flags = WH_NVM_FLAGS_USAGE_ANY;
+ meta->len = WH_TEST_KWE2E_KEYSIZE;
+ for (i = 0; i < sizeof(meta->label); i++) {
+ meta->label[i] = (uint8_t)(0xA0 + i);
+ }
+}
+
+/* Plain client KEK, cached through the ordinary client API. Enough for wrap
+ * and unwrap-and-export, which never require a trusted KEK */
+static int _CacheKek(whClientContext* client, whKeyId* outKekId)
+{
+ whKeyId kekId = WH_TEST_KWE2E_KEK_ID;
+ uint8_t label[WH_NVM_LABEL_LEN] = "KwE2E KEK";
+ uint8_t kek[WH_TEST_KWE2E_KEYSIZE];
+ size_t i;
+
+ for (i = 0; i < sizeof(kek); i++) {
+ kek[i] = (uint8_t)(0xC2 ^ i);
+ }
+
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCache(client, WH_NVM_FLAGS_USAGE_WRAP,
+ label, (uint16_t)sizeof(label),
+ kek, sizeof(kek), &kekId));
+ *outKekId = kekId;
+ return WH_ERROR_OK;
+}
+
+/* Frame the payload already in _pkt behind the header, send it, and spin for
+ * the reply. seq comes from the caller: this path never advances comm->seq */
+static int _Exchange(whClientContext* client, uint16_t magic, uint16_t kind,
+ uint16_t seq, uint16_t reqDataSz, uint16_t* outRespDataSz)
+{
+ const whTransportClientCb* cb;
+ void* tctx;
+ whCommHeader* hdr;
+ int swap;
+ int rc;
+ uint32_t spins;
+ uint16_t size;
+
+ cb = client->comm->transport_cb;
+ tctx = client->comm->transport_context;
+ hdr = (whCommHeader*)_pkt.bytes;
+ swap = (magic != WH_COMM_MAGIC_NATIVE);
+
+ if (cb == NULL || cb->Send == NULL || cb->Recv == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ hdr->magic = magic;
+ hdr->kind = _Swap16If(swap, kind);
+ hdr->seq = _Swap16If(swap, seq);
+ hdr->aux = _Swap16If(swap, WH_COMM_AUX_REQ_NORMAL);
+
+ spins = 0;
+ do {
+ rc = cb->Send(tctx, (uint16_t)(sizeof(*hdr) + reqDataSz), _pkt.bytes);
+ spins++;
+ } while (rc == WH_ERROR_NOTREADY && spins < WH_TEST_KWE2E_MAX_SPINS);
+ if (rc != WH_ERROR_OK) {
+ WH_ERROR_PRINT("e2e: transport Send failed %d\n", rc);
+ return (rc == WH_ERROR_NOTREADY) ? WH_ERROR_ABORTED : rc;
+ }
+
+ /* out_size is the destination capacity on the way in, and the true
+ * response length on the way out, so reset it before every attempt */
+ spins = 0;
+ do {
+ size = (uint16_t)sizeof(_pkt.bytes);
+ rc = cb->Recv(tctx, &size, _pkt.bytes);
+ spins++;
+ } while (rc == WH_ERROR_NOTREADY && spins < WH_TEST_KWE2E_MAX_SPINS);
+ if (rc != WH_ERROR_OK) {
+ WH_ERROR_PRINT("e2e: transport Recv failed %d\n", rc);
+ return (rc == WH_ERROR_NOTREADY) ? WH_ERROR_ABORTED : rc;
+ }
+
+ /* Bound the reported length like wh_CommClient_RecvResponse does: the
+ * transport clamps its copy but still reports the peer's true length */
+ WH_TEST_ASSERT_RETURN(size >= sizeof(*hdr));
+ WH_TEST_ASSERT_RETURN(size <= sizeof(_pkt.bytes));
+
+ /* The server must echo the magic it was addressed with, and answer the
+ * message it was sent */
+ WH_TEST_ASSERT_RETURN(hdr->magic == magic);
+ WH_TEST_ASSERT_RETURN(_Swap16If(swap, hdr->kind) == kind);
+ WH_TEST_ASSERT_RETURN(_Swap16If(swap, hdr->seq) == seq);
+
+ *outRespDataSz = (uint16_t)(size - sizeof(*hdr));
+ return WH_ERROR_OK;
+}
+
+/* Wrap a plaintext key, presenting header, metadata trailer and key in the
+ * byte order the given magic denotes */
+static int _WrapWithMagic(whClientContext* client, uint16_t magic,
+ whKeyId kekId, const whNvmMetadata* metaIn,
+ const uint8_t* keyIn, uint16_t keySz,
+ uint8_t* wrappedOut, uint16_t* wrappedInOutSz)
+{
+ whMessageKeystore_KeyWrapRequest wireReq;
+ whMessageKeystore_KeyWrapResponse wireResp;
+ whNvmMetadata wireMeta;
+ uint8_t* data;
+ int swap;
+ uint16_t respDataSz = 0;
+ uint16_t wrappedSz;
+
+ swap = (magic != WH_COMM_MAGIC_NATIVE);
+ data = _pkt.bytes + sizeof(whCommHeader);
+
+ memset(&wireReq, 0, sizeof(wireReq));
+ wireReq.keySz = _Swap16If(swap, keySz);
+ wireReq.serverKeyId = _Swap16If(swap, (uint16_t)kekId);
+ wireReq.cipherType = _Swap16If(swap, (uint16_t)WC_CIPHER_AES_GCM);
+ _SwapMetadataIf(swap, metaIn, &wireMeta);
+
+ memcpy(data, &wireReq, sizeof(wireReq));
+ memcpy(data + sizeof(wireReq), &wireMeta, sizeof(wireMeta));
+ memcpy(data + sizeof(wireReq) + sizeof(wireMeta), keyIn, keySz);
+
+ WH_TEST_RETURN_ON_FAIL(_Exchange(
+ client, magic, WH_MESSAGE_KIND(WH_MESSAGE_GROUP_KEY, WH_KEY_KEYWRAP),
+ (uint16_t)(client->comm->seq + 1),
+ (uint16_t)(sizeof(wireReq) + sizeof(wireMeta) + keySz), &respDataSz));
+
+ WH_TEST_ASSERT_RETURN(respDataSz >= sizeof(wireResp));
+ memcpy(&wireResp, data, sizeof(wireResp));
+ WH_TEST_ASSERT_RETURN((int32_t)_Swap32If(swap, wireResp.rc) == WH_ERROR_OK);
+ WH_TEST_ASSERT_RETURN(_Swap16If(swap, wireResp.cipherType) ==
+ (uint16_t)WC_CIPHER_AES_GCM);
+
+ wrappedSz = _Swap16If(swap, wireResp.wrappedKeySz);
+ WH_TEST_ASSERT_RETURN(wrappedSz <= *wrappedInOutSz);
+ WH_TEST_ASSERT_RETURN(respDataSz >= sizeof(wireResp) + wrappedSz);
+
+ /* The blob is opaque bytes, so it needs no translation */
+ memcpy(wrappedOut, data + sizeof(wireResp), wrappedSz);
+ *wrappedInOutSz = wrappedSz;
+
+ return WH_ERROR_OK;
+}
+
+/* Unwrap-and-export the blob and decode the metadata trailer the server
+ * returns ahead of the key */
+static int _UnwrapWithMagic(whClientContext* client, uint16_t magic,
+ whKeyId kekId, const uint8_t* wrappedIn,
+ uint16_t wrappedSz, whNvmMetadata* metaOut,
+ uint8_t* keyOut, uint16_t* keyInOutSz, int expectRc)
+{
+ whMessageKeystore_KeyUnwrapAndExportRequest wireReq;
+ whMessageKeystore_KeyUnwrapAndExportResponse wireResp;
+ whNvmMetadata wireMeta;
+ uint8_t* data;
+ int swap;
+ uint16_t respDataSz = 0;
+ uint16_t keySz;
+
+ swap = (magic != WH_COMM_MAGIC_NATIVE);
+ data = _pkt.bytes + sizeof(whCommHeader);
+
+ memset(&wireReq, 0, sizeof(wireReq));
+ wireReq.wrappedKeySz = _Swap16If(swap, wrappedSz);
+ wireReq.serverKeyId = _Swap16If(swap, (uint16_t)kekId);
+ wireReq.cipherType = _Swap16If(swap, (uint16_t)WC_CIPHER_AES_GCM);
+
+ memcpy(data, &wireReq, sizeof(wireReq));
+ memcpy(data + sizeof(wireReq), wrappedIn, wrappedSz);
+
+ WH_TEST_RETURN_ON_FAIL(_Exchange(
+ client, magic,
+ WH_MESSAGE_KIND(WH_MESSAGE_GROUP_KEY, WH_KEY_KEYUNWRAPEXPORT),
+ (uint16_t)(client->comm->seq + 1),
+ (uint16_t)(sizeof(wireReq) + wrappedSz), &respDataSz));
+
+ WH_TEST_ASSERT_RETURN(respDataSz >= sizeof(wireResp));
+ memcpy(&wireResp, data, sizeof(wireResp));
+ WH_TEST_ASSERT_RETURN((int32_t)_Swap32If(swap, wireResp.rc) == expectRc);
+ WH_TEST_ASSERT_RETURN(_Swap16If(swap, wireResp.cipherType) ==
+ (uint16_t)WC_CIPHER_AES_GCM);
+
+ keySz = _Swap16If(swap, wireResp.keySz);
+
+ if (expectRc != WH_ERROR_OK) {
+ /* Over the wire, through the buffer the server uses for both request
+ * and response: the trailer ships but must carry nothing */
+ WH_TEST_ASSERT_RETURN(keySz == 0);
+ WH_TEST_ASSERT_RETURN(respDataSz ==
+ sizeof(wireResp) + sizeof(wireMeta));
+ WH_TEST_ASSERT_RETURN(_IsZeroed(data + sizeof(wireResp),
+ sizeof(wireMeta)));
+ return WH_ERROR_OK;
+ }
+
+ WH_TEST_ASSERT_RETURN(keySz <= *keyInOutSz);
+ WH_TEST_ASSERT_RETURN(respDataSz >=
+ sizeof(wireResp) + sizeof(wireMeta) + keySz);
+
+ memcpy(&wireMeta, data + sizeof(wireResp), sizeof(wireMeta));
+ _SwapMetadataIf(swap, &wireMeta, metaOut);
+ memcpy(keyOut, data + sizeof(wireResp) + sizeof(wireMeta), keySz);
+ *keyInOutSz = keySz;
+
+ return WH_ERROR_OK;
+}
+
+static int _RoundTrip(whClientContext* client, uint16_t magic, whKeyId kekId)
+{
+ whNvmMetadata metaIn;
+ whNvmMetadata metaOut;
+ uint8_t keyIn[WH_TEST_KWE2E_KEYSIZE];
+ uint8_t keyOut[WH_TEST_KWE2E_KEYSIZE];
+ uint8_t wrapped[WH_TEST_KWE2E_WRAPPED_SIZE];
+ uint16_t wrappedSz = (uint16_t)sizeof(wrapped);
+ uint16_t keyOutSz = (uint16_t)sizeof(keyOut);
+ size_t i;
+
+ for (i = 0; i < sizeof(keyIn); i++) {
+ keyIn[i] = (uint8_t)(0x7B ^ i);
+ }
+ memset(&metaOut, 0, sizeof(metaOut));
+ memset(keyOut, 0, sizeof(keyOut));
+ _FillMetadata(&metaIn, WH_CLIENT_KEYID_MAKE_WRAPPED_META(
+ client->comm->client_id, WH_TEST_KWE2E_META_ID));
+
+ WH_TEST_RETURN_ON_FAIL(_WrapWithMagic(client, magic, kekId, &metaIn, keyIn,
+ (uint16_t)sizeof(keyIn), wrapped,
+ &wrappedSz));
+ WH_TEST_RETURN_ON_FAIL(_UnwrapWithMagic(client, magic, kekId, wrapped,
+ wrappedSz, &metaOut, keyOut,
+ &keyOutSz, WH_ERROR_OK));
+
+ WH_TEST_ASSERT_RETURN(metaOut.id == metaIn.id);
+ WH_TEST_ASSERT_RETURN(metaOut.access == metaIn.access);
+ WH_TEST_ASSERT_RETURN(metaOut.flags == metaIn.flags);
+ WH_TEST_ASSERT_RETURN(metaOut.len == metaIn.len);
+ WH_TEST_ASSERT_RETURN(
+ 0 == memcmp(metaOut.label, metaIn.label, sizeof(metaIn.label)));
+ WH_TEST_ASSERT_RETURN(keyOutSz == sizeof(keyIn));
+ WH_TEST_ASSERT_RETURN(0 == memcmp(keyOut, keyIn, sizeof(keyIn)));
+
+ return WH_ERROR_OK;
+}
+
+/* Wrap a NONEXPORTABLE key and try to export it: the scrub meeting the
+ * request/response buffer aliasing the product actually uses */
+static int _DeniedExport(whClientContext* client, uint16_t magic, whKeyId kekId)
+{
+ whNvmMetadata metaIn;
+ whNvmMetadata metaOut;
+ uint8_t keyIn[WH_TEST_KWE2E_KEYSIZE];
+ uint8_t keyOut[WH_TEST_KWE2E_KEYSIZE];
+ uint8_t wrapped[WH_TEST_KWE2E_WRAPPED_SIZE];
+ uint16_t wrappedSz = (uint16_t)sizeof(wrapped);
+ uint16_t keyOutSz = (uint16_t)sizeof(keyOut);
+ size_t i;
+
+ for (i = 0; i < sizeof(keyIn); i++) {
+ keyIn[i] = (uint8_t)(0x7B ^ i);
+ }
+ memset(&metaOut, 0, sizeof(metaOut));
+ memset(keyOut, 0, sizeof(keyOut));
+ _FillMetadata(&metaIn, WH_CLIENT_KEYID_MAKE_WRAPPED_META(
+ client->comm->client_id, WH_TEST_KWE2E_META_ID));
+ metaIn.flags = WH_NVM_FLAGS_NONEXPORTABLE | WH_NVM_FLAGS_USAGE_ENCRYPT;
+
+ WH_TEST_RETURN_ON_FAIL(_WrapWithMagic(client, magic, kekId, &metaIn, keyIn,
+ (uint16_t)sizeof(keyIn), wrapped,
+ &wrappedSz));
+
+ return _UnwrapWithMagic(client, magic, kekId, wrapped, wrappedSz, &metaOut,
+ keyOut, &keyOutSz, WH_ERROR_ACCESS);
+}
+
+/* Same cases twice: once addressed as a same-endian peer, once as a
+ * peer whose byte order differs from the server's */
+static int _RunMagic(whClientContext* client, uint16_t magic, whKeyId kekId)
+{
+ WH_TEST_RETURN_ON_FAIL(_RoundTrip(client, magic, kekId));
+ WH_TEST_RETURN_ON_FAIL(_DeniedExport(client, magic, kekId));
+
+ return WH_ERROR_OK;
+}
+
+static int _whTest_KeyWrapE2ERoundTrip(whClientContext* client)
+{
+ int ret;
+ whKeyId kekId = WH_KEYID_ERASED;
+
+ WH_TEST_RETURN_ON_FAIL(_CacheKek(client, &kekId));
+
+ ret = _RunMagic(client, WH_COMM_MAGIC_NATIVE, kekId);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("e2e under native magic: ret=%d\n", ret);
+ }
+ else {
+ ret = _RunMagic(client, WH_COMM_MAGIC_SWAP, kekId);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("e2e under swapped magic: ret=%d\n", ret);
+ }
+ }
+
+ /* Reaches here on the assertion-failure paths too, which all follow a
+ * completed exchange. Only a server that never replies defeats it */
+ (void)wh_Client_KeyEvict(client, kekId);
+
+ return ret;
+}
+
+int whTest_KeyWrapEndianE2E(whClientContext* client)
+{
+ if (client == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ WH_TEST_RETURN_ON_FAIL(_whTest_KeyWrapE2ERoundTrip(client));
+
+ return WH_ERROR_OK;
+}
+
+#endif /* !WOLFHSM_CFG_NO_CRYPTO && WOLFHSM_CFG_KEYWRAP && !NO_AES &&
+ * HAVE_AESGCM */
diff --git a/test-refactor/misc/wh_test_check_struct_padding.c b/test-refactor/misc/wh_test_check_struct_padding.c
index a4b5a3aa8..68b04f5ea 100644
--- a/test-refactor/misc/wh_test_check_struct_padding.c
+++ b/test-refactor/misc/wh_test_check_struct_padding.c
@@ -38,6 +38,8 @@ whMessageCustomCb_Request whMessageCustomCb_Request_test;
whMessageCustomCb_Response whMessageCustomCb_Response_test;
#include "wolfhsm/wh_message_nvm.h"
+/* Raw wire struct: the key wrap trailers memcpy it across the boundary */
+whNvmMetadata whNvmMetadata_test;
whMessageNvm_SimpleResponse whMessageNvm_SimpleResponse_test;
whMessageNvm_InitRequest whMessageNvm_InitRequest_test;
whMessageNvm_InitResponse whMessageNvm_InitResponse_test;
diff --git a/test-refactor/server/wh_test_keywrap_endian.c b/test-refactor/server/wh_test_keywrap_endian.c
new file mode 100644
index 000000000..d36c1b6d5
--- /dev/null
+++ b/test-refactor/server/wh_test_keywrap_endian.c
@@ -0,0 +1,720 @@
+/*
+ * 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_keywrap_endian.c
+ *
+ * Byte-order coverage for the key wrap metadata trailer, which crosses the
+ * comm boundary as a raw whNvmMetadata instead of a flattened message:
+ *
+ * _whTest_KeyWrapMetadataTranslate - the translate helper itself
+ * _whTest_KeyWrapRoundTrip - wrap -> unwrap-and-export, and
+ * wrap -> unwrap-and-cache
+ * _whTest_KeyWrapRejectsNonWrappedId - wrap refuses a non-WRAPPED id
+ * _whTest_KeyWrapRejectsNonExportable - export refuses NONEXPORTABLE
+ * _whTest_KeyWrapMalformedNoResidue - a failed export ships no residue
+ * _whTest_KeyWrapCrossMagic - wrap under one byte order, consume
+ * the blob under the other
+ *
+ * All but the first run under native and swapped magic, the latter modelling a
+ * client whose endianness differs from the server's. Client and server share a
+ * process in every harness here, so the dispatch is driven through
+ * wh_Server_HandleKeyRequest to reach that path at all.
+ */
+
+#include "wolfhsm/wh_settings.h"
+
+#if defined(WOLFHSM_CFG_ENABLE_SERVER) && !defined(WOLFHSM_CFG_NO_CRYPTO) && \
+ defined(WOLFHSM_CFG_KEYWRAP) && !defined(NO_AES) && defined(HAVE_AESGCM)
+
+#include
+#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_message.h"
+#include "wolfhsm/wh_message_keystore.h"
+#include "wolfhsm/wh_message_nvm.h"
+#include "wolfhsm/wh_server.h"
+#include "wolfhsm/wh_server_keystore.h"
+
+#include "wh_test_common.h"
+#include "wh_test_list.h"
+
+/* Client-facing ids, kept clear of the ranges other suites use */
+#define WH_TEST_KWE_KEK_ID 0x71
+#define WH_TEST_KWE_TRUSTED_KEK_ID 0x73
+#define WH_TEST_KWE_META_ID 0x25
+
+#define WH_TEST_KWE_KEYSIZE 32
+#define WH_TEST_KWE_WRAPPED_SIZE \
+ (WH_KEYWRAP_AES_GCM_HEADER_SIZE + sizeof(whNvmMetadata) + \
+ WH_TEST_KWE_KEYSIZE)
+
+/* Non-palindromic access mask: a missed translation changes its value */
+#define WH_TEST_KWE_ACCESS 0x1234
+
+/* Stand-in keyId for the pure translation test, TYPE=WRAPPED USER=0 ID=0x25 */
+#define WH_TEST_KWE_RAW_KEYID 0x4025
+
+/* TYPE=SHE, but byte-reversed it is WH_TEST_KWE_RAW_KEYID (TYPE=WRAPPED) */
+#define WH_TEST_KWE_SWAPS_TO_WRAPPED_KEYID 0x2540
+
+/* Packet buffers the server casts to its message structs, so give them
+ * pointer-grade alignment. Static to keep 16KB off the stack */
+static union {
+ uint64_t align[(WOLFHSM_CFG_COMM_DATA_LEN + 7) / 8];
+ uint8_t bytes[WOLFHSM_CFG_COMM_DATA_LEN];
+} _req, _resp;
+
+static uint16_t _Swap16(uint16_t val)
+{
+ return (uint16_t)(((val & 0xFF00u) >> 8) | ((val & 0x00FFu) << 8));
+}
+
+/* Wire images are built and decoded with these rather than the library
+ * Translate* helpers, so a bug inside those cannot cancel itself out here */
+static uint16_t _Swap16If(int swap, uint16_t val)
+{
+ return (swap != 0) ? _Swap16(val) : val;
+}
+
+static uint32_t _Swap32If(int swap, uint32_t val)
+{
+ if (swap == 0) {
+ return val;
+ }
+ return ((val & 0xFF000000u) >> 24) | ((val & 0x00FF0000u) >> 8) |
+ ((val & 0x0000FF00u) << 8) | ((val & 0x000000FFu) << 24);
+}
+
+static void _SwapMetadataIf(int swap, const whNvmMetadata* src,
+ whNvmMetadata* dest)
+{
+ dest->id = _Swap16If(swap, src->id);
+ dest->access = _Swap16If(swap, src->access);
+ dest->flags = _Swap16If(swap, src->flags);
+ dest->len = _Swap16If(swap, src->len);
+ memcpy(dest->label, src->label, sizeof(dest->label));
+}
+
+static int _MagicSwaps(uint16_t magic)
+{
+ return magic != WH_COMM_MAGIC_NATIVE;
+}
+
+/* Every 16-bit field takes a value that differs from its byte-swapped self,
+ * so an untranslated field cannot pass unnoticed */
+static void _FillMetadata(whNvmMetadata* meta, whKeyId id)
+{
+ size_t i;
+
+ memset(meta, 0, sizeof(*meta));
+ meta->id = id;
+ meta->access = WH_TEST_KWE_ACCESS;
+ meta->flags = WH_NVM_FLAGS_USAGE_ANY;
+ meta->len = WH_TEST_KWE_KEYSIZE;
+ for (i = 0; i < sizeof(meta->label); i++) {
+ meta->label[i] = (uint8_t)(0xA0 + i);
+ }
+}
+
+/* Sentinel the response buffer is primed with. Zeroing it instead would make
+ * the scrub assertions below pass whether or not the server wrote anything */
+#define WH_TEST_KWE_RESP_FILL 0xA5
+
+static int _IsZeroed(const uint8_t* buf, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++) {
+ if (buf[i] != 0) {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+static whKeyId _KekKeyId(whServerContext* server, uint16_t rawId)
+{
+ return WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, server->comm->client_id, rawId);
+}
+
+/* A wrapped keyId whose two bytes are equal, so it reads the same either way
+ * round. Lets a subtest past the wrap-side gate to reach another field */
+static whKeyId _PalindromeKeyId(whServerContext* server)
+{
+ uint16_t hi = (uint16_t)((WH_KEYTYPE_WRAPPED << 4) |
+ (server->comm->client_id & 0xF));
+
+ return WH_MAKE_KEYID(WH_KEYTYPE_WRAPPED, server->comm->client_id, hi);
+}
+
+static int _whTest_KeyWrapMetadataTranslate(void)
+{
+ whNvmMetadata in;
+ whNvmMetadata out;
+ whNvmMetadata back;
+
+ _FillMetadata(&in, WH_TEST_KWE_RAW_KEYID);
+
+ /* A same-endian peer sees the struct unchanged */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_OK == wh_MessageNvm_TranslateMetadata(
+ WH_COMM_MAGIC_NATIVE, &in, &out));
+ WH_TEST_ASSERT_RETURN(0 == memcmp(&in, &out, sizeof(in)));
+
+ /* A swapped peer sees every field byte-reversed, label untouched */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_OK == wh_MessageNvm_TranslateMetadata(
+ WH_COMM_MAGIC_SWAP, &in, &out));
+ WH_TEST_ASSERT_RETURN(out.id == _Swap16(in.id));
+ WH_TEST_ASSERT_RETURN(out.access == _Swap16(in.access));
+ WH_TEST_ASSERT_RETURN(out.flags == _Swap16(in.flags));
+ WH_TEST_ASSERT_RETURN(out.len == _Swap16(in.len));
+ WH_TEST_ASSERT_RETURN(0 ==
+ memcmp(out.label, in.label, sizeof(in.label)));
+
+ /* decode(encode(m)) == m, including in place */
+ WH_TEST_ASSERT_RETURN(WH_ERROR_OK == wh_MessageNvm_TranslateMetadata(
+ WH_COMM_MAGIC_SWAP, &out, &back));
+ WH_TEST_ASSERT_RETURN(0 == memcmp(&in, &back, sizeof(in)));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_OK == wh_MessageNvm_TranslateMetadata(
+ WH_COMM_MAGIC_SWAP, &out, &out));
+ WH_TEST_ASSERT_RETURN(0 == memcmp(&in, &out, sizeof(in)));
+
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_MessageNvm_TranslateMetadata(
+ WH_COMM_MAGIC_NATIVE, NULL, &out));
+ WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS ==
+ wh_MessageNvm_TranslateMetadata(
+ WH_COMM_MAGIC_NATIVE, &in, NULL));
+
+ return WH_ERROR_OK;
+}
+
+/* Cache a KEK straight into the server keystore. Unwrap-and-cache also demands
+ * WH_NVM_FLAGS_TRUSTED, which only the server side can set */
+static int _CacheKek(whServerContext* server, uint16_t rawId, whNvmFlags flags)
+{
+ whNvmMetadata meta;
+ uint8_t kek[WH_TEST_KWE_KEYSIZE];
+ size_t i;
+
+ for (i = 0; i < sizeof(kek); i++) {
+ kek[i] = (uint8_t)(0xC2 ^ i);
+ }
+
+ memset(&meta, 0, sizeof(meta));
+ meta.id = _KekKeyId(server, rawId);
+ meta.len = sizeof(kek);
+ meta.flags = flags;
+ memcpy(meta.label, "KwEndian KEK", sizeof("KwEndian KEK"));
+
+ return wh_Server_KeystoreCacheKey(server, &meta, kek);
+}
+
+/* Wrap a key, presenting header, metadata trailer and key in the byte order
+ * the magic denotes. expectRc lets a caller assert a rejection instead */
+static int _WrapWithMagic(whServerContext* server, uint16_t magic,
+ uint16_t kekRawId, const whNvmMetadata* metaIn,
+ const uint8_t* keyIn, uint16_t keySz,
+ uint8_t* wrappedOut, uint16_t* wrappedInOutSz,
+ int expectRc)
+{
+ int ret;
+ int swap = _MagicSwaps(magic);
+ uint16_t reqSize;
+ uint16_t respSize = 0;
+ whMessageKeystore_KeyWrapRequest wireReq;
+ whMessageKeystore_KeyWrapResponse resp;
+ whMessageKeystore_KeyWrapResponse wireResp;
+ whNvmMetadata wireMeta;
+
+ memset(_req.bytes, 0, sizeof(_req.bytes));
+ memset(_resp.bytes, WH_TEST_KWE_RESP_FILL, sizeof(_resp.bytes));
+ memset(&wireReq, 0, sizeof(wireReq));
+
+ wireReq.keySz = _Swap16If(swap, keySz);
+ wireReq.serverKeyId = _Swap16If(swap, kekRawId);
+ wireReq.cipherType = _Swap16If(swap, (uint16_t)WC_CIPHER_AES_GCM);
+ _SwapMetadataIf(swap, metaIn, &wireMeta);
+ memcpy(_req.bytes, &wireReq, sizeof(wireReq));
+ memcpy(_req.bytes + sizeof(wireReq), &wireMeta, sizeof(wireMeta));
+ memcpy(_req.bytes + sizeof(wireReq) + sizeof(wireMeta), keyIn, keySz);
+ reqSize = (uint16_t)(sizeof(wireReq) + sizeof(wireMeta) + keySz);
+
+ ret = wh_Server_HandleKeyRequest(server, magic, WH_KEY_KEYWRAP, reqSize,
+ _req.bytes, &respSize, _resp.bytes);
+ WH_TEST_ASSERT_RETURN(ret == expectRc);
+
+ memcpy(&wireResp, _resp.bytes, sizeof(wireResp));
+ resp.rc = _Swap32If(swap, wireResp.rc);
+ resp.wrappedKeySz = _Swap16If(swap, wireResp.wrappedKeySz);
+ resp.cipherType = _Swap16If(swap, wireResp.cipherType);
+ WH_TEST_ASSERT_RETURN((int)resp.rc == expectRc);
+ WH_TEST_ASSERT_RETURN(resp.cipherType == (uint16_t)WC_CIPHER_AES_GCM);
+ if (expectRc != WH_ERROR_OK) {
+ return WH_ERROR_OK;
+ }
+ WH_TEST_ASSERT_RETURN(resp.wrappedKeySz <= *wrappedInOutSz);
+
+ /* The blob is opaque bytes, so it needs no translation */
+ memcpy(wrappedOut, _resp.bytes + sizeof(resp), resp.wrappedKeySz);
+ *wrappedInOutSz = resp.wrappedKeySz;
+
+ return WH_ERROR_OK;
+}
+
+/* Unwrap-and-export, decoding the metadata trailer returned ahead of the key.
+ * expectRc lets a caller assert a policy rejection instead */
+static int _UnwrapWithMagic(whServerContext* server, uint16_t magic,
+ uint16_t kekRawId, const uint8_t* wrappedIn,
+ uint16_t wrappedSz, whNvmMetadata* metaOut,
+ uint8_t* keyOut, uint16_t* keyInOutSz, int expectRc)
+{
+ int ret;
+ int swap = _MagicSwaps(magic);
+ uint16_t reqSize;
+ uint16_t respSize = 0;
+ whMessageKeystore_KeyUnwrapAndExportRequest wireReq;
+ whMessageKeystore_KeyUnwrapAndExportResponse resp;
+ whMessageKeystore_KeyUnwrapAndExportResponse wireResp;
+ whNvmMetadata wireMeta;
+
+ memset(_req.bytes, 0, sizeof(_req.bytes));
+ memset(_resp.bytes, WH_TEST_KWE_RESP_FILL, sizeof(_resp.bytes));
+ memset(&wireReq, 0, sizeof(wireReq));
+
+ wireReq.wrappedKeySz = _Swap16If(swap, wrappedSz);
+ wireReq.serverKeyId = _Swap16If(swap, kekRawId);
+ wireReq.cipherType = _Swap16If(swap, (uint16_t)WC_CIPHER_AES_GCM);
+ memcpy(_req.bytes, &wireReq, sizeof(wireReq));
+ memcpy(_req.bytes + sizeof(wireReq), wrappedIn, wrappedSz);
+ reqSize = (uint16_t)(sizeof(wireReq) + wrappedSz);
+
+ ret = wh_Server_HandleKeyRequest(server, magic, WH_KEY_KEYUNWRAPEXPORT,
+ reqSize, _req.bytes, &respSize,
+ _resp.bytes);
+ WH_TEST_ASSERT_RETURN(ret == expectRc);
+
+ memcpy(&wireResp, _resp.bytes, sizeof(wireResp));
+ resp.rc = _Swap32If(swap, wireResp.rc);
+ resp.keySz = _Swap16If(swap, wireResp.keySz);
+ resp.cipherType = _Swap16If(swap, wireResp.cipherType);
+ WH_TEST_ASSERT_RETURN((int)resp.rc == expectRc);
+ WH_TEST_ASSERT_RETURN(resp.cipherType == (uint16_t)WC_CIPHER_AES_GCM);
+
+ /* The trailer ships whatever the outcome, even with keySz zeroed */
+ WH_TEST_ASSERT_RETURN(respSize ==
+ sizeof(resp) + sizeof(wireMeta) + resp.keySz);
+
+ memcpy(&wireMeta, _resp.bytes + sizeof(resp), sizeof(wireMeta));
+ if (expectRc != WH_ERROR_OK) {
+ /* A denied request carries back neither metadata nor the plaintext key
+ * the server already decrypted into the shared buffer */
+ WH_TEST_ASSERT_RETURN(resp.keySz == 0);
+ WH_TEST_ASSERT_RETURN(
+ _IsZeroed(_resp.bytes + sizeof(resp),
+ sizeof(wireMeta) + WH_TEST_KWE_KEYSIZE));
+ return WH_ERROR_OK;
+ }
+ WH_TEST_ASSERT_RETURN(resp.keySz <= *keyInOutSz);
+
+ _SwapMetadataIf(swap, &wireMeta, metaOut);
+ memcpy(keyOut, _resp.bytes + sizeof(resp) + sizeof(wireMeta), resp.keySz);
+ *keyInOutSz = resp.keySz;
+
+ return WH_ERROR_OK;
+}
+
+/* Unwrap-and-cache under the trusted KEK. It stores what it decrypts without
+ * translating: the other half of the blobs-hold-native-metadata invariant */
+static int _UnwrapCacheWithMagic(whServerContext* server, uint16_t magic,
+ const uint8_t* wrappedIn, uint16_t wrappedSz)
+{
+ int ret;
+ int swap = _MagicSwaps(magic);
+ uint16_t reqSize;
+ uint16_t respSize = 0;
+ whMessageKeystore_KeyUnwrapAndCacheRequest wireReq;
+ whMessageKeystore_KeyUnwrapAndCacheResponse resp;
+ whMessageKeystore_KeyUnwrapAndCacheResponse wireResp;
+
+ memset(_req.bytes, 0, sizeof(_req.bytes));
+ memset(_resp.bytes, WH_TEST_KWE_RESP_FILL, sizeof(_resp.bytes));
+ memset(&wireReq, 0, sizeof(wireReq));
+
+ wireReq.wrappedKeySz = _Swap16If(swap, wrappedSz);
+ wireReq.serverKeyId = _Swap16If(swap, WH_TEST_KWE_TRUSTED_KEK_ID);
+ wireReq.cipherType = _Swap16If(swap, (uint16_t)WC_CIPHER_AES_GCM);
+ memcpy(_req.bytes, &wireReq, sizeof(wireReq));
+ memcpy(_req.bytes + sizeof(wireReq), wrappedIn, wrappedSz);
+ reqSize = (uint16_t)(sizeof(wireReq) + wrappedSz);
+
+ ret = wh_Server_HandleKeyRequest(server, magic, WH_KEY_KEYUNWRAPCACHE,
+ reqSize, _req.bytes, &respSize,
+ _resp.bytes);
+ WH_TEST_ASSERT_RETURN(ret == WH_ERROR_OK);
+
+ memcpy(&wireResp, _resp.bytes, sizeof(wireResp));
+ resp.rc = _Swap32If(swap, wireResp.rc);
+ resp.keyId = _Swap16If(swap, wireResp.keyId);
+ /* A byte-swapped len fails the handler's len-vs-key-size check */
+ WH_TEST_ASSERT_RETURN(resp.rc == WH_ERROR_OK);
+
+ return WH_ERROR_OK;
+}
+
+/* Wrap under the trusted KEK, prime the cache from that blob, and read the
+ * cached key back to confirm native metadata and key material landed */
+static int _CacheRoundTrip(whServerContext* server, uint16_t magic,
+ const whNvmMetadata* metaIn, const uint8_t* keyIn,
+ uint16_t keySz)
+{
+ int ret;
+ whNvmMetadata metaBack;
+ uint8_t keyBack[WH_TEST_KWE_KEYSIZE];
+ uint32_t keyBackSz = sizeof(keyBack);
+ uint8_t wrapped[WH_TEST_KWE_WRAPPED_SIZE];
+ uint16_t wrappedSz = (uint16_t)sizeof(wrapped);
+
+ memset(&metaBack, 0, sizeof(metaBack));
+ memset(keyBack, 0, sizeof(keyBack));
+
+ WH_TEST_RETURN_ON_FAIL(_WrapWithMagic(server, magic,
+ WH_TEST_KWE_TRUSTED_KEK_ID, metaIn,
+ keyIn, keySz, wrapped, &wrappedSz,
+ WH_ERROR_OK));
+ WH_TEST_RETURN_ON_FAIL(
+ _UnwrapCacheWithMagic(server, magic, wrapped, wrappedSz));
+
+ ret = wh_Server_KeystoreReadKey(server, metaIn->id, &metaBack, keyBack,
+ &keyBackSz);
+ (void)wh_Server_KeystoreEvictKey(server, metaIn->id);
+ WH_TEST_ASSERT_RETURN(ret == WH_ERROR_OK);
+
+ WH_TEST_ASSERT_RETURN(metaBack.id == metaIn->id);
+ WH_TEST_ASSERT_RETURN(metaBack.access == metaIn->access);
+ WH_TEST_ASSERT_RETURN(metaBack.len == metaIn->len);
+ /* The cache path strips server-only bits from the client's flags */
+ WH_TEST_ASSERT_RETURN(metaBack.flags ==
+ (metaIn->flags & ~WH_NVM_FLAGS_SERVER_ONLY));
+ WH_TEST_ASSERT_RETURN(
+ 0 == memcmp(metaBack.label, metaIn->label, sizeof(metaIn->label)));
+ WH_TEST_ASSERT_RETURN(keyBackSz == keySz);
+ WH_TEST_ASSERT_RETURN(0 == memcmp(keyBack, keyIn, keySz));
+
+ return WH_ERROR_OK;
+}
+
+/* Wrap must refuse a non-WRAPPED keyId. 0x2540 is TYPE=SHE, but byte-reversed
+ * it reads 0x4025 = TYPE=WRAPPED, so a missed ingress translation lets it in */
+static int _whTest_KeyWrapRejectsNonWrappedId(whServerContext* server,
+ uint16_t magic)
+{
+ whNvmMetadata metaIn;
+ uint8_t keyIn[WH_TEST_KWE_KEYSIZE];
+ uint8_t wrapped[WH_TEST_KWE_WRAPPED_SIZE];
+ uint16_t wrappedSz = (uint16_t)sizeof(wrapped);
+ size_t i;
+
+ for (i = 0; i < sizeof(keyIn); i++) {
+ keyIn[i] = (uint8_t)(0x7B ^ i);
+ }
+ _FillMetadata(&metaIn, WH_TEST_KWE_SWAPS_TO_WRAPPED_KEYID);
+
+ return _WrapWithMagic(server, magic, WH_TEST_KWE_KEK_ID, &metaIn, keyIn,
+ (uint16_t)sizeof(keyIn), wrapped, &wrappedSz,
+ WH_ERROR_BADARGS);
+}
+
+/* Unwrap-and-export must refuse a NONEXPORTABLE key. Bit 10 swaps into bit 2,
+ * so flags 0x0024, with bit 10 clear, exposes a missed translation */
+static int _whTest_KeyWrapRejectsNonExportable(whServerContext* server,
+ uint16_t magic)
+{
+ whNvmMetadata metaIn;
+ whNvmMetadata metaOut;
+ uint8_t keyIn[WH_TEST_KWE_KEYSIZE];
+ uint8_t keyOut[WH_TEST_KWE_KEYSIZE];
+ uint8_t wrapped[WH_TEST_KWE_WRAPPED_SIZE];
+ uint16_t wrappedSz = (uint16_t)sizeof(wrapped);
+ uint16_t keyOutSz = (uint16_t)sizeof(keyOut);
+ size_t i;
+
+ for (i = 0; i < sizeof(keyIn); i++) {
+ keyIn[i] = (uint8_t)(0x7B ^ i);
+ }
+ memset(&metaOut, 0, sizeof(metaOut));
+ memset(keyOut, 0, sizeof(keyOut));
+
+ /* A palindromic keyId survives the wrap-side ISWRAPPED gate under either
+ * byte order, so the flags check below is what the subtest exercises */
+ _FillMetadata(&metaIn, _PalindromeKeyId(server));
+ metaIn.flags = WH_NVM_FLAGS_NONEXPORTABLE | WH_NVM_FLAGS_USAGE_ENCRYPT;
+
+ WH_TEST_RETURN_ON_FAIL(_WrapWithMagic(server, magic, WH_TEST_KWE_KEK_ID,
+ &metaIn, keyIn,
+ (uint16_t)sizeof(keyIn), wrapped,
+ &wrappedSz, WH_ERROR_OK));
+
+ return _UnwrapWithMagic(server, magic, WH_TEST_KWE_KEK_ID, wrapped,
+ wrappedSz, &metaOut, keyOut, &keyOutSz,
+ WH_ERROR_ACCESS);
+}
+
+/* Wrap as a peer of one byte order, consume the blob as the other: what a
+ * mixed-endian deployment sharing a KEK actually does */
+static int _whTest_KeyWrapCrossMagic(whServerContext* server,
+ uint16_t wrapMagic, uint16_t useMagic)
+{
+ whNvmMetadata metaIn;
+ whNvmMetadata metaOut;
+ uint8_t keyIn[WH_TEST_KWE_KEYSIZE];
+ uint8_t keyOut[WH_TEST_KWE_KEYSIZE];
+ uint8_t wrapped[WH_TEST_KWE_WRAPPED_SIZE];
+ uint16_t wrappedSz = (uint16_t)sizeof(wrapped);
+ uint16_t keyOutSz = (uint16_t)sizeof(keyOut);
+ size_t i;
+
+ for (i = 0; i < sizeof(keyIn); i++) {
+ keyIn[i] = (uint8_t)(0x7B ^ i);
+ }
+ memset(&metaOut, 0, sizeof(metaOut));
+ memset(keyOut, 0, sizeof(keyOut));
+ _FillMetadata(&metaIn,
+ WH_MAKE_KEYID(WH_KEYTYPE_WRAPPED, server->comm->client_id,
+ WH_TEST_KWE_META_ID));
+
+ /* Export leg: wrap under one magic, unwrap-and-export under the other */
+ WH_TEST_RETURN_ON_FAIL(_WrapWithMagic(server, wrapMagic,
+ WH_TEST_KWE_KEK_ID, &metaIn, keyIn,
+ (uint16_t)sizeof(keyIn), wrapped,
+ &wrappedSz, WH_ERROR_OK));
+ WH_TEST_RETURN_ON_FAIL(_UnwrapWithMagic(server, useMagic,
+ WH_TEST_KWE_KEK_ID, wrapped,
+ wrappedSz, &metaOut, keyOut,
+ &keyOutSz, WH_ERROR_OK));
+
+ WH_TEST_ASSERT_RETURN(metaOut.id == metaIn.id);
+ WH_TEST_ASSERT_RETURN(metaOut.access == metaIn.access);
+ WH_TEST_ASSERT_RETURN(metaOut.flags == metaIn.flags);
+ WH_TEST_ASSERT_RETURN(metaOut.len == metaIn.len);
+ WH_TEST_ASSERT_RETURN(
+ 0 == memcmp(metaOut.label, metaIn.label, sizeof(metaIn.label)));
+ WH_TEST_ASSERT_RETURN(keyOutSz == sizeof(keyIn));
+ WH_TEST_ASSERT_RETURN(0 == memcmp(keyOut, keyIn, sizeof(keyIn)));
+
+ /* Cache leg: wrap under one magic, prime the cache under the other */
+ wrappedSz = (uint16_t)sizeof(wrapped);
+ WH_TEST_RETURN_ON_FAIL(_WrapWithMagic(server, wrapMagic,
+ WH_TEST_KWE_TRUSTED_KEK_ID, &metaIn,
+ keyIn, (uint16_t)sizeof(keyIn),
+ wrapped, &wrappedSz, WH_ERROR_OK));
+ WH_TEST_RETURN_ON_FAIL(
+ _UnwrapCacheWithMagic(server, useMagic, wrapped, wrappedSz));
+ (void)wh_Server_KeystoreEvictKey(server, metaIn.id);
+
+ return WH_ERROR_OK;
+}
+
+/* The two paths that fail before any trailer is written. One ships anyway,
+ * because *out_resp_size always counts one, so it must not carry residue */
+static int _whTest_KeyWrapMalformedNoResidue(whServerContext* server,
+ uint16_t magic)
+{
+ whMessageKeystore_KeyUnwrapAndExportRequest req;
+ whMessageKeystore_KeyUnwrapAndExportRequest wireReq;
+ whMessageKeystore_KeyUnwrapAndExportResponse resp;
+ whMessageKeystore_KeyUnwrapAndExportResponse wireResp;
+ int ret;
+ size_t pass;
+
+ /* pass 0: req_size short of the fixed struct, so the handler never runs.
+ * pass 1: wrappedKeySz claims more data than the request carries */
+ for (pass = 0; pass < 2; pass++) {
+ uint16_t respSize = 0;
+ uint16_t reqSize;
+ int expectRc;
+
+ memset(_req.bytes, WH_TEST_KWE_RESP_FILL, sizeof(_req.bytes));
+ memset(_resp.bytes, WH_TEST_KWE_RESP_FILL, sizeof(_resp.bytes));
+ memset(&req, 0, sizeof(req));
+
+ req.wrappedKeySz = WH_TEST_KWE_KEYSIZE;
+ req.serverKeyId = WH_TEST_KWE_KEK_ID;
+ req.cipherType = WC_CIPHER_AES_GCM;
+ (void)wh_MessageKeystore_TranslateKeyUnwrapAndExportRequest(magic, &req,
+ &wireReq);
+ memcpy(_req.bytes, &wireReq, sizeof(wireReq));
+
+ if (pass == 0) {
+ reqSize = (uint16_t)(sizeof(wireReq) - 1);
+ expectRc = WH_ERROR_BADARGS;
+ }
+ else {
+ reqSize = (uint16_t)sizeof(wireReq);
+ expectRc = WH_ERROR_BUFFER_SIZE;
+ }
+
+ ret = wh_Server_HandleKeyRequest(server, magic, WH_KEY_KEYUNWRAPEXPORT,
+ reqSize, _req.bytes, &respSize,
+ _resp.bytes);
+ WH_TEST_ASSERT_RETURN(ret == expectRc);
+
+ memcpy(&wireResp, _resp.bytes, sizeof(wireResp));
+ (void)wh_MessageKeystore_TranslateKeyUnwrapAndExportResponse(
+ magic, &wireResp, &resp);
+ WH_TEST_ASSERT_RETURN((int)resp.rc == expectRc);
+ WH_TEST_ASSERT_RETURN(resp.keySz == 0);
+
+ /* The trailer ships regardless, so it must be zeroed, not the 0xA5
+ * the buffer was primed with */
+ WH_TEST_ASSERT_RETURN(respSize ==
+ sizeof(resp) + sizeof(whNvmMetadata));
+ WH_TEST_ASSERT_RETURN(
+ _IsZeroed(_resp.bytes + sizeof(resp), sizeof(whNvmMetadata)));
+ }
+
+ return WH_ERROR_OK;
+}
+
+static int _whTest_KeyWrapRoundTrip(whServerContext* server, uint16_t magic)
+{
+ whNvmMetadata metaIn;
+ whNvmMetadata metaOut;
+ uint8_t keyIn[WH_TEST_KWE_KEYSIZE];
+ uint8_t keyOut[WH_TEST_KWE_KEYSIZE];
+ uint8_t wrapped[WH_TEST_KWE_WRAPPED_SIZE];
+ uint16_t wrappedSz = (uint16_t)sizeof(wrapped);
+ uint16_t keyOutSz = (uint16_t)sizeof(keyOut);
+ size_t i;
+
+ for (i = 0; i < sizeof(keyIn); i++) {
+ keyIn[i] = (uint8_t)(0x7B ^ i);
+ }
+ memset(&metaOut, 0, sizeof(metaOut));
+ memset(keyOut, 0, sizeof(keyOut));
+ _FillMetadata(&metaIn,
+ WH_MAKE_KEYID(WH_KEYTYPE_WRAPPED, server->comm->client_id,
+ WH_TEST_KWE_META_ID));
+
+ WH_TEST_RETURN_ON_FAIL(_WrapWithMagic(server, magic, WH_TEST_KWE_KEK_ID,
+ &metaIn, keyIn,
+ (uint16_t)sizeof(keyIn), wrapped,
+ &wrappedSz, WH_ERROR_OK));
+ WH_TEST_RETURN_ON_FAIL(_UnwrapWithMagic(server, magic, WH_TEST_KWE_KEK_ID,
+ wrapped, wrappedSz, &metaOut,
+ keyOut, &keyOutSz, WH_ERROR_OK));
+
+ WH_TEST_ASSERT_RETURN(metaOut.id == metaIn.id);
+ WH_TEST_ASSERT_RETURN(metaOut.access == metaIn.access);
+ WH_TEST_ASSERT_RETURN(metaOut.flags == metaIn.flags);
+ WH_TEST_ASSERT_RETURN(metaOut.len == metaIn.len);
+ WH_TEST_ASSERT_RETURN(
+ 0 == memcmp(metaOut.label, metaIn.label, sizeof(metaIn.label)));
+ WH_TEST_ASSERT_RETURN(keyOutSz == sizeof(keyIn));
+ WH_TEST_ASSERT_RETURN(0 == memcmp(keyOut, keyIn, sizeof(keyIn)));
+
+ WH_TEST_RETURN_ON_FAIL(_CacheRoundTrip(server, magic, &metaIn, keyIn,
+ (uint16_t)sizeof(keyIn)));
+
+ return WH_ERROR_OK;
+}
+
+/* Every case that depends on byte order, run under one magic */
+static int _RunMagic(whServerContext* server, uint16_t magic)
+{
+ WH_TEST_RETURN_ON_FAIL(_whTest_KeyWrapRoundTrip(server, magic));
+ WH_TEST_RETURN_ON_FAIL(_whTest_KeyWrapRejectsNonWrappedId(server, magic));
+ WH_TEST_RETURN_ON_FAIL(_whTest_KeyWrapRejectsNonExportable(server, magic));
+ WH_TEST_RETURN_ON_FAIL(_whTest_KeyWrapMalformedNoResidue(server, magic));
+
+ return WH_ERROR_OK;
+}
+
+int whTest_KeyWrapEndian(whServerContext* server)
+{
+ int ret;
+
+ if (server == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ WH_TEST_RETURN_ON_FAIL(_whTest_KeyWrapMetadataTranslate());
+
+ /* Fallthrough from here on, not WH_TEST_RETURN_ON_FAIL: once either KEK is
+ * cached it must leave the shared keystore on every exit path */
+ ret = _CacheKek(server, WH_TEST_KWE_KEK_ID, WH_NVM_FLAGS_USAGE_WRAP);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("cache plain KEK: ret=%d\n", ret);
+ }
+
+ if (ret == WH_ERROR_OK) {
+ ret = _CacheKek(server, WH_TEST_KWE_TRUSTED_KEK_ID,
+ WH_NVM_FLAGS_USAGE_WRAP | WH_NVM_FLAGS_TRUSTED);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("cache trusted KEK: ret=%d\n", ret);
+ }
+ }
+
+ if (ret == WH_ERROR_OK) {
+ ret = _RunMagic(server, WH_COMM_MAGIC_NATIVE);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("under native magic: ret=%d\n", ret);
+ }
+ }
+ if (ret == WH_ERROR_OK) {
+ ret = _RunMagic(server, WH_COMM_MAGIC_SWAP);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("under swapped magic: ret=%d\n", ret);
+ }
+ }
+
+ /* The blob is server-native whoever wrapped it, so the two byte orders
+ * must interoperate through it in both directions */
+ if (ret == WH_ERROR_OK) {
+ ret = _whTest_KeyWrapCrossMagic(server, WH_COMM_MAGIC_SWAP,
+ WH_COMM_MAGIC_NATIVE);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("wrap swapped, use native: ret=%d\n", ret);
+ }
+ }
+ if (ret == WH_ERROR_OK) {
+ ret = _whTest_KeyWrapCrossMagic(server, WH_COMM_MAGIC_NATIVE,
+ WH_COMM_MAGIC_SWAP);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("wrap native, use swapped: ret=%d\n", ret);
+ }
+ }
+
+ (void)wh_Server_KeystoreEvictKey(server,
+ _KekKeyId(server, WH_TEST_KWE_KEK_ID));
+ (void)wh_Server_KeystoreEvictKey(
+ server, _KekKeyId(server, WH_TEST_KWE_TRUSTED_KEK_ID));
+
+ return ret;
+}
+
+#endif /* WOLFHSM_CFG_ENABLE_SERVER && !WOLFHSM_CFG_NO_CRYPTO &&
+ * WOLFHSM_CFG_KEYWRAP && !NO_AES && HAVE_AESGCM */
diff --git a/test-refactor/wh_test_list.c b/test-refactor/wh_test_list.c
index 70f7d0396..bad99353c 100644
--- a/test-refactor/wh_test_list.c
+++ b/test-refactor/wh_test_list.c
@@ -77,6 +77,8 @@ WH_TEST_DECL(whTest_CryptoMlDsaBufferTooSmall);
WH_TEST_DECL(whTest_CryptoRsaBufferTooSmall);
WH_TEST_DECL(whTest_CryptoSha256);
WH_TEST_DECL(whTest_KeyWrap);
+WH_TEST_DECL(whTest_KeyWrapEndian);
+WH_TEST_DECL(whTest_KeyWrapEndianE2E);
WH_TEST_DECL(whTest_She);
WH_TEST_DECL(whTest_SheKeywrapInterop);
WH_TEST_DECL(whTest_SheMasterEcuKeyFallback);
@@ -120,6 +122,7 @@ const whTestCase whTestsServer[] = {
{ "whTest_SheReqSizeChecking", whTest_SheReqSizeChecking },
{ "whTest_HwKeystoreServer", whTest_HwKeystoreServer },
{ "whTest_SheStateGate", whTest_SheStateGate },
+ { "whTest_KeyWrapEndian", whTest_KeyWrapEndian },
};
const size_t whTestsServerCount = ARRAY_SIZE(whTestsServer);
@@ -148,6 +151,7 @@ const whTestCase whTestsClient[] = {
{"whTest_CryptoRsaBufferTooSmall", whTest_CryptoRsaBufferTooSmall},
{"whTest_CryptoSha256", whTest_CryptoSha256},
{"whTest_KeyWrap", whTest_KeyWrap},
+ {"whTest_KeyWrapEndianE2E", whTest_KeyWrapEndianE2E},
{"whTest_She", whTest_She},
{"whTest_Echo", whTest_Echo},
{"whTest_NvmDma", whTest_NvmDma},
diff --git a/wolfhsm/wh_message_nvm.h b/wolfhsm/wh_message_nvm.h
index 023158dd4..048953d2b 100644
--- a/wolfhsm/wh_message_nvm.h
+++ b/wolfhsm/wh_message_nvm.h
@@ -53,6 +53,11 @@ enum WH_MESSAGE_NVM_MAX_ENUM {
WH_MESSAGE_NVM_MAX_READ_LEN = WOLFHSM_CFG_COMM_DATA_LEN - sizeof(int32_t),
};
+/* Translate a whNvmMetadata carried on the wire as a raw struct, rather than
+ * flattened into message fields. In-place safe (src may equal dest) */
+int wh_MessageNvm_TranslateMetadata(uint16_t magic, const whNvmMetadata* src,
+ whNvmMetadata* dest);
+
/* Simple reusable response message */
typedef struct {
int32_t rc;