From 36a5956ed7d1c56699d18b25125d348581e80d65 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 | 56 ++++++++- src/wh_server_crypto.c | 19 ++- .../client-server/wh_test_crypto_rsa.c | 110 ++++++++++++++++++ test/wh_test_crypto.c | 94 +++++++++++++++ wolfhsm/wh_client_crypto.h | 33 ++++++ 5 files changed, 309 insertions(+), 3 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index d11e64609..e078c34f1 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,58 @@ 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 and stamp the + * client's HSM devId so pub is immediately usable both as the + * exported public key and as a handle to the cached private key, + * without the caller re-initializing it. */ + wh_Client_RsaSetKeyId(pub, key_id); + pub->devId = WH_CLIENT_DEVID(ctx); + } + else if (!WH_KEYID_ISERASED(key_id)) { + /* The server committed a key but the best-effort export returned no + * usable public key (empty response body when it did not fit, or a + * client-side deserialize failure). Roll back so the operation is + * atomic and no cache slot is orphaned. key_id is only set from a + * parsed server response, so it is non-erased only when a key was + * actually committed - safe even when the caller supplied an + * explicit keyId. */ + (void)wh_Client_KeyEvict(ctx, key_id); + *inout_key_id = WH_KEYID_ERASED; + } + } + 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..12f5ad40e 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -375,9 +375,26 @@ 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 public key export: when the serialized + * public key fits in the response body, return it so the + * client can skip a separate ExportPublicKey call. When it + * does not fit (small comm buffer or a large key), leave the + * body empty and keep the cached key. Plain MakeCacheKey + * callers ignore the body and see no regression; + * MakeCacheKeyAndExportPublic callers detect the empty body + * and evict the key themselves. */ + int pub_ret = wc_RsaKeyToPublicDer(rsa, out, max_size); + if (pub_ret > 0) { + der_size = (uint16_t)pub_ret; + } + else { + der_size = 0; + } + } 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..cab778202 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,114 @@ 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] = {0}; + 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; + } + } + } + } + + /* Encrypt locally with the independently exported public key (refPub) the + * client holds. */ + if (ret == 0) { + encLen = wc_RsaPublicEncrypt((byte*)plainText, sizeof(plainText), + (byte*)cipherText, sizeof(cipherText), + refPub, rng); + if (encLen < 0) { + WH_ERROR_PRINT("PublicEncrypt with keygen pub failed %d\n", encLen); + ret = encLen; + } + } + + /* Decrypt on the HSM using genPub directly as the HSM private-key handle + * (no separate key object). */ + if (ret == 0) { + decLen = wc_RsaPrivateDecrypt((byte*)cipherText, encLen, + (byte*)finalText, sizeof(finalText), + genPub); + 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(refPub); + (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 +498,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..ce7af0492 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -721,6 +721,100 @@ 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 locally with the + * exported public key (refPub) the client holds, then decrypt on the + * HSM using genPub directly as the private-key handle (no separate key + * object). */ + if (ret == 0) { + int encLen = wc_RsaPublicEncrypt( + (byte*)plainText, sizeof(plainText), (byte*)cipherText, + sizeof(cipherText), refPub, rng); + if (encLen < 0) { + WH_ERROR_PRINT("PublicEncrypt with keygen pub failed %d\n", + encLen); + ret = encLen; + } + else { + int decLen = wc_RsaPrivateDecrypt( + (byte*)cipherText, encLen, (byte*)finalText, + sizeof(finalText), genPub); + 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; + } + } + } + + 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..e998b73bd 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -1082,6 +1082,39 @@ int wh_Client_RsaMakeCacheKey(whClientContext* ctx, whKeyId* inout_key_id, whNvmFlags flags, uint32_t label_len, uint8_t* label); +/** + * @brief Generate an RSA key in the server key cache and return its public key + * in one round-trip. + * + * Combines a cache keygen and a public-key export so the client avoids a + * separate wh_Client_RsaExportPublicKey call. On success inout_key_id holds the + * cached keyId and pub is populated with the public key, associated with that + * keyId, and stamped with the client's HSM devId, so it is immediately usable + * both as the exported public key and as a handle to the cached private key. + * + * @param[in] ctx Pointer to the client context. + * @param[in] size Size of the key to generate in bits (e.g. 2048). + * @param[in] e Public exponent to use (e.g. WC_RSA_EXPONENT / 65537). + * @param[in,out] inout_key_id Set to WH_KEYID_ERASED to have the server select + * a unique id for this key. + * @param[in] flags Optional flags to associate with the key. Must not include + * WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + * @param[in] label_len Size of the label up to WH_NVM_LABEL_LEN. Set to 0 if + * not used. + * @param[in] label Optional label to associate with the key. Set to NULL if not + * used. + * @param[out] pub Key struct populated with the returned public key. + * @return int Returns 0 on success or a negative error code on failure. + * @note pub is stamped with the HSM devId, so follow-on wolfCrypt operations + * route to the server. Its public-key material is populated for local + * encoding (e.g. wc_*PublicKeyToDer); to use pub for a purely-local + * public-key operation, reset pub->devId = INVALID_DEVID first. + */ +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 c0659650aea207f1ec6ccfd598010dc441083017 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 | 70 ++++++++++-- src/wh_server_crypto.c | 30 +++--- .../client-server/wh_test_crypto_ecc.c | 102 ++++++++++++++++++ test/wh_test_crypto.c | 97 +++++++++++++++++ wolfhsm/wh_client_crypto.h | 33 ++++++ 5 files changed, 312 insertions(+), 20 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index e078c34f1..3d979c578 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,14 +2021,17 @@ 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); uint16_t der_size = (uint16_t)(res->len); - /* Ephemeral key — leave devCtx ERASED */ + /* Leave devCtx ERASED here: ephemeral keygen keeps it that way, + * and the cache-and-export wrapper overwrites it with the cached + * keyId after this returns. */ wh_Client_EccSetKeyId(out_key, WH_KEYID_ERASED); ret = wh_Crypto_EccDeserializeKeyDer(key_der, der_size, out_key); @@ -2036,8 +2039,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 +2107,58 @@ 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 and stamp the + * client's HSM devId so pub is immediately usable both as the + * exported public key and as a handle to the cached private key, + * without the caller re-initializing it. */ + wh_Client_EccSetKeyId(pub, key_id); + pub->devId = WH_CLIENT_DEVID(ctx); + } + else if (!WH_KEYID_ISERASED(key_id)) { + /* The server committed a key but the best-effort export returned no + * usable public key (empty response body when it did not fit, or a + * client-side deserialize failure). Roll back so the operation is + * atomic and no cache slot is orphaned. key_id is only set from a + * parsed server response, so it is non-erased only when a key was + * actually committed - safe even when the caller supplied an + * explicit keyId. */ + (void)wh_Client_KeyEvict(ctx, key_id); + *inout_key_id = WH_KEYID_ERASED; + } + } + 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 12f5ad40e..5c4f2c175 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -1218,13 +1218,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 @@ -1245,11 +1238,24 @@ 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 public key export: when the serialized + * public key fits in the response body, return it so the + * client can skip a separate ExportPublicKey call. When it + * does not fit (small comm buffer or a large key), leave the + * body empty and keep the cached key. Plain MakeCacheKey + * callers ignore the body and see no regression; + * MakeCacheKeyAndExportPublic callers detect the empty body + * and evict the key themselves. */ + int pub_ret = + wc_EccPublicKeyToDer(key, res_out, max_size, 1); + if (pub_ret > 0) { + res_size = (uint16_t)pub_ret; + } + else { + res_size = 0; + } + } } } 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..7be8fa082 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,106 @@ 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] = {0}; + 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; + } + } + } + } + + /* Sign on the server using genPub directly as the HSM private-key handle + * (no separate key object), then verify locally with the independently + * exported public key (refPub) the client holds. */ + if (ret == 0) { + ret = wc_ecc_sign_hash(hash, sizeof(hash), sig, &sigLen, rng, genPub); + if (ret != 0) { + WH_ERROR_PRINT("HSM ECC sign failed %d\n", ret); + } + } + if (ret == 0) { + ret = wc_ecc_verify_hash(sig, sigLen, hash, sizeof(hash), &verify, + refPub); + 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(refPub); + 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 +1843,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 ce7af0492..651c11eb1 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -1576,6 +1576,103 @@ 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 using genPub directly as the + * private-key handle (no separate key object), then verify locally with + * the exported public key (refPub) the client holds. */ + if (ret == 0) { + ret = wc_ecc_sign_hash(hash, sizeof(hash), sig, &sigLen, rng, + genPub); + if (ret != 0) { + WH_ERROR_PRINT("HSM ECC sign failed %d\n", ret); + } + } + if (ret == 0) { + ret = wc_ecc_verify_hash(sig, sigLen, hash, sizeof(hash), &verify, + refPub); + 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 e998b73bd..8278fbb97 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -511,6 +511,39 @@ int wh_Client_EccMakeCacheKey(whClientContext* ctx, whKeyId *inout_key_id, whNvmFlags flags, uint16_t label_len, uint8_t* label); +/** + * @brief Generate an ECC key pair in the server key cache and return its public + * key in one round-trip. + * + * Combines a cache keygen and a public-key export so the client avoids a + * separate wh_Client_EccExportPublicKey call. On success inout_key_id holds the + * cached keyId and pub is populated with the public key, associated with that + * keyId, and stamped with the client's HSM devId, so it is immediately usable + * both as the exported public key and as a handle to the cached private key. + * + * @param[in] ctx Pointer to the client context. + * @param[in] size Size of the key to generate in bytes (e.g. 32 for P-256). + * @param[in] curveId wolfCrypt curve identifier (e.g. ECC_SECP256R1). + * @param[in,out] inout_key_id Set to WH_KEYID_ERASED to have the server select + * a unique id for this key. Must not be NULL. + * @param[in] flags Optional flags to associate with the key. Must not include + * WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + * @param[in] label_len Size of the label up to WH_NVM_LABEL_LEN. Set to 0 if + * not used. + * @param[in] label Optional label to associate with the key. Set to NULL if not + * used. + * @param[out] pub Key struct populated with the returned public key. + * @return int Returns 0 on success or a negative error code on failure. + * @note pub is stamped with the HSM devId, so follow-on wolfCrypt operations + * route to the server. Its public-key material is populated for local + * encoding (e.g. wc_*PublicKeyToDer); to use pub for a purely-local + * public-key operation, reset pub->devId = INVALID_DEVID first. + */ +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 58cfa8cdf7e519b01ee1b21121561964be6730f1 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 | 66 +++++++++- src/wh_server_crypto.c | 21 ++++ .../client-server/wh_test_crypto_curve25519.c | 113 ++++++++++++++++++ test/wh_test_crypto.c | 111 +++++++++++++++++ wolfhsm/wh_client_crypto.h | 34 ++++++ 5 files changed, 340 insertions(+), 5 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index 3d979c578..902dd550b 100644 --- a/src/wh_client_crypto.c +++ b/src/wh_client_crypto.c @@ -3075,13 +3075,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, @@ -3105,6 +3118,49 @@ 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) { + /* Stamp the cached keyId and the client's HSM devId so pub is + * immediately usable as a handle to the cached private key. The keyId + * is set here (not only inside _Curve25519MakeKey) because a public-key + * deserialize that retries parameter sets can re-init pub and clear + * it. */ + wh_Client_Curve25519SetKeyId(pub, *inout_key_id); + pub->devId = WH_CLIENT_DEVID(ctx); + } + else if (!WH_KEYID_ISERASED(*inout_key_id) && + (WH_KEYID_ISERASED(in_keyId) || (ret == WH_ERROR_ABORTED))) { + /* The server committed a key but the best-effort export returned no + * public key (empty response body when it did not fit). Roll back so the + * operation is atomic and no cache slot is orphaned. A non-DMA keygen + * only yields WH_ERROR_ABORTED after the server has committed and + * returned the keyId, so evicting is safe even when the caller supplied + * an explicit keyId. */ + (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 5c4f2c175..615710e9f 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -2084,6 +2084,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)) { @@ -2104,6 +2107,24 @@ 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 public key export: when the serialized + * public key fits in the response body, return it so the + * client can skip a separate ExportPublicKey call. When it + * does not fit (small comm buffer or a large key), leave the + * body empty and keep the cached key. Plain MakeCacheKey + * callers ignore the body and see no regression; + * MakeCacheKeyAndExportPublic callers detect the empty body + * and evict the key themselves. */ + int pub_ret = + wc_Curve25519PublicKeyToDer(key, out, max_size, 1); + if (pub_ret > 0) { + ser_size = (uint16_t)pub_ret; + } + else { + ser_size = 0; + } + } } } 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..0bd8ea6eb 100644 --- a/test-refactor/client-server/wh_test_crypto_curve25519.c +++ b/test-refactor/client-server/wh_test_crypto_curve25519.c @@ -355,10 +355,123 @@ 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 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 using genPub directly as the HSM private-key + * handle (no separate key object): our local private key * genPub's + * exported public key (computed locally) must equal genPub's HSM private + * key * our local public key (computed on the server). */ + 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) { + secLen = sizeof(sharedHsm); + ret = wc_curve25519_shared_secret(genPub, localKey, sharedHsm, &secLen); + if (ret != 0) { + WH_ERROR_PRINT("HSM Curve25519 shared secret failed %d\n", ret); + } + } + 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 651c11eb1..12fa9c2da 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -4409,6 +4409,109 @@ 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 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; + (void)devId; + + 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 using genPub directly as the HSM private-key + * handle (no separate key object): our local private key * genPub's + * exported public key (computed locally) must equal genPub's HSM private + * key * our local public key (computed on the server). */ + 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) { + len = sizeof(shared_hsm); + ret = wc_curve25519_shared_secret(genPub, localKey, shared_hsm, &len); + if (ret != 0) { + WH_ERROR_PRINT("HSM Curve25519 shared secret failed %d\n", ret); + } + } + 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 @@ -16487,6 +16590,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 8278fbb97..0d4e2ac1e 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -280,6 +280,40 @@ int wh_Client_Curve25519MakeCacheKey(whClientContext* ctx, whKeyId *inout_key_id, whNvmFlags flags, const uint8_t* label, uint16_t label_len); +/** + * @brief Generate a Curve25519 key in the server key cache and return its + * public key in one round-trip. + * + * Combines a cache keygen and a public-key export so the client avoids a + * separate wh_Client_Curve25519ExportPublicKey call. On success inout_key_id + * holds the cached keyId and pub is populated with the public key, associated + * with that keyId, and stamped with the client's HSM devId, so it is + * immediately usable both as the exported public key and as a handle to the + * cached private key. + * + * @param[in] ctx Pointer to the client context. + * @param[in] size Size of the key to generate in bytes, normally set to + * CURVE25519_KEY_SIZE. + * @param[in,out] inout_key_id Set to WH_KEYID_ERASED to have the server select + * a unique id for this key. + * @param[in] flags Optional flags to associate with the key. Must not include + * WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + * @param[in] label Optional label to associate with the key. Set to NULL if not + * used. + * @param[in] label_len Size of the label up to WH_NVM_LABEL_LEN. Set to 0 if + * not used. + * @param[out] pub Key struct populated with the returned public key. + * @return int Returns 0 on success or a negative error code on failure. + * @note pub is stamped with the HSM devId, so follow-on wolfCrypt operations + * route to the server. Its public-key material is populated for local + * encoding (e.g. wc_*PublicKeyToDer); to use pub for a purely-local + * public-key operation, reset pub->devId = INVALID_DEVID first. + */ +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 3b4536d0021e6dd60b8de655963ceda22ac65784 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 | 56 ++++++++++- src/wh_server_crypto.c | 18 ++++ .../client-server/wh_test_crypto_ed25519.c | 92 ++++++++++++++++++ test/wh_test_crypto.c | 95 +++++++++++++++++++ wolfhsm/wh_client_crypto.h | 34 +++++++ 5 files changed, 292 insertions(+), 3 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index 902dd550b..5c655fdfb 100644 --- a/src/wh_client_crypto.c +++ b/src/wh_client_crypto.c @@ -3514,8 +3514,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; @@ -3586,7 +3586,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) + @@ -3596,6 +3599,9 @@ static int _Ed25519MakeKey(whClientContext* ctx, whKeyId* inout_key_id, } ret = wh_Crypto_Ed25519DeserializeKeyDer(out, outSz, key); } + else { + ret = WH_ERROR_ABORTED; + } } } @@ -3622,6 +3628,50 @@ 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) { + /* Stamp the cached keyId and the client's HSM devId so pub is + * immediately usable as a handle to the cached private key. The keyId + * is set here (not only inside _Ed25519MakeKey) because a public-key + * deserialize that retries parameter sets can re-init pub and clear + * it. */ + wh_Client_Ed25519SetKeyId(pub, *inout_key_id); + pub->devId = WH_CLIENT_DEVID(ctx); + } + else if (!WH_KEYID_ISERASED(*inout_key_id) && + (WH_KEYID_ISERASED(in_keyId) || (ret == WH_ERROR_ABORTED))) { + /* The server committed a key but the best-effort export returned no + * public key (empty response body when it did not fit). Roll back so the + * operation is atomic and no cache slot is orphaned. A non-DMA keygen + * only yields WH_ERROR_ABORTED after the server has committed and + * returned the keyId, so evicting is safe even when the caller supplied + * an explicit keyId. */ + (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 615710e9f..301c83f93 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -2341,6 +2341,24 @@ 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 public key export: when the serialized + * public key fits in the response body, return it so the + * client can skip a separate ExportPublicKey call. When it + * does not fit (small comm buffer or a large key), leave the + * body empty and keep the cached key. Plain MakeCacheKey + * callers ignore the body and see no regression; + * MakeCacheKeyAndExportPublic callers detect the empty body + * and evict the key themselves. */ + int pub_ret = + wc_Ed25519PublicKeyToDer(key, res_out, max_size, 1); + if (pub_ret > 0) { + ser_size = (uint16_t)pub_ret; + } + else { + ser_size = 0; + } + } } } 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..a3eb56921 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,96 @@ 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}; + 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; + } + } + } + } + + /* Sign on the server using genPub directly as the HSM private-key handle + * (no separate key object), then verify locally with the independently + * exported public key (refPub) the client holds. */ + if (ret == 0) { + ret = wh_Client_Ed25519Sign(ctx, genPub, 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); + } + } + if (ret == 0) { + ret = wc_ed25519_verify_msg(sig, sigSz, msg, (word32)sizeof(msg), + &verified, refPub); + 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); + } + + 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 +735,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 12fa9c2da..3388c212d 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -3710,6 +3710,93 @@ 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}; + 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 using genPub directly as the private-key + * handle (no separate key object), then verify locally with the exported + * public key (refPub) the client holds. */ + if (ret == 0) { + ret = wh_Client_Ed25519Sign(ctx, genPub, 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); + } + } + if (ret == 0) { + ret = wc_ed25519_verify_msg(sig, sigSz, msg, (word32)sizeof(msg), + &verified, refPub); + 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) { @@ -16558,6 +16645,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 0d4e2ac1e..96af81523 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -1005,6 +1005,40 @@ 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 in the server key cache and return its public + * key in one round-trip. + * + * Combines a cache keygen and a public-key export so the client avoids a + * separate wh_Client_Ed25519ExportPublicKey call. On success inout_key_id holds + * the cached keyId and pub is populated with the public key, associated with + * that keyId, and stamped with the client's HSM devId, so it is immediately + * usable both as the exported public key and as a handle to the cached private + * key. + * + * @param[in] ctx Pointer to the client context. + * @param[in,out] inout_key_id Set to WH_KEYID_ERASED to have the server select + * a unique id for this key. + * @param[in] flags Optional flags to associate with the key. Must not include + * WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + * @param[in] label_len Size of the label up to WH_NVM_LABEL_LEN. Set to 0 if + * not used. + * @param[in] label Optional label to associate with the key. Set to NULL if not + * used. + * @param[out] pub Key struct populated with the returned public key. + * @return int Returns 0 on success or a negative error code on failure. + * @note pub is stamped with the HSM devId, so follow-on wolfCrypt operations + * route to the server. Its public-key material is populated for local + * encoding (e.g. wc_*PublicKeyToDer); to use pub for a purely-local + * public-key operation, reset pub->devId = INVALID_DEVID first. + */ +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 35bfcf16de2b0f6ede38643b1c3ad40f47b488eb 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 | 151 +++++++++++-- src/wh_server_crypto.c | 65 +++++- .../client-server/wh_test_crypto_mldsa.c | 204 +++++++++++++++++ test/wh_test_crypto.c | 211 ++++++++++++++++++ wolfhsm/wh_client_crypto.h | 74 ++++++ 5 files changed, 682 insertions(+), 23 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index 5c655fdfb..282586ac5 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 */ @@ -9613,7 +9613,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; @@ -9696,13 +9696,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( @@ -9733,6 +9748,52 @@ int wh_Client_MlDsaMakeCacheKey(whClientContext* ctx, int size, int level, label, NULL); } +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) { + /* Stamp the cached keyId and the client's HSM devId so pub is + * immediately usable as a handle to the cached private key. The keyId + * is set here (not only inside _MlDsaMakeKey) because a public-key + * deserialize that retries parameter sets can re-init pub and clear + * it. */ + wh_Client_MlDsaSetKeyId(pub, *inout_key_id); + pub->devId = WH_CLIENT_DEVID(ctx); + } + else if (!WH_KEYID_ISERASED(*inout_key_id) && + (WH_KEYID_ISERASED(in_keyId) || (ret == WH_ERROR_ABORTED))) { + /* The server committed a key but the best-effort export returned no + * public key (empty response body when it did not fit). Roll back so the + * operation is atomic and no cache slot is orphaned. A non-DMA keygen + * only yields WH_ERROR_ABORTED after the server has committed and + * returned the keyId, so evicting is safe even when the caller supplied + * an explicit keyId. */ + (void)wh_Client_KeyEvict(ctx, *inout_key_id); + *inout_key_id = WH_KEYID_ERASED; + } + return ret; +} + int wh_Client_MlDsaMakeExportKey(whClientContext* ctx, int level, int size, wc_MlDsaKey* key) { @@ -10119,7 +10180,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; @@ -10224,20 +10285,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); } } } @@ -10261,6 +10327,51 @@ int wh_Client_MlDsaMakeExportKeyDma(whClientContext* ctx, int level, 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) { + /* Stamp the cached keyId and the client's HSM devId so pub is + * immediately usable as a handle to the cached private key. The keyId + * is set here (not only inside _MlDsaMakeKeyDma) because a public-key + * deserialize that retries parameter sets can re-init pub and clear + * it. */ + wh_Client_MlDsaSetKeyId(pub, *inout_key_id); + pub->devId = WH_CLIENT_DEVID(ctx); + } + else if (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. + * Unlike the non-DMA ...AndExportPublic wrapper, this deliberately does + * NOT also roll back a caller-supplied explicit keyId via a + * ret == WH_ERROR_ABORTED check: the DMA keygen handler can itself + * return WH_ERROR_ABORTED before committing a key, so that discriminator + * is not safe here. In practice the client DMA buffer is always sized + * >= the public key and the server self-evicts on its own serialize/DMA + * failures, so the explicit-keyId orphan case is not reachable. */ + (void)wh_Client_KeyEvict(ctx, *inout_key_id); + *inout_key_id = WH_KEYID_ERASED; + } + return ret; +} + 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 301c83f93..6698024a2 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -4863,6 +4863,27 @@ 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 public key export: when the + * serialized public key fits in the response body, + * return it so the client can skip a separate + * ExportPublicKey call. When it does not fit (small + * comm buffer or a large key), leave the body empty + * and keep the cached key. Plain MakeCacheKey callers + * ignore the body and see no regression; + * MakeCacheKeyAndExportPublic callers detect the + * empty body and evict the key themselves. */ + int pub_ret = wc_MlDsaKey_PublicKeyToDer( + key, res_out, max_size, 1); + if (pub_ret > 0) { + res_size = (uint16_t)pub_ret; + } + else { + res_size = 0; + } + } +#endif /* WOLFSSL_MLDSA_PUBLIC_KEY */ } } } @@ -6446,6 +6467,8 @@ static int _HandleMlDsaKeyGenDma(whServerContext* ctx, uint16_t magic, whMessageCrypto_MlDsaKeyGenDmaRequest req; whMessageCrypto_MlDsaKeyGenDmaResponse res; + memset(&res, 0, sizeof(res)); + if (inSize < sizeof(whMessageCrypto_MlDsaKeyGenDmaRequest)) { return WH_ERROR_BADARGS; } @@ -6520,11 +6543,47 @@ 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 + /* Stream the public key back through the client's DMA + * buffer so it gets the pubkey without a separate + * ExportPublicKey call. A freshly generated key must + * serialize, so treat a failure as fatal: evict the + * just-committed key and propagate the error rather + * than returning a keyId with no public 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; + } + else { + ret = (pub_ret < 0) ? pub_ret + : WH_ERROR_ABORTED; + } + (void)wh_Server_DmaProcessClientAddress( + ctx, req.key.addr, &clientOutAddr, keySize, + WH_DMA_OPER_CLIENT_WRITE_POST, + (whServerDmaFlags){0}); + } + else { + ret = rc; + } + if (ret != 0) { + (void)wh_Server_KeystoreEvictKey(ctx, keyId); } } +#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..9c35e6519 100644 --- a/test-refactor/client-server/wh_test_crypto_mldsa.c +++ b/test-refactor/client-server/wh_test_crypto_mldsa.c @@ -237,6 +237,104 @@ 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}; + 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 using genPub directly as the HSM private-key handle + * (no separate key object), then verify with its exported public key. */ + if (ret == 0) { + ret = wh_Client_MlDsaSign(ctx, msg, sizeof(msg), sig, &sigLen, genPub, + NULL, 0, WC_HASH_TYPE_NONE); + if (ret != 0) { + WH_ERROR_PRINT("HSM ML-DSA sign failed %d\n", ret); + } + } + 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 +604,106 @@ 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}; + 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) using genPub directly as the HSM private-key + * handle (no separate key object), then verify with its exported public + * key. */ + if (ret == 0) { + ret = wh_Client_MlDsaSignDma(ctx, msg, sizeof(msg), sig, &sigLen, + genPub, NULL, 0, WC_HASH_TYPE_NONE); + if (ret != 0) { + WH_ERROR_PRINT("HSM ML-DSA DMA sign failed %d\n", ret); + } + } + 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 +1339,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 3388c212d..485cd3fb9 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -12592,6 +12592,101 @@ 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}; + 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 using genPub directly as the private-key + * handle (no separate key object), then verify with its exported public + * key. */ + if (ret == 0) { + ret = wh_Client_MlDsaSign(ctx, msg, sizeof(msg), sig, &sigLen, genPub, + NULL, 0, WC_HASH_TYPE_NONE); + if (ret != 0) { + WH_ERROR_PRINT("HSM ML-DSA sign failed %d\n", ret); + } + } + 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 @@ -12932,6 +13027,102 @@ 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}; + 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) using genPub directly as the + * private-key handle (no separate key object), then verify with its + * exported public key. */ + if (ret == 0) { + ret = wh_Client_MlDsaSignDma(ctx, msg, sizeof(msg), sig, &sigLen, + genPub, NULL, 0, WC_HASH_TYPE_NONE); + if (ret != 0) { + WH_ERROR_PRINT("HSM ML-DSA DMA sign failed %d\n", ret); + } + } + 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 */ @@ -16886,6 +17077,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 @@ -16905,6 +17106,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 96af81523..f51f9b0b3 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -3012,6 +3012,45 @@ 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); + +/** + * @brief Generate an ML-DSA key in the server key cache and return its public + * key in one round-trip. + * + * Combines a cache keygen and a public-key export so the client avoids a + * separate wh_Client_MlDsaExportPublicKey call. On success inout_key_id holds + * the cached keyId and pub is populated with the public key, associated with + * that keyId, and stamped with the client's HSM devId, so it is immediately + * usable both as the exported public key and as a handle to the cached private + * key. + * + * @param[in] ctx Pointer to the client context. + * @param[in] size Size of the key to generate. + * @param[in] level ML-DSA security level of the key to generate. + * @param[in,out] inout_key_id Set to WH_KEYID_ERASED to have the server select + * a unique id for this key. + * @param[in] flags Optional flags to associate with the key. Must not include + * WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + * @param[in] label_len Size of the label up to WH_NVM_LABEL_LEN. Set to 0 if + * not used. + * @param[in] label Optional label to associate with the key. Set to NULL if not + * used. + * @param[out] pub Key struct populated with the returned public key. + * @return int Returns 0 on success or a negative error code on failure. + * @note pub is stamped with the HSM devId, so follow-on wolfCrypt operations + * route to the server. Its public-key material is populated for local + * encoding (e.g. wc_*PublicKeyToDer); to use pub for a purely-local + * public-key operation, reset pub->devId = INVALID_DEVID first. + * @note The server only serves this request when WOLFSSL_MLDSA_PUBLIC_KEY is + * compiled in; otherwise the call returns an error. + */ +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); /** * @brief Sign a message using a ML-DSA private key. * @@ -3150,6 +3189,41 @@ int wh_Client_MlDsaExportPublicKeyDma(whClientContext* ctx, whKeyId keyId, int wh_Client_MlDsaMakeExportKeyDma(whClientContext* ctx, int level, wc_MlDsaKey* key); +/** + * @brief DMA variant: generate an ML-DSA key in the server key cache and return + * its public key in one round-trip. + * + * Streams the public key back through the client's DMA buffer so the client + * avoids a separate wh_Client_MlDsaExportPublicKeyDma call. On success + * inout_key_id holds the cached keyId and pub is populated with the public key, + * associated with that keyId, and stamped with the client's HSM devId, so it is + * immediately usable both as the exported public key and as a handle to the + * cached private key. + * + * @param[in] ctx Pointer to the client context. + * @param[in] level ML-DSA security level of the key to generate. + * @param[in,out] inout_key_id Set to WH_KEYID_ERASED to have the server select + * a unique id for this key. + * @param[in] flags Optional flags to associate with the key. Must not include + * WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + * @param[in] label_len Size of the label up to WH_NVM_LABEL_LEN. Set to 0 if + * not used. + * @param[in] label Optional label to associate with the key. Set to NULL if not + * used. + * @param[out] pub Key struct populated with the returned public key. + * @return int Returns 0 on success or a negative error code on failure. + * @note pub is stamped with the HSM devId, so follow-on wolfCrypt operations + * route to the server. Its public-key material is populated for local + * encoding (e.g. wc_*PublicKeyToDer); to use pub for a purely-local + * public-key operation, reset pub->devId = INVALID_DEVID first. + * @note The server only serves this request when WOLFSSL_MLDSA_PUBLIC_KEY is + * compiled in; otherwise the call returns an error. + */ +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); + /** * @brief Sign a message using ML-DSA with DMA. From f70023ab606f1b1952456af2c60f39d4ca186946 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 | 127 +++++++++-- src/wh_server_crypto.c | 62 +++++- test/wh_test_crypto.c | 435 ++++++++++++++++++++++++++++++++++++- wolfhsm/wh_client_crypto.h | 68 ++++++ 4 files changed, 675 insertions(+), 17 deletions(-) diff --git a/src/wh_client_crypto.c b/src/wh_client_crypto.c index 282586ac5..b80250785 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 */ @@ -10793,7 +10793,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; @@ -10870,7 +10870,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) + @@ -10883,6 +10886,9 @@ static int _MlKemMakeKey(whClientContext* ctx, int level, key_raw, (uint16_t)res->len, key); } } + else { + ret = WH_ERROR_ABORTED; + } } } return ret; @@ -10900,6 +10906,50 @@ 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) { + /* Stamp the cached keyId and the client's HSM devId so pub is + * immediately usable as a handle to the cached private key. The keyId + * is set here (not only inside _MlKemMakeKey) because a public-key + * deserialize that retries parameter sets can re-init pub and clear + * it. */ + wh_Client_MlKemSetKeyId(pub, *inout_key_id); + pub->devId = WH_CLIENT_DEVID(ctx); + } + else if (!WH_KEYID_ISERASED(*inout_key_id) && + (WH_KEYID_ISERASED(in_keyId) || (ret == WH_ERROR_ABORTED))) { + /* The server committed a key but the best-effort export returned no + * public key (empty response body when it did not fit). Roll back so the + * operation is atomic and no cache slot is orphaned. A non-DMA keygen + * only yields WH_ERROR_ABORTED after the server has committed and + * returned the keyId, so evicting is safe even when the caller supplied + * an explicit keyId. */ + (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) { @@ -11244,7 +11294,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; @@ -11329,16 +11379,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); } } + } } @@ -11357,6 +11413,51 @@ 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) { + /* Stamp the cached keyId and the client's HSM devId so pub is + * immediately usable as a handle to the cached private key. The keyId + * is set here (not only inside _MlKemMakeKeyDma) because a public-key + * deserialize that retries parameter sets can re-init pub and clear + * it. */ + wh_Client_MlKemSetKeyId(pub, *inout_key_id); + pub->devId = WH_CLIENT_DEVID(ctx); + } + else if (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. + * Unlike the non-DMA ...AndExportPublic wrapper, this deliberately does + * NOT also roll back a caller-supplied explicit keyId via a + * ret == WH_ERROR_ABORTED check: the DMA keygen handler can itself + * return WH_ERROR_ABORTED before committing a key, so that discriminator + * is not safe here. In practice the client DMA buffer is always sized + * >= the public key and the server self-evicts on its own serialize/DMA + * failures, so the explicit-keyId orphan case is not reachable. */ + (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 6698024a2..db2259181 100644 --- a/src/wh_server_crypto.c +++ b/src/wh_server_crypto.c @@ -5223,6 +5223,26 @@ static int _HandleMlKemKeyGen(whServerContext* ctx, uint16_t magic, int devId, req.flags, label_size, req.label); } + if (ret == WH_ERROR_OK) { + /* Best-effort public key export: when the serialized + * public key fits in the response body, return it so the + * client can skip a separate ExportPublicKey call. When it + * does not fit (small comm buffer or a large key), leave the + * body empty and keep the cached key. Plain MakeCacheKey + * callers ignore the body and see no regression; + * MakeCacheKeyAndExportPublic callers detect the empty body + * and evict the key themselves. */ + 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; + } + else { + res_size = 0; + } + } } } wc_MlKemKey_Free(key); @@ -7009,10 +7029,46 @@ 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; + } + /* Stream the public key back through the client's DMA + * buffer so it gets the pubkey without a separate + * ExportPublicKey call. A freshly generated key must + * serialize, so treat a failure as fatal: evict the + * just-committed key and propagate the error rather than + * returning a keyId with no public key. */ + if (ret == WH_ERROR_OK) { + word32 pubSize = 0; + if ((wc_MlKemKey_PublicKeySize(key, &pubSize) != 0) || + ((uint64_t)pubSize > req.key.sz)) { + ret = WH_ERROR_ABORTED; } + else { + ret = wh_Server_DmaProcessClientAddress( + ctx, req.key.addr, &clientOutAddr, pubSize, + WH_DMA_OPER_CLIENT_WRITE_PRE, + (whServerDmaFlags){0}); + if (ret == WH_ERROR_OK) { + if (wc_MlKemKey_EncodePublicKey( + key, (uint8_t*)clientOutAddr, pubSize) == + 0) { + keySize = (uint16_t)pubSize; + } + else { + ret = WH_ERROR_ABORTED; + } + (void)wh_Server_DmaProcessClientAddress( + ctx, req.key.addr, &clientOutAddr, keySize, + WH_DMA_OPER_CLIENT_WRITE_POST, + (whServerDmaFlags){0}); + } + } + if (ret != WH_ERROR_OK) { + (void)wh_Server_KeystoreEvictKey(ctx, keyId); + } + } + if (ret == WH_ERROR_OK) { + res.keyId = wh_KeyId_TranslateToClient(keyId); + res.keySize = keySize; } } } diff --git a/test/wh_test_crypto.c b/test/wh_test_crypto.c index 485cd3fb9..f4cecc100 100644 --- a/test/wh_test_crypto.c +++ b/test/wh_test_crypto.c @@ -14139,6 +14139,136 @@ 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}; + int genInited = 0; + int refInited = 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); + (void)devId; + + 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 locally with the exported public key (refPub) + * the client holds, decapsulate on the HSM using genPub directly as the + * private-key handle (no separate key object). */ + if (ret == 0) { + ret = wc_MlKemKey_CipherTextSize(refPub, &ctLen); + if (ret == 0) { + ret = wc_MlKemKey_SharedSecretSize(refPub, &ssEncLen); + } + if (ret == 0) { + ssDecLen = ssEncLen; + ret = wc_MlKemKey_Encapsulate(refPub, 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 = wh_Client_MlKemDecapsulate(ctx, genPub, 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 (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) @@ -14319,6 +14449,136 @@ 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}; + int genInited = 0; + int refInited = 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); + (void)devId; + + 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 locally with the exported public key (refPub) + * the client holds, decapsulate on the HSM using genPub directly as the + * private-key handle (no separate key object). */ + if (ret == 0) { + ret = wc_MlKemKey_CipherTextSize(refPub, &ctLen); + if (ret == 0) { + ret = wc_MlKemKey_SharedSecretSize(refPub, &ssEncLen); + } + if (ret == 0) { + ssDecLen = ssEncLen; + ret = wc_MlKemKey_Encapsulate(refPub, 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 = wh_Client_MlKemDecapsulate(ctx, genPub, 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 (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) { @@ -16593,6 +16853,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 */ @@ -16769,9 +17182,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); } @@ -17171,6 +17588,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); @@ -17184,6 +17609,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 f51f9b0b3..f213f5e8d 100644 --- a/wolfhsm/wh_client_crypto.h +++ b/wolfhsm/wh_client_crypto.h @@ -3401,6 +3401,41 @@ int wh_Client_MlKemMakeCacheKey(whClientContext* ctx, int level, whKeyId* inout_key_id, whNvmFlags flags, uint16_t label_len, uint8_t* label); +/** + * @brief Generate an ML-KEM key in the server key cache and return its public + * key in one round-trip. + * + * Combines a cache keygen and a public-key export so the client avoids a + * separate wh_Client_MlKemExportPublicKey call. On success inout_key_id holds + * the cached keyId and pub is populated with the public key, associated with + * that keyId, and stamped with the client's HSM devId, so it is immediately + * usable both as the exported public key and as a handle to the cached private + * key. + * + * @param[in] ctx Pointer to the client context. + * @param[in] level ML-KEM security level (WC_ML_KEM_512/768/1024). + * @param[in,out] inout_key_id Set to WH_KEYID_ERASED to have the server select + * a unique id for this key. + * @param[in] flags Optional flags to associate with the key. Must not include + * WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + * @param[in] label_len Size of the label up to WH_NVM_LABEL_LEN. Set to 0 if + * not used. + * @param[in] label Optional label to associate with the key. Set to NULL if not + * used. + * @param[out] pub Key struct populated with the returned public key. + * @return int Returns 0 on success or a negative error code on failure. + * @note pub is stamped with the HSM devId, so follow-on wolfCrypt operations + * route to the server. Its public-key material is populated for local + * encoding (e.g. wc_*PublicKeyToDer); to use pub for a purely-local + * public-key operation, reset pub->devId = INVALID_DEVID first. + */ +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. * @@ -3508,6 +3543,39 @@ int wh_Client_MlKemExportPublicKeyDma(whClientContext* ctx, whKeyId keyId, int wh_Client_MlKemMakeExportKeyDma(whClientContext* ctx, int level, MlKemKey* key); +/** + * @brief DMA variant: generate an ML-KEM key in the server key cache and return + * its public key in one round-trip. + * + * Streams the public key back through the client's DMA buffer so the client + * avoids a separate wh_Client_MlKemExportPublicKeyDma call. On success + * inout_key_id holds the cached keyId and pub is populated with the public key, + * associated with that keyId, and stamped with the client's HSM devId, so it is + * immediately usable both as the exported public key and as a handle to the + * cached private key. + * + * @param[in] ctx Pointer to the client context. + * @param[in] level ML-KEM security level (WC_ML_KEM_512/768/1024). + * @param[in,out] inout_key_id Set to WH_KEYID_ERASED to have the server select + * a unique id for this key. + * @param[in] flags Optional flags to associate with the key. Must not include + * WH_NVM_FLAGS_EPHEMERAL (returns WH_ERROR_BADARGS). + * @param[in] label_len Size of the label up to WH_NVM_LABEL_LEN. Set to 0 if + * not used. + * @param[in] label Optional label to associate with the key. Set to NULL if not + * used. + * @param[out] pub Key struct populated with the returned public key. + * @return int Returns 0 on success or a negative error code on failure. + * @note pub is stamped with the HSM devId, so follow-on wolfCrypt operations + * route to the server. Its public-key material is populated for local + * encoding (e.g. wc_*PublicKeyToDer); to use pub for a purely-local + * public-key operation, reset pub->devId = INVALID_DEVID first. + */ +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. *