diff --git a/docs/src/5-Features.md b/docs/src/5-Features.md index 743a87c2f..e6f235cc3 100644 --- a/docs/src/5-Features.md +++ b/docs/src/5-Features.md @@ -365,7 +365,7 @@ The lifecycle exposed to clients consists of three primary operations: - **Wrap**: the client supplies plaintext key bytes, a metadata template, and the keyId of a server-resident KEK; the server encrypts the (metadata || key) blob with the KEK and returns the wrapped blob to the client. The plaintext is not written to NVM as part of this operation. - **Unwrap-and-export**: the client supplies a wrapped blob and the KEK's keyId; the server decrypts the blob, authenticates the tag, and returns the recovered metadata and key bytes to the client. This is the operation used by host-side workflows that need to consume the key off-device, for example to inject it into a non-HSM peer. -- **Unwrap-and-cache**: the client supplies a wrapped blob and the KEK's keyId; the server decrypts the blob and installs the recovered key directly into the keystore cache as if `wh_Client_KeyCache` had been called locally with the recovered bytes. This is the more common operation in production deployments, since it lets a key live on disk in encrypted form and be hydrated into the HSM at runtime without the plaintext ever transiting the client. Clients can then commit the unwrapped key to NVM if they wish. +- **Unwrap-and-cache**: the client supplies a wrapped blob and the KEK's keyId; the server decrypts the blob and installs the recovered key directly into the keystore cache as if `wh_Client_KeyCache` had been called locally with the recovered bytes. This is the more common operation in production deployments, since it lets a key live on disk in encrypted form and be hydrated into the HSM at runtime without the plaintext ever transiting the client. Unwrapped keys are cache-only and cannot be committed to NVM. In all three operations the KEK is identified by its existing keyId in the keystore, must carry the `WH_NVM_FLAGS_USAGE_WRAP` usage flag, and is enforced server-side by the keystore policy machinery. A key without the `WRAP` usage flag cannot be used to wrap or unwrap regardless of any client request. diff --git a/test-refactor/client-server/wh_test_crypto_keywrap.c b/test-refactor/client-server/wh_test_crypto_keywrap.c new file mode 100644 index 000000000..42412c8e6 --- /dev/null +++ b/test-refactor/client-server/wh_test_crypto_keywrap.c @@ -0,0 +1,177 @@ +/* + * 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_crypto_keywrap.c + * + * Single-client key wrap tests over AES-GCM: + * _whTest_CryptoKeyWrapSameOwner - wrap a local key, then unwrap and + * export it as the owner and confirm the + * recovered key matches the original + * _whTest_CryptoKeyWrapNoNvmPersist - an unwrapped key is cache-only, so + * commit and erase must reject a wrapped + * key id while eviction still succeeds + */ + +#include "wolfhsm/wh_settings.h" + +#if !defined(WOLFHSM_CFG_NO_CRYPTO) && defined(WOLFHSM_CFG_KEYWRAP) && \ + !defined(NO_AES) && defined(HAVE_AESGCM) + +#include +#include + +#include "wolfssl/wolfcrypt/settings.h" +#include "wolfssl/wolfcrypt/types.h" +#include "wolfssl/wolfcrypt/aes.h" + +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_common.h" +#include "wolfhsm/wh_client.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + +#define KEYWRAP_TEST_AESGCM_IV_SIZE 12 +#define KEYWRAP_TEST_AESGCM_TAG_SIZE 16 +#define KEYWRAP_TEST_WRAPPED_KEY_SIZE \ + (KEYWRAP_TEST_AESGCM_IV_SIZE + KEYWRAP_TEST_AESGCM_TAG_SIZE + \ + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + +#define KEYWRAP_TEST_WRAPKEY_ID 1 +#define KEYWRAP_TEST_PLAINKEY_ID 2 + +/* + * Wrap a local key, then unwrap and export it as the owning client. + * The recovered key must match the original plaintext. + */ +static int _whTest_CryptoKeyWrapSameOwner(whClientContext* client) +{ + whKeyId wrapKeyId = KEYWRAP_TEST_WRAPKEY_ID; + uint8_t wrapKey[AES_256_KEY_SIZE] = "LocalWrapKey2Test9aXXXXXXXXXX!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "LocalPlainKey2Test9aXXXXXXXXX!"; + uint8_t wrappedKey[KEYWRAP_TEST_WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t unwrappedKey[AES_256_KEY_SIZE] = {0}; + uint16_t unwrappedKeySz = sizeof(unwrappedKey); + whNvmMetadata meta = {0}; + whNvmMetadata exportMeta = {0}; + + WH_TEST_DEBUG_PRINT("Test: local wrap key, local key, same owner\n"); + + /* Cache a local wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCache( + client, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKeySameOwner", + sizeof("WrapKeySameOwner"), wrapKey, sizeof(wrapKey), &wrapKeyId)); + + /* Wrap a local key owned by this client */ + meta.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(client->comm->client_id, + KEYWRAP_TEST_PLAINKEY_ID); + meta.len = sizeof(plainKey); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrap(client, WC_CIPHER_AES_GCM, + wrapKeyId, plainKey, + sizeof(plainKey), &meta, + wrappedKey, &wrappedKeySz)); + + /* Owner unwraps and exports the local key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExport( + client, WC_CIPHER_AES_GCM, wrapKeyId, wrappedKey, sizeof(wrappedKey), + &exportMeta, unwrappedKey, &unwrappedKeySz)); + + /* Recovered key matches the original */ + WH_TEST_ASSERT_RETURN(0 == + memcmp(unwrappedKey, plainKey, sizeof(plainKey))); + + /* Evict the wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvict(client, wrapKeyId)); + + WH_TEST_PRINT(" PASS: local wrap key, local key, same owner\n"); + + return WH_ERROR_OK; +} + +/* + * An unwrapped key is cache-only; the unwrapped key must never reach NVM. + * This prevents an unwrapped key from shadowing a protected NVM entry, so + * commit and erase must reject a wrapped key id while eviction succeeds. + */ +static int _whTest_CryptoKeyWrapNoNvmPersist(whClientContext* client) +{ + int ret; + whKeyId wrapKeyId = KEYWRAP_TEST_WRAPKEY_ID; + whKeyId cachedKeyId = 0; + uint8_t wrapKey[AES_256_KEY_SIZE] = "NoNvmWrapKeyTest123456789aXX!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "NoNvmPlainKeyTest12345678aXX!"; + uint8_t wrappedKey[KEYWRAP_TEST_WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + whNvmMetadata meta = {0}; + + WH_TEST_DEBUG_PRINT("Test: unwrapped key cannot persist to NVM\n"); + + /* Cache a local wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCache( + client, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKeyNoNvm", + sizeof("WrapKeyNoNvm"), wrapKey, sizeof(wrapKey), &wrapKeyId)); + + /* Wrap a local key */ + meta.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(client->comm->client_id, + KEYWRAP_TEST_PLAINKEY_ID); + meta.len = sizeof(plainKey); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrap(client, WC_CIPHER_AES_GCM, + wrapKeyId, plainKey, + sizeof(plainKey), &meta, + wrappedKey, &wrappedKeySz)); + + /* Unwrap and cache the key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndCache( + client, WC_CIPHER_AES_GCM, wrapKeyId, wrappedKey, sizeof(wrappedKey), + &cachedKeyId)); + + /* Commit must refuse to persist a wrapped key to NVM */ + ret = wh_Client_KeyCommit(client, + WH_CLIENT_KEYID_MAKE_WRAPPED(cachedKeyId)); + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_ABORTED); + + /* Erase must likewise refuse a wrapped key */ + ret = wh_Client_KeyErase(client, WH_CLIENT_KEYID_MAKE_WRAPPED(cachedKeyId)); + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_ABORTED); + + /* Cache-only eviction must still succeed */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvict( + client, WH_CLIENT_KEYID_MAKE_WRAPPED(cachedKeyId))); + + /* Evict the wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvict(client, wrapKeyId)); + + WH_TEST_PRINT(" PASS: unwrapped key cannot persist to NVM\n"); + + return WH_ERROR_OK; +} + +int whTest_Crypto_KeyWrap(whClientContext* ctx) +{ + /* A preceding suite may leave the DMA-preferred dispatch mode set; reset + * to the std path so this suite runs the same way in every config. */ + (void)wh_Client_SetDmaMode(ctx, 0); + WH_TEST_RETURN_ON_FAIL(_whTest_CryptoKeyWrapSameOwner(ctx)); + WH_TEST_RETURN_ON_FAIL(_whTest_CryptoKeyWrapNoNvmPersist(ctx)); + return WH_ERROR_OK; +} + +#endif /* !WOLFHSM_CFG_NO_CRYPTO && WOLFHSM_CFG_KEYWRAP && \ + !NO_AES && HAVE_AESGCM */ diff --git a/test-refactor/misc/wh_test_multiclient.c b/test-refactor/misc/wh_test_multiclient.c index df9e74f45..c06a475f4 100644 --- a/test-refactor/misc/wh_test_multiclient.c +++ b/test-refactor/misc/wh_test_multiclient.c @@ -953,75 +953,6 @@ static int _testWrappedKey_GlobalWrap_LocalKey_NonOwnerFails( #undef WRAPPED_KEY_SIZE } -/* - * Test 9a: Local wrapping key + Local wrapped key (Positive - Same owner) - * - Client 1 caches a local wrapping key - * - Client 1 wraps a LOCAL key (USER=client1_id) - * - Client 1 unwraps and exports successfully - */ -static int _testWrappedKey_LocalWrap_LocalKey_SameOwner( - whClientContext* client1, whServerContext* server1, - whClientContext* client2, whServerContext* server2) -{ - int ret; - whKeyId serverKeyId = DUMMY_KEYID_1; /* Local wrapping key */ - uint16_t client1Id = WH_TEST_DEFAULT_CLIENT_ID; - uint8_t wrapKey[AES_256_KEY_SIZE] = "LocalWrapKey2Test9aXXXXXXXXXX!"; - uint8_t plainKey[AES_256_KEY_SIZE] = "LocalPlainKey2Test9aXXXXXXXXX!"; -#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) - uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; - uint16_t wrappedKeySz = sizeof(wrappedKey); - uint8_t unwrappedKey[AES_256_KEY_SIZE] = {0}; - uint16_t unwrappedKeySz = sizeof(unwrappedKey); - whNvmMetadata meta = {0}; - - WH_TEST_DEBUG_PRINT("Test 9a: Local wrap key + Local wrapped key (Same owner)\n"); - - /* Client 1 caches a LOCAL wrapping key */ - WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( - client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey_9a", - sizeof("WrapKey_9a"), wrapKey, sizeof(wrapKey), serverKeyId)); - WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); - WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); - - /* Client 1 wraps a LOCAL key (USER=client1_id) */ - serverKeyId = DUMMY_KEYID_1; /* Use local wrapping key */ - meta.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(client1Id, DUMMY_KEYID_2); - meta.len = sizeof(plainKey); - WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client1, WC_CIPHER_AES_GCM, - serverKeyId, plainKey, - sizeof(plainKey), &meta)); - WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); - WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( - client1, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); - - /* Client 1 (owner) unwraps and exports the local key */ - WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportRequest( - client1, WC_CIPHER_AES_GCM, serverKeyId, wrappedKey, - sizeof(wrappedKey))); - WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); - WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportResponse( - client1, WC_CIPHER_AES_GCM, &meta, unwrappedKey, &unwrappedKeySz)); - - /* Verify the unwrapped key matches the original */ - WH_TEST_ASSERT_RETURN(0 == - memcmp(unwrappedKey, plainKey, sizeof(plainKey))); - - /* Clean up */ - serverKeyId = DUMMY_KEYID_1; - WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); - WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); - WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); - - WH_TEST_PRINT(" PASS: Local wrap key + Local wrapped key (Same owner)\n"); - - (void)ret; - (void)client2; - (void)server2; - return 0; -#undef WRAPPED_KEY_SIZE -} - /* * Test 9b: Local wrapping key + Local wrapped key (Negative - No access without * wrap key) @@ -1240,6 +1171,7 @@ static int _testWrappedKey_LocalWrap_GlobalKey_NonOwnerNoWrapKey( return 0; #undef WRAPPED_KEY_SIZE } + #endif /* WOLFHSM_CFG_KEYWRAP */ /* @@ -1399,9 +1331,6 @@ static int _runGlobalKeysTests(whClientContext* client1, WH_TEST_RETURN_ON_FAIL(_testWrappedKey_GlobalWrap_LocalKey_NonOwnerFails( client1, server1, client2, server2)); - WH_TEST_RETURN_ON_FAIL(_testWrappedKey_LocalWrap_LocalKey_SameOwner( - client1, server1, client2, server2)); - WH_TEST_RETURN_ON_FAIL( _testWrappedKey_LocalWrap_LocalKey_NoAccessWithoutWrapKey( client1, server1, client2, server2)); diff --git a/test-refactor/wh_test_list.c b/test-refactor/wh_test_list.c index f6572bf8f..bd82b759b 100644 --- a/test-refactor/wh_test_list.c +++ b/test-refactor/wh_test_list.c @@ -59,6 +59,7 @@ WH_TEST_DECL(whTest_Crypto_Ecc); WH_TEST_DECL(whTest_Crypto_Ed25519); WH_TEST_DECL(whTest_Crypto_Kdf); WH_TEST_DECL(whTest_Crypto_KeyPolicy); +WH_TEST_DECL(whTest_Crypto_KeyWrap); WH_TEST_DECL(whTest_Crypto_Lms); WH_TEST_DECL(whTest_Crypto_MlDsa); WH_TEST_DECL(whTest_Crypto_Rng); @@ -120,6 +121,7 @@ const whTestCase whTestsClient[] = { {"whTest_Crypto_Ed25519", whTest_Crypto_Ed25519}, {"whTest_Crypto_Kdf", whTest_Crypto_Kdf}, {"whTest_Crypto_KeyPolicy", whTest_Crypto_KeyPolicy}, + {"whTest_Crypto_KeyWrap", whTest_Crypto_KeyWrap}, {"whTest_Crypto_Lms", whTest_Crypto_Lms}, {"whTest_Crypto_MlDsa", whTest_Crypto_MlDsa}, {"whTest_Crypto_Rng", whTest_Crypto_Rng}, diff --git a/test/wh_test_keywrap.c b/test/wh_test_keywrap.c index 2b89db6b2..62928c452 100644 --- a/test/wh_test_keywrap.c +++ b/test/wh_test_keywrap.c @@ -355,6 +355,56 @@ static int _AesGcm_TestKeyUnwrapUnderflow(whClientContext* client) return WH_ERROR_OK; } +/* Wrapped keys are cache-only: they must never be persisted to or destroyed + * in NVM. This invariant is what keeps an unwrapped key from shadowing a + * protected NVM entry, so commit and erase must reject a wrapped key id. */ +static int _AesGcm_TestKeyWrapNoNvm(whClientContext* client) +{ + int ret; + uint8_t plainKey[WH_TEST_AES_KEYSIZE] = {0}; + 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 = "NoNvm Wrapped Key", + .len = WH_TEST_AES_KEYSIZE, + .flags = WH_NVM_FLAGS_USAGE_ANY, + }; + + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrap( + client, WC_CIPHER_AES_GCM, WH_TEST_KEKID, plainKey, sizeof(plainKey), + &metadata, wrappedKey, &wrappedKeySz)); + + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndCache( + client, WC_CIPHER_AES_GCM, WH_TEST_KEKID, wrappedKey, wrappedKeySz, + &wrappedKeyId)); + + /* The returned id carries the wrapped flag; commit must refuse it */ + ret = wh_Client_KeyCommit(client, wrappedKeyId); + if (ret != WH_ERROR_ABORTED) { + WH_ERROR_PRINT("KeyCommit of wrapped key expected ABORTED, got %d\n", + ret); + (void)wh_Client_KeyEvict(client, wrappedKeyId); + return WH_TEST_FAIL; + } + + /* Erase must likewise refuse a wrapped key */ + ret = wh_Client_KeyErase(client, wrappedKeyId); + if (ret != WH_ERROR_ABORTED) { + WH_ERROR_PRINT("KeyErase of wrapped key expected ABORTED, got %d\n", + ret); + (void)wh_Client_KeyEvict(client, wrappedKeyId); + return WH_TEST_FAIL; + } + + /* Cache-only eviction must still succeed */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvict(client, wrappedKeyId)); + + return WH_ERROR_OK; +} + static int _AesGcm_TestDataUnwrapUnderflow(whClientContext* client) { int ret; @@ -651,6 +701,13 @@ int whTest_Client_KeyWrap(whClientContext* client) ret); } } + + if (ret == WH_ERROR_OK) { + ret = _AesGcm_TestKeyWrapNoNvm(client); + if (ret != WH_ERROR_OK) { + WH_ERROR_PRINT("Failed to _AesGcm_TestKeyWrapNoNvm %d\n", ret); + } + } #endif _CleanupServerKek(client);