diff --git a/test-refactor/README.md b/test-refactor/README.md
index 6de6046f9..a33c6067c 100644
--- a/test-refactor/README.md
+++ b/test-refactor/README.md
@@ -91,6 +91,7 @@ Translated tests:
| `wh_test_posix_threadsafe_stress.c::whTest_ThreadSafeStress` | called directly from `posix/wh_test_posix_main.c` | POSIX port-specific (direct call) | |
| `wh_test_check_struct_padding.c` | `misc/wh_test_check_struct_padding.c` | Build-time (compile-only) | Wire-format `-Wpadded` audit; the POSIX Makefile compiles it with `-Wpadded -DWH_PADDING_CHECK`. Not a runtime test, so not registered in `wh_test_list.c` |
| `wh_test_auth.c` (`whTest_AuthMEM` / `whTest_AuthTest` sub-tests) | `client-server/wh_test_auth.c::{whTest_AuthBadArgs, whTest_AuthLogin, whTest_AuthLogout, whTest_AuthAddUser, whTest_AuthDeleteUser, whTest_AuthSetPermissions, whTest_AuthSetCredentials, whTest_AuthRequestAuthorization}` | Client | Under `WOLFHSM_CFG_ENABLE_AUTHENTICATION` the POSIX server installs an auth context + admin user and the client logs in as admin at connect, so the ordinary client tests run authorized; each auth test brackets its own session (logout to start clean, restore admin on exit). Uses the blocking client API; the legacy own-server setup and single-thread manual-pump are dropped. Build with `make AUTH=1`. The TCP/client-only variant (`whTest_AuthTCP`) is not ported |
+| `wh_test_keywrap.c::whTest_KeyWrapClientConfig` | `client-server/wh_test_keywrap.c::whTest_KeyWrap` | Client | Caches a test KEK (`WH_NVM_FLAGS_USAGE_WRAP`) then runs the AES-GCM wrap path: wrap a random key, unwrap-and-cache it, AES-GCM round trip with the cached key, unwrap-and-export and match key + metadata, plus the data-wrap round trip and the key/data unwrap underflow bad-args checks. Gated on `WOLFHSM_CFG_KEYWRAP`; the legacy own-server/auth setup is dropped since the harness supplies a connected, authorized client |
Not yet migrated (still live in `wolfHSM/test/`):
@@ -101,7 +102,6 @@ Not yet migrated (still live in `wolfHSM/test/`):
| `wh_test_crypto.c::whTest_Crypto` | Remaining crypto coverage not yet split out: the AES async family (comm-buffer `whTest_CryptoAesAsync`/`AesAsyncKat` + DMA `whTest_CryptoAesDmaAsync`/`AesDmaAsyncKat`, round-trip & KAT). ECC DMA export-public and the ML-DSA wolfCrypt-API path are now migrated. |
| `wh_test_crypto.c::whTest_KeyCache`, `whTest_NonExportableKeystore` | Keystore tests (key-cache lifecycle and non-exportable-flag enforcement) dispatched from the legacy `whTest_Crypto`. The per-algorithm suites use `wh_Client_KeyCache`, but these dedicated keystore tests are not yet split out. |
| `wh_test_crypto_affinity.c::whTest_CryptoAffinity` | |
-| `wh_test_keywrap.c::whTest_KeyWrapClientConfig` | |
| `wh_test_multiclient.c::whTest_MultiClient` | |
| `wh_test_lock.c::whTest_LockConfig`, `whTest_LockPosix` | `whTest_LockConfig` to be reworked to fit the Misc group, likely with a context param. |
| `wh_test_log.c::whTest_Log`, `whTest_LogBackend_RunAll` | `whTest_LogBackend_RunAll` to be reworked to fit the Misc group, likely with a context param. |
diff --git a/test-refactor/client-server/wh_test_keywrap.c b/test-refactor/client-server/wh_test_keywrap.c
new file mode 100644
index 000000000..c0a215749
--- /dev/null
+++ b/test-refactor/client-server/wh_test_keywrap.c
@@ -0,0 +1,412 @@
+/*
+ * 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.c
+ *
+ * Key-wrap and data-wrap lifecycle against the shared client group:
+ * _AesGcm_TestKeyWrap - wrap a random key under the server
+ * KEK, unwrap-and-cache it, use the
+ * cached key for an AES-GCM round trip,
+ * then unwrap-and-export and confirm the
+ * key and metadata match the originals
+ * _AesGcm_TestKeyUnwrapUnderflow - tiny wrapped-key sizes must return
+ * WH_ERROR_BADARGS, not underflow
+ * _AesGcm_TestDataWrap - wrap and unwrap opaque data, confirm
+ * the round trip preserves the bytes
+ * _AesGcm_TestDataUnwrapUnderflow- tiny wrapped-data sizes must return
+ * WH_ERROR_BADARGS, not underflow
+ *
+ * The shared port server provisions no KEK, so each entry point caches a
+ * test KEK first and evicts it on exit. The legacy own-server/auth setup
+ * is dropped; the harness supplies a connected, authorized client.
+ */
+
+#include "wolfhsm/wh_settings.h"
+
+#include
+#include /* For memset, memcpy */
+
+#if defined(WOLFHSM_CFG_KEYWRAP) && defined(WOLFHSM_CFG_ENABLE_CLIENT) && \
+ !defined(WOLFHSM_CFG_NO_CRYPTO)
+
+#include "wolfssl/wolfcrypt/settings.h"
+#include "wolfssl/wolfcrypt/types.h"
+#include "wolfssl/wolfcrypt/aes.h"
+#include "wolfssl/wolfcrypt/random.h"
+
+#include "wolfhsm/wh_error.h"
+#include "wolfhsm/wh_common.h"
+#include "wolfhsm/wh_client.h"
+#include "wolfhsm/wh_client_crypto.h"
+
+#include "wh_test_common.h"
+#include "wh_test_list.h"
+
+/* Common defines */
+#define WH_TEST_KEKID 10
+
+/* AES GCM Specific defines */
+#ifdef HAVE_AESGCM
+
+#define WH_TEST_AESGCM_KEYID 20
+#define WH_TEST_AES_KEYSIZE 32
+#define WH_TEST_AES_WRAPPED_KEYSIZE \
+ (WH_KEYWRAP_AES_GCM_HEADER_SIZE + WH_TEST_AES_KEYSIZE + \
+ sizeof(whNvmMetadata))
+
+#endif /* HAVE_AESGCM */
+
+static int _InitServerKek(whClientContext* client)
+{
+ /* IMPORTANT NOTE: Server KEK is typically intrinsic or set during
+ * provisioning. Uploading the KEK via the client is for testing purposes
+ * only and not intended as a recommendation */
+ whKeyId serverKeyId = WH_TEST_KEKID;
+ whNvmFlags flags = WH_NVM_FLAGS_NONEXPORTABLE | WH_NVM_FLAGS_USAGE_WRAP;
+ uint8_t label[WH_NVM_LABEL_LEN] = "Server KEK key";
+ uint8_t kek[] = {0x03, 0x03, 0x0d, 0xd9, 0xeb, 0x18, 0x17, 0x2e,
+ 0x06, 0x6e, 0x19, 0xce, 0x98, 0x44, 0x54, 0x0d,
+ 0x78, 0xa0, 0xbe, 0xe7, 0x35, 0x43, 0x40, 0xa4,
+ 0x22, 0x8a, 0xd1, 0x0e, 0xa3, 0x63, 0x1c, 0x0b};
+
+ return wh_Client_KeyCache(client, flags, label, sizeof(label), kek,
+ sizeof(kek), &serverKeyId);
+}
+
+static int _CleanupServerKek(whClientContext* client)
+{
+ return wh_Client_KeyEvict(client, WH_TEST_KEKID);
+}
+
+#ifdef HAVE_AESGCM
+
+static int _AesGcm_TestKeyWrap(whClientContext* client, WC_RNG* rng)
+{
+
+ int ret = 0;
+ uint8_t plainKey[WH_TEST_AES_KEYSIZE];
+ uint8_t tmpPlainKey[WH_TEST_AES_KEYSIZE];
+ uint16_t tmpPlainKeySz = sizeof(tmpPlainKey);
+ uint8_t wrappedKey[WH_TEST_AES_WRAPPED_KEYSIZE];
+ uint16_t wrappedKeySz = sizeof(wrappedKey);
+ whKeyId wrappedKeyId = WH_KEYID_ERASED;
+ whNvmMetadata metadata = {
+ .id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(client->comm->client_id,
+ WH_TEST_AESGCM_KEYID),
+ .label = "AES Key Label",
+ .len = WH_TEST_AES_KEYSIZE,
+ .flags = WH_NVM_FLAGS_USAGE_ANY,
+ };
+ whNvmMetadata tmpMetadata = {0};
+
+ Aes aes[1];
+ const uint8_t plaintext[] = "hello, wolfSSL AES-GCM!";
+ uint8_t ciphertext[sizeof(plaintext)];
+ uint8_t decrypted[sizeof(plaintext)];
+
+ uint8_t tag[WH_KEYWRAP_AES_GCM_TAG_SIZE];
+ uint8_t iv[WH_KEYWRAP_AES_GCM_IV_SIZE];
+ const uint8_t aad[] = {0xfe, 0xed, 0xfa, 0xce, 0xde, 0xad, 0xbe,
+ 0xef, 0xfe, 0xed, 0xfa, 0xce, 0xde, 0xad,
+ 0xbe, 0xef, 0xab, 0xad, 0xda, 0xd2};
+
+
+ ret = wc_RNG_GenerateBlock(rng, plainKey, sizeof(plainKey));
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_RNG_GenerateBlock for key data %d\n", ret);
+ return ret;
+ }
+
+ ret = wh_Client_KeyWrap(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID, plainKey,
+ sizeof(plainKey), &metadata, wrappedKey,
+ &wrappedKeySz);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wh_Client_AesGcmKeyWrap %d\n", ret);
+ return ret;
+ }
+
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ wrappedKey, wrappedKeySz, &wrappedKeyId);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wh_Client_AesGcmKeyWrapCache %d\n", ret);
+ return ret;
+ }
+
+ /* Initialize AES context */
+ ret = wc_AesInit(aes, NULL, WH_CLIENT_DEVID(client));
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_AesInit %d\n", ret);
+ return ret;
+ }
+
+ ret =
+ wh_Client_AesSetKeyId(aes, WH_CLIENT_KEYID_MAKE_WRAPPED(wrappedKeyId));
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wh_Client_AesSetKeyId %d\n", ret);
+ return ret;
+ }
+
+ /* Generate a random IV */
+ ret = wc_RNG_GenerateBlock(rng, iv, sizeof(iv));
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_RNG_GenerateBlock for AES-GCM key %d\n",
+ ret);
+ return ret;
+ }
+
+ /* Request the server to encrypt some data using the
+ * unwrapped and cached key via the key ID */
+ ret = wc_AesGcmEncrypt(aes, ciphertext, plaintext, sizeof(plaintext), iv,
+ sizeof(iv), tag, sizeof(tag), aad, sizeof(aad));
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_AesGcmEncrypt %d\n", ret);
+ return ret;
+ }
+
+ /* Request the server to decrypt the encrypted data using the
+ * unwrapped and cached key via the key ID */
+ ret = wc_AesGcmDecrypt(aes, decrypted, /* out */
+ ciphertext, sizeof(ciphertext), /* in, inLen */
+ iv, sizeof(iv), /* iv, ivLen */
+ tag, sizeof(tag), /* authTag, authTagSz */
+ aad, sizeof(aad)); /* authIn (AAD), authInSz */
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_AesGcmDecrypt %d\n", ret);
+ return ret;
+ }
+
+ /* Check if the decrypted data matches an expected value */
+ if (memcmp(decrypted, plaintext, sizeof(decrypted)) != 0) {
+ WH_ERROR_PRINT("Decrypted value does not match expected value\n");
+ return -1;
+ }
+
+ ret = wh_Client_KeyUnwrapAndExport(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ wrappedKey, wrappedKeySz, &tmpMetadata,
+ tmpPlainKey, &tmpPlainKeySz);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wh_Client_KeyUnwrapAndExport %d\n", ret);
+ return ret;
+ }
+
+ if (memcmp(plainKey, tmpPlainKey, sizeof(plainKey)) != 0) {
+ WH_ERROR_PRINT("AES GCM wrap/unwrap key failed to match\n");
+ return -1;
+ }
+
+ if (memcmp(&metadata, &tmpMetadata, sizeof(metadata)) != 0) {
+ WH_ERROR_PRINT("AES GCM wrap/unwrap metadata failed to match\n");
+ return -1;
+ }
+
+ /* Cache a local key using the same numeric ID to confirm coexistence */
+ {
+ whKeyId localKeyId = WH_TEST_AESGCM_KEYID;
+ uint8_t localLabel[WH_NVM_LABEL_LEN] = "LocalKeySameId";
+ const uint8_t localKey[WH_TEST_AES_KEYSIZE] = {0};
+
+ ret = wh_Client_KeyCache(client, WH_NVM_FLAGS_NONE, localLabel,
+ (uint16_t)sizeof("LocalKeySameId"),
+ (uint8_t*)localKey, sizeof(localKey),
+ &localKeyId);
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to cache local key with shared ID %d\n", ret);
+ return ret;
+ }
+ if (localKeyId != WH_TEST_AESGCM_KEYID) {
+ WH_ERROR_PRINT("Local key ID mismatch (expected %u, got %u)\n",
+ WH_TEST_AESGCM_KEYID, localKeyId);
+ return WH_ERROR_ABORTED;
+ }
+ WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvict(client, localKeyId));
+ }
+
+ wh_Client_KeyEvict(client, wrappedKeyId);
+ wc_AesFree(aes);
+
+ return ret;
+}
+
+static int _AesGcm_TestDataWrap(whClientContext* client)
+{
+ int ret = 0;
+ uint8_t data[] = "Example data!";
+ uint8_t unwrappedData[sizeof(data)] = {0};
+ uint32_t unwrappedDataSz = sizeof(unwrappedData);
+ uint8_t wrappedData[sizeof(data) + WH_KEYWRAP_AES_GCM_HEADER_SIZE] = {0};
+ uint32_t wrappedDataSz = sizeof(wrappedData);
+
+ ret = wh_Client_DataWrap(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID, data,
+ sizeof(data), wrappedData, &wrappedDataSz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to wh_Client_DataWrap %d\n", ret);
+ return ret;
+ }
+
+ ret = wh_Client_DataUnwrap(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ wrappedData, sizeof(wrappedData), unwrappedData,
+ &unwrappedDataSz);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to wh_Client_DataUnwrap %d\n", ret);
+ return ret;
+ }
+
+ if (memcmp(data, unwrappedData, sizeof(data)) != 0) {
+ WH_ERROR_PRINT("Unwrapped data failed to match input data\n");
+ return -1;
+ }
+
+ return ret;
+}
+
+static int _AesGcm_TestKeyUnwrapUnderflow(whClientContext* client)
+{
+ int ret;
+ uint8_t dummyBuf[1] = {0};
+ whNvmMetadata tmpMetadata = {0};
+ uint8_t tmpKey[WH_TEST_AES_KEYSIZE] = {0};
+ uint16_t tmpKeySz = sizeof(tmpKey);
+ whKeyId wrappedKeyId = WH_KEYID_ERASED;
+
+ /* wrappedKeySz=0: must return WH_ERROR_BADARGS, not underflow */
+ ret = wh_Client_KeyUnwrapAndExport(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ dummyBuf, 0, &tmpMetadata, tmpKey,
+ &tmpKeySz);
+ if (ret != WH_ERROR_BADARGS) {
+ WH_ERROR_PRINT("KeyUnwrapAndExport(sz=0) expected BADARGS, got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+
+ /* wrappedKeySz=1: must return WH_ERROR_BADARGS, not underflow */
+ tmpKeySz = sizeof(tmpKey);
+ ret = wh_Client_KeyUnwrapAndExport(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ dummyBuf, 1, &tmpMetadata, tmpKey,
+ &tmpKeySz);
+ if (ret != WH_ERROR_BADARGS) {
+ WH_ERROR_PRINT("KeyUnwrapAndExport(sz=1) expected BADARGS, got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+
+ /* wrappedKeySz=0: test KeyUnwrapAndCache path */
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ dummyBuf, 0, &wrappedKeyId);
+ if (ret != WH_ERROR_BADARGS) {
+ WH_ERROR_PRINT("KeyUnwrapAndCache(sz=0) expected BADARGS, got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+
+ /* wrappedKeySz=1: test KeyUnwrapAndCache path */
+ ret = wh_Client_KeyUnwrapAndCache(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ dummyBuf, 1, &wrappedKeyId);
+ if (ret != WH_ERROR_BADARGS) {
+ WH_ERROR_PRINT("KeyUnwrapAndCache(sz=1) expected BADARGS, got %d\n",
+ ret);
+ return WH_TEST_FAIL;
+ }
+
+ return WH_ERROR_OK;
+}
+
+static int _AesGcm_TestDataUnwrapUnderflow(whClientContext* client)
+{
+ int ret;
+ uint8_t dummyBuf[1] = {0};
+ uint8_t outBuf[32] = {0};
+ uint32_t outSz = sizeof(outBuf);
+
+ /* wrappedDataSz=0: must return WH_ERROR_BADARGS, not underflow */
+ ret = wh_Client_DataUnwrap(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ dummyBuf, 0, outBuf, &outSz);
+ if (ret != WH_ERROR_BADARGS) {
+ WH_ERROR_PRINT("DataUnwrap(sz=0) expected BADARGS, got %d\n", ret);
+ return WH_TEST_FAIL;
+ }
+
+ /* wrappedDataSz=1: must return WH_ERROR_BADARGS, not underflow */
+ outSz = sizeof(outBuf);
+ ret = wh_Client_DataUnwrap(client, WC_CIPHER_AES_GCM, WH_TEST_KEKID,
+ dummyBuf, 1, outBuf, &outSz);
+ if (ret != WH_ERROR_BADARGS) {
+ WH_ERROR_PRINT("DataUnwrap(sz=1) expected BADARGS, got %d\n", ret);
+ return WH_TEST_FAIL;
+ }
+
+ return WH_ERROR_OK;
+}
+
+#endif /* HAVE_AESGCM */
+
+int whTest_KeyWrap(whClientContext* client)
+{
+ int ret = 0;
+ WC_RNG rng[1];
+
+ if (client == NULL) {
+ return WH_ERROR_BADARGS;
+ }
+
+ WH_TEST_RETURN_ON_FAIL(_InitServerKek(client));
+
+ ret = wc_InitRng_ex(rng, NULL, WH_CLIENT_DEVID(client));
+ if (ret != 0) {
+ WH_ERROR_PRINT("Failed to wc_InitRng_ex %d\n", ret);
+ (void)_CleanupServerKek(client);
+ return ret;
+ }
+
+#ifdef HAVE_AESGCM
+ ret = _AesGcm_TestKeyWrap(client, rng);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to _AesGcm_TestKeyWrap %d\n", ret);
+ }
+
+ if (ret == WH_ERROR_OK) {
+ ret = _AesGcm_TestKeyUnwrapUnderflow(client);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to _AesGcm_TestKeyUnwrapUnderflow %d\n", ret);
+ }
+ }
+
+ if (ret == WH_ERROR_OK) {
+ ret = _AesGcm_TestDataWrap(client);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to _AesGcm_TestDataWrap %d\n", ret);
+ }
+ }
+
+ if (ret == WH_ERROR_OK) {
+ ret = _AesGcm_TestDataUnwrapUnderflow(client);
+ if (ret != WH_ERROR_OK) {
+ WH_ERROR_PRINT("Failed to _AesGcm_TestDataUnwrapUnderflow %d\n",
+ ret);
+ }
+ }
+#endif /* HAVE_AESGCM */
+
+ (void)_CleanupServerKek(client);
+ (void)wc_FreeRng(rng);
+
+ return ret;
+}
+
+#endif /* WOLFHSM_CFG_KEYWRAP && WOLFHSM_CFG_ENABLE_CLIENT && \
+ !WOLFHSM_CFG_NO_CRYPTO */
diff --git a/test-refactor/wh_test_list.c b/test-refactor/wh_test_list.c
index a7ff07128..1d9a20fa2 100644
--- a/test-refactor/wh_test_list.c
+++ b/test-refactor/wh_test_list.c
@@ -58,6 +58,7 @@ WH_TEST_DECL(whTest_Crypto_Sha);
WH_TEST_DECL(whTest_Echo);
WH_TEST_DECL(whTest_ServerInfo);
WH_TEST_DECL(whTest_WolfCryptTest);
+WH_TEST_DECL(whTest_KeyWrap);
WH_TEST_DECL(whTest_AuthBadArgs);
WH_TEST_DECL(whTest_AuthLogin);
WH_TEST_DECL(whTest_AuthLogout);
@@ -98,6 +99,7 @@ const whTestCase whTestsClient[] = {
{ "whTest_Echo", whTest_Echo },
{ "whTest_ServerInfo", whTest_ServerInfo },
{ "whTest_WolfCryptTest", whTest_WolfCryptTest },
+ { "whTest_KeyWrap", whTest_KeyWrap },
{ "whTest_AuthBadArgs", whTest_AuthBadArgs },
{ "whTest_AuthLogin", whTest_AuthLogin },
{ "whTest_AuthLogout", whTest_AuthLogout },