Skip to content

Commit 84c66a6

Browse files
committed
Add ExportPublicKey API for cached public keys
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.
1 parent 655e1c9 commit 84c66a6

13 files changed

Lines changed: 2075 additions & 4 deletions

docs/src-ja/chapter05.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,67 @@ wolfHSMのキャッシュスロットの数は`WOLFHSM_NUM_RAMKEYS`で設定さ
301301
302302
`wh_Client_KeyErase`は、指定された鍵をキャッシュから削除し、NVMからも消去します。
303303
304+
## キャッシュされた鍵の公開鍵のみのエクスポート
305+
306+
`wh_Client_KeyExport`は、キャッシュされている鍵のバイト列をそのまま返します。公開鍵ペアがキャッシュされている場合、秘密鍵もクライアント側に送信されてしまうため、クライアント側で公開鍵のみを必要とする用途(署名検証、証明書作成など)では、HSM内に鍵を保持するセキュリティ上の利点が失われます。
307+
308+
wolfHSMはこの用途のために、キャッシュされた公開鍵ペアのキーIDを指定すると、サーバー側で公開鍵部分のみをDER形式で再生成し、クライアント側でwolfCryptの鍵オブジェクトにデシリアライズする専用のパスを提供します。秘密鍵はHSM内に残ります。
309+
310+
アルゴリズムごとのヘルパー関数がトランスポート層をラップしています:
311+
312+
```c
313+
int wh_Client_RsaExportPublicKey(whClientContext* ctx, whKeyId keyId,
314+
RsaKey* key, uint32_t label_len, uint8_t* label);
315+
int wh_Client_EccExportPublicKey(whClientContext* ctx, whKeyId keyId,
316+
ecc_key* key, uint16_t label_len, uint8_t* label);
317+
int wh_Client_Ed25519ExportPublicKey(whClientContext* ctx, whKeyId keyId,
318+
ed25519_key* key, uint16_t label_len, uint8_t* label);
319+
int wh_Client_Curve25519ExportPublicKey(whClientContext* ctx, whKeyId keyId,
320+
curve25519_key* key, uint16_t label_len, uint8_t* label);
321+
int wh_Client_MlDsaExportPublicKey(whClientContext* ctx, whKeyId keyId,
322+
MlDsaKey* key, uint16_t label_len, uint8_t* label);
323+
```
324+
325+
例: HSM上でRSAの鍵ペアを生成し秘密鍵を`NONEXPORTABLE`でマークしたうえで、クライアント側で公開鍵を取得し、HSMがキャッシュされた秘密鍵で生成した署名を検証する。
326+
327+
```c
328+
whKeyId keyId = WH_KEYID_ERASED;
329+
RsaKey pub;
330+
331+
/* 1. HSM内で鍵ペアを生成。NONEXPORTABLEはフルエクスポートをブロックする。 */
332+
wh_Client_RsaMakeCacheKey(&clientCtx, 2048, 0x10001, &keyId,
333+
WH_NVM_FLAGS_USAGE_SIGN | WH_NVM_FLAGS_USAGE_VERIFY |
334+
WH_NVM_FLAGS_NONEXPORTABLE, 0, NULL);
335+
336+
/* 2. 公開鍵のみを取得する。NONEXPORTABLEはこのパスをブロックしない。 */
337+
wc_InitRsaKey_ex(&pub, NULL, INVALID_DEVID);
338+
wh_Client_RsaExportPublicKey(&clientCtx, keyId, &pub, 0, NULL);
339+
/* pub.type == RSA_PUBLIC; クライアント側の検証に使用可能 */
340+
```
341+
342+
### `WH_NVM_FLAGS_NONEXPORTABLE`との関係
343+
344+
`NONEXPORTABLE`は`wh_Client_KeyExport`および各アルゴリズムのフルエクスポート関数をブロックしますが、公開鍵のみのエクスポートパスは**ブロックしません**。公開鍵は機密性を持たない情報であり、もし公開鍵の取り出しも禁止してしまうと、キャッシュされた鍵を外部での検証や鍵配送に使えなくなるためです。公開鍵の取り出しも禁止したい場合は、そもそも鍵を生成・インポートしないか、用途が終わった段階で`wh_Client_KeyErase`で削除してください。
345+
346+
### DMAバリアント
347+
348+
`WOLFHSM_CFG_DMA`が有効な場合、サーバーが公開鍵のDERをクライアント提供のバッファに直接DMAで書き込む並列APIが利用できます。これにより通信バッファ経由のコピーを回避できます。セマンティクス(`NONEXPORTABLE`の取り扱い、アルゴリズムセレクタ、エラー処理)は非DMAパスと同一です。
349+
350+
```c
351+
int wh_Client_KeyExportPublicDma(whClientContext* c, whKeyId keyId,
352+
uint16_t algo, const void* keyAddr, uint16_t keySz,
353+
uint8_t* label, uint16_t labelSz, uint16_t* outSz);
354+
355+
int wh_Client_MlDsaExportPublicKeyDma(whClientContext* ctx, whKeyId keyId,
356+
MlDsaKey* key, uint16_t label_len, uint8_t* label);
357+
```
358+
359+
`wh_Client_KeyExportPublicDma`は汎用トランスポートで、呼び出し側は生の公開鍵DERを受け取って自身でデシリアライズします。`wh_Client_MlDsaExportPublicKeyDma`は既存の`wh_Client_MlDsaExportKeyDma`に対応するML-DSA専用ヘルパーで、ML-DSAの大きな公開鍵DERを通信バッファにコピーせずに済ませたい場合に使用できます。
360+
361+
### ワイヤプロトコル
362+
363+
公開鍵のみのエクスポートは、専用のキーストアアクション`WH_KEY_EXPORT_PUBLIC`および`WH_KEY_EXPORT_PUBLIC_DMA`を使用します。キャッシュされた鍵は不透明なDERバイト列として保存されているため、サーバー側でどのように解釈すべきかをリクエストごとにアルゴリズムセレクタ(`wolfhsm/wh_common.h``WH_KEY_ALGO_*`)で指定します。独自トランスポートを実装する場合は、`WH_KEY_EXPORT`/`WH_KEY_EXPORT_DMA`と同様にこれらのアクションもルーティングしてください。
364+
304365
## 暗号操作
305366

306367
クライアントアプリケーションでwolfCryptを使用する場合、`devId`引数として`WOLFHSM_DEV_ID`を渡すことで、互換性のある暗号化操作をwolfHSMサーバーで実行できます。

docs/src/chapter05.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,96 @@ wh_Client_KeyErase(clientCtx, keyId);
287287
`wh_Client_KeyExport` will read the key contents out of the HSM back to the client.
288288
`wh_Client_KeyErase` will remove the indicated key from cache and erase it from NVM.
289289

290+
## Exporting only the public half of a cached key
291+
292+
`wh_Client_KeyExport` returns the raw cached key bytes. For a cached public-key
293+
keypair this includes the private material, which defeats the security benefit
294+
of leaving the key inside the HSM when the caller only needs the public half
295+
(for example, to verify a signature on the client side or to package the key
296+
into a certificate).
297+
298+
wolfHSM provides a dedicated public-only export path that, given a key ID for
299+
a cached public-key keypair, re-emits only the public portion on the server
300+
and deserializes it into a wolfCrypt key object on the client. The private
301+
key never leaves the HSM.
302+
303+
Per-algorithm helpers wrap the transport layer:
304+
305+
```c
306+
int wh_Client_RsaExportPublicKey(whClientContext* ctx, whKeyId keyId,
307+
RsaKey* key, uint32_t label_len, uint8_t* label);
308+
int wh_Client_EccExportPublicKey(whClientContext* ctx, whKeyId keyId,
309+
ecc_key* key, uint16_t label_len, uint8_t* label);
310+
int wh_Client_Ed25519ExportPublicKey(whClientContext* ctx, whKeyId keyId,
311+
ed25519_key* key, uint16_t label_len, uint8_t* label);
312+
int wh_Client_Curve25519ExportPublicKey(whClientContext* ctx, whKeyId keyId,
313+
curve25519_key* key, uint16_t label_len, uint8_t* label);
314+
int wh_Client_MlDsaExportPublicKey(whClientContext* ctx, whKeyId keyId,
315+
MlDsaKey* key, uint16_t label_len, uint8_t* label);
316+
```
317+
318+
Example: generate an RSA keypair on the HSM with the private key marked
319+
`NONEXPORTABLE`, obtain the public key on the client, and verify a signature
320+
the HSM produced using the cached private key.
321+
322+
```c
323+
whKeyId keyId = WH_KEYID_ERASED;
324+
RsaKey pub;
325+
326+
/* 1. Generate keypair inside the HSM. NONEXPORTABLE blocks full export. */
327+
wh_Client_RsaMakeCacheKey(&clientCtx, 2048, 0x10001, &keyId,
328+
WH_NVM_FLAGS_USAGE_SIGN | WH_NVM_FLAGS_USAGE_VERIFY |
329+
WH_NVM_FLAGS_NONEXPORTABLE, 0, NULL);
330+
331+
/* 2. Fetch only the public half. NONEXPORTABLE does NOT block this. */
332+
wc_InitRsaKey_ex(&pub, NULL, INVALID_DEVID);
333+
wh_Client_RsaExportPublicKey(&clientCtx, keyId, &pub, 0, NULL);
334+
/* pub.type == RSA_PUBLIC; usable for client-side verification */
335+
```
336+
337+
### Interaction with `WH_NVM_FLAGS_NONEXPORTABLE`
338+
339+
`NONEXPORTABLE` blocks `wh_Client_KeyExport` and the per-algorithm full-export
340+
helpers. It does **not** block the public-only export path, because public key
341+
material is non-sensitive — preventing it from ever leaving the HSM would make
342+
the cached key unusable for any external verification or key-transport use
343+
case. If you want to block even public extraction, do not generate or import
344+
the key in the first place, or remove it with `wh_Client_KeyErase` after it
345+
has served its purpose.
346+
347+
### DMA variant
348+
349+
For builds with `WOLFHSM_CFG_DMA` enabled, parallel APIs let the server
350+
write the public DER directly into a client-provided buffer using the
351+
existing DMA transport, avoiding the comm-buffer copy. The semantics
352+
(NONEXPORTABLE carve-out, algo selector, error handling) are identical to
353+
the non-DMA path.
354+
355+
```c
356+
int wh_Client_KeyExportPublicDma(whClientContext* c, whKeyId keyId,
357+
uint16_t algo, const void* keyAddr, uint16_t keySz,
358+
uint8_t* label, uint16_t labelSz, uint16_t* outSz);
359+
360+
int wh_Client_MlDsaExportPublicKeyDma(whClientContext* ctx, whKeyId keyId,
361+
MlDsaKey* key, uint16_t label_len, uint8_t* label);
362+
```
363+
364+
`wh_Client_KeyExportPublicDma` is the generic transport — callers receive
365+
raw public DER and deserialize it themselves. `wh_Client_MlDsaExportPublicKeyDma`
366+
mirrors the existing `wh_Client_MlDsaExportKeyDma` per-algorithm helper for
367+
the case where ML-DSA's large public DER benefits most from skipping the
368+
comm-buffer staging.
369+
370+
### Wire protocol
371+
372+
The public-only export uses dedicated keystore actions,
373+
`WH_KEY_EXPORT_PUBLIC` and `WH_KEY_EXPORT_PUBLIC_DMA`, with a per-request
374+
algorithm selector (`WH_KEY_ALGO_*` in `wolfhsm/wh_common.h`) because
375+
cached keys are stored as opaque DER bytes and the server needs to know
376+
how to decode them. Integrators implementing custom transports should
377+
route these actions the same way they route `WH_KEY_EXPORT` and
378+
`WH_KEY_EXPORT_DMA`.
379+
290380
## Key Revocation
291381
292382
Key revocation updates key metadata to prevent further cryptographic use without destroying storage. Revocation clears all `WH_NVM_FLAGS_USAGE_*` bits and sets `WH_NVM_FLAGS_NONMODIFIABLE`. The revoked state is persisted when the key is already committed to NVM.

src/wh_client.c

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,94 @@ int wh_Client_KeyExport(whClientContext* c, whKeyId keyId, uint8_t* label,
913913
return ret;
914914
}
915915

916+
int wh_Client_KeyExportPublicRequest(whClientContext* c, whKeyId keyId,
917+
uint16_t algo)
918+
{
919+
whMessageKeystore_ExportPublicRequest* req = NULL;
920+
921+
if (c == NULL || keyId == WH_KEYID_ERASED) {
922+
return WH_ERROR_BADARGS;
923+
}
924+
925+
req = (whMessageKeystore_ExportPublicRequest*)wh_CommClient_GetDataPtr(
926+
c->comm);
927+
if (req == NULL) {
928+
return WH_ERROR_BADARGS;
929+
}
930+
req->id = keyId;
931+
req->algo = algo;
932+
933+
return wh_Client_SendRequest(c, WH_MESSAGE_GROUP_KEY,
934+
WH_KEY_EXPORT_PUBLIC, sizeof(*req),
935+
(uint8_t*)req);
936+
}
937+
938+
int wh_Client_KeyExportPublicResponse(whClientContext* c, uint8_t* label,
939+
uint16_t labelSz, uint8_t* out,
940+
uint16_t* outSz)
941+
{
942+
uint16_t group;
943+
uint16_t action;
944+
uint16_t size;
945+
int ret;
946+
whMessageKeystore_ExportPublicResponse* resp = NULL;
947+
uint8_t* packOut;
948+
949+
if (c == NULL || outSz == NULL) {
950+
return WH_ERROR_BADARGS;
951+
}
952+
953+
resp = (whMessageKeystore_ExportPublicResponse*)wh_CommClient_GetDataPtr(
954+
c->comm);
955+
if (resp == NULL) {
956+
return WH_ERROR_BADARGS;
957+
}
958+
packOut = (uint8_t*)(resp + 1);
959+
960+
ret = wh_Client_RecvResponse(c, &group, &action, &size, (uint8_t*)resp);
961+
if (ret == WH_ERROR_OK) {
962+
if (resp->rc != 0) {
963+
ret = resp->rc;
964+
}
965+
else {
966+
if (out == NULL) {
967+
*outSz = resp->len;
968+
}
969+
else if (*outSz < resp->len) {
970+
ret = WH_ERROR_ABORTED;
971+
}
972+
else {
973+
memcpy(out, packOut, resp->len);
974+
*outSz = resp->len;
975+
}
976+
if ((ret == WH_ERROR_OK) && (label != NULL)) {
977+
if (labelSz > sizeof(resp->label)) {
978+
memcpy(label, resp->label, WH_NVM_LABEL_LEN);
979+
}
980+
else {
981+
memcpy(label, resp->label, labelSz);
982+
}
983+
}
984+
}
985+
}
986+
return ret;
987+
}
988+
989+
int wh_Client_KeyExportPublic(whClientContext* c, whKeyId keyId, uint16_t algo,
990+
uint8_t* label, uint16_t labelSz, uint8_t* out,
991+
uint16_t* outSz)
992+
{
993+
int ret;
994+
ret = wh_Client_KeyExportPublicRequest(c, keyId, algo);
995+
if (ret == 0) {
996+
do {
997+
ret = wh_Client_KeyExportPublicResponse(c, label, labelSz, out,
998+
outSz);
999+
} while (ret == WH_ERROR_NOTREADY);
1000+
}
1001+
return ret;
1002+
}
1003+
9161004
int wh_Client_KeyCommitRequest(whClientContext* c, whNvmId keyId)
9171005
{
9181006
whMessageKeystore_CommitRequest* req = NULL;
@@ -1525,6 +1613,94 @@ int wh_Client_KeyExportDma(whClientContext* c, uint16_t keyId,
15251613
}
15261614
return ret;
15271615
}
1616+
1617+
int wh_Client_KeyExportPublicDmaRequest(whClientContext* c, whKeyId keyId,
1618+
uint16_t algo, const void* keyAddr,
1619+
uint16_t keySz)
1620+
{
1621+
whMessageKeystore_ExportPublicDmaRequest* req = NULL;
1622+
1623+
if (c == NULL || keyId == WH_KEYID_ERASED) {
1624+
return WH_ERROR_BADARGS;
1625+
}
1626+
1627+
req =
1628+
(whMessageKeystore_ExportPublicDmaRequest*)wh_CommClient_GetDataPtr(
1629+
c->comm);
1630+
if (req == NULL) {
1631+
return WH_ERROR_BADARGS;
1632+
}
1633+
req->id = keyId;
1634+
req->algo = algo;
1635+
req->key.addr = (uint64_t)((uintptr_t)keyAddr);
1636+
req->key.sz = keySz;
1637+
1638+
return wh_Client_SendRequest(c, WH_MESSAGE_GROUP_KEY,
1639+
WH_KEY_EXPORT_PUBLIC_DMA, sizeof(*req),
1640+
(uint8_t*)req);
1641+
}
1642+
1643+
int wh_Client_KeyExportPublicDmaResponse(whClientContext* c, uint8_t* label,
1644+
uint16_t labelSz, uint16_t* outSz)
1645+
{
1646+
uint16_t resp_group;
1647+
uint16_t resp_action;
1648+
uint16_t resp_size;
1649+
int rc;
1650+
whMessageKeystore_ExportPublicDmaResponse* resp = NULL;
1651+
1652+
if (c == NULL || outSz == NULL) {
1653+
return WH_ERROR_BADARGS;
1654+
}
1655+
1656+
resp =
1657+
(whMessageKeystore_ExportPublicDmaResponse*)wh_CommClient_GetDataPtr(
1658+
c->comm);
1659+
if (resp == NULL) {
1660+
return WH_ERROR_BADARGS;
1661+
}
1662+
1663+
rc = wh_Client_RecvResponse(c, &resp_group, &resp_action, &resp_size,
1664+
(uint8_t*)resp);
1665+
if (rc == 0) {
1666+
if ((resp_group != WH_MESSAGE_GROUP_KEY) ||
1667+
(resp_action != WH_KEY_EXPORT_PUBLIC_DMA) ||
1668+
(resp_size != sizeof(*resp))) {
1669+
rc = WH_ERROR_ABORTED;
1670+
}
1671+
else {
1672+
if (resp->rc != 0) {
1673+
rc = resp->rc;
1674+
}
1675+
else {
1676+
*outSz = resp->len;
1677+
if (label != NULL) {
1678+
if (labelSz > WH_NVM_LABEL_LEN) {
1679+
labelSz = WH_NVM_LABEL_LEN;
1680+
}
1681+
memcpy(label, resp->label, labelSz);
1682+
}
1683+
}
1684+
}
1685+
}
1686+
return rc;
1687+
}
1688+
1689+
int wh_Client_KeyExportPublicDma(whClientContext* c, whKeyId keyId,
1690+
uint16_t algo, const void* keyAddr,
1691+
uint16_t keySz, uint8_t* label,
1692+
uint16_t labelSz, uint16_t* outSz)
1693+
{
1694+
int ret;
1695+
ret = wh_Client_KeyExportPublicDmaRequest(c, keyId, algo, keyAddr, keySz);
1696+
if (ret == 0) {
1697+
do {
1698+
ret = wh_Client_KeyExportPublicDmaResponse(c, label, labelSz,
1699+
outSz);
1700+
} while (ret == WH_ERROR_NOTREADY);
1701+
}
1702+
return ret;
1703+
}
15281704
#endif /* WOLFHSM_CFG_DMA */
15291705

15301706
#endif /* WOLFHSM_CFG_ENABLE_CLIENT */

0 commit comments

Comments
 (0)