From 9be189c3069bf5c5dc7e73f755fcc5c1b0838d2e Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Tue, 21 Jul 2026 17:00:34 +0900 Subject: [PATCH] Translate the key wrap metadata trailer --- src/wh_message_nvm.c | 20 + src/wh_server_keystore.c | 100 ++- test-refactor/README.md | 2 + .../wh_test_keywrap_endian_e2e.c | 459 +++++++++++ .../misc/wh_test_check_struct_padding.c | 2 + test-refactor/server/wh_test_keywrap_endian.c | 720 ++++++++++++++++++ test-refactor/wh_test_list.c | 4 + wolfhsm/wh_message_nvm.h | 5 + 8 files changed, 1284 insertions(+), 28 deletions(-) create mode 100644 test-refactor/client-server/wh_test_keywrap_endian_e2e.c create mode 100644 test-refactor/server/wh_test_keywrap_endian.c 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;