From 9e717fac44ab6572141a57fb9ad8275729ccdbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 14 Jul 2026 11:51:46 +0200 Subject: [PATCH 1/6] Return RSA public key from cached keygen Backport the LMS/XMSS keygen ergonomic to RSA: when a key is generated and cached in the HSM (non-ephemeral), the server now also serializes the generated public key into the keygen response body. The client gets both the keyId and a usable public key in one round-trip instead of following up with a separate wh_Client_RsaExportPublicKey call. The response reuses the existing keyId + len + body layout, so there is no wire-format change. Existing wh_Client_RsaMakeCacheKey callers ignore the extra body bytes and are unaffected. Add wh_Client_RsaMakeCacheKeyAndExportPublic, which caches the key and returns its public key into the caller's RsaKey object, pointing that object at the cached keyId for follow-on HSM operations. Add tests in both the main and refactored crypto test suites that cross-check the keygen-returned public key against wh_Client_RsaExportPublicKey and prove it is usable via an encrypt/HSM-decrypt round-trip. --- src/wh_client_crypto.c | 49 +++++++- src/wh_server_crypto.c | 12 +- .../client-server/wh_test_crypto_rsa.c | 116 ++++++++++++++++++ test/wh_test_crypto.c | 101 +++++++++++++++ wolfhsm/wh_client_crypto.h | 11 ++ 5 files changed, 286 insertions(+), 3 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index d11e64609..7088ad2b4 100644 --- a/src/wh_client_crypto.c +++ b/src/wh_client_crypto.c @@ -98,7 +98,7 @@ static int _Curve25519MakeKey(whClientContext* ctx, uint16_t size, /* Shared async halves used by the RsaMakeCacheKey/RsaMakeExportKey wrappers. */ static int _RsaMakeKeyRequest(whClientContext* ctx, uint32_t size, uint32_t e, whKeyId key_id, whNvmFlags flags, - uint32_t label_len, uint8_t* label); + uint32_t label_len, const uint8_t* label); static int _RsaMakeKeyResponse(whClientContext* ctx, whKeyId* out_key_id, RsaKey* out_rsa); #endif @@ -4191,7 +4191,7 @@ int wh_Client_RsaExportPublicKey(whClientContext* ctx, whKeyId keyId, static int _RsaMakeKeyRequest(whClientContext* ctx, uint32_t size, uint32_t e, whKeyId key_id, whNvmFlags flags, - uint32_t label_len, uint8_t* label) + uint32_t label_len, const uint8_t* label) { whMessageCrypto_RsaKeyGenRequest* req = NULL; uint8_t* dataPtr = NULL; @@ -4352,6 +4352,51 @@ int wh_Client_RsaMakeCacheKey(whClientContext* ctx, uint32_t size, uint32_t e, return ret; } +int wh_Client_RsaMakeCacheKeyAndExportPublic(whClientContext* ctx, + uint32_t size, uint32_t e, + whKeyId* inout_key_id, + whNvmFlags flags, + uint32_t label_len, + const uint8_t* label, + RsaKey* pub) +{ + int ret; + whKeyId in_keyId; + whKeyId key_id = WH_KEYID_ERASED; + + if ((ctx == NULL) || (inout_key_id == NULL) || (pub == NULL)) { + return WH_ERROR_BADARGS; + } + + /* Ephemeral keygen belongs to the export pair, not the cache pair. */ + if (flags & WH_NVM_FLAGS_EPHEMERAL) { + return WH_ERROR_BADARGS; + } + + in_keyId = *inout_key_id; + ret = _RsaMakeKeyRequest(ctx, size, e, in_keyId, flags, label_len, + label); + if (ret == WH_ERROR_OK) { + do { + ret = _RsaMakeKeyResponse(ctx, &key_id, pub); + } while (ret == WH_ERROR_NOTREADY); + if (ret >= 0) { + *inout_key_id = key_id; + /* Associate the returned key with the cached keyId. The caller must + * still set the HSM devId (e.g. wc_InitRsaKey_ex with the HSM devId) + * before follow-on operations route to the server. */ + wh_Client_RsaSetKeyId(pub, key_id); + } + else if (WH_KEYID_ISERASED(in_keyId) && !WH_KEYID_ISERASED(key_id)) { + /* The server auto-assigned and committed a key but the export + * failed. Roll back so the operation is atomic and no cache slot is + * orphaned. */ + (void)wh_Client_KeyEvict(ctx, key_id); + } + } + return ret; +} + int wh_Client_RsaMakeExportKey(whClientContext* ctx, uint32_t size, uint32_t e, RsaKey* rsa) { diff --git a/src/wh_server_crypto.c b/src/wh_server_crypto.c index b35e8e4a0..dd7901595 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -375,9 +375,19 @@ static int _HandleRsaKeyGen(whServerContext* ctx, uint16_t magic, int devId, label_size, label); } WH_DEBUG_SERVER_VERBOSE("RsaKeyGen CacheKeyRsa: keyId:%u, ret:%d\n", key_id, ret); + if (ret == 0) { + /* Best-effort: also export the public key into the response + * body so the client avoids a separate ExportPublicKey + * call. On failure leave the body empty and still return the + * cached keyId rather than orphaning the committed key. */ + int pub_ret = wc_RsaKeyToPublicDer(rsa, out, max_size); + if (pub_ret > 0) { + der_size = (uint16_t)pub_ret; + } + } if (ret == 0) { res.keyId = wh_KeyId_TranslateToClient(key_id); - res.len = 0; + res.len = der_size; } } } diff --git a/test-refactor/client-server/wh_test_crypto_rsa.c b/test-refactor/client-server/wh_test_crypto_rsa.c index e859e7f7c..dc7451369 100644 --- a/test-refactor/client-server/wh_test_crypto_rsa.c +++ b/test-refactor/client-server/wh_test_crypto_rsa.c @@ -34,6 +34,7 @@ #include "wolfssl/wolfcrypt/settings.h" #include "wolfssl/wolfcrypt/types.h" #include "wolfssl/wolfcrypt/rsa.h" +#include "wolfssl/wolfcrypt/asn.h" #include "wolfssl/wolfcrypt/random.h" #include "wolfhsm/wh_error.h" @@ -322,6 +323,120 @@ static int _whTest_CryptoRsaExportPublicKey(whClientContext* ctx) return ret; } +/* One keygen call caches the private key and returns the public key. Verify + * the returned public key byte-matches a separate wh_Client_RsaExportPublicKey + * and that it round-trips against the cached private key. */ +static int _whTest_CryptoRsaCacheKeyAndExportPublic(whClientContext* ctx) +{ + int devId = WH_CLIENT_DEVID(ctx); + int ret = WH_ERROR_OK; + WC_RNG rng[1]; + RsaKey genPub[1]; + RsaKey refPub[1]; + RsaKey hsmKey[1]; + char plainText[sizeof(WH_TEST_RSA_PLAINTEXT)] = WH_TEST_RSA_PLAINTEXT; + char cipherText[RSA_KEY_BYTES]; + char finalText[RSA_KEY_BYTES]; + whKeyId keyId = WH_KEYID_ERASED; + byte genDer[2048]; + byte refDer[2048]; + int genDerSz = 0; + int refDerSz = 0; + int encLen = 0; + int decLen; + + memset(cipherText, 0, sizeof(cipherText)); + memset(finalText, 0, sizeof(finalText)); + + ret = wc_InitRng_ex(rng, NULL, devId); + if (ret != 0) { + WH_ERROR_PRINT("Failed to wc_InitRng_ex %d\n", ret); + return ret; + } + + ret = wc_InitRsaKey_ex(genPub, NULL, INVALID_DEVID); + if (ret != 0) { + WH_ERROR_PRINT("Failed to wc_InitRsaKey_ex %d\n", ret); + (void)wc_FreeRng(rng); + return ret; + } + + ret = wh_Client_RsaMakeCacheKeyAndExportPublic( + ctx, RSA_KEY_BITS, RSA_EXPONENT, &keyId, + WH_NVM_FLAGS_USAGE_ENCRYPT | WH_NVM_FLAGS_USAGE_DECRYPT, 0, NULL, + genPub); + if (ret != 0) { + WH_ERROR_PRINT("RsaMakeCacheKeyAndExportPublic failed %d\n", ret); + } + + /* Cross-check against a separate public export of the same keyId. */ + if (ret == 0) { + ret = wc_InitRsaKey_ex(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wh_Client_RsaExportPublicKey(ctx, keyId, refPub, 0, NULL); + if (ret != 0) { + WH_ERROR_PRINT("wh_Client_RsaExportPublicKey failed %d\n", ret); + } + else { + genDerSz = + wc_RsaKeyToPublicDer(genPub, genDer, sizeof(genDer)); + refDerSz = + wc_RsaKeyToPublicDer(refPub, refDer, sizeof(refDer)); + if ((genDerSz <= 0) || (genDerSz != refDerSz) || + (memcmp(genDer, refDer, (size_t)genDerSz) != 0)) { + WH_ERROR_PRINT("keygen pubkey mismatch vs export\n"); + ret = -1; + } + } + (void)wc_FreeRsaKey(refPub); + } + } + + /* Encrypt with the keygen-returned public key. */ + if (ret == 0) { + encLen = wc_RsaPublicEncrypt((byte*)plainText, sizeof(plainText), + (byte*)cipherText, sizeof(cipherText), + genPub, rng); + if (encLen < 0) { + WH_ERROR_PRINT("PublicEncrypt with keygen pub failed %d\n", encLen); + ret = encLen; + } + } + + /* Decrypt on the HSM with the cached private key. */ + if (ret == 0) { + ret = wc_InitRsaKey_ex(hsmKey, NULL, devId); + if (ret == 0) { + ret = wh_Client_RsaSetKeyId(hsmKey, keyId); + } + if (ret == 0) { + decLen = wc_RsaPrivateDecrypt((byte*)cipherText, encLen, + (byte*)finalText, sizeof(finalText), + hsmKey); + if (decLen < 0) { + WH_ERROR_PRINT("HSM PrivateDecrypt failed %d\n", decLen); + ret = decLen; + } + else if (memcmp(plainText, finalText, sizeof(plainText)) != 0) { + WH_ERROR_PRINT("RSA keygen-pub round-trip mismatch\n"); + ret = -1; + } + } + (void)wc_FreeRsaKey(hsmKey); + } + + (void)wc_FreeRsaKey(genPub); + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + (void)wc_FreeRng(rng); + + if (ret == 0) { + WH_TEST_PRINT("RSA CACHE-AND-EXPORT-PUBLIC DEVID=0x%X SUCCESS\n", devId); + } + return ret; +} + /* Exercises wh_Client_RsaFunction with an undersized output buffer. */ static int _whTest_CryptoRsaBufferTooSmall(whClientContext* ctx) { @@ -389,6 +504,7 @@ int whTest_Crypto_Rsa(whClientContext* ctx) { WH_TEST_RETURN_ON_FAIL(_whTest_CryptoRsa(ctx)); WH_TEST_RETURN_ON_FAIL(_whTest_CryptoRsaExportPublicKey(ctx)); + WH_TEST_RETURN_ON_FAIL(_whTest_CryptoRsaCacheKeyAndExportPublic(ctx)); WH_TEST_RETURN_ON_FAIL(_whTest_CryptoRsaBufferTooSmall(ctx)); return 0; } diff --git a/test/wh_test_crypto.c b/test/wh_test_crypto.c index 3ef889e7e..8a7b9f7d9 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -721,6 +721,107 @@ static int whTest_CryptoRsa(whClientContext* ctx, int devId, WC_RNG* rng) } } + /* Cache-and-export-public: a single keygen call returns the public key */ + if (ret == 0) { + RsaKey genPub[1]; + RsaKey refPub[1]; + whKeyId cacheId = WH_KEYID_ERASED; + byte genDer[2048]; + byte refDer[2048]; + int genDerSz = 0; + int refDerSz = 0; + int genInit = 0; + int refInit = 0; + + memset(cipherText, 0, sizeof(cipherText)); + memset(finalText, 0, sizeof(finalText)); + + ret = wc_InitRsaKey_ex(genPub, NULL, INVALID_DEVID); + if (ret == 0) { + genInit = 1; + ret = wh_Client_RsaMakeCacheKeyAndExportPublic( + ctx, RSA_KEY_BITS, RSA_EXPONENT, &cacheId, + WH_NVM_FLAGS_USAGE_ENCRYPT | WH_NVM_FLAGS_USAGE_DECRYPT, 0, + NULL, genPub); + if (ret != 0) { + WH_ERROR_PRINT("RsaMakeCacheKeyAndExportPublic failed %d\n", + ret); + } + } + + /* Cross-check the keygen-returned public key against a separate + * ExportPublicKey call on the same cached keyId. */ + if (ret == 0) { + ret = wc_InitRsaKey_ex(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + refInit = 1; + ret = wh_Client_RsaExportPublicKey(ctx, cacheId, refPub, 0, + NULL); + if (ret != 0) { + WH_ERROR_PRINT("RsaExportPublicKey failed %d\n", ret); + } + } + } + if (ret == 0) { + genDerSz = wc_RsaKeyToPublicDer(genPub, genDer, sizeof(genDer)); + refDerSz = wc_RsaKeyToPublicDer(refPub, refDer, sizeof(refDer)); + if ((genDerSz <= 0) || (genDerSz != refDerSz) || + (memcmp(genDer, refDer, (size_t)genDerSz) != 0)) { + WH_ERROR_PRINT("keygen pubkey mismatch vs ExportPublicKey\n"); + ret = -1; + } + } + + /* Prove the returned public key is usable: encrypt with it, then + * decrypt on the HSM using the cached private key. */ + if (ret == 0) { + int encLen = wc_RsaPublicEncrypt( + (byte*)plainText, sizeof(plainText), (byte*)cipherText, + sizeof(cipherText), genPub, rng); + if (encLen < 0) { + WH_ERROR_PRINT("PublicEncrypt with keygen pub failed %d\n", + encLen); + ret = encLen; + } + else { + RsaKey hsmKey[1]; + ret = wc_InitRsaKey_ex(hsmKey, NULL, WH_CLIENT_DEVID(ctx)); + if (ret == 0) { + ret = wh_Client_RsaSetKeyId(hsmKey, cacheId); + } + if (ret == 0) { + int decLen = wc_RsaPrivateDecrypt( + (byte*)cipherText, encLen, (byte*)finalText, + sizeof(finalText), hsmKey); + if (decLen < 0) { + WH_ERROR_PRINT("HSM PrivateDecrypt failed %d\n", decLen); + ret = decLen; + } + else if (memcmp(plainText, finalText, sizeof(plainText)) != + 0) { + WH_ERROR_PRINT("keygen-pub round-trip mismatch\n"); + ret = -1; + } + } + (void)wc_FreeRsaKey(hsmKey); + } + } + + if (genInit != 0) { + (void)wc_FreeRsaKey(genPub); + } + if (refInit != 0) { + (void)wc_FreeRsaKey(refPub); + } + if (!WH_KEYID_ISERASED(cacheId)) { + (void)wh_Client_KeyEvict(ctx, cacheId); + } + + if (ret == 0) { + WH_TEST_PRINT("RSA CACHE-AND-EXPORT-PUBLIC SUCCESS\n"); + } + } + if (ret == 0) { WH_TEST_PRINT("RSA SUCCESS\n"); } diff --git a/wolfhsm/wh_client_crypto.h b/wolfhsm/wh_client_crypto.h index 7698b877b..4b0c32c3c 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -1082,6 +1082,17 @@ int wh_Client_RsaMakeCacheKey(whClientContext* ctx, whKeyId* inout_key_id, whNvmFlags flags, uint32_t label_len, uint8_t* label); +/* Generate an RSA key on the server, cache it, and return its public key in one + * round-trip (avoids a separate wh_Client_RsaExportPublicKey call). On success + * inout_key_id holds the cached keyId and pub is populated with the public key + * and associated with that keyId; the caller must still set the HSM devId on pub + * before follow-on operations route to the server. flags must not include + * WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). */ +int wh_Client_RsaMakeCacheKeyAndExportPublic(whClientContext* ctx, + uint32_t size, uint32_t e, + whKeyId* inout_key_id, whNvmFlags flags, + uint32_t label_len, const uint8_t* label, RsaKey* pub); + /* TODO: Request server to perform the RSA function */ int wh_Client_RsaFunction(whClientContext* ctx, RsaKey* key, int rsa_type, From 70567881d361a3926ae56f5aaae473fe5fc9d329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 14 Jul 2026 11:57:06 +0200 Subject: [PATCH 2/6] Return ECC public key from cached keygen Extend the cached ECC keygen path to serialize the generated public key into the keygen response body, so the client receives both the keyId and a usable public key in one round-trip instead of a follow-up wh_Client_EccExportPublicKey call. The response reuses the existing keyId + len + body layout, so there is no wire-format change and existing wh_Client_EccMakeCacheKey callers are unaffected. Add wh_Client_EccMakeCacheKeyAndExportPublic, which caches the key and returns its public key into the caller's ecc_key object, pointing that object at the cached keyId for follow-on HSM operations. Also drop the stale "TODO: RSA has the following" comment blocks in _HandleEccKeyGen that this change resolves. Add tests in both crypto test suites that cross-check the keygen-returned public key against wh_Client_EccExportPublicKey and verify an HSM-produced signature with it. --- src/wh_client_crypto.c | 59 ++++++++-- src/wh_server_crypto.c | 23 ++-- .../client-server/wh_test_crypto_ecc.c | 109 ++++++++++++++++++ test/wh_test_crypto.c | 103 +++++++++++++++++ wolfhsm/wh_client_crypto.h | 11 ++ 5 files changed, 286 insertions(+), 19 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index 7088ad2b4..3ad7f4ac8 100644 --- a/src/wh_client_crypto.c +++ b/src/wh_client_crypto.c @@ -81,7 +81,7 @@ * and the blocking wrappers wh_Client_EccMakeCacheKey/EccMakeExportKey. */ static int _EccMakeKeyRequest(whClientContext* ctx, int size, int curveId, whKeyId key_id, whNvmFlags flags, - uint16_t label_len, uint8_t* label); + uint16_t label_len, const uint8_t* label); static int _EccMakeKeyResponse(whClientContext* ctx, whKeyId* out_key_id, ecc_key* out_key); #endif /* HAVE_ECC */ @@ -1931,7 +1931,7 @@ int wh_Client_EccExportPublicKey(whClientContext* ctx, whKeyId keyId, * blocking poll) to consume the reply. */ static int _EccMakeKeyRequest(whClientContext* ctx, int size, int curveId, whKeyId key_id, whNvmFlags flags, - uint16_t label_len, uint8_t* label) + uint16_t label_len, const uint8_t* label) { whMessageCrypto_EccKeyGenRequest* req = NULL; uint8_t* dataPtr = NULL; @@ -2021,9 +2021,10 @@ static int _EccMakeKeyResponse(whClientContext* ctx, whKeyId* out_key_id, *out_key_id = key_id; } - /* If a DER blob is present (ephemeral/export keygen), deserialize it - * into the supplied key context. When called from the cache wrapper, - * out_key is NULL and the server returns an empty body. */ + /* A DER blob is present for both ephemeral/export keygen and the + * cache-and-export path. When key material was requested (out_key != + * NULL) but the server returned an empty body, treat it as an error so + * a caller of ...AndExportPublic never receives an unpopulated key. */ if (out_key != NULL) { if (res->len > 0) { uint8_t* key_der = (uint8_t*)(res + 1); @@ -2036,8 +2037,7 @@ static int _EccMakeKeyResponse(whClientContext* ctx, whKeyId* out_key_id, der_size); } else { - /* Cached key — stamp the assigned keyId */ - wh_Client_EccSetKeyId(out_key, key_id); + ret = WH_ERROR_ABORTED; } } } @@ -2105,6 +2105,51 @@ int wh_Client_EccMakeCacheKey(whClientContext* ctx, int size, int curveId, return ret; } +int wh_Client_EccMakeCacheKeyAndExportPublic(whClientContext* ctx, int size, + int curveId, + whKeyId* inout_key_id, + whNvmFlags flags, + uint16_t label_len, + const uint8_t* label, + ecc_key* pub) +{ + int ret; + whKeyId in_keyId; + whKeyId key_id = WH_KEYID_ERASED; + + if ((ctx == NULL) || (inout_key_id == NULL) || (pub == NULL)) { + return WH_ERROR_BADARGS; + } + + /* Ephemeral keygen belongs to the export pair, not the cache pair. */ + if (flags & WH_NVM_FLAGS_EPHEMERAL) { + return WH_ERROR_BADARGS; + } + + in_keyId = *inout_key_id; + ret = _EccMakeKeyRequest(ctx, size, curveId, in_keyId, flags, + label_len, label); + if (ret == WH_ERROR_OK) { + do { + ret = _EccMakeKeyResponse(ctx, &key_id, pub); + } while (ret == WH_ERROR_NOTREADY); + if (ret >= 0) { + *inout_key_id = key_id; + /* Associate the returned key with the cached keyId. The caller must + * still set the HSM devId (e.g. wc_ecc_init_ex with the HSM devId) + * before follow-on operations route to the server. */ + wh_Client_EccSetKeyId(pub, key_id); + } + else if (WH_KEYID_ISERASED(in_keyId) && !WH_KEYID_ISERASED(key_id)) { + /* The server auto-assigned and committed a key but the export + * failed. Roll back so the operation is atomic and no cache slot is + * orphaned. */ + (void)wh_Client_KeyEvict(ctx, key_id); + } + } + return ret; +} + int wh_Client_EccMakeExportKey(whClientContext* ctx, int size, int curveId, ecc_key* key) { diff --git a/src/wh_server_crypto.c b/src/wh_server_crypto.c index dd7901595..2a35d8661 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -1211,13 +1211,6 @@ static int _HandleEccKeyGen(whServerContext* ctx, uint16_t magic, int devId, key_id = WH_KEYID_ERASED; ret = wh_Crypto_EccSerializeKeyDer(key, max_size, res_out, &res_size); - /* TODO: RSA has the following, should we do the same? */ - /* - if (ret == 0) { - res.keyId = 0; - res.len = res_size; - } - */ } else { /* Must import the key into the cache and return keyid @@ -1238,11 +1231,17 @@ static int _HandleEccKeyGen(whServerContext* ctx, uint16_t magic, int devId, label_size, label); } WH_DEBUG_SERVER("CacheImport: keyId:%u, ret:%d\n", key_id, ret); - /* TODO: RSA has the following, should we do the same? */ - /* - res.keyId = WH_KEYID_ID(key_id); - res.len = 0; - */ + if (ret == 0) { + /* Best-effort: also export the public key into the response + * body so the client avoids a separate ExportPublicKey + * call. On failure leave the body empty and still return the + * cached keyId rather than orphaning the committed key. */ + int pub_ret = + wc_EccPublicKeyToDer(key, res_out, max_size, 1); + if (pub_ret > 0) { + res_size = (uint16_t)pub_ret; + } + } } } wc_ecc_free(key); diff --git a/test-refactor/client-server/wh_test_crypto_ecc.c b/test-refactor/client-server/wh_test_crypto_ecc.c index 28962a60b..6089aabac 100644 --- a/test-refactor/client-server/wh_test_crypto_ecc.c +++ b/test-refactor/client-server/wh_test_crypto_ecc.c @@ -38,6 +38,7 @@ #include "wolfssl/wolfcrypt/settings.h" #include "wolfssl/wolfcrypt/types.h" #include "wolfssl/wolfcrypt/ecc.h" +#include "wolfssl/wolfcrypt/asn.h" #include "wolfssl/wolfcrypt/random.h" #include "wolfhsm/wh_error.h" @@ -547,6 +548,113 @@ static int _whTest_CryptoEccExportPublicKey(whClientContext* ctx) return ret; } +/* One keygen call caches the private key and returns the public key. Verify + * the returned public key byte-matches wh_Client_EccExportPublicKey and that + * it verifies a signature made by the cached private key. */ +static int _whTest_CryptoEccCacheKeyAndExportPublic(whClientContext* ctx) +{ + int devId = WH_CLIENT_DEVID(ctx); + int ret = 0; + WC_RNG rng[1]; + ecc_key genPub[1]; + ecc_key refPub[1]; + ecc_key hsmKey[1]; + whKeyId keyId = WH_KEYID_ERASED; + uint8_t hash[TEST_ECC_KEYSIZE] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20}; + uint8_t sig[ECC_MAX_SIG_SIZE]; + word32 sigLen = sizeof(sig); + int verify = 0; + byte genDer[256]; + byte refDer[256]; + int genDerSz = 0; + int refDerSz = 0; + + ret = wc_InitRng_ex(rng, NULL, devId); + if (ret != 0) { + WH_ERROR_PRINT("Failed to wc_InitRng_ex %d\n", ret); + return ret; + } + + ret = wc_ecc_init_ex(genPub, NULL, INVALID_DEVID); + if (ret != 0) { + WH_ERROR_PRINT("Failed to wc_ecc_init_ex %d\n", ret); + (void)wc_FreeRng(rng); + return ret; + } + + ret = wh_Client_EccMakeCacheKeyAndExportPublic( + ctx, TEST_ECC_KEYSIZE, TEST_ECC_CURVE_ID, &keyId, + WH_NVM_FLAGS_USAGE_SIGN | WH_NVM_FLAGS_USAGE_VERIFY, 0, NULL, genPub); + if (ret != 0) { + WH_ERROR_PRINT("EccMakeCacheKeyAndExportPublic failed %d\n", ret); + } + + /* Cross-check against a separate public export of the same keyId. */ + if (ret == 0) { + ret = wc_ecc_init_ex(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wh_Client_EccExportPublicKey(ctx, keyId, refPub, 0, NULL); + if (ret != 0) { + WH_ERROR_PRINT("wh_Client_EccExportPublicKey failed %d\n", ret); + } + else { + genDerSz = + wc_EccPublicKeyToDer(genPub, genDer, sizeof(genDer), 1); + refDerSz = + wc_EccPublicKeyToDer(refPub, refDer, sizeof(refDer), 1); + if ((genDerSz <= 0) || (genDerSz != refDerSz) || + (memcmp(genDer, refDer, (size_t)genDerSz) != 0)) { + WH_ERROR_PRINT("keygen pubkey mismatch vs export\n"); + ret = -1; + } + } + wc_ecc_free(refPub); + } + } + + /* Sign on the server, verify with the keygen-returned public key. */ + if (ret == 0) { + ret = wc_ecc_init_ex(hsmKey, NULL, devId); + if (ret == 0) { + ret = wh_Client_EccSetKeyId(hsmKey, keyId); + } + if (ret == 0) { + ret = wc_ecc_sign_hash(hash, sizeof(hash), sig, &sigLen, rng, + hsmKey); + if (ret != 0) { + WH_ERROR_PRINT("HSM ECC sign failed %d\n", ret); + } + } + wc_ecc_free(hsmKey); + } + if (ret == 0) { + ret = wc_ecc_verify_hash(sig, sigLen, hash, sizeof(hash), &verify, + genPub); + if ((ret != 0) || (verify != 1)) { + WH_ERROR_PRINT("verify with keygen pub failed ret=%d verify=%d\n", + ret, verify); + if (ret == 0) { + ret = -1; + } + } + } + + wc_ecc_free(genPub); + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + (void)wc_FreeRng(rng); + + if (ret == 0) { + WH_TEST_PRINT("ECC CACHE-AND-EXPORT-PUBLIC DEVID=0x%X SUCCESS\n", devId); + } + return ret; +} + #if !defined(WOLF_CRYPTO_CB_ONLY_ECC) /* Curve sizes used by the cross-verify and async test families. */ @@ -1742,6 +1850,7 @@ int whTest_Crypto_Ecc(whClientContext* ctx) WH_TEST_RETURN_ON_FAIL(_whTest_CryptoEcc(ctx)); WH_TEST_RETURN_ON_FAIL(_whTest_CryptoEccCacheDuplicate(ctx)); WH_TEST_RETURN_ON_FAIL(_whTest_CryptoEccExportPublicKey(ctx)); + WH_TEST_RETURN_ON_FAIL(_whTest_CryptoEccCacheKeyAndExportPublic(ctx)); #if !defined(WOLF_CRYPTO_CB_ONLY_ECC) WH_TEST_RETURN_ON_FAIL(_whTest_CryptoEccCrossVerify(ctx)); WH_TEST_RETURN_ON_FAIL(_whTest_CryptoEccAsync(ctx)); diff --git a/test/wh_test_crypto.c b/test/wh_test_crypto.c index 8a7b9f7d9..f89fc4457 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -1583,6 +1583,109 @@ static int whTest_CryptoEcc(whClientContext* ctx, int devId, WC_RNG* rng) } } + /* Cache-and-export-public: a single keygen call returns the public key */ + if (ret == 0) { + whKeyId cacheId = WH_KEYID_ERASED; + ecc_key genPub[1]; + ecc_key refPub[1]; + byte genDer[256]; + byte refDer[256]; + int genDerSz = 0; + int refDerSz = 0; + int genInit = 0; + int refInit = 0; + uint8_t sig[ECC_MAX_SIG_SIZE]; + word32 sigLen = sizeof(sig); + int verify = 0; + uint8_t label[] = "ecc-cache-export"; + uint8_t readLabel[WH_NVM_LABEL_LEN] = {0}; + + ret = wc_ecc_init_ex(genPub, NULL, INVALID_DEVID); + if (ret == 0) { + genInit = 1; + /* Exercise label forwarding on the cache-and-export path. */ + ret = wh_Client_EccMakeCacheKeyAndExportPublic( + ctx, TEST_ECC_KEYSIZE, TEST_ECC_CURVE_ID, &cacheId, + WH_NVM_FLAGS_USAGE_SIGN | WH_NVM_FLAGS_USAGE_VERIFY, + sizeof(label), label, genPub); + if (ret != 0) { + WH_ERROR_PRINT("EccMakeCacheKeyAndExportPublic failed %d\n", + ret); + } + } + + /* Cross-check the keygen-returned public key against ExportPublicKey, + * and confirm the label was stored with the cached key. */ + if (ret == 0) { + ret = wc_ecc_init_ex(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + refInit = 1; + ret = wh_Client_EccExportPublicKey( + ctx, cacheId, refPub, sizeof(readLabel), readLabel); + if (ret != 0) { + WH_ERROR_PRINT("wh_Client_EccExportPublicKey failed %d\n", + ret); + } + else if (memcmp(readLabel, label, sizeof(label)) != 0) { + WH_ERROR_PRINT("keygen label not stored with cached key\n"); + ret = -1; + } + } + } + if (ret == 0) { + genDerSz = wc_EccPublicKeyToDer(genPub, genDer, sizeof(genDer), 1); + refDerSz = wc_EccPublicKeyToDer(refPub, refDer, sizeof(refDer), 1); + if ((genDerSz <= 0) || (genDerSz != refDerSz) || + (memcmp(genDer, refDer, (size_t)genDerSz) != 0)) { + WH_ERROR_PRINT("keygen pubkey mismatch vs ExportPublicKey\n"); + ret = -1; + } + } + + /* Prove usability: sign on the HSM, verify with the keygen pubkey. */ + if (ret == 0) { + ecc_key hsmKey[1]; + ret = wc_ecc_init_ex(hsmKey, NULL, devId); + if (ret == 0) { + ret = wh_Client_EccSetKeyId(hsmKey, cacheId); + } + if (ret == 0) { + ret = wc_ecc_sign_hash(hash, sizeof(hash), sig, &sigLen, rng, + hsmKey); + if (ret != 0) { + WH_ERROR_PRINT("HSM ECC sign failed %d\n", ret); + } + } + wc_ecc_free(hsmKey); + } + if (ret == 0) { + ret = wc_ecc_verify_hash(sig, sigLen, hash, sizeof(hash), &verify, + genPub); + if ((ret != 0) || (verify != 1)) { + WH_ERROR_PRINT( + "verify with keygen pub failed ret=%d verify=%d\n", ret, + verify); + if (ret == 0) { + ret = -1; + } + } + } + + if (genInit != 0) { + wc_ecc_free(genPub); + } + if (refInit != 0) { + wc_ecc_free(refPub); + } + if (!WH_KEYID_ISERASED(cacheId)) { + (void)wh_Client_KeyEvict(ctx, cacheId); + } + + if (ret == 0) { + WH_TEST_PRINT("ECC CACHE-AND-EXPORT-PUBLIC SUCCESS\n"); + } + } + if (ret == 0) { WH_TEST_PRINT("ECC SUCCESS\n"); } diff --git a/wolfhsm/wh_client_crypto.h b/wolfhsm/wh_client_crypto.h index 4b0c32c3c..d2bdd9ac9 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -511,6 +511,17 @@ int wh_Client_EccMakeCacheKey(whClientContext* ctx, whKeyId *inout_key_id, whNvmFlags flags, uint16_t label_len, uint8_t* label); +/* Generate an ECC key on the server, cache it, and return its public key in one + * round-trip (avoids a separate wh_Client_EccExportPublicKey call). On success + * inout_key_id holds the cached keyId and pub is populated with the public key + * and associated with that keyId; the caller must still set the HSM devId on pub + * before follow-on operations route to the server. flags must not include + * WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). */ +int wh_Client_EccMakeCacheKeyAndExportPublic(whClientContext* ctx, + int size, int curveId, + whKeyId *inout_key_id, whNvmFlags flags, + uint16_t label_len, const uint8_t* label, ecc_key* pub); + /** * @brief Compute an ECDH shared secret using a public and private ECC key. * From 149eff6bc4919f09b6d22fb197509a0f74b47c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 14 Jul 2026 12:02:35 +0200 Subject: [PATCH 3/6] Return Curve25519 public key from cached keygen Extend the cached Curve25519 keygen path to serialize the generated public key into the keygen response body, so the client receives both the keyId and a usable public key in one round-trip instead of a follow-up wh_Client_Curve25519ExportPublicKey call. The response reuses the existing keyId + len + body layout, so there is no wire-format change. _Curve25519MakeKey now deserializes the response body whenever it is present rather than only for EPHEMERAL keygen, so the cached path can hand the public key back through the same key object. Existing MakeCacheKey (key == NULL) and MakeExportKey callers are unaffected. Add wh_Client_Curve25519MakeCacheKeyAndExportPublic plus tests in both crypto test suites that cross-check the keygen-returned public key against wh_Client_Curve25519ExportPublicKey and prove it via an X25519 shared-secret round-trip with the cached private key. --- src/wh_client_crypto.c | 53 +++++++- src/wh_server_crypto.c | 14 ++ .../client-server/wh_test_crypto_curve25519.c | 120 ++++++++++++++++++ test/wh_test_crypto.c | 117 +++++++++++++++++ wolfhsm/wh_client_crypto.h | 11 ++ 5 files changed, 310 insertions(+), 5 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index 3ad7f4ac8..d40f205ff 100644 --- a/src/wh_client_crypto.c +++ b/src/wh_client_crypto.c @@ -3066,13 +3066,26 @@ static int _Curve25519MakeKey(whClientContext* ctx, uint16_t size, /* Update the context if provided */ if (key != NULL) { - uint16_t der_size = (uint16_t)(res->len); - uint8_t* key_der = (uint8_t*)(res + 1); - /* Set the key_id. Should be ERASED if EPHEMERAL */ + uint16_t der_size = (uint16_t)(res->len); + uint8_t* key_der = (uint8_t*)(res + 1); + const size_t hdr_sz = + sizeof(whMessageCrypto_GenericResponseHeader) + + sizeof(*res); + /* Set the key_id. ERASED for EPHEMERAL, cached id otherwise. */ wh_Client_Curve25519SetKeyId(key, key_id); - if (flags & WH_NVM_FLAGS_EPHEMERAL) { - /* Response has the exported key */ + /* Response carries the exported key (EPHEMERAL) or the public + * key (cached keygen). An empty body means the caller requested + * key material the server did not return; also reject a length + * that does not fit the received frame before deserializing. */ + if (der_size == 0) { + ret = WH_ERROR_ABORTED; + } + else if ((data_len < hdr_sz) || + (res->len > (data_len - hdr_sz))) { + ret = WH_ERROR_ABORTED; + } + else { ret = wh_Crypto_Curve25519DeserializeKey(key_der, der_size, key); WH_DEBUG_VERBOSE_HEXDUMP("[client] KeyGen export:", key_der, @@ -3096,6 +3109,36 @@ int wh_Client_Curve25519MakeCacheKey(whClientContext* ctx, uint16_t size, NULL); } +int wh_Client_Curve25519MakeCacheKeyAndExportPublic( + whClientContext* ctx, uint16_t size, whKeyId* inout_key_id, + whNvmFlags flags, const uint8_t* label, uint16_t label_len, + curve25519_key* pub) +{ + int ret; + whKeyId in_keyId; + + if ((ctx == NULL) || (inout_key_id == NULL) || (pub == NULL)) { + return WH_ERROR_BADARGS; + } + + /* Ephemeral keygen belongs to the export path, not the cache path. */ + if (flags & WH_NVM_FLAGS_EPHEMERAL) { + return WH_ERROR_BADARGS; + } + + in_keyId = *inout_key_id; + ret = _Curve25519MakeKey(ctx, size, inout_key_id, flags, label, label_len, + pub); + if ((ret < 0) && WH_KEYID_ISERASED(in_keyId) && + !WH_KEYID_ISERASED(*inout_key_id)) { + /* The server auto-assigned and committed a key but the export failed. + * Roll back so the operation is atomic and no cache slot is orphaned. */ + (void)wh_Client_KeyEvict(ctx, *inout_key_id); + *inout_key_id = WH_KEYID_ERASED; + } + return ret; +} + int wh_Client_Curve25519MakeExportKey(whClientContext* ctx, uint16_t size, curve25519_key* key) { diff --git a/src/wh_server_crypto.c b/src/wh_server_crypto.c index 2a35d8661..97a9ff6bb 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -2070,6 +2070,9 @@ static int _HandleCurve25519KeyGen(whServerContext* ctx, uint16_t magic, ret = wh_Crypto_Curve25519SerializeKey(key, out, &ser_size); } else { + uint16_t max_size = + (uint16_t)(WOLFHSM_CFG_COMM_DATA_LEN - + (out - (uint8_t*)cryptoDataOut)); ser_size = 0; /* Must import the key into the cache and return keyid */ if (WH_KEYID_ISERASED(key_id)) { @@ -2090,6 +2093,17 @@ static int _HandleCurve25519KeyGen(whServerContext* ctx, uint16_t magic, } WH_DEBUG_SERVER_VERBOSE("CacheImport: keyId:%u, ret:%d\n", key_id, ret); + if (ret == 0) { + /* Best-effort: also export the public key into the response + * body so the client avoids a separate ExportPublicKey + * call. On failure leave the body empty and still return the + * cached keyId rather than orphaning the committed key. */ + int pub_ret = + wc_Curve25519PublicKeyToDer(key, out, max_size, 1); + if (pub_ret > 0) { + ser_size = (uint16_t)pub_ret; + } + } } } wc_curve25519_free(key); diff --git a/test-refactor/client-server/wh_test_crypto_curve25519.c b/test-refactor/client-server/wh_test_crypto_curve25519.c index a72a8a4c0..17fc7a797 100644 --- a/test-refactor/client-server/wh_test_crypto_curve25519.c +++ b/test-refactor/client-server/wh_test_crypto_curve25519.c @@ -355,10 +355,130 @@ static int _whTest_CryptoCurve25519ExportPublicKey(whClientContext* ctx) return ret; } +/* One keygen call caches the private key and returns the public key. Verify + * the returned public key byte-matches wh_Client_Curve25519ExportPublicKey and + * that an X25519 shared secret round-trips against the cached private key. */ +static int _whTest_CryptoCurve25519CacheKeyAndExportPublic(whClientContext* ctx) +{ + int devId = WH_CLIENT_DEVID(ctx); + int ret = 0; + WC_RNG rng[1]; + curve25519_key genPub[1] = {0}; + curve25519_key refPub[1] = {0}; + curve25519_key hsmPriv[1] = {0}; + curve25519_key localKey[1] = {0}; + uint8_t genRaw[CURVE25519_KEYSIZE] = {0}; + uint8_t refRaw[CURVE25519_KEYSIZE] = {0}; + word32 genRawLen = sizeof(genRaw); + word32 refRawLen = sizeof(refRaw); + uint8_t sharedHsm[CURVE25519_KEYSIZE] = {0}; + uint8_t sharedLocal[CURVE25519_KEYSIZE] = {0}; + word32 secLen = 0; + whKeyId keyId = WH_KEYID_ERASED; + + ret = wc_InitRng_ex(rng, NULL, devId); + if (ret != 0) { + WH_ERROR_PRINT("Failed to wc_InitRng_ex %d\n", ret); + return ret; + } + + ret = wc_curve25519_init_ex(genPub, NULL, INVALID_DEVID); + if (ret != 0) { + WH_ERROR_PRINT("Failed to wc_curve25519_init_ex %d\n", ret); + (void)wc_FreeRng(rng); + return ret; + } + + ret = wh_Client_Curve25519MakeCacheKeyAndExportPublic( + ctx, (uint16_t)CURVE25519_KEYSIZE, &keyId, WH_NVM_FLAGS_USAGE_DERIVE, + NULL, 0, genPub); + if (ret != 0) { + WH_ERROR_PRINT("Curve25519MakeCacheKeyAndExportPublic failed %d\n", ret); + } + + /* Cross-check against a separate public export of the same keyId. */ + if (ret == 0) { + ret = wc_curve25519_init_ex(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wh_Client_Curve25519ExportPublicKey(ctx, keyId, refPub, 0, + NULL); + if (ret != 0) { + WH_ERROR_PRINT( + "wh_Client_Curve25519ExportPublicKey failed %d\n", ret); + } + } + } + if (ret == 0) { + ret = wc_curve25519_export_public(genPub, genRaw, &genRawLen); + if (ret == 0) { + ret = wc_curve25519_export_public(refPub, refRaw, &refRawLen); + } + if (ret != 0) { + WH_ERROR_PRINT("Curve25519 export_public failed %d\n", ret); + } + else if ((genRawLen != refRawLen) || + (memcmp(genRaw, refRaw, genRawLen) != 0)) { + WH_ERROR_PRINT("keygen pubkey mismatch vs export\n"); + ret = -1; + } + } + + /* Shared-secret round-trip: local priv * keygen pub vs HSM priv * local + * pub. Both must agree. */ + if (ret == 0) { + ret = wc_curve25519_init_ex(localKey, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_curve25519_make_key(rng, CURVE25519_KEYSIZE, localKey); + } + } + if (ret == 0) { + secLen = sizeof(sharedLocal); + ret = wc_curve25519_shared_secret(localKey, genPub, sharedLocal, + &secLen); + if (ret != 0) { + WH_ERROR_PRINT("Local Curve25519 shared secret failed %d\n", ret); + } + } + if (ret == 0) { + ret = wc_curve25519_init_ex(hsmPriv, NULL, devId); + if (ret == 0) { + ret = wh_Client_Curve25519SetKeyId(hsmPriv, keyId); + } + if (ret == 0) { + secLen = sizeof(sharedHsm); + ret = wc_curve25519_shared_secret(hsmPriv, localKey, sharedHsm, + &secLen); + if (ret != 0) { + WH_ERROR_PRINT("HSM Curve25519 shared secret failed %d\n", ret); + } + } + wc_curve25519_free(hsmPriv); + } + if (ret == 0 && memcmp(sharedHsm, sharedLocal, secLen) != 0) { + WH_ERROR_PRINT("Curve25519 keygen-pub shared secret mismatch\n"); + ret = -1; + } + + wc_curve25519_free(localKey); + wc_curve25519_free(refPub); + wc_curve25519_free(genPub); + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + (void)wc_FreeRng(rng); + + if (ret == 0) { + WH_TEST_PRINT("CURVE25519 CACHE-AND-EXPORT-PUBLIC DEVID=0x%X SUCCESS\n", + devId); + } + return ret; +} + int whTest_Crypto_Curve25519(whClientContext* ctx) { WH_TEST_RETURN_ON_FAIL(_whTest_CryptoCurve25519(ctx)); WH_TEST_RETURN_ON_FAIL(_whTest_CryptoCurve25519ExportPublicKey(ctx)); + WH_TEST_RETURN_ON_FAIL(_whTest_CryptoCurve25519CacheKeyAndExportPublic(ctx)); return 0; } #endif /* HAVE_CURVE25519 */ diff --git a/test/wh_test_crypto.c b/test/wh_test_crypto.c index f89fc4457..cc72c10dc 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -4422,6 +4422,115 @@ static int whTest_CryptoCurve25519ExportPublic(whClientContext* ctx, int devId, } return ret; } + +/* One keygen call caches the private key and returns the public key. Verify + * the returned public key byte-matches wh_Client_Curve25519ExportPublicKey and + * that an X25519 shared secret round-trips against the cached private key. */ +static int whTest_CryptoCurve25519CacheKeyAndExportPublic(whClientContext* ctx, + int devId, + WC_RNG* rng) +{ + int ret = 0; + whKeyId keyId = WH_KEYID_ERASED; + curve25519_key genPub[1] = {0}; + curve25519_key refPub[1] = {0}; + curve25519_key hsmPriv[1] = {0}; + curve25519_key localKey[1] = {0}; + uint8_t genRaw[CURVE25519_KEYSIZE] = {0}; + uint8_t refRaw[CURVE25519_KEYSIZE] = {0}; + word32 genRawLen = sizeof(genRaw); + word32 refRawLen = sizeof(refRaw); + uint8_t shared_hsm[CURVE25519_KEYSIZE] = {0}; + uint8_t shared_local[CURVE25519_KEYSIZE] = {0}; + word32 len = 0; + + ret = wc_curve25519_init_ex(genPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wh_Client_Curve25519MakeCacheKeyAndExportPublic( + ctx, (uint16_t)CURVE25519_KEYSIZE, &keyId, + WH_NVM_FLAGS_USAGE_DERIVE, NULL, 0, genPub); + if (ret != 0) { + WH_ERROR_PRINT( + "Curve25519MakeCacheKeyAndExportPublic failed %d\n", ret); + } + } + + /* Cross-check the keygen-returned public key against ExportPublicKey. */ + if (ret == 0) { + ret = wc_curve25519_init_ex(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wh_Client_Curve25519ExportPublicKey(ctx, keyId, refPub, 0, + NULL); + if (ret != 0) { + WH_ERROR_PRINT( + "wh_Client_Curve25519ExportPublicKey failed %d\n", ret); + } + } + } + if (ret == 0) { + ret = wc_curve25519_export_public(genPub, genRaw, &genRawLen); + if (ret == 0) { + ret = wc_curve25519_export_public(refPub, refRaw, &refRawLen); + } + if (ret != 0) { + WH_ERROR_PRINT("Curve25519 export_public failed %d\n", ret); + } + else if ((genRawLen != refRawLen) || + (memcmp(genRaw, refRaw, genRawLen) != 0)) { + WH_ERROR_PRINT("keygen pubkey mismatch vs ExportPublicKey\n"); + ret = -1; + } + } + + /* Shared-secret round-trip proves the keygen pubkey pairs with the cached + * private key. */ + if (ret == 0) { + ret = wc_curve25519_init_ex(localKey, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_curve25519_make_key(rng, CURVE25519_KEYSIZE, localKey); + } + } + if (ret == 0) { + len = sizeof(shared_local); + ret = wc_curve25519_shared_secret(localKey, genPub, shared_local, &len); + if (ret != 0) { + WH_ERROR_PRINT("Local Curve25519 shared secret failed %d\n", ret); + } + } + if (ret == 0) { + ret = wc_curve25519_init_ex(hsmPriv, NULL, devId); + if (ret == 0) { + ret = wh_Client_Curve25519SetKeyId(hsmPriv, keyId); + } + if (ret == 0) { + len = sizeof(shared_hsm); + ret = wc_curve25519_shared_secret(hsmPriv, localKey, shared_hsm, + &len); + if (ret != 0) { + WH_ERROR_PRINT("HSM Curve25519 shared secret failed %d\n", ret); + } + } + wc_curve25519_free(hsmPriv); + } + if (ret == 0) { + if (memcmp(shared_hsm, shared_local, len) != 0) { + WH_ERROR_PRINT("Curve25519 keygen-pub shared secret mismatch\n"); + ret = -1; + } + } + + wc_curve25519_free(localKey); + wc_curve25519_free(refPub); + wc_curve25519_free(genPub); + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + + if (ret == 0) { + WH_TEST_PRINT("CURVE25519 CACHE-AND-EXPORT-PUBLIC SUCCESS\n"); + } + return ret; +} #endif /* HAVE_CURVE25519 */ #ifndef NO_SHA256 @@ -16500,6 +16609,14 @@ int whTest_CryptoClientConfig(whClientConfig* config) WH_ERROR_PRINT("Curve25519 export-public test failed: %d\n", ret); } } + if (ret == 0) { + ret = whTest_CryptoCurve25519CacheKeyAndExportPublic( + client, WH_CLIENT_DEVID(client), rng); + if (ret != 0) { + WH_ERROR_PRINT( + "Curve25519 cache-and-export-public test failed: %d\n", ret); + } + } #endif /* HAVE_CURVE25519 */ #ifndef NO_SHA256 diff --git a/wolfhsm/wh_client_crypto.h b/wolfhsm/wh_client_crypto.h index d2bdd9ac9..e83f82c55 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -280,6 +280,17 @@ int wh_Client_Curve25519MakeCacheKey(whClientContext* ctx, whKeyId *inout_key_id, whNvmFlags flags, const uint8_t* label, uint16_t label_len); +/* Generate a Curve25519 key on the server, cache it, and return its public key + * in one round-trip (avoids a separate wh_Client_Curve25519ExportPublicKey + * call). On success inout_key_id holds the cached keyId and pub is populated + * with the public key and associated with that keyId; the caller must still set + * the HSM devId on pub before follow-on operations route to the server. flags + * must not include WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). */ +int wh_Client_Curve25519MakeCacheKeyAndExportPublic(whClientContext* ctx, + uint16_t size, + whKeyId *inout_key_id, whNvmFlags flags, + const uint8_t* label, uint16_t label_len, curve25519_key* pub); + /** * @brief Generate a Curve25519 key by the server and export to the client * From 73fe7ef6080d48a41100f245ecac79b3c4d71d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 14 Jul 2026 12:07:53 +0200 Subject: [PATCH 4/6] Return Ed25519 public key from cached keygen Extend the cached Ed25519 keygen path to serialize the generated public key into the keygen response body, so the client receives both the keyId and a usable public key in one round-trip instead of a follow-up wh_Client_Ed25519ExportPublicKey call. The response reuses the existing keyId + outSz + body layout, so there is no wire-format change. _Ed25519MakeKey now deserializes the response body whenever it is present rather than only for EPHEMERAL keygen, so the cached path can hand the public key back through the same key object. Existing MakeCacheKey (key == NULL) and MakeExportKey callers are unaffected. Add wh_Client_Ed25519MakeCacheKeyAndExportPublic plus tests in both crypto test suites that cross-check the keygen-returned public key against wh_Client_Ed25519ExportPublicKey and verify an HSM-produced signature with it. --- src/wh_client_crypto.c | 43 +++++++- src/wh_server_crypto.c | 11 ++ .../client-server/wh_test_crypto_ed25519.c | 98 +++++++++++++++++ test/wh_test_crypto.c | 101 ++++++++++++++++++ wolfhsm/wh_client_crypto.h | 16 +++ 5 files changed, 266 insertions(+), 3 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index d40f205ff..b1d331dc8 100644 --- a/src/wh_client_crypto.c +++ b/src/wh_client_crypto.c @@ -3492,8 +3492,8 @@ int wh_Client_Ed25519ExportPublicKey(whClientContext* ctx, whKeyId keyId, } static int _Ed25519MakeKey(whClientContext* ctx, whKeyId* inout_key_id, - whNvmFlags flags, uint16_t label_len, uint8_t* label, - ed25519_key* key) + whNvmFlags flags, uint16_t label_len, + const uint8_t* label, ed25519_key* key) { int ret = WH_ERROR_OK; whKeyId key_id = WH_KEYID_ERASED; @@ -3564,7 +3564,10 @@ static int _Ed25519MakeKey(whClientContext* ctx, whKeyId* inout_key_id, } if (key != NULL) { wh_Client_Ed25519SetKeyId(key, key_id); - if (flags & WH_NVM_FLAGS_EPHEMERAL) { + /* Response carries the exported key (EPHEMERAL) or the public key + * (cached keygen). An empty body means the caller requested key + * material the server did not return. */ + if (res->outSz > 0) { uint8_t* out = (uint8_t*)(res + 1); uint16_t outSz = (uint16_t)res->outSz; if (outSz + sizeof(whMessageCrypto_GenericResponseHeader) + @@ -3574,6 +3577,9 @@ static int _Ed25519MakeKey(whClientContext* ctx, whKeyId* inout_key_id, } ret = wh_Crypto_Ed25519DeserializeKeyDer(out, outSz, key); } + else { + ret = WH_ERROR_ABORTED; + } } } @@ -3600,6 +3606,37 @@ int wh_Client_Ed25519MakeCacheKey(whClientContext* ctx, whKeyId* inout_key_id, return _Ed25519MakeKey(ctx, inout_key_id, flags, label_len, label, NULL); } +int wh_Client_Ed25519MakeCacheKeyAndExportPublic(whClientContext* ctx, + whKeyId* inout_key_id, + whNvmFlags flags, + uint16_t label_len, + const uint8_t* label, + ed25519_key* pub) +{ + int ret; + whKeyId in_keyId; + + if ((ctx == NULL) || (inout_key_id == NULL) || (pub == NULL)) { + return WH_ERROR_BADARGS; + } + + /* Ephemeral keygen belongs to the export path, not the cache path. */ + if (flags & WH_NVM_FLAGS_EPHEMERAL) { + return WH_ERROR_BADARGS; + } + + in_keyId = *inout_key_id; + ret = _Ed25519MakeKey(ctx, inout_key_id, flags, label_len, label, pub); + if ((ret < 0) && WH_KEYID_ISERASED(in_keyId) && + !WH_KEYID_ISERASED(*inout_key_id)) { + /* The server auto-assigned and committed a key but the export failed. + * Roll back so the operation is atomic and no cache slot is orphaned. */ + (void)wh_Client_KeyEvict(ctx, *inout_key_id); + *inout_key_id = WH_KEYID_ERASED; + } + return ret; +} + int wh_Client_Ed25519Sign(whClientContext* ctx, ed25519_key* key, const uint8_t* msg, uint32_t msgLen, uint8_t type, const uint8_t* context, uint32_t contextLen, diff --git a/src/wh_server_crypto.c b/src/wh_server_crypto.c index 97a9ff6bb..58a15df4c 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -2320,6 +2320,17 @@ static int _HandleEd25519KeyGen(whServerContext* ctx, uint16_t magic, int devId, ret = wh_Server_CacheImportEd25519Key( ctx, key, key_id, flags, label_size, label); } + if (ret == 0) { + /* Best-effort: also export the public key into the response + * body so the client avoids a separate ExportPublicKey + * call. On failure leave the body empty and still return the + * cached keyId rather than orphaning the committed key. */ + int pub_ret = + wc_Ed25519PublicKeyToDer(key, res_out, max_size, 1); + if (pub_ret > 0) { + ser_size = (uint16_t)pub_ret; + } + } } } wc_ed25519_free(key); diff --git a/test-refactor/client-server/wh_test_crypto_ed25519.c b/test-refactor/client-server/wh_test_crypto_ed25519.c index 63ab0937a..9b2bf4e33 100644 --- a/test-refactor/client-server/wh_test_crypto_ed25519.c +++ b/test-refactor/client-server/wh_test_crypto_ed25519.c @@ -37,6 +37,7 @@ #include "wolfssl/wolfcrypt/settings.h" #include "wolfssl/wolfcrypt/types.h" #include "wolfssl/wolfcrypt/ed25519.h" +#include "wolfssl/wolfcrypt/asn.h" #include "wolfssl/wolfcrypt/random.h" #include "wolfhsm/wh_error.h" @@ -561,6 +562,102 @@ static int _whTest_CryptoEd25519ExportPublicKey(whClientContext* ctx) return ret; } +/* One keygen call caches the private key and returns the public key. Verify + * the returned public key byte-matches wh_Client_Ed25519ExportPublicKey and + * that it verifies a signature made by the cached private key. */ +static int _whTest_CryptoEd25519CacheKeyAndExportPublic(whClientContext* ctx) +{ + int devId = WH_CLIENT_DEVID(ctx); + int ret = 0; + ed25519_key genPub[1] = {0}; + ed25519_key refPub[1] = {0}; + ed25519_key hsmKey[1] = {0}; + whKeyId keyId = WH_KEYID_ERASED; + byte msg[] = "Ed25519 cache-export-public message"; + byte sig[ED25519_SIG_SIZE]; + uint32_t sigSz = sizeof(sig); + int verified = 0; + byte genDer[128]; + byte refDer[128]; + int genDerSz = 0; + int refDerSz = 0; + + ret = wc_ed25519_init_ex(genPub, NULL, INVALID_DEVID); + if (ret != 0) { + WH_ERROR_PRINT("Failed to wc_ed25519_init_ex %d\n", ret); + return ret; + } + + ret = wh_Client_Ed25519MakeCacheKeyAndExportPublic( + ctx, &keyId, WH_NVM_FLAGS_USAGE_SIGN | WH_NVM_FLAGS_USAGE_VERIFY, 0, + NULL, genPub); + if (ret != 0) { + WH_ERROR_PRINT("Ed25519MakeCacheKeyAndExportPublic failed %d\n", ret); + } + + /* Cross-check against a separate public export of the same keyId. */ + if (ret == 0) { + ret = wc_ed25519_init_ex(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wh_Client_Ed25519ExportPublicKey(ctx, keyId, refPub, 0, NULL); + if (ret != 0) { + WH_ERROR_PRINT("wh_Client_Ed25519ExportPublicKey failed %d\n", + ret); + } + else { + genDerSz = + wc_Ed25519PublicKeyToDer(genPub, genDer, sizeof(genDer), 1); + refDerSz = + wc_Ed25519PublicKeyToDer(refPub, refDer, sizeof(refDer), 1); + if ((genDerSz <= 0) || (genDerSz != refDerSz) || + (memcmp(genDer, refDer, (size_t)genDerSz) != 0)) { + WH_ERROR_PRINT("keygen pubkey mismatch vs export\n"); + ret = -1; + } + } + wc_ed25519_free(refPub); + } + } + + /* Sign on the server, verify with the keygen-returned public key. */ + if (ret == 0) { + ret = wc_ed25519_init_ex(hsmKey, NULL, devId); + if (ret == 0) { + ret = wh_Client_Ed25519SetKeyId(hsmKey, keyId); + } + if (ret == 0) { + ret = wh_Client_Ed25519Sign(ctx, hsmKey, msg, (uint32_t)sizeof(msg), + (uint8_t)Ed25519, NULL, 0, sig, &sigSz); + if (ret != 0) { + WH_ERROR_PRINT("HSM Ed25519 sign failed %d\n", ret); + } + } + wc_ed25519_free(hsmKey); + } + if (ret == 0) { + ret = wc_ed25519_verify_msg(sig, sigSz, msg, (word32)sizeof(msg), + &verified, genPub); + if ((ret != 0) || (verified != 1)) { + WH_ERROR_PRINT("verify with keygen pub failed ret=%d verify=%d\n", + ret, verified); + if (ret == 0) { + ret = -1; + } + } + } + + wc_ed25519_free(genPub); + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + + if (ret == 0) { + WH_TEST_PRINT("Ed25519 CACHE-AND-EXPORT-PUBLIC DEVID=0x%X SUCCESS\n", + devId); + } + return ret; +} + /* Exercises wh_Client_Ed25519Sign with an undersized output buffer: must * return WH_ERROR_BUFFER_SIZE and report the required signature length. */ @@ -644,6 +741,7 @@ int whTest_Crypto_Ed25519(whClientContext* ctx) WH_TEST_RETURN_ON_FAIL(_whTest_CryptoEd25519Dma(ctx)); #endif WH_TEST_RETURN_ON_FAIL(_whTest_CryptoEd25519ExportPublicKey(ctx)); + WH_TEST_RETURN_ON_FAIL(_whTest_CryptoEd25519CacheKeyAndExportPublic(ctx)); WH_TEST_RETURN_ON_FAIL(_whTest_CryptoEd25519BufferTooSmall(ctx)); return 0; } diff --git a/test/wh_test_crypto.c b/test/wh_test_crypto.c index cc72c10dc..53a59c814 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -3723,6 +3723,99 @@ static int whTest_CryptoEd25519ExportPublic(whClientContext* ctx, int devId, return ret; } +/* One keygen call caches the private key and returns the public key. Verify + * the returned public key byte-matches wh_Client_Ed25519ExportPublicKey and + * that it verifies a signature made by the cached private key. */ +static int whTest_CryptoEd25519CacheKeyAndExportPublic(whClientContext* ctx, + int devId, WC_RNG* rng) +{ + int ret = 0; + whKeyId keyId = WH_KEYID_ERASED; + ed25519_key genPub[1] = {0}; + ed25519_key refPub[1] = {0}; + ed25519_key hsmKey[1] = {0}; + byte msg[] = "Ed25519 cache-export-public message"; + byte sig[ED25519_SIG_SIZE]; + uint32_t sigSz = sizeof(sig); + int verified = 0; + byte genDer[128]; + byte refDer[128]; + int genDerSz = 0; + int refDerSz = 0; + (void)devId; + + ret = wc_ed25519_init_ex(genPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wh_Client_Ed25519MakeCacheKeyAndExportPublic( + ctx, &keyId, WH_NVM_FLAGS_USAGE_SIGN | WH_NVM_FLAGS_USAGE_VERIFY, 0, + NULL, genPub); + if (ret != 0) { + WH_ERROR_PRINT("Ed25519MakeCacheKeyAndExportPublic failed %d\n", + ret); + } + } + + /* Cross-check the keygen-returned public key against ExportPublicKey. */ + if (ret == 0) { + ret = wc_ed25519_init_ex(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wh_Client_Ed25519ExportPublicKey(ctx, keyId, refPub, 0, NULL); + if (ret != 0) { + WH_ERROR_PRINT("wh_Client_Ed25519ExportPublicKey failed %d\n", + ret); + } + } + } + if (ret == 0) { + genDerSz = wc_Ed25519PublicKeyToDer(genPub, genDer, sizeof(genDer), 1); + refDerSz = wc_Ed25519PublicKeyToDer(refPub, refDer, sizeof(refDer), 1); + if ((genDerSz <= 0) || (genDerSz != refDerSz) || + (memcmp(genDer, refDer, (size_t)genDerSz) != 0)) { + WH_ERROR_PRINT("keygen pubkey mismatch vs ExportPublicKey\n"); + ret = -1; + } + } + + /* Prove usability: sign on the HSM, verify with the keygen pubkey. */ + if (ret == 0) { + ret = wc_ed25519_init_ex(hsmKey, NULL, WH_CLIENT_DEVID(ctx)); + if (ret == 0) { + ret = wh_Client_Ed25519SetKeyId(hsmKey, keyId); + } + if (ret == 0) { + ret = wh_Client_Ed25519Sign(ctx, hsmKey, msg, (uint32_t)sizeof(msg), + (uint8_t)Ed25519, NULL, 0, sig, &sigSz); + if (ret != 0) { + WH_ERROR_PRINT("HSM Ed25519 sign failed %d\n", ret); + } + } + wc_ed25519_free(hsmKey); + } + if (ret == 0) { + ret = wc_ed25519_verify_msg(sig, sigSz, msg, (word32)sizeof(msg), + &verified, genPub); + if ((ret != 0) || (verified != 1)) { + WH_ERROR_PRINT("verify with keygen pub failed ret=%d verify=%d\n", + ret, verified); + if (ret == 0) { + ret = -1; + } + } + } + + wc_ed25519_free(refPub); + wc_ed25519_free(genPub); + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + + (void)rng; + if (ret == 0) { + WH_TEST_PRINT("Ed25519 CACHE-AND-EXPORT-PUBLIC SUCCESS\n"); + } + return ret; +} + #ifdef WOLFHSM_CFG_DMA static int whTest_CryptoEd25519Dma(whClientContext* ctx, int devId, WC_RNG* rng) { @@ -16577,6 +16670,14 @@ int whTest_CryptoClientConfig(whClientConfig* config) WH_ERROR_PRINT("Ed25519 export-public test failed: %d\n", ret); } } + if (ret == 0) { + ret = whTest_CryptoEd25519CacheKeyAndExportPublic( + client, WH_CLIENT_DEVID(client), rng); + if (ret != 0) { + WH_ERROR_PRINT( + "Ed25519 cache-and-export-public test failed: %d\n", ret); + } + } #ifdef WOLFHSM_CFG_DMA (void)wh_Client_SetDmaMode(client, 1); if (ret == 0) { diff --git a/wolfhsm/wh_client_crypto.h b/wolfhsm/wh_client_crypto.h index e83f82c55..930629385 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -960,6 +960,22 @@ int wh_Client_Ed25519MakeCacheKey(whClientContext* ctx, whKeyId* inout_key_id, whNvmFlags flags, uint16_t label_len, uint8_t* label); +/** + * @brief Create a new Ed25519 key on the server, cache it, and return its + * public key in one round-trip (avoids a separate + * wh_Client_Ed25519ExportPublicKey call). On success inout_key_id holds + * the cached keyId and pub is populated with the public key and + * associated with that keyId; the caller must still set the HSM devId on + * pub before follow-on operations route to the server. flags must not + * include WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + */ +int wh_Client_Ed25519MakeCacheKeyAndExportPublic(whClientContext* ctx, + whKeyId* inout_key_id, + whNvmFlags flags, + uint16_t label_len, + const uint8_t* label, + ed25519_key* pub); + /** * @brief Sign a message using an Ed25519 key on the server. */ From db5ed41b5bc6372f71b326e05bb866d213db6fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 14 Jul 2026 12:26:41 +0200 Subject: [PATCH 5/6] Return ML-DSA public key from cached keygen Extend both the non-DMA and DMA cached ML-DSA keygen paths to return the generated public key to the client in one round-trip instead of a follow-up wh_Client_MlDsaExportPublicKey call. Non-DMA: the cache branch of _HandleMlDsaKeyGen serializes the public key into the response body (reusing the existing keyId + len + body layout). DMA: the cache branch of _HandleMlDsaKeyGenDma streams the public key back through the client's existing key DMA buffer and reports its size in keySize. Both are guarded by WOLFSSL_MLDSA_PUBLIC_KEY, matching the keystore public export path. No wire-format changes. _MlDsaMakeKey and _MlDsaMakeKeyDma now deserialize the response body/buffer whenever it is present rather than only for EPHEMERAL keygen, so the cached paths can hand the public key back through the same key object. Existing MakeCacheKey (key == NULL) and MakeExportKey callers are unaffected. Add wh_Client_MlDsaMakeCacheKeyAndExportPublic and wh_Client_MlDsaMakeCacheKeyDma, plus non-DMA and DMA tests in both crypto test suites that cross-check the keygen-returned public key against wh_Client_MlDsaExportPublicKey[Dma] and verify an HSM-produced signature with it. --- src/wh_client_crypto.c | 127 ++++++++-- src/wh_server_crypto.c | 45 +++- .../client-server/wh_test_crypto_mldsa.c | 223 +++++++++++++++++ test/wh_test_crypto.c | 229 ++++++++++++++++++ wolfhsm/wh_client_crypto.h | 32 +++ 5 files changed, 633 insertions(+), 23 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index b1d331dc8..d87ca3211 100644 --- a/src/wh_client_crypto.c +++ b/src/wh_client_crypto.c @@ -127,12 +127,12 @@ static int _CmacKdfMakeKey(whClientContext* ctx, whKeyId saltKeyId, /* Make a ML-DSA key on the server based on the flags */ static int _MlDsaMakeKey(whClientContext* ctx, int size, int level, whKeyId* inout_key_id, whNvmFlags flags, - uint16_t label_len, uint8_t* label, wc_MlDsaKey* key); + uint16_t label_len, const uint8_t* label, wc_MlDsaKey* key); #ifdef WOLFHSM_CFG_DMA static int _MlDsaMakeKeyDma(whClientContext* ctx, int level, whKeyId* inout_key_id, whNvmFlags flags, - uint16_t label_len, uint8_t* label, wc_MlDsaKey* key); + uint16_t label_len, const uint8_t* label, wc_MlDsaKey* key); #endif /* WOLFHSM_CFG_DMA */ #endif /* WOLFSSL_HAVE_MLDSA */ @@ -9571,7 +9571,7 @@ int wh_Client_MlDsaExportPublicKey(whClientContext* ctx, whKeyId keyId, static int _MlDsaMakeKey(whClientContext* ctx, int size, int level, whKeyId* inout_key_id, whNvmFlags flags, - uint16_t label_len, uint8_t* label, wc_MlDsaKey* key) + uint16_t label_len, const uint8_t* label, wc_MlDsaKey* key) { int ret = WH_ERROR_OK; whKeyId key_id = WH_KEYID_ERASED; @@ -9654,13 +9654,28 @@ static int _MlDsaMakeKey(whClientContext* ctx, int size, int level, /* Update the context if provided */ if (key != NULL) { - uint16_t der_size = (uint16_t)(res->len); - /* Set the key_id. Should be ERASED if EPHEMERAL */ + uint16_t der_size = (uint16_t)(res->len); + const size_t hdr_sz = + sizeof(whMessageCrypto_GenericResponseHeader) + + sizeof(*res); + /* Set the key_id. ERASED for EPHEMERAL, cached id + * otherwise. */ wh_Client_MlDsaSetKeyId(key, key_id); - if (flags & WH_NVM_FLAGS_EPHEMERAL) { + /* Response carries the exported key (EPHEMERAL) or + * the public key (cached keygen). An empty body + * means the caller requested key material the server + * did not return; also reject a length that does not + * fit the received frame before deserializing. */ + if (der_size == 0) { + ret = WH_ERROR_ABORTED; + } + else if ((res_len < hdr_sz) || + (res->len > (res_len - hdr_sz))) { + ret = WH_ERROR_ABORTED; + } + else { uint8_t* key_der = (uint8_t*)(res + 1); - /* Response has the exported key */ ret = wh_Crypto_MlDsaDeserializeKeyDer( key_der, der_size, key); WH_DEBUG_VERBOSE_HEXDUMP( @@ -9691,6 +9706,41 @@ int wh_Client_MlDsaMakeCacheKey(whClientContext* ctx, int size, int level, label, NULL); } +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY +int wh_Client_MlDsaMakeCacheKeyAndExportPublic(whClientContext* ctx, int size, + int level, + whKeyId* inout_key_id, + whNvmFlags flags, + uint16_t label_len, + const uint8_t* label, + wc_MlDsaKey* pub) +{ + int ret; + whKeyId in_keyId; + + if ((ctx == NULL) || (inout_key_id == NULL) || (pub == NULL)) { + return WH_ERROR_BADARGS; + } + + /* Ephemeral keygen belongs to the export path, not the cache path. */ + if (flags & WH_NVM_FLAGS_EPHEMERAL) { + return WH_ERROR_BADARGS; + } + + in_keyId = *inout_key_id; + ret = _MlDsaMakeKey(ctx, size, level, inout_key_id, flags, label_len, label, + pub); + if ((ret < 0) && WH_KEYID_ISERASED(in_keyId) && + !WH_KEYID_ISERASED(*inout_key_id)) { + /* The server auto-assigned and committed a key but the export failed. + * Roll back so the operation is atomic and no cache slot is orphaned. */ + (void)wh_Client_KeyEvict(ctx, *inout_key_id); + *inout_key_id = WH_KEYID_ERASED; + } + return ret; +} +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ + int wh_Client_MlDsaMakeExportKey(whClientContext* ctx, int level, int size, wc_MlDsaKey* key) { @@ -10077,7 +10127,7 @@ int wh_Client_MlDsaExportPublicKeyDma(whClientContext* ctx, whKeyId keyId, static int _MlDsaMakeKeyDma(whClientContext* ctx, int level, whKeyId* inout_key_id, whNvmFlags flags, - uint16_t label_len, uint8_t* label, wc_MlDsaKey* key) + uint16_t label_len, const uint8_t* label, wc_MlDsaKey* key) { int ret = WH_ERROR_OK; whKeyId key_id = WH_KEYID_ERASED; @@ -10182,20 +10232,25 @@ static int _MlDsaMakeKeyDma(whClientContext* ctx, int level, /* Update the context if provided */ if (key != NULL) { - /* Set the key_id. Should be ERASED if EPHEMERAL */ + /* Set the key_id. ERASED for EPHEMERAL, cached id + * otherwise. */ wh_Client_MlDsaSetKeyId(key, key_id); - if (flags & WH_NVM_FLAGS_EPHEMERAL) { - /* Bound the server-reported key size to the DMA - * buffer capacity before deserializing */ - if (res->keySize > sizeof(buffer)) { - ret = WH_ERROR_ABORTED; - } - else { - /* Response has the exported key */ - ret = wh_Crypto_MlDsaDeserializeKeyDer( - buffer, res->keySize, key); - } + /* buffer holds the exported key (EPHEMERAL) or the public + * key (cached keygen); keySize bounds the DMA write. An + * empty result means the caller requested key material the + * server did not return. */ + if (res->keySize == 0) { + ret = WH_ERROR_ABORTED; + } + /* Bound the server-reported key size to the DMA buffer + * capacity before deserializing */ + else if (res->keySize > sizeof(buffer)) { + ret = WH_ERROR_ABORTED; + } + else { + ret = wh_Crypto_MlDsaDeserializeKeyDer( + buffer, res->keySize, key); } } } @@ -10219,6 +10274,38 @@ int wh_Client_MlDsaMakeExportKeyDma(whClientContext* ctx, int level, key); } +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY +int wh_Client_MlDsaMakeCacheKeyDma(whClientContext* ctx, int level, + whKeyId* inout_key_id, whNvmFlags flags, + uint16_t label_len, const uint8_t* label, + wc_MlDsaKey* pub) +{ + int ret; + whKeyId in_keyId; + + if ((ctx == NULL) || (inout_key_id == NULL) || (pub == NULL)) { + return WH_ERROR_BADARGS; + } + + /* Ephemeral keygen belongs to the export path, not the cache path. */ + if (flags & WH_NVM_FLAGS_EPHEMERAL) { + return WH_ERROR_BADARGS; + } + + in_keyId = *inout_key_id; + ret = _MlDsaMakeKeyDma(ctx, level, inout_key_id, flags, label_len, label, + pub); + if ((ret < 0) && WH_KEYID_ISERASED(in_keyId) && + !WH_KEYID_ISERASED(*inout_key_id)) { + /* The server auto-assigned and committed a key but the export failed. + * Roll back so the operation is atomic and no cache slot is orphaned. */ + (void)wh_Client_KeyEvict(ctx, *inout_key_id); + *inout_key_id = WH_KEYID_ERASED; + } + return ret; +} +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ + int wh_Client_MlDsaSignDma(whClientContext* ctx, const byte* in, word32 in_len, byte* out, word32* out_len, wc_MlDsaKey* key, diff --git a/src/wh_server_crypto.c b/src/wh_server_crypto.c index 58a15df4c..2ecd50726 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -4835,6 +4835,20 @@ static int _HandleMlDsaKeyGen(whServerContext* ctx, uint16_t magic, int devId, } WH_DEBUG_SERVER("CacheImport: keyId:%u, ret:%d\n", key_id, ret); +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY + if (ret == 0) { + /* Best-effort: also export the public key into the + * response body so the client avoids a separate + * ExportPublicKey call. On failure leave the body + * empty and still return the cached keyId rather + * than orphaning the committed key. */ + int pub_ret = wc_MlDsaKey_PublicKeyToDer( + key, res_out, max_size, 1); + if (pub_ret > 0) { + res_size = (uint16_t)pub_ret; + } + } +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ } } } @@ -6492,11 +6506,36 @@ static int _HandleMlDsaKeyGenDma(whServerContext* ctx, uint16_t magic, req.label); WH_DEBUG_SERVER("CacheImport: keyId:%u, ret:%d\n", keyId, ret); - if (ret == 0) { - res.keyId = wh_KeyId_TranslateToClient(keyId); - res.keySize = keySize; + } +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY + /* Best-effort: stream the public key back through the + * client's DMA buffer so it gets the pubkey without a + * separate ExportPublicKey call. On failure leave + * keySize 0 and still return the cached keyId rather + * than orphaning the committed key. */ + if (ret == 0) { + int rc = wh_Server_DmaProcessClientAddress( + ctx, req.key.addr, &clientOutAddr, req.key.sz, + WH_DMA_OPER_CLIENT_WRITE_PRE, + (whServerDmaFlags){0}); + if (rc == 0) { + int pub_ret = wc_MlDsaKey_PublicKeyToDer( + key, (byte*)clientOutAddr, + (word32)req.key.sz, 1); + if (pub_ret > 0) { + keySize = (uint16_t)pub_ret; + } + (void)wh_Server_DmaProcessClientAddress( + ctx, req.key.addr, &clientOutAddr, keySize, + WH_DMA_OPER_CLIENT_WRITE_POST, + (whServerDmaFlags){0}); } } +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ + if (ret == 0) { + res.keyId = wh_KeyId_TranslateToClient(keyId); + res.keySize = keySize; + } } } } diff --git a/test-refactor/client-server/wh_test_crypto_mldsa.c b/test-refactor/client-server/wh_test_crypto_mldsa.c index 4b69738bd..330d5f91d 100644 --- a/test-refactor/client-server/wh_test_crypto_mldsa.c +++ b/test-refactor/client-server/wh_test_crypto_mldsa.c @@ -237,6 +237,114 @@ static int _whTest_CryptoMlDsaExportPublicKey(whClientContext* ctx) return ret; } +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY +/* One keygen call caches the private key and returns the public key. Verify + * the returned public key byte-matches wh_Client_MlDsaExportPublicKey and that + * it verifies a signature made by the cached private key. */ +static int _whTest_CryptoMlDsaCacheKeyAndExportPublic(whClientContext* ctx) +{ + int devId = WH_CLIENT_DEVID(ctx); + int ret = 0; + MlDsaKey genPub[1] = {0}; + MlDsaKey refPub[1] = {0}; + MlDsaKey hsmKey[1] = {0}; + whKeyId keyId = WH_KEYID_ERASED; + byte msg[] = "ML-DSA cache-export-public message"; + byte sig[DILITHIUM_MAX_SIG_SIZE]; + word32 sigLen = sizeof(sig); + int verified = 0; + byte genDer[DILITHIUM_MAX_PUB_KEY_DER_SIZE]; + byte refDer[DILITHIUM_MAX_PUB_KEY_DER_SIZE]; + int genDerSz = 0; + int refDerSz = 0; + + ret = wc_MlDsaKey_Init(genPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(genPub, WC_ML_DSA_44); + } + if (ret != 0) { + WH_ERROR_PRINT("Failed to init/setparams ML-DSA key %d\n", ret); + return ret; + } + + ret = wh_Client_MlDsaMakeCacheKeyAndExportPublic( + ctx, 0, WC_ML_DSA_44, &keyId, + WH_NVM_FLAGS_USAGE_SIGN | WH_NVM_FLAGS_USAGE_VERIFY, 0, NULL, genPub); + if (ret != 0) { + WH_ERROR_PRINT("MlDsaMakeCacheKeyAndExportPublic failed %d\n", ret); + } + + /* Cross-check against a separate public export of the same keyId. */ + if (ret == 0) { + ret = wc_MlDsaKey_Init(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(refPub, WC_ML_DSA_44); + } + if (ret == 0) { + ret = wh_Client_MlDsaExportPublicKey(ctx, keyId, refPub, 0, NULL); + if (ret != 0) { + WH_ERROR_PRINT("wh_Client_MlDsaExportPublicKey failed %d\n", + ret); + } + else { + genDerSz = wc_MlDsaKey_PublicKeyToDer(genPub, genDer, + sizeof(genDer), 1); + refDerSz = wc_MlDsaKey_PublicKeyToDer(refPub, refDer, + sizeof(refDer), 1); + if ((genDerSz <= 0) || (genDerSz != refDerSz) || + (memcmp(genDer, refDer, (size_t)genDerSz) != 0)) { + WH_ERROR_PRINT("keygen pubkey mismatch vs export\n"); + ret = -1; + } + } + wc_MlDsaKey_Free(refPub); + } + } + + /* Sign on the server, verify with the keygen-returned public key. */ + if (ret == 0) { + ret = wc_MlDsaKey_Init(hsmKey, NULL, devId); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(hsmKey, WC_ML_DSA_44); + } + if (ret == 0) { + ret = wh_Client_MlDsaSetKeyId(hsmKey, keyId); + } + if (ret == 0) { + ret = wh_Client_MlDsaSign(ctx, msg, sizeof(msg), sig, &sigLen, + hsmKey, NULL, 0, WC_HASH_TYPE_NONE); + if (ret != 0) { + WH_ERROR_PRINT("HSM ML-DSA sign failed %d\n", ret); + } + } + wc_MlDsaKey_Free(hsmKey); + } + if (ret == 0) { + ret = wh_Client_MlDsaVerify(ctx, sig, sigLen, msg, sizeof(msg), + &verified, genPub, NULL, 0, + WC_HASH_TYPE_NONE); + if ((ret != 0) || (verified != 1)) { + WH_ERROR_PRINT("verify with keygen pub failed ret=%d verify=%d\n", + ret, verified); + if (ret == 0) { + ret = -1; + } + } + } + + wc_MlDsaKey_Free(genPub); + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + + if (ret == 0) { + WH_TEST_PRINT("ML-DSA CACHE-AND-EXPORT-PUBLIC DEVID=0x%X SUCCESS\n", + devId); + } + return ret; +} +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ + #ifdef WOLFHSM_CFG_DMA static int _whTest_CryptoMlDsaDmaClient(whClientContext* ctx) { @@ -506,6 +614,115 @@ static int _whTest_CryptoMlDsaExportPublicKeyDma(whClientContext* ctx) } return ret; } + +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY +/* DMA variant: one keygen call caches the private key and streams the public + * key back through the client's DMA buffer. Verify it byte-matches + * wh_Client_MlDsaExportPublicKeyDma and that it verifies an HSM signature. */ +static int _whTest_CryptoMlDsaCacheKeyAndExportPublicDma(whClientContext* ctx) +{ + int devId = WH_CLIENT_DEVID(ctx); + int ret = 0; + MlDsaKey genPub[1] = {0}; + MlDsaKey refPub[1] = {0}; + MlDsaKey hsmKey[1] = {0}; + whKeyId keyId = WH_KEYID_ERASED; + byte msg[] = "ML-DSA DMA cache-export-public message"; + byte sig[DILITHIUM_MAX_SIG_SIZE]; + word32 sigLen = sizeof(sig); + int verified = 0; + byte genDer[DILITHIUM_MAX_PUB_KEY_DER_SIZE]; + byte refDer[DILITHIUM_MAX_PUB_KEY_DER_SIZE]; + int genDerSz = 0; + int refDerSz = 0; + + ret = wc_MlDsaKey_Init(genPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(genPub, WC_ML_DSA_44); + } + if (ret != 0) { + WH_ERROR_PRINT("Failed to init/setparams ML-DSA key %d\n", ret); + return ret; + } + + ret = wh_Client_MlDsaMakeCacheKeyDma( + ctx, WC_ML_DSA_44, &keyId, + WH_NVM_FLAGS_USAGE_SIGN | WH_NVM_FLAGS_USAGE_VERIFY, 0, NULL, genPub); + if (ret != 0) { + WH_ERROR_PRINT("MlDsaMakeCacheKeyDma failed %d\n", ret); + } + + /* Cross-check against a separate public export of the same keyId. */ + if (ret == 0) { + ret = wc_MlDsaKey_Init(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(refPub, WC_ML_DSA_44); + } + if (ret == 0) { + ret = wh_Client_MlDsaExportPublicKeyDma(ctx, keyId, refPub, 0, NULL); + if (ret != 0) { + WH_ERROR_PRINT("wh_Client_MlDsaExportPublicKeyDma failed %d\n", + ret); + } + else { + genDerSz = wc_MlDsaKey_PublicKeyToDer(genPub, genDer, + sizeof(genDer), 1); + refDerSz = wc_MlDsaKey_PublicKeyToDer(refPub, refDer, + sizeof(refDer), 1); + if ((genDerSz <= 0) || (genDerSz != refDerSz) || + (memcmp(genDer, refDer, (size_t)genDerSz) != 0)) { + WH_ERROR_PRINT("keygen pubkey (DMA) mismatch vs export\n"); + ret = -1; + } + } + wc_MlDsaKey_Free(refPub); + } + } + + /* Sign on the server (DMA), verify with the keygen-returned public key. */ + if (ret == 0) { + ret = wc_MlDsaKey_Init(hsmKey, NULL, devId); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(hsmKey, WC_ML_DSA_44); + } + if (ret == 0) { + ret = wh_Client_MlDsaSetKeyId(hsmKey, keyId); + } + if (ret == 0) { + ret = wh_Client_MlDsaSignDma(ctx, msg, sizeof(msg), sig, &sigLen, + hsmKey, NULL, 0, WC_HASH_TYPE_NONE); + if (ret != 0) { + WH_ERROR_PRINT("HSM ML-DSA DMA sign failed %d\n", ret); + } + } + wc_MlDsaKey_Free(hsmKey); + } + if (ret == 0) { + ret = wh_Client_MlDsaVerifyDma(ctx, sig, sigLen, msg, sizeof(msg), + &verified, genPub, NULL, 0, + WC_HASH_TYPE_NONE); + if ((ret != 0) || (verified != 1)) { + WH_ERROR_PRINT( + "DMA verify with keygen pub failed ret=%d verify=%d\n", ret, + verified); + if (ret == 0) { + ret = -1; + } + } + } + + wc_MlDsaKey_Free(genPub); + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + + if (ret == 0) { + WH_TEST_PRINT("ML-DSA CACHE-AND-EXPORT-PUBLIC DMA DEVID=0x%X SUCCESS\n", + devId); + } + return ret; +} +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ #endif /* WOLFHSM_CFG_DMA */ #endif /* !WOLFSSL_DILITHIUM_NO_VERIFY && !WOLFSSL_DILITHIUM_NO_SIGN && \ @@ -1141,10 +1358,16 @@ int whTest_Crypto_MlDsa(whClientContext* ctx) WH_TEST_RETURN_ON_FAIL(_whTest_CryptoMlDsaClient(ctx)); WH_TEST_RETURN_ON_FAIL(_whTest_CryptoMlDsaExportPublicKey(ctx)); +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY + WH_TEST_RETURN_ON_FAIL(_whTest_CryptoMlDsaCacheKeyAndExportPublic(ctx)); +#endif WH_TEST_RETURN_ON_FAIL(_whTest_CryptoMlDsaBufferTooSmall(ctx)); #ifdef WOLFHSM_CFG_DMA WH_TEST_RETURN_ON_FAIL(_whTest_CryptoMlDsaDmaClient(ctx)); WH_TEST_RETURN_ON_FAIL(_whTest_CryptoMlDsaExportPublicKeyDma(ctx)); +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY + WH_TEST_RETURN_ON_FAIL(_whTest_CryptoMlDsaCacheKeyAndExportPublicDma(ctx)); +#endif #endif #endif #if !defined(WOLFSSL_DILITHIUM_NO_VERIFY) && \ diff --git a/test/wh_test_crypto.c b/test/wh_test_crypto.c index 53a59c814..94652d0c4 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -12617,6 +12617,110 @@ static int whTestCrypto_MlDsaExportPublic(whClientContext* ctx, int devId, } return ret; } + +/* One keygen call caches the private key and returns the public key. Verify + * the returned public key byte-matches wh_Client_MlDsaExportPublicKey and that + * it verifies a signature made by the cached private key. */ +static int whTestCrypto_MlDsaCacheKeyAndExportPublic(whClientContext* ctx, + int devId, WC_RNG* rng, + int level) +{ + int ret = 0; + whKeyId keyId = WH_KEYID_ERASED; + wc_MlDsaKey genPub[1] = {0}; + wc_MlDsaKey refPub[1] = {0}; + wc_MlDsaKey hsmKey[1] = {0}; + byte msg[] = "ML-DSA cache-export-public message"; + byte sig[MLDSA_MAX_SIG_SIZE]; + word32 sigLen = sizeof(sig); + int verified = 0; + byte genDer[MLDSA_MAX_BOTH_KEY_DER_SIZE]; + byte refDer[MLDSA_MAX_BOTH_KEY_DER_SIZE]; + int genDerSz = 0; + int refDerSz = 0; + (void)devId; + (void)rng; + + ret = wc_MlDsaKey_Init(genPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(genPub, level); + } + if (ret == 0) { + ret = wh_Client_MlDsaMakeCacheKeyAndExportPublic( + ctx, 0, level, &keyId, + WH_NVM_FLAGS_USAGE_SIGN | WH_NVM_FLAGS_USAGE_VERIFY, 0, NULL, + genPub); + if (ret != 0) { + WH_ERROR_PRINT("MlDsaMakeCacheKeyAndExportPublic failed %d\n", ret); + } + } + + /* Cross-check the keygen-returned public key against ExportPublicKey. */ + if (ret == 0) { + ret = wc_MlDsaKey_Init(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(refPub, level); + } + if (ret == 0) { + ret = wh_Client_MlDsaExportPublicKey(ctx, keyId, refPub, 0, NULL); + if (ret != 0) { + WH_ERROR_PRINT("wh_Client_MlDsaExportPublicKey failed %d\n", + ret); + } + } + } + if (ret == 0) { + genDerSz = wc_MlDsaKey_PublicKeyToDer(genPub, genDer, sizeof(genDer), 1); + refDerSz = wc_MlDsaKey_PublicKeyToDer(refPub, refDer, sizeof(refDer), 1); + if ((genDerSz <= 0) || (genDerSz != refDerSz) || + (memcmp(genDer, refDer, (size_t)genDerSz) != 0)) { + WH_ERROR_PRINT("keygen pubkey mismatch vs ExportPublicKey\n"); + ret = -1; + } + } + + /* Prove usability: sign on the HSM, verify with the keygen pubkey. */ + if (ret == 0) { + ret = wc_MlDsaKey_Init(hsmKey, NULL, WH_CLIENT_DEVID(ctx)); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(hsmKey, level); + } + if (ret == 0) { + ret = wh_Client_MlDsaSetKeyId(hsmKey, keyId); + } + if (ret == 0) { + ret = wh_Client_MlDsaSign(ctx, msg, sizeof(msg), sig, &sigLen, + hsmKey, NULL, 0, WC_HASH_TYPE_NONE); + if (ret != 0) { + WH_ERROR_PRINT("HSM ML-DSA sign failed %d\n", ret); + } + } + wc_MlDsaKey_Free(hsmKey); + } + if (ret == 0) { + ret = wh_Client_MlDsaVerify(ctx, sig, sigLen, msg, sizeof(msg), + &verified, genPub, NULL, 0, + WC_HASH_TYPE_NONE); + if ((ret != 0) || (verified != 1)) { + WH_ERROR_PRINT("verify with keygen pub failed ret=%d verify=%d\n", + ret, verified); + if (ret == 0) { + ret = -1; + } + } + } + + wc_MlDsaKey_Free(refPub); + wc_MlDsaKey_Free(genPub); + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + + if (ret == 0) { + WH_TEST_PRINT("ML-DSA CACHE-AND-EXPORT-PUBLIC SUCCESS\n"); + } + return ret; +} #endif /* WOLFSSL_MLDSA_PUBLIC_KEY && ML_DSA_44 available */ #ifdef WOLFHSM_CFG_DMA @@ -12957,6 +13061,111 @@ static int whTestCrypto_MlDsaExportPublicDma(whClientContext* ctx, int devId, } return ret; } + +/* DMA variant: one keygen call caches the private key and streams the public + * key back through the client's DMA buffer. Verify it byte-matches + * wh_Client_MlDsaExportPublicKeyDma and that it verifies an HSM signature. */ +static int whTestCrypto_MlDsaCacheKeyAndExportPublicDma(whClientContext* ctx, + int devId, WC_RNG* rng, + int level) +{ + int ret = 0; + whKeyId keyId = WH_KEYID_ERASED; + wc_MlDsaKey genPub[1] = {0}; + wc_MlDsaKey refPub[1] = {0}; + wc_MlDsaKey hsmKey[1] = {0}; + byte msg[] = "ML-DSA DMA cache-export-public message"; + byte sig[MLDSA_MAX_SIG_SIZE]; + word32 sigLen = sizeof(sig); + int verified = 0; + byte genDer[MLDSA_MAX_PUB_KEY_DER_SIZE]; + byte refDer[MLDSA_MAX_PUB_KEY_DER_SIZE]; + int genDerSz = 0; + int refDerSz = 0; + (void)devId; + (void)rng; + + ret = wc_MlDsaKey_Init(genPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(genPub, level); + } + if (ret == 0) { + ret = wh_Client_MlDsaMakeCacheKeyDma( + ctx, level, &keyId, + WH_NVM_FLAGS_USAGE_SIGN | WH_NVM_FLAGS_USAGE_VERIFY, 0, NULL, + genPub); + if (ret != 0) { + WH_ERROR_PRINT("MlDsaMakeCacheKeyDma failed %d\n", ret); + } + } + + /* Cross-check the keygen-returned public key against ExportPublicKeyDma. */ + if (ret == 0) { + ret = wc_MlDsaKey_Init(refPub, NULL, INVALID_DEVID); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(refPub, level); + } + if (ret == 0) { + ret = wh_Client_MlDsaExportPublicKeyDma(ctx, keyId, refPub, 0, NULL); + if (ret != 0) { + WH_ERROR_PRINT("wh_Client_MlDsaExportPublicKeyDma failed %d\n", + ret); + } + } + } + if (ret == 0) { + genDerSz = wc_MlDsaKey_PublicKeyToDer(genPub, genDer, sizeof(genDer), 1); + refDerSz = wc_MlDsaKey_PublicKeyToDer(refPub, refDer, sizeof(refDer), 1); + if ((genDerSz <= 0) || (genDerSz != refDerSz) || + (memcmp(genDer, refDer, (size_t)genDerSz) != 0)) { + WH_ERROR_PRINT("keygen pubkey (DMA) mismatch vs export\n"); + ret = -1; + } + } + + /* Prove usability: sign on the HSM (DMA), verify with the keygen pubkey. */ + if (ret == 0) { + ret = wc_MlDsaKey_Init(hsmKey, NULL, WH_CLIENT_DEVID(ctx)); + if (ret == 0) { + ret = wc_MlDsaKey_SetParams(hsmKey, level); + } + if (ret == 0) { + ret = wh_Client_MlDsaSetKeyId(hsmKey, keyId); + } + if (ret == 0) { + ret = wh_Client_MlDsaSignDma(ctx, msg, sizeof(msg), sig, &sigLen, + hsmKey, NULL, 0, WC_HASH_TYPE_NONE); + if (ret != 0) { + WH_ERROR_PRINT("HSM ML-DSA DMA sign failed %d\n", ret); + } + } + wc_MlDsaKey_Free(hsmKey); + } + if (ret == 0) { + ret = wh_Client_MlDsaVerifyDma(ctx, sig, sigLen, msg, sizeof(msg), + &verified, genPub, NULL, 0, + WC_HASH_TYPE_NONE); + if ((ret != 0) || (verified != 1)) { + WH_ERROR_PRINT( + "DMA verify with keygen pub failed ret=%d verify=%d\n", ret, + verified); + if (ret == 0) { + ret = -1; + } + } + } + + wc_MlDsaKey_Free(refPub); + wc_MlDsaKey_Free(genPub); + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + + if (ret == 0) { + WH_TEST_PRINT("ML-DSA CACHE-AND-EXPORT-PUBLIC DMA SUCCESS\n"); + } + return ret; +} #endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ #endif /* WOLFHSM_CFG_DMA */ @@ -16911,6 +17120,16 @@ int whTest_CryptoClientConfig(whClientConfig* config) level, ret); } } + if (ret == 0) { + ret = whTestCrypto_MlDsaCacheKeyAndExportPublic( + client, WH_CLIENT_DEVID(client), rng, level); + if (ret != 0) { + WH_ERROR_PRINT( + "ML-DSA cache-and-export-public test failed " + "(level %d): %d\n", + level, ret); + } + } #endif #ifdef WOLFHSM_CFG_DMA @@ -16930,6 +17149,16 @@ int whTest_CryptoClientConfig(whClientConfig* config) level, ret); } } + if (ret == 0) { + ret = whTestCrypto_MlDsaCacheKeyAndExportPublicDma( + client, WH_CLIENT_DEVID(client), rng, level); + if (ret != 0) { + WH_ERROR_PRINT( + "ML-DSA cache-and-export-public DMA test failed " + "(level %d): %d\n", + level, ret); + } + } #endif #endif /* WOLFHSM_CFG_DMA*/ } diff --git a/wolfhsm/wh_client_crypto.h b/wolfhsm/wh_client_crypto.h index 930629385..46ff4b610 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -2927,6 +2927,24 @@ int wh_Client_MlDsaMakeExportKey(whClientContext* ctx, int level, int size, int wh_Client_MlDsaMakeCacheKey(whClientContext* ctx, int size, int level, whKeyId* inout_key_id, whNvmFlags flags, uint16_t label_len, uint8_t* label); + +/* Generate an ML-DSA key on the server, cache it, and return its public key in + * one round-trip (avoids a separate wh_Client_MlDsaExportPublicKey call). On + * success inout_key_id holds the cached keyId and pub is populated with the + * public key and associated with that keyId; the caller must still set the HSM + * devId on pub before follow-on operations route to the server. flags must not + * include WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + * Only available when the server public-key export is compiled in + * (WOLFSSL_MLDSA_PUBLIC_KEY), matching the server-side gate. */ +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY +int wh_Client_MlDsaMakeCacheKeyAndExportPublic(whClientContext* ctx, int size, + int level, + whKeyId* inout_key_id, + whNvmFlags flags, + uint16_t label_len, + const uint8_t* label, + wc_MlDsaKey* pub); +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ /** * @brief Sign a message using a ML-DSA private key. * @@ -3065,6 +3083,20 @@ int wh_Client_MlDsaExportPublicKeyDma(whClientContext* ctx, whKeyId keyId, int wh_Client_MlDsaMakeExportKeyDma(whClientContext* ctx, int level, wc_MlDsaKey* key); +/* DMA variant: generate an ML-DSA key on the server, cache it, and return its + * public key into pub (associated with the cached keyId), avoiding a separate + * wh_Client_MlDsaExportPublicKeyDma call. The caller must still set the HSM + * devId on pub before follow-on operations route to the server. flags must not + * include WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + * Only available when the server public-key export is compiled in + * (WOLFSSL_MLDSA_PUBLIC_KEY), matching the server-side gate. */ +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY +int wh_Client_MlDsaMakeCacheKeyDma(whClientContext* ctx, int level, + whKeyId* inout_key_id, whNvmFlags flags, + uint16_t label_len, const uint8_t* label, + wc_MlDsaKey* pub); +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ + /** * @brief Sign a message using ML-DSA with DMA. From c25623bf34576d3b5dd2226c810b32f680d32684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Tue, 14 Jul 2026 12:36:27 +0200 Subject: [PATCH 6/6] Return ML-KEM public key from cached keygen Extend both the non-DMA and DMA cached ML-KEM keygen paths to return the generated public key to the client in one round-trip instead of a follow-up wh_Client_MlKemExportPublicKey call. Non-DMA: the cache branch of _HandleMlKemKeyGen encodes the public key into the response body (reusing the existing keyId + len + body layout). DMA: the cache branch of _HandleMlKemKeyGenDma streams the public key back through the client's existing key DMA buffer and reports its size in keySize. Both use wc_MlKemKey_PublicKeySize / wc_MlKemKey_EncodePublicKey, matching the keystore public export path. No wire-format changes. _MlKemMakeKey and _MlKemMakeKeyDma now deserialize the response body/buffer whenever it is present rather than only for EPHEMERAL keygen, so the cached paths can hand the public key back through the same key object. Existing MakeCacheKey (key == NULL) and MakeExportKey callers are unaffected. Add wh_Client_MlKemMakeCacheKeyAndExportPublic and wh_Client_MlKemMakeCacheKeyDma, plus non-DMA and DMA tests that cross-check the keygen-returned public key against wh_Client_MlKemExportPublicKey[Dma] and prove it via a KEM encapsulate/decapsulate round-trip with the cached private key. --- src/wh_client_crypto.c | 99 ++++++-- src/wh_server_crypto.c | 43 +++- test/wh_test_crypto.c | 455 ++++++++++++++++++++++++++++++++++++- wolfhsm/wh_client_crypto.h | 23 ++ 4 files changed, 603 insertions(+), 17 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index d87ca3211..6eccb95d8 100644 --- a/src/wh_client_crypto.c +++ b/src/wh_client_crypto.c @@ -139,11 +139,11 @@ static int _MlDsaMakeKeyDma(whClientContext* ctx, int level, #ifdef WOLFSSL_HAVE_MLKEM static int _MlKemMakeKey(whClientContext* ctx, int level, whKeyId* inout_key_id, whNvmFlags flags, - uint16_t label_len, uint8_t* label, MlKemKey* key); + uint16_t label_len, const uint8_t* label, MlKemKey* key); #ifdef WOLFHSM_CFG_DMA static int _MlKemMakeKeyDma(whClientContext* ctx, int level, whKeyId* inout_key_id, whNvmFlags flags, - uint16_t label_len, uint8_t* label, MlKemKey* key); + uint16_t label_len, const uint8_t* label, MlKemKey* key); #endif /* WOLFHSM_CFG_DMA */ #endif /* WOLFSSL_HAVE_MLKEM */ @@ -10727,7 +10727,7 @@ int wh_Client_MlKemExportPublicKey(whClientContext* ctx, whKeyId keyId, static int _MlKemMakeKey(whClientContext* ctx, int level, whKeyId* inout_key_id, whNvmFlags flags, - uint16_t label_len, uint8_t* label, MlKemKey* key) + uint16_t label_len, const uint8_t* label, MlKemKey* key) { int ret = WH_ERROR_OK; whKeyId key_id = WH_KEYID_ERASED; @@ -10804,7 +10804,10 @@ static int _MlKemMakeKey(whClientContext* ctx, int level, } if (key != NULL) { wh_Client_MlKemSetKeyId(key, key_id); - if ((flags & WH_NVM_FLAGS_EPHEMERAL) != 0) { + /* Response carries the exported key (EPHEMERAL) or the public key + * (cached keygen). An empty body means the caller requested key + * material the server did not return. */ + if (res->len > 0) { uint8_t* key_raw = (uint8_t*)(res + 1); const size_t hdr_sz = sizeof(whMessageCrypto_GenericResponseHeader) + @@ -10817,6 +10820,9 @@ static int _MlKemMakeKey(whClientContext* ctx, int level, key_raw, (uint16_t)res->len, key); } } + else { + ret = WH_ERROR_ABORTED; + } } } return ret; @@ -10834,6 +10840,37 @@ int wh_Client_MlKemMakeCacheKey(whClientContext* ctx, int level, label, NULL); } +int wh_Client_MlKemMakeCacheKeyAndExportPublic(whClientContext* ctx, int level, + whKeyId* inout_key_id, + whNvmFlags flags, + uint16_t label_len, + const uint8_t* label, + MlKemKey* pub) +{ + int ret; + whKeyId in_keyId; + + if ((ctx == NULL) || (inout_key_id == NULL) || (pub == NULL)) { + return WH_ERROR_BADARGS; + } + + /* Ephemeral keygen belongs to the export path, not the cache path. */ + if (flags & WH_NVM_FLAGS_EPHEMERAL) { + return WH_ERROR_BADARGS; + } + + in_keyId = *inout_key_id; + ret = _MlKemMakeKey(ctx, level, inout_key_id, flags, label_len, label, pub); + if ((ret < 0) && WH_KEYID_ISERASED(in_keyId) && + !WH_KEYID_ISERASED(*inout_key_id)) { + /* The server auto-assigned and committed a key but the export failed. + * Roll back so the operation is atomic and no cache slot is orphaned. */ + (void)wh_Client_KeyEvict(ctx, *inout_key_id); + *inout_key_id = WH_KEYID_ERASED; + } + return ret; +} + int wh_Client_MlKemMakeExportKey(whClientContext* ctx, int level, MlKemKey* key) { @@ -11178,7 +11215,7 @@ int wh_Client_MlKemExportPublicKeyDma(whClientContext* ctx, whKeyId keyId, static int _MlKemMakeKeyDma(whClientContext* ctx, int level, whKeyId* inout_key_id, whNvmFlags flags, - uint16_t label_len, uint8_t* label, MlKemKey* key) + uint16_t label_len, const uint8_t* label, MlKemKey* key) { int ret = WH_ERROR_OK; whKeyId key_id = WH_KEYID_ERASED; @@ -11263,16 +11300,22 @@ static int _MlKemMakeKeyDma(whClientContext* ctx, int level, } if (key != NULL) { wh_Client_MlKemSetKeyId(key, key_id); - if ((flags & WH_NVM_FLAGS_EPHEMERAL) != 0) { - if (res->keySize > buffer_len) { - ret = WH_ERROR_BADARGS; - } - else { - ret = wh_Crypto_MlKemDeserializeKey( - buffer, (uint16_t)res->keySize, key); - } + /* buffer holds the exported key (EPHEMERAL) or the public key + * (cached keygen); keySize bounds the DMA write. An empty result + * means the caller requested key material the server did not + * return. */ + if (res->keySize == 0) { + ret = WH_ERROR_ABORTED; + } + else if (res->keySize > buffer_len) { + ret = WH_ERROR_BADARGS; + } + else { + ret = wh_Crypto_MlKemDeserializeKey( + buffer, (uint16_t)res->keySize, key); } } + } } @@ -11291,6 +11334,36 @@ int wh_Client_MlKemMakeExportKeyDma(whClientContext* ctx, int level, key); } +int wh_Client_MlKemMakeCacheKeyDma(whClientContext* ctx, int level, + whKeyId* inout_key_id, whNvmFlags flags, + uint16_t label_len, const uint8_t* label, + MlKemKey* pub) +{ + int ret; + whKeyId in_keyId; + + if ((ctx == NULL) || (inout_key_id == NULL) || (pub == NULL)) { + return WH_ERROR_BADARGS; + } + + /* Ephemeral keygen belongs to the export path, not the cache path. */ + if (flags & WH_NVM_FLAGS_EPHEMERAL) { + return WH_ERROR_BADARGS; + } + + in_keyId = *inout_key_id; + ret = _MlKemMakeKeyDma(ctx, level, inout_key_id, flags, label_len, label, + pub); + if ((ret < 0) && WH_KEYID_ISERASED(in_keyId) && + !WH_KEYID_ISERASED(*inout_key_id)) { + /* The server auto-assigned and committed a key but the export failed. + * Roll back so the operation is atomic and no cache slot is orphaned. */ + (void)wh_Client_KeyEvict(ctx, *inout_key_id); + *inout_key_id = WH_KEYID_ERASED; + } + return ret; +} + int wh_Client_MlKemEncapsulateDma(whClientContext* ctx, MlKemKey* key, uint8_t* ct, uint32_t* inout_ct_len, uint8_t* ss, uint32_t* inout_ss_len) diff --git a/src/wh_server_crypto.c b/src/wh_server_crypto.c index 2ecd50726..de02f258d 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -5188,6 +5188,19 @@ static int _HandleMlKemKeyGen(whServerContext* ctx, uint16_t magic, int devId, req.flags, label_size, req.label); } + if (ret == WH_ERROR_OK) { + /* Best-effort: also export the public key into the response + * body so the client avoids a separate ExportPublicKey + * call. On failure leave the body empty and still return the + * cached keyId rather than orphaning the committed key. */ + word32 pubSize = 0; + if ((wc_MlKemKey_PublicKeySize(key, &pubSize) == 0) && + ((uint32_t)pubSize <= (uint32_t)max_size) && + (wc_MlKemKey_EncodePublicKey(key, res_out, pubSize) == + 0)) { + res_size = (uint16_t)pubSize; + } + } } } wc_MlKemKey_Free(key); @@ -6961,11 +6974,35 @@ static int _HandleMlKemKeyGenDma(whServerContext* ctx, uint16_t magic, ret = wh_Server_MlKemKeyCacheImport( ctx, key, keyId, req.flags, req.labelSize, req.label); - if (ret == WH_ERROR_OK) { - res.keyId = wh_KeyId_TranslateToClient(keyId); - res.keySize = keySize; + } + /* Best-effort: stream the public key back through the + * client's DMA buffer so it gets the pubkey without a + * separate ExportPublicKey call. On failure leave keySize 0 + * and still return the cached keyId rather than orphaning + * the committed key. */ + if (ret == WH_ERROR_OK) { + word32 pubSize = 0; + if ((wc_MlKemKey_PublicKeySize(key, &pubSize) == 0) && + ((uint64_t)pubSize <= req.key.sz) && + (wh_Server_DmaProcessClientAddress( + ctx, req.key.addr, &clientOutAddr, pubSize, + WH_DMA_OPER_CLIENT_WRITE_PRE, + (whServerDmaFlags){0}) == 0)) { + if (wc_MlKemKey_EncodePublicKey( + key, (uint8_t*)clientOutAddr, pubSize) == + 0) { + keySize = (uint16_t)pubSize; + } + (void)wh_Server_DmaProcessClientAddress( + ctx, req.key.addr, &clientOutAddr, keySize, + WH_DMA_OPER_CLIENT_WRITE_POST, + (whServerDmaFlags){0}); } } + if (ret == WH_ERROR_OK) { + res.keyId = wh_KeyId_TranslateToClient(keyId); + res.keySize = keySize; + } } } wc_MlKemKey_Free(key); diff --git a/test/wh_test_crypto.c b/test/wh_test_crypto.c index 94652d0c4..b9dfcae2b 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -14182,6 +14182,146 @@ static int whTestCrypto_MlKemExportPublic(whClientContext* ctx, int devId, return ret; } +/* One keygen call caches the private key and returns the public key. Verify + * the returned public key byte-matches wh_Client_MlKemExportPublicKey and that + * a KEM encapsulate/decapsulate round-trips against the cached private key. */ +static int whTestCrypto_MlKemCacheKeyAndExportPublic(whClientContext* ctx, + int devId, WC_RNG* rng) +{ + int ret = 0; + int levels[3]; + int levelCnt = 0; + int i; + + levelCnt = whTestCrypto_MlKemGetLevels( + levels, (int)(sizeof(levels) / sizeof(levels[0]))); + + for (i = 0; (ret == 0) && (i < levelCnt); i++) { + whKeyId keyId = WH_KEYID_ERASED; + MlKemKey genPub[1] = {0}; + MlKemKey refPub[1] = {0}; + MlKemKey cached[1] = {0}; + int genInited = 0; + int refInited = 0; + int cachedInited = 0; + byte genRaw[WC_ML_KEM_MAX_PUBLIC_KEY_SIZE]; + byte refRaw[WC_ML_KEM_MAX_PUBLIC_KEY_SIZE]; + word32 genRawSz = 0; + word32 refRawSz = 0; + byte ct[WC_ML_KEM_MAX_CIPHER_TEXT_SIZE]; + byte ssEnc[WC_ML_KEM_SS_SZ]; + byte ssDec[WC_ML_KEM_SS_SZ]; + word32 ctLen = sizeof(ct); + word32 ssEncLen = sizeof(ssEnc); + word32 ssDecLen = sizeof(ssDec); + + ret = wc_MlKemKey_Init(genPub, levels[i], NULL, INVALID_DEVID); + if (ret == 0) { + genInited = 1; + ret = wh_Client_MlKemMakeCacheKeyAndExportPublic( + ctx, levels[i], &keyId, WH_NVM_FLAGS_USAGE_DERIVE, 0, NULL, + genPub); + if (ret != 0) { + WH_ERROR_PRINT( + "MlKemMakeCacheKeyAndExportPublic failed level=%d %d\n", + levels[i], ret); + } + } + + /* Cross-check the keygen-returned public key against ExportPublicKey. */ + if (ret == 0) { + ret = wc_MlKemKey_Init(refPub, levels[i], NULL, INVALID_DEVID); + if (ret == 0) { + refInited = 1; + ret = wh_Client_MlKemExportPublicKey(ctx, keyId, refPub, 0, + NULL); + if (ret != 0) { + WH_ERROR_PRINT( + "wh_Client_MlKemExportPublicKey failed level=%d %d\n", + levels[i], ret); + } + } + } + if (ret == 0) { + ret = wc_MlKemKey_PublicKeySize(genPub, &genRawSz); + if (ret == 0) { + ret = wc_MlKemKey_EncodePublicKey(genPub, genRaw, genRawSz); + } + if (ret == 0) { + ret = wc_MlKemKey_PublicKeySize(refPub, &refRawSz); + } + if (ret == 0) { + ret = wc_MlKemKey_EncodePublicKey(refPub, refRaw, refRawSz); + } + if ((ret == 0) && ((genRawSz != refRawSz) || + (memcmp(genRaw, refRaw, genRawSz) != 0))) { + WH_ERROR_PRINT( + "keygen pubkey mismatch vs ExportPublicKey level=%d\n", + levels[i]); + ret = -1; + } + } + + /* Roundtrip: encapsulate with the keygen public key, decapsulate on + * the server-cached private key. */ + if (ret == 0) { + ret = wc_MlKemKey_CipherTextSize(genPub, &ctLen); + if (ret == 0) { + ret = wc_MlKemKey_SharedSecretSize(genPub, &ssEncLen); + } + if (ret == 0) { + ssDecLen = ssEncLen; + ret = wc_MlKemKey_Encapsulate(genPub, ct, ssEnc, rng); + if (ret != 0) { + WH_ERROR_PRINT( + "Encapsulate against keygen pub failed level=%d %d\n", + levels[i], ret); + } + } + } + if (ret == 0) { + ret = wc_MlKemKey_Init(cached, levels[i], NULL, devId); + if (ret == 0) { + cachedInited = 1; + ret = wh_Client_MlKemSetKeyId(cached, keyId); + } + } + if (ret == 0) { + ret = wh_Client_MlKemDecapsulate(ctx, cached, ct, ctLen, ssDec, + &ssDecLen); + if (ret != 0) { + WH_ERROR_PRINT("Server decapsulate failed level=%d %d\n", + levels[i], ret); + } + else if ((ssEncLen != ssDecLen) || + (memcmp(ssEnc, ssDec, ssEncLen) != 0)) { + WH_ERROR_PRINT( + "ML-KEM keygen-pub roundtrip ss mismatch level=%d\n", + levels[i]); + ret = -1; + } + } + + if (cachedInited) { + wc_MlKemKey_Free(cached); + } + if (refInited) { + wc_MlKemKey_Free(refPub); + } + if (genInited) { + wc_MlKemKey_Free(genPub); + } + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + } + + if (ret == 0) { + WH_TEST_PRINT("ML-KEM CACHE-AND-EXPORT-PUBLIC SUCCESS\n"); + } + return ret; +} + #ifdef WOLFHSM_CFG_DMA static int whTestCrypto_MlKemExportPublicDma(whClientContext* ctx, int devId, WC_RNG* rng) @@ -14362,6 +14502,146 @@ static int whTestCrypto_MlKemExportPublicDma(whClientContext* ctx, int devId, return ret; } +/* DMA variant: one keygen call caches the private key and streams the public + * key back through the client's DMA buffer. Verify it byte-matches + * wh_Client_MlKemExportPublicKeyDma and that a KEM round-trips against the + * cached private key. */ +static int whTestCrypto_MlKemCacheKeyAndExportPublicDma(whClientContext* ctx, + int devId, WC_RNG* rng) +{ + int ret = 0; + int levels[3]; + int levelCnt = 0; + int i; + + levelCnt = whTestCrypto_MlKemGetLevels( + levels, (int)(sizeof(levels) / sizeof(levels[0]))); + + for (i = 0; (ret == 0) && (i < levelCnt); i++) { + whKeyId keyId = WH_KEYID_ERASED; + MlKemKey genPub[1] = {0}; + MlKemKey refPub[1] = {0}; + MlKemKey cached[1] = {0}; + int genInited = 0; + int refInited = 0; + int cachedInited = 0; + byte genRaw[WC_ML_KEM_MAX_PUBLIC_KEY_SIZE]; + byte refRaw[WC_ML_KEM_MAX_PUBLIC_KEY_SIZE]; + word32 genRawSz = 0; + word32 refRawSz = 0; + byte ct[WC_ML_KEM_MAX_CIPHER_TEXT_SIZE]; + byte ssEnc[WC_ML_KEM_SS_SZ]; + byte ssDec[WC_ML_KEM_SS_SZ]; + word32 ctLen = sizeof(ct); + word32 ssEncLen = sizeof(ssEnc); + word32 ssDecLen = sizeof(ssDec); + + ret = wc_MlKemKey_Init(genPub, levels[i], NULL, INVALID_DEVID); + if (ret == 0) { + genInited = 1; + ret = wh_Client_MlKemMakeCacheKeyDma( + ctx, levels[i], &keyId, WH_NVM_FLAGS_USAGE_DERIVE, 0, NULL, + genPub); + if (ret != 0) { + WH_ERROR_PRINT("MlKemMakeCacheKeyDma failed level=%d %d\n", + levels[i], ret); + } + } + + /* Cross-check against a separate public DMA export of the same keyId. */ + if (ret == 0) { + ret = wc_MlKemKey_Init(refPub, levels[i], NULL, INVALID_DEVID); + if (ret == 0) { + refInited = 1; + ret = wh_Client_MlKemExportPublicKeyDma(ctx, keyId, refPub, 0, + NULL); + if (ret != 0) { + WH_ERROR_PRINT( + "wh_Client_MlKemExportPublicKeyDma failed level=%d %d\n", + levels[i], ret); + } + } + } + if (ret == 0) { + ret = wc_MlKemKey_PublicKeySize(genPub, &genRawSz); + if (ret == 0) { + ret = wc_MlKemKey_EncodePublicKey(genPub, genRaw, genRawSz); + } + if (ret == 0) { + ret = wc_MlKemKey_PublicKeySize(refPub, &refRawSz); + } + if (ret == 0) { + ret = wc_MlKemKey_EncodePublicKey(refPub, refRaw, refRawSz); + } + if ((ret == 0) && ((genRawSz != refRawSz) || + (memcmp(genRaw, refRaw, genRawSz) != 0))) { + WH_ERROR_PRINT( + "keygen pubkey (DMA) mismatch vs export level=%d\n", + levels[i]); + ret = -1; + } + } + + /* Roundtrip: encapsulate with the keygen public key, decapsulate on + * the server-cached private key. */ + if (ret == 0) { + ret = wc_MlKemKey_CipherTextSize(genPub, &ctLen); + if (ret == 0) { + ret = wc_MlKemKey_SharedSecretSize(genPub, &ssEncLen); + } + if (ret == 0) { + ssDecLen = ssEncLen; + ret = wc_MlKemKey_Encapsulate(genPub, ct, ssEnc, rng); + if (ret != 0) { + WH_ERROR_PRINT( + "Encapsulate against keygen pub (DMA) failed level=%d " + "%d\n", levels[i], ret); + } + } + } + if (ret == 0) { + ret = wc_MlKemKey_Init(cached, levels[i], NULL, devId); + if (ret == 0) { + cachedInited = 1; + ret = wh_Client_MlKemSetKeyId(cached, keyId); + } + } + if (ret == 0) { + ret = wh_Client_MlKemDecapsulate(ctx, cached, ct, ctLen, ssDec, + &ssDecLen); + if (ret != 0) { + WH_ERROR_PRINT("Server decapsulate (DMA) failed level=%d %d\n", + levels[i], ret); + } + else if ((ssEncLen != ssDecLen) || + (memcmp(ssEnc, ssDec, ssEncLen) != 0)) { + WH_ERROR_PRINT( + "ML-KEM DMA keygen-pub roundtrip ss mismatch level=%d\n", + levels[i]); + ret = -1; + } + } + + if (cachedInited) { + wc_MlKemKey_Free(cached); + } + if (refInited) { + wc_MlKemKey_Free(refPub); + } + if (genInited) { + wc_MlKemKey_Free(genPub); + } + if (!WH_KEYID_ISERASED(keyId)) { + (void)wh_Client_KeyEvict(ctx, keyId); + } + } + + if (ret == 0) { + WH_TEST_PRINT("ML-KEM CACHE-AND-EXPORT-PUBLIC DMA SUCCESS\n"); + } + return ret; +} + static int whTestCrypto_MlKemDmaClient(whClientContext* ctx, int devId, WC_RNG* rng) { @@ -16636,6 +16916,159 @@ int whTest_CryptoKeyRevocationAesCbc(whClientContext* client, WC_RNG* rng) #endif /* !NO_AES && HAVE_AES_CBC && \ WOLFHSM_CFG_TEST_ALLOW_PERSISTENT_NVM_ARTIFACTS */ +/* Negative tests: every cache-and-export keygen function must reject + * WH_NVM_FLAGS_EPHEMERAL, a NULL inout_key_id, and a NULL pub with + * WH_ERROR_BADARGS, before contacting the server. level is passed as 0 for the + * PQC calls since the argument guards run before any level validation. */ +static int whTest_CryptoMakeCacheKeyExportPublicArgs(whClientContext* ctx) +{ + int ret = 0; + whKeyId keyId = WH_KEYID_ERASED; + +#if !defined(NO_RSA) && defined(WOLFSSL_KEY_GEN) + { + RsaKey rsa[1] = {0}; + if (wh_Client_RsaMakeCacheKeyAndExportPublic( + ctx, 2048, WC_RSA_EXPONENT, &keyId, WH_NVM_FLAGS_EPHEMERAL, 0, + NULL, rsa) != WH_ERROR_BADARGS || + wh_Client_RsaMakeCacheKeyAndExportPublic( + NULL, 2048, WC_RSA_EXPONENT, &keyId, WH_NVM_FLAGS_NONE, 0, NULL, + rsa) != WH_ERROR_BADARGS || + wh_Client_RsaMakeCacheKeyAndExportPublic( + ctx, 2048, WC_RSA_EXPONENT, NULL, WH_NVM_FLAGS_NONE, 0, NULL, + rsa) != WH_ERROR_BADARGS || + wh_Client_RsaMakeCacheKeyAndExportPublic( + ctx, 2048, WC_RSA_EXPONENT, &keyId, WH_NVM_FLAGS_NONE, 0, NULL, + NULL) != WH_ERROR_BADARGS) { + WH_ERROR_PRINT("RSA cache-export arg validation failed\n"); + ret = -1; + } + } +#endif +#ifdef HAVE_ECC + if (ret == 0) { + ecc_key ecc[1] = {0}; + if (wh_Client_EccMakeCacheKeyAndExportPublic( + ctx, 32, ECC_SECP256R1, &keyId, WH_NVM_FLAGS_EPHEMERAL, 0, NULL, + ecc) != WH_ERROR_BADARGS || + wh_Client_EccMakeCacheKeyAndExportPublic( + NULL, 32, ECC_SECP256R1, &keyId, WH_NVM_FLAGS_NONE, 0, NULL, + ecc) != WH_ERROR_BADARGS || + wh_Client_EccMakeCacheKeyAndExportPublic( + ctx, 32, ECC_SECP256R1, NULL, WH_NVM_FLAGS_NONE, 0, NULL, + ecc) != WH_ERROR_BADARGS || + wh_Client_EccMakeCacheKeyAndExportPublic( + ctx, 32, ECC_SECP256R1, &keyId, WH_NVM_FLAGS_NONE, 0, NULL, + NULL) != WH_ERROR_BADARGS) { + WH_ERROR_PRINT("ECC cache-export arg validation failed\n"); + ret = -1; + } + } +#endif +#ifdef HAVE_CURVE25519 + if (ret == 0) { + curve25519_key cv[1] = {0}; + if (wh_Client_Curve25519MakeCacheKeyAndExportPublic( + ctx, CURVE25519_KEYSIZE, &keyId, WH_NVM_FLAGS_EPHEMERAL, NULL, 0, + cv) != WH_ERROR_BADARGS || + wh_Client_Curve25519MakeCacheKeyAndExportPublic( + ctx, CURVE25519_KEYSIZE, NULL, WH_NVM_FLAGS_NONE, NULL, 0, + cv) != WH_ERROR_BADARGS || + wh_Client_Curve25519MakeCacheKeyAndExportPublic( + ctx, CURVE25519_KEYSIZE, &keyId, WH_NVM_FLAGS_NONE, NULL, 0, + NULL) != WH_ERROR_BADARGS) { + WH_ERROR_PRINT("Curve25519 cache-export arg validation failed\n"); + ret = -1; + } + } +#endif +#ifdef HAVE_ED25519 + if (ret == 0) { + ed25519_key ed[1] = {0}; + if (wh_Client_Ed25519MakeCacheKeyAndExportPublic( + ctx, &keyId, WH_NVM_FLAGS_EPHEMERAL, 0, NULL, ed) != + WH_ERROR_BADARGS || + wh_Client_Ed25519MakeCacheKeyAndExportPublic( + ctx, NULL, WH_NVM_FLAGS_NONE, 0, NULL, ed) != WH_ERROR_BADARGS || + wh_Client_Ed25519MakeCacheKeyAndExportPublic( + ctx, &keyId, WH_NVM_FLAGS_NONE, 0, NULL, NULL) != + WH_ERROR_BADARGS) { + WH_ERROR_PRINT("Ed25519 cache-export arg validation failed\n"); + ret = -1; + } + } +#endif +#ifdef WOLFSSL_MLDSA_PUBLIC_KEY + if (ret == 0) { + wc_MlDsaKey mldsa[1] = {0}; + if (wh_Client_MlDsaMakeCacheKeyAndExportPublic( + ctx, 0, 0, &keyId, WH_NVM_FLAGS_EPHEMERAL, 0, NULL, mldsa) != + WH_ERROR_BADARGS || + wh_Client_MlDsaMakeCacheKeyAndExportPublic( + ctx, 0, 0, NULL, WH_NVM_FLAGS_NONE, 0, NULL, mldsa) != + WH_ERROR_BADARGS || + wh_Client_MlDsaMakeCacheKeyAndExportPublic( + ctx, 0, 0, &keyId, WH_NVM_FLAGS_NONE, 0, NULL, NULL) != + WH_ERROR_BADARGS) { + WH_ERROR_PRINT("ML-DSA cache-export arg validation failed\n"); + ret = -1; + } +#ifdef WOLFHSM_CFG_DMA + if (ret == 0 && + (wh_Client_MlDsaMakeCacheKeyDma( + ctx, 0, &keyId, WH_NVM_FLAGS_EPHEMERAL, 0, NULL, mldsa) != + WH_ERROR_BADARGS || + wh_Client_MlDsaMakeCacheKeyDma( + ctx, 0, NULL, WH_NVM_FLAGS_NONE, 0, NULL, mldsa) != + WH_ERROR_BADARGS || + wh_Client_MlDsaMakeCacheKeyDma( + ctx, 0, &keyId, WH_NVM_FLAGS_NONE, 0, NULL, NULL) != + WH_ERROR_BADARGS)) { + WH_ERROR_PRINT("ML-DSA DMA cache-export arg validation failed\n"); + ret = -1; + } +#endif /* WOLFHSM_CFG_DMA */ + } +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ +#ifdef WOLFSSL_HAVE_MLKEM + if (ret == 0) { + MlKemKey mlkem[1] = {0}; + if (wh_Client_MlKemMakeCacheKeyAndExportPublic( + ctx, 0, &keyId, WH_NVM_FLAGS_EPHEMERAL, 0, NULL, mlkem) != + WH_ERROR_BADARGS || + wh_Client_MlKemMakeCacheKeyAndExportPublic( + ctx, 0, NULL, WH_NVM_FLAGS_NONE, 0, NULL, mlkem) != + WH_ERROR_BADARGS || + wh_Client_MlKemMakeCacheKeyAndExportPublic( + ctx, 0, &keyId, WH_NVM_FLAGS_NONE, 0, NULL, NULL) != + WH_ERROR_BADARGS) { + WH_ERROR_PRINT("ML-KEM cache-export arg validation failed\n"); + ret = -1; + } +#ifdef WOLFHSM_CFG_DMA + if (ret == 0 && + (wh_Client_MlKemMakeCacheKeyDma( + ctx, 0, &keyId, WH_NVM_FLAGS_EPHEMERAL, 0, NULL, mlkem) != + WH_ERROR_BADARGS || + wh_Client_MlKemMakeCacheKeyDma( + ctx, 0, NULL, WH_NVM_FLAGS_NONE, 0, NULL, mlkem) != + WH_ERROR_BADARGS || + wh_Client_MlKemMakeCacheKeyDma( + ctx, 0, &keyId, WH_NVM_FLAGS_NONE, 0, NULL, NULL) != + WH_ERROR_BADARGS)) { + WH_ERROR_PRINT("ML-KEM DMA cache-export arg validation failed\n"); + ret = -1; + } +#endif /* WOLFHSM_CFG_DMA */ + } +#endif /* WOLFSSL_HAVE_MLKEM */ + + if (ret == 0) { + WH_TEST_PRINT("KEYGEN-EXPORT-PUBLIC ARG VALIDATION SUCCESS\n"); + } + return ret; +} + /* WH_TEST_DMA_MODE_CNT (number of cryptoCb dispatch modes to exercise) is * provided by wh_test_common.h */ @@ -16812,9 +17245,13 @@ int whTest_CryptoClientConfig(whClientConfig* config) #endif /* WOLFHSM_CFG_DMA */ #endif /* WOLFSSL_CMAC && !NO_AES && WOLFSSL_AES_DIRECT */ -#ifndef NO_RSA /* Once-run public-key tests use the std (non-DMA) dispatch mode. */ (void)wh_Client_SetDmaMode(client, 0); + if (ret == 0) { + ret = whTest_CryptoMakeCacheKeyExportPublicArgs(client); + } + +#ifndef NO_RSA if (ret == 0) { ret = whTest_CryptoRsa(client, WH_CLIENT_DEVID(client), rng); } @@ -17214,6 +17651,14 @@ int whTest_CryptoClientConfig(whClientConfig* config) WH_ERROR_PRINT("ML-KEM export-public test failed: %d\n", ret); } } + if (ret == 0) { + ret = whTestCrypto_MlKemCacheKeyAndExportPublic( + client, WH_CLIENT_DEVID(client), rng); + if (ret != 0) { + WH_ERROR_PRINT( + "ML-KEM cache-and-export-public test failed: %d\n", ret); + } + } #ifdef WOLFHSM_CFG_DMA (void)wh_Client_SetDmaMode(client, 1); @@ -17227,6 +17672,14 @@ int whTest_CryptoClientConfig(whClientConfig* config) WH_ERROR_PRINT("ML-KEM export-public DMA test failed: %d\n", ret); } } + if (ret == 0) { + ret = whTestCrypto_MlKemCacheKeyAndExportPublicDma( + client, WH_CLIENT_DEVID(client), rng); + if (ret != 0) { + WH_ERROR_PRINT( + "ML-KEM cache-and-export-public DMA test failed: %d\n", ret); + } + } #endif /* WOLFHSM_CFG_DMA */ #endif /* !defined(WOLFSSL_MLKEM_NO_MAKE_KEY) && \ !defined(WOLFSSL_MLKEM_NO_ENCAPSULATE) && \ diff --git a/wolfhsm/wh_client_crypto.h b/wolfhsm/wh_client_crypto.h index 46ff4b610..4de153da1 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -3274,6 +3274,19 @@ int wh_Client_MlKemMakeCacheKey(whClientContext* ctx, int level, whKeyId* inout_key_id, whNvmFlags flags, uint16_t label_len, uint8_t* label); +/* Generate an ML-KEM key on the server, cache it, and return its public key in + * one round-trip (avoids a separate wh_Client_MlKemExportPublicKey call). On + * success inout_key_id holds the cached keyId and pub is populated with the + * public key and associated with that keyId; the caller must still set the HSM + * devId on pub before follow-on operations route to the server. flags must not + * include WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). */ +int wh_Client_MlKemMakeCacheKeyAndExportPublic(whClientContext* ctx, int level, + whKeyId* inout_key_id, + whNvmFlags flags, + uint16_t label_len, + const uint8_t* label, + MlKemKey* pub); + /** * @brief Perform ML-KEM encapsulation using a server-cached public key. * @@ -3381,6 +3394,16 @@ int wh_Client_MlKemExportPublicKeyDma(whClientContext* ctx, whKeyId keyId, int wh_Client_MlKemMakeExportKeyDma(whClientContext* ctx, int level, MlKemKey* key); +/* DMA variant: generate an ML-KEM key on the server, cache it, and return its + * public key into pub (associated with the cached keyId), avoiding a separate + * wh_Client_MlKemExportPublicKeyDma call. The caller must still set the HSM + * devId on pub before follow-on operations route to the server. flags must not + * include WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). */ +int wh_Client_MlKemMakeCacheKeyDma(whClientContext* ctx, int level, + whKeyId* inout_key_id, whNvmFlags flags, + uint16_t label_len, const uint8_t* label, + MlKemKey* pub); + /** * @brief Perform ML-KEM encapsulation using DMA. *