Skip to content

Add ExportPublicKey API for cached asymmetric keys#346

Merged
rizlik merged 4 commits intowolfSSL:mainfrom
Frauschi:export_pub_key
May 7, 2026
Merged

Add ExportPublicKey API for cached asymmetric keys#346
rizlik merged 4 commits intowolfSSL:mainfrom
Frauschi:export_pub_key

Conversation

@Frauschi
Copy link
Copy Markdown
Contributor

Add ExportPublicKey API for cached asymmetric key objects

Summary

Adds a dedicated path for extracting only the public half of a cached public-key keypair, so callers that need a public key on the client side (signature verification, certificate building, key transport, etc.) no longer have to pull the private material out of the HSM.

Previously, the only way to get the public half of a cached keypair was to call wh_Client_<Algo>ExportKey(), which goes through the algorithm-agnostic wh_Client_KeyExport() and returns the raw cached DER — including any private material. For the common case "I cached a keypair for on-HSM signing and I just need the public key on the client," shipping the private key defeats the security benefit of caching.

This PR adds:

  • Non-DMA path: new keystore action WH_KEY_EXPORT_PUBLIC + per-algorithm client wrappers.
  • DMA path: parallel WH_KEY_EXPORT_PUBLIC_DMA + wh_Client_KeyExportPublicDma generic transport + per-algorithm DMA wrapper for ML-DSA (matches the existing ML-DSA-only DMA-export precedent).

Algorithms wired end-to-end

Algorithm Non-DMA client wrapper DMA
RSA wh_Client_RsaExportPublicKey via generic transport
ECC wh_Client_EccExportPublicKey via generic transport
Ed25519 wh_Client_Ed25519ExportPublicKey via generic transport
Curve25519 wh_Client_Curve25519ExportPublicKey via generic transport
ML-DSA (Dilithium) wh_Client_MlDsaExportPublicKey wh_Client_MlDsaExportPublicKeyDma

Design notes

  • Single keystore action with an algo selector. Cached keys are stored as opaque DER — NVM metadata does not record an algorithm type — so the server needs the algorithm to know how to decode. Using one wire action keeps the translate/lock/label plumbing shared with WH_KEY_EXPORT instead of duplicating it per algorithm. The selector is a new WH_KEY_ALGO_* enum in wolfhsm/wh_common.h.
  • NONEXPORTABLE carve-out. WH_NVM_FLAGS_NONEXPORTABLE blocks full-export but not public-only export, because public material is non-sensitive and blocking it would make cached keys unusable for any external verification or key-transport use case. This is a dedicated WH_KS_OP_EXPORT_PUBLIC branch in _KeystoreCheckPolicy (not a silent bypass) and is called out explicitly in the docs.
  • Reuse. The server handler deserializes directly from cacheBuf/cacheMeta using the existing wh_Crypto_*DeserializeKeyDer helpers (which already fall back to public-only decode), then re-emits public-only DER via the matching wc_*PublicKeyToDer. No new server-side crypto helpers introduced.
  • DMA staging with zero extra server stack. The DMA handler stages the public DER in the unused tail of resp_packet (the DMA response struct itself only occupies the header), then whServerDma_CopyToClients it into the client-provided buffer. The response sent over the wire is just sizeof(resp).
  • Error handling. Wrong algo selector → ASN parse error bubbles up. Unknown keyId → WH_ERROR_NOTFOUND. wc_*PublicKeyToDer returning 0 → WH_ERROR_ABORTED (explicitly, not a silent zero-length success). Too-small DMA client buffer → WH_ERROR_NOSPACE.

Wire protocol

New keystore actions (appended to enum WH_KEY_ENUM so existing numeric values are preserved):

  • WH_KEY_EXPORT_PUBLIC
  • WH_KEY_EXPORT_PUBLIC_DMA (under WOLFHSM_CFG_DMA)

Each takes a uint16_t algo selector alongside the standard keyId. Integrators with custom transports route these the same way they route WH_KEY_EXPORT / WH_KEY_EXPORT_DMA.

Test plan

End-to-end per algorithm:

  • RSANONEXPORTABLE cached key → full export denied with WH_ERROR_ACCESSwh_Client_RsaExportPublicKey succeeds → client-side wc_RsaPublicEncrypt / HSM-side wc_RsaPrivateDecrypt round-trips plaintext. Includes wrong-algo and unknown-keyId (WH_ERROR_NOTFOUND) negative cases.
  • ECC — HSM signs with cached private, client verifies locally with exported public; asserts type == ECC_PUBLICKEY.
  • Ed25519 — HSM signs via wh_Client_Ed25519Sign, client verifies with wc_ed25519_verify_msg; asserts pubKeySet==1 && privKeySet==0.
  • Curve25519 — X25519 shared-secret round-trip: local_priv·hsm_pub == hsm_priv·local_pub.
  • ML-DSA — MakeCacheKey (level 2) → wh_Client_MlDsaExportPublicKey → asserts pubKeySet==1 && prvKeySet==0.

DMA-specific coverage:

  • ECC DMA — HSM sign (non-DMA cryptoCb) + client verify, where the public half is pulled via wh_Client_KeyExportPublicDma (generic transport).
  • ML-DSA DMAwh_Client_MlDsaExportPublicKeyDma + flag assertions, plus a byte-identity check comparing DMA-path DER vs. non-DMA-path DER for the same cached key, plus a WH_ERROR_NOSPACE negative test with an undersized client buffer.

Docs updated in docs/src/chapter05.md and docs/src-ja/chapter05.md.

