Skip to content

Return a generated public key from cached keygen#458

Merged
bigbrett merged 6 commits into
wolfSSL:mainfrom
Frauschi:public_key_export
Jul 22, 2026
Merged

Return a generated public key from cached keygen#458
bigbrett merged 6 commits into
wolfSSL:mainfrom
Frauschi:public_key_export

Conversation

@Frauschi

Copy link
Copy Markdown
Contributor

Summary

Cached key generation previously returned only a keyId — to get the public key of a freshly generated cached key, the client had to make a second ExportPublicKey round-trip. This PR adds the new LMS/XMSS ergonomic (keygen also hands back the public key in one round-trip, introduced in #352) to the remaining public-key algorithms: RSA, ECC, Curve25519, Ed25519, ML-DSA, and ML-KEM.

The server serializes the public-only key into the existing keygen response body, so there are no new wire structures and the change is fully backward-compatible. Existing MakeCacheKey callers keep working unchanged.

What changed

New client API (wolfhsm/wh_client_crypto.h) — opt-in wrappers that cache the key and return its public part into the caller's key object, stamping the cached keyId on it:

  • wh_Client_RsaMakeCacheKeyAndExportPublic(...)
  • wh_Client_EccMakeCacheKeyAndExportPublic(...)
  • wh_Client_Curve25519MakeCacheKeyAndExportPublic(...)
  • wh_Client_Ed25519MakeCacheKeyAndExportPublic(...)
  • wh_Client_MlDsaMakeCacheKeyAndExportPublic(...) + wh_Client_MlDsaMakeCacheKeyDma(...)
  • wh_Client_MlKemMakeCacheKeyAndExportPublic(...) + wh_Client_MlKemMakeCacheKeyDma(...)

Server (src/wh_server_crypto.c) — on the cache path, serializes the public-only key into the keygen response body using the same helpers as the keystore export path (wc_RsaKeyToPublicDer, wc_EccPublicKeyToDer(...,1), wc_Curve25519PublicKeyToDer(...,1), wc_Ed25519PublicKeyToDer(...,1), wc_MlDsaKey_PublicKeyToDer(...,1), wc_MlKemKey_*). DMA paths reuse the key buffer + keySize.

Client (src/wh_client_crypto.c) — the new wrappers parse the public key from the response body and set the returned keyId.

Design notes

  • Wire-compatible / backward-compatible. Reuses the existing keygen response body (keyId + len + trailing DER); no new request/response structs, no translate or padding-test changes. Callers of the existing MakeCacheKey functions ignore the extra bytes. Existing public API signatures are untouched.

  • Fail-closed export on the server. The server always serializes the public key into the cache-path response body. A freshly generated key is expected to serialize, so a failure is treated as fatal: the server evicts the just-committed key and returns the error rather than handing back a keyId for a key with no public part — no cache slots are orphaned.

  • Strict contract on the client. The ...AndExportPublic wrappers return WH_ERROR_ABORTED if the server returns an empty body, while still propagating the keyId so the caller can evict the cached key.

  • CryptoCb deliberately unchanged. wc_MakeRsaKey and friends stay ephemeral (the full key is returned to the client). Cached keygen keys have no auto-eviction (wolfCrypt does not route wc_*_free through the CryptoCb), so routing keygen through the cache would leak HSM cache slots on a make/use/free loop and change durability semantics. The new behavior is reachable only via the native MakeCacheKeyAndExportPublic functions.

  • ML-DSA public-key serialization is guarded by WOLFSSL_MLDSA_PUBLIC_KEY.

Testing

Tests were added to both suites:

  • test/wh_test_crypto.c (main suite) — all six algorithms, including ML-KEM.
  • test-refactor/client-server/wh_test_crypto_{rsa,ecc,curve25519,ed25519,mldsa}.c (the refactor suite has no ML-KEM file yet, so ML-KEM is main-suite only).

Each test cross-checks the keygen-returned public key against wh_Client_<Algo>ExportPublicKey (byte-equal) and proves it with a real operation — encrypt/HSM-decrypt, HSM-sign/client-verify, or ECDH round-trip. A negative test covers the EPHEMERAL + NULL-arg contract.

@Frauschi Frauschi assigned Frauschi and wolfSSL-Bot and unassigned Frauschi Jul 17, 2026
bigbrett
bigbrett previously approved these changes Jul 20, 2026

@bigbrett bigbrett left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, flagged a few random nits I'm totally fine with you skipping if not important. @rizlik over to you.

Comment thread src/wh_client_crypto.c
Comment thread src/wh_server_crypto.c Outdated
@bigbrett bigbrett assigned rizlik and unassigned wolfSSL-Bot Jul 20, 2026
@bigbrett
bigbrett requested a review from rizlik July 20, 2026 19:32

@rizlik rizlik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only doubt is that the ML-DSA MakeCacheKey response now carries the public key, which may not fit in the comm buffer, making the API fail on constrained devices. Those devices can use the new DMA make-and-export API, but it's probably worth a comment somewhere so that users know how to fix this when they upgrade.
Beside that, LGTM

Frauschi added 6 commits July 22, 2026 09:25
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.
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.
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.
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.
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.
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.
@Frauschi

Copy link
Copy Markdown
Contributor Author

@rizlik As discussed offline, I modified the logic to make the public key export "best effort". The server tries to store the public key in the response buffer. If the buffer is too small, no public key is stored there, but the response is sent anyway. On the client, the response is parsed. For existing MakeCacheKey() callers, both cases (public key present and no public key present as buffer-too-small) result in the same existing behavior to not cause any regressions. In the new MakeCacheKeyAndExportPublic() API, a missing public key in the response is handled as an error (evict the generated key from the server and return the error to the caller).

@Frauschi
Frauschi requested a review from rizlik July 22, 2026 10:12
@rizlik

rizlik commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

LGTM. @bigbrett are you OK with the new best-effor behaviour?

@rizlik rizlik assigned bigbrett and unassigned rizlik Jul 22, 2026
@bigbrett
bigbrett merged commit 993b2cb into wolfSSL:main Jul 22, 2026
108 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants