Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/5-Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,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.

Expand Down
86 changes: 86 additions & 0 deletions test-refactor/misc/wh_test_multiclient.c
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,89 @@ static int _testWrappedKey_LocalWrap_GlobalKey_NonOwnerNoWrapKey(
return 0;
#undef WRAPPED_KEY_SIZE
}

/*
* Test: Unwrapped key cannot persist to NVM
* - Wrapped keys are cache-only; the unwrapped key must never reach NVM
* - This invariant prevents an unwrapped key from shadowing a protected NVM
* entry, so commit and erase must reject a wrapped key id
*/
static int _testWrappedKey_UnwrapCache_NoNvmPersist(
whClientContext* client1, whServerContext* server1,
whClientContext* client2, whServerContext* server2)
{
int ret;
whKeyId serverKeyId = DUMMY_KEYID_1; /* Local wrap key */
whKeyId cachedKeyId = 0;
uint16_t client1Id = WH_TEST_DEFAULT_CLIENT_ID;
uint8_t wrapKey[AES_256_KEY_SIZE] = "NoNvmWrapKeyTest123456789aXX!";
uint8_t plainKey[AES_256_KEY_SIZE] = "NoNvmPlainKeyTest12345678aXX!";
#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata))
uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0};
uint16_t wrappedKeySz = sizeof(wrappedKey);
whNvmMetadata meta = {0};

WH_TEST_DEBUG_PRINT("Test: Unwrapped key cannot persist to NVM\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_NoNvm",
sizeof("WrapKey_NoNvm"), 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 */
serverKeyId = DUMMY_KEYID_1;
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 unwraps and caches the key */
WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndCacheRequest(
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_KeyUnwrapAndCacheResponse(
client1, WC_CIPHER_AES_GCM, &cachedKeyId));

/* Commit must refuse to persist a wrapped key to NVM */
WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCommitRequest(
client1, WH_CLIENT_KEYID_MAKE_WRAPPED(cachedKeyId)));
WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1));
ret = wh_Client_KeyCommitResponse(client1);
WH_TEST_ASSERT_RETURN(ret == WH_ERROR_ABORTED);

/* Erase must likewise refuse a wrapped key */
WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEraseRequest(
client1, WH_CLIENT_KEYID_MAKE_WRAPPED(cachedKeyId)));
WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1));
ret = wh_Client_KeyEraseResponse(client1);
WH_TEST_ASSERT_RETURN(ret == WH_ERROR_ABORTED);

/* Cache-only eviction must still succeed */
WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(
client1, WH_CLIENT_KEYID_MAKE_WRAPPED(cachedKeyId)));
WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1));
WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1));

/* Evict the wrapping key */
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: Unwrapped key cannot persist to NVM\n");

(void)client2;
(void)server2;
return 0;
#undef WRAPPED_KEY_SIZE
}
#endif /* WOLFHSM_CFG_KEYWRAP */

/*
Expand Down Expand Up @@ -1412,6 +1495,9 @@ static int _runGlobalKeysTests(whClientContext* client1,
WH_TEST_RETURN_ON_FAIL(
_testWrappedKey_LocalWrap_GlobalKey_NonOwnerNoWrapKey(
client1, server1, client2, server2));

WH_TEST_RETURN_ON_FAIL(_testWrappedKey_UnwrapCache_NoNvmPersist(
client1, server1, client2, server2));
#endif

WH_TEST_PRINT("All Global Keys Tests PASSED ===\n");
Expand Down
57 changes: 57 additions & 0 deletions test/wh_test_keywrap.c
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,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;
Expand Down Expand Up @@ -374,6 +424,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);
Expand Down
Loading