@Frauschi Frauschi self-assigned this Apr 24, 2026
@Frauschi
Copy link
Copy Markdown
Contributor Author

Currently, this work is orthogonal to #336, so this PR is missing ML-KEM support and #336 is missing support for this new API. Depending on what goes into main first, the other one needs some updates.

Main goal for now is to get some feedback on the general API and design.

Copy link
Copy Markdown
Contributor

@bigbrett bigbrett left a comment

Choose a reason for hiding this comment

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

Overall, looks great. A few nits and suggetsions, with some architectural things as well.

One thing I'm not sure about is the bypass of the NONEXPORTABLE attribute. I totally see why you would do it this way, but I don't want to prevent a customer from making an object fully nonexportable (e.g. unreadable) for whatever reason. Therefore I wonder if it makes sense to have additional attributes here that only apply to keys? Happy to brainstorm this.

Comment thread wolfhsm/wh_client.h Outdated
Comment thread wolfhsm/wh_common.h
Comment thread src/wh_client.c Outdated
Comment thread src/wh_client.c Outdated
Comment thread src/wh_client_crypto.c
Comment thread src/wh_server_keystore.c
Comment thread src/wh_server_keystore.c Outdated
@Frauschi
Copy link
Copy Markdown
Contributor Author

Addressed the review comments (except for two which are open for discussion).

I also had to increase WOLFHSM_CFG_SERVER_KEYCACHE_BIG_BUFSIZE from 4096 to 5120 to incorporate the maximum ML-DSA key size. This issue was never triggered previously, as wh_Client_MlDsaMakeCacheKey() has never been called in any test before.

@bigbrett bigbrett assigned rizlik and bigbrett and unassigned Frauschi Apr 29, 2026
@bigbrett bigbrett requested a review from rizlik April 29, 2026 16:47
@bigbrett
Copy link
Copy Markdown
Contributor

bigbrett commented May 5, 2026

@Frauschi can you pls resolve conflicts

Frauschi added 3 commits May 6, 2026 09:18
Introduces a new keystore action WH_KEY_EXPORT_PUBLIC that re-emits only
the public portion of a cached public-key object, so callers that need a
public key for a client-side operation (signature verification, key
transport, etc.) no longer have to pull private material out of the HSM.
A new WH_KS_OP_EXPORT_PUBLIC policy branch gates the path and
intentionally bypasses NONEXPORTABLE since public material is
non-sensitive.

Wired end-to-end for RSA, ECC, Ed25519, Curve25519, and ML-DSA, with
per-algorithm client wrappers (wh_Client_<Algo>ExportPublicKey) and
smoke tests that round-trip real operations (sign/verify, ECDH) against
the exported public keys, plus a negative test for unknown keyId.

Also adds a DMA variant (WH_KEY_EXPORT_PUBLIC_DMA) with a generic client
transport and an ML-DSA-specific wrapper, byte-identity cross-validation
against the non-DMA path, and a NOSPACE bounds-check test.

Documentation added to docs/src/chapter05.md and docs/src-ja/chapter05.md.
New message structs registered in the padding-check test.
@Frauschi
Copy link
Copy Markdown
Contributor Author

Frauschi commented May 6, 2026

Now properly rebased to main with conflicts fixed.

@Frauschi Frauschi requested a review from bigbrett May 6, 2026 07:30
Copy link
Copy Markdown
Contributor

@rizlik rizlik left a comment

Choose a reason for hiding this comment

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

I don’t like the server trusting the client to specify which algorithm to use when decoding the DER-encoded key.

It’s probably “safe,” since decoding should fail if the algorithm is wrong, but I don’t think we should rely on DER encoding to defend against this client-induced type confusion.

What are your thoughts?

@bigbrett
Copy link
Copy Markdown
Contributor

bigbrett commented May 6, 2026

@rizlik I see your point but what are the alternatives? Pretty sure this is the "blind deserialization" problem we face basically everywhere, not just in this PR. At some point, there is a DER blob stored in NVM and the server needs to know "how" to decode it. The client is the one that is requesting an operation to be done with said DER blob, so at some level it is always "assumed" that the DER blob is a properly formatted key of a given type, and then DER deserialization will fail if this is not the case.

Do you have any other ideas? I suppose that a type could be supplied in the NVM metadata for keys that are provisioned offline, but that really doesn't buy you much more "safety" and is also outside the scope of this PR as it would apply to all algos

Comment thread src/wh_client_crypto.c Outdated
@rizlik
Copy link
Copy Markdown
Contributor

rizlik commented May 6, 2026

@bigbrett isn't DER decoding currently

@rizlik I see your point but what are the alternatives? Pretty sure this is the "blind deserialization" problem we face basically everywhere, not just in this PR. At some point, there is a DER blob stored in NVM and the server needs to know "how" to decode it. The client is the one that is requesting an operation to be done with said DER blob, so at some level it is always "assumed" that the DER blob is a properly formatted key of a given type, and then DER deserialization will fail if this is not the case.

Do you have any other ideas? I suppose that a type could be supplied in the NVM metadata for keys that are provisioned offline, but that really doesn't buy you much more "safety" and is also outside the scope of this PR as it would apply to all algos

You are right, this is a currently existing problem. outside scope for this PR.

@bigbrett bigbrett removed their assignment May 6, 2026
@rizlik rizlik merged commit 27a3c57 into wolfSSL:main May 7, 2026
92 checks passed
@Frauschi Frauschi deleted the export_pub_key branch May 7, 2026 06:53
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.

3 participants