From 0f7d2fad9008957bf8439b70ae523d836a1febe9 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Wed, 24 Jun 2026 13:30:12 -0700 Subject: [PATCH] Add refactored tests for keywrap, coverage matches --- test-refactor/README.md | 2 +- test-refactor/client-server/wh_test_keywrap.c | 412 ++++++++++++++++++ test-refactor/wh_test_list.c | 2 + 3 files changed, 415 insertions(+), 1 deletion(-) create mode 100644 test-refactor/client-server/wh_test_keywrap.c 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 },