-
Notifications
You must be signed in to change notification settings - Fork 31
add support for wrapped certs #318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,290 @@ | ||||||||||||||||||||||||||||
| # Wrapped Cached Certificates | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ## Overview | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| This documents the "wrapped certificate" use case, showing how to leverage the | ||||||||||||||||||||||||||||
| certificate manager to use trusted root certificates that live in the server's | ||||||||||||||||||||||||||||
| **keystore cache** (RAM) after being unwrapped via keywrap funcitonality, | ||||||||||||||||||||||||||||
| rather than exclusively in **NVM** (flash). A root certificate is wrapped | ||||||||||||||||||||||||||||
| (AES-GCM encrypted) by the server, handed back to the client as an opaque blob, | ||||||||||||||||||||||||||||
| and later unwrapped into the server's key cache on demand. Once cached, it can | ||||||||||||||||||||||||||||
| be used in all certificate verification paths — standard, DMA, and ACERT — | ||||||||||||||||||||||||||||
| exactly like an NVM-resident root certificate. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| This is useful when a client needs to use a trusted root for verification but | ||||||||||||||||||||||||||||
| does not want to (or cannot) commit it to NVM. The wrapped blob can be stored | ||||||||||||||||||||||||||||
| cheaply on the client side, while the server only holds the unwrapped plaintext | ||||||||||||||||||||||||||||
| in its volatile cache for as long as it is needed. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ## High-Level Usage | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| The lifecycle has three stages: **wrap**, **unwrap-and-cache**, and **use**. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ### 1. Provision a wrapping key (KEK) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Before wrapping anything the server needs an AES-256 key to use as the | ||||||||||||||||||||||||||||
| key-encryption key. Cache it on the server with the `WH_NVM_FLAGS_USAGE_WRAP` | ||||||||||||||||||||||||||||
| flag: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ```c | ||||||||||||||||||||||||||||
| whKeyId kekId = 10; | ||||||||||||||||||||||||||||
| uint8_t kek[32] = { /* 256-bit AES key */ }; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| wh_Client_KeyCache(client, | ||||||||||||||||||||||||||||
| WH_NVM_FLAGS_USAGE_WRAP, NULL, 0, | ||||||||||||||||||||||||||||
| kek, sizeof(kek), &kekId); | ||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| The KEK is now sitting in the server's `localCache` (or `globalCache` if marked | ||||||||||||||||||||||||||||
| global), indexed by `kekId`. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ### 2. Wrap the certificate | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Call `wh_Client_CertWrap` with the raw certificate DER and the KEK's ID. The | ||||||||||||||||||||||||||||
| server encrypts the certificate using AES-GCM and returns the wrapped blob: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ```c | ||||||||||||||||||||||||||||
| uint8_t wrappedCert[2048]; | ||||||||||||||||||||||||||||
| uint16_t wrappedCertSz = sizeof(wrappedCert); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /* Build metadata: id embeds TYPE=WRAPPED and the client's USER id; | ||||||||||||||||||||||||||||
| * caller controls flags, access, and optionally label */ | ||||||||||||||||||||||||||||
| whNvmMetadata certMeta = {0}; | ||||||||||||||||||||||||||||
| certMeta.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META( | ||||||||||||||||||||||||||||
| client->comm->client_id, 5); | ||||||||||||||||||||||||||||
| certMeta.flags = WH_NVM_FLAGS_USAGE_ANY; | ||||||||||||||||||||||||||||
| certMeta.access = WH_NVM_ACCESS_ANY; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| wh_Client_CertWrap(client, WC_CIPHER_AES_GCM, kekId, | ||||||||||||||||||||||||||||
| rootCaCert, rootCaCertLen, | ||||||||||||||||||||||||||||
| &certMeta, | ||||||||||||||||||||||||||||
| wrappedCert, &wrappedCertSz); | ||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| After this call: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| | Data | Location | | ||||||||||||||||||||||||||||
| |---|---| | ||||||||||||||||||||||||||||
| | KEK | Server key cache (`localCache[kekId]`) | | ||||||||||||||||||||||||||||
| | Wrapped cert blob (ciphertext + GCM tag + IV + metadata) | Client memory (`wrappedCert` buffer) | | ||||||||||||||||||||||||||||
| | Raw certificate | Nowhere on the server — only the client supplied it transiently | | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| The client can now persist `wrappedCert` to its own storage (file, flash, | ||||||||||||||||||||||||||||
| external memory, etc.). | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ### 3. Unwrap and cache the certificate on the server | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| When the client needs the root for verification, it pushes the wrapped blob back | ||||||||||||||||||||||||||||
| to the server: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ```c | ||||||||||||||||||||||||||||
| whKeyId cachedCertId = WH_KEYID_ERASED; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| wh_Client_CertUnwrapAndCache(client, WC_CIPHER_AES_GCM, kekId, | ||||||||||||||||||||||||||||
| wrappedCert, wrappedCertSz, | ||||||||||||||||||||||||||||
| &cachedCertId); | ||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| The server decrypts the blob using the KEK, verifies the GCM authentication | ||||||||||||||||||||||||||||
| tag, and places the plaintext certificate into its key cache. The returned | ||||||||||||||||||||||||||||
| `cachedCertId` is the server-internal key ID (with `TYPE=WH_KEYTYPE_WRAPPED` | ||||||||||||||||||||||||||||
| already encoded). | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| After this call: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| | Data | Location | | ||||||||||||||||||||||||||||
| |---|---| | ||||||||||||||||||||||||||||
| | KEK | Server key cache | | ||||||||||||||||||||||||||||
| | Plaintext certificate | Server key cache (`localCache[cachedCertId]`) | | ||||||||||||||||||||||||||||
| | Wrapped cert blob | Still in client memory (unchanged) | | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ### 4. Use the cached cert for verification | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Pass the cached cert's ID — decorated with the wrapped flag — as the trusted | ||||||||||||||||||||||||||||
| root to any verify API: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ```c | ||||||||||||||||||||||||||||
| int32_t verifyResult; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| wh_Client_CertVerify(client, | ||||||||||||||||||||||||||||
| intermediateCert, intermediateCertLen, | ||||||||||||||||||||||||||||
| WH_CLIENT_KEYID_MAKE_WRAPPED(cachedCertId), | ||||||||||||||||||||||||||||
| &verifyResult); | ||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| `WH_CLIENT_KEYID_MAKE_WRAPPED(cachedCertId)` sets bit 9 | ||||||||||||||||||||||||||||
| (`WH_KEYID_CLIENT_WRAPPED_FLAG = 0x0200`) on the ID the client sends to the | ||||||||||||||||||||||||||||
| server. This is the signal that tells the server "this root cert is in the | ||||||||||||||||||||||||||||
| cache, not in NVM." | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| The same pattern works for: | ||||||||||||||||||||||||||||
| - `wh_Client_CertVerifyDma` (DMA path) | ||||||||||||||||||||||||||||
| - `wh_Client_CertReadTrusted` / `wh_Client_CertReadTrustedDma` (read-back) | ||||||||||||||||||||||||||||
| - `wh_Client_CertVerifyAcert` / `wh_Client_CertVerifyAcertDma` (attribute certs) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ### 5. Cleanup | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Evict the cached cert and KEK when done: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ```c | ||||||||||||||||||||||||||||
| wh_Client_KeyEvict(client, WH_CLIENT_KEYID_MAKE_WRAPPED(cachedCertId)); | ||||||||||||||||||||||||||||
| wh_Client_KeyEvict(client, kekId); | ||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ## Low-Level Implementation Details | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ### Client-side functions | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Nine thin wrappers in `src/wh_client_cert.c` (guarded by | ||||||||||||||||||||||||||||
| `WOLFHSM_CFG_KEYWRAP`), mirroring the Key wrap/unwrap API: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| - **`wh_Client_CertWrap`** / **`wh_Client_CertWrapRequest`** / | ||||||||||||||||||||||||||||
| **`wh_Client_CertWrapResponse`** — Wrap a certificate. Accepts a | ||||||||||||||||||||||||||||
| caller-provided `whNvmMetadata*` (with `id`, `flags`, `access`, and | ||||||||||||||||||||||||||||
| optionally `label` set by the caller), sets `meta->len = certSz`, then | ||||||||||||||||||||||||||||
| delegates to the corresponding `wh_Client_KeyWrap*` function. The metadata's | ||||||||||||||||||||||||||||
| `id` field must have `TYPE=WH_KEYTYPE_WRAPPED` encoded via | ||||||||||||||||||||||||||||
| `WH_CLIENT_KEYID_MAKE_WRAPPED_META`. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| - **`wh_Client_CertUnwrapAndExport`** / **`wh_Client_CertUnwrapAndExportRequest`** / | ||||||||||||||||||||||||||||
| **`wh_Client_CertUnwrapAndExportResponse`** — Unwrap a wrapped certificate | ||||||||||||||||||||||||||||
| and export both the plaintext certificate and its metadata back to the client. | ||||||||||||||||||||||||||||
| Delegates to the corresponding `wh_Client_KeyUnwrapAndExport*` function. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| - **`wh_Client_CertUnwrapAndCache`** / **`wh_Client_CertUnwrapAndCacheRequest`** / | ||||||||||||||||||||||||||||
| **`wh_Client_CertUnwrapAndCacheResponse`** — Unwrap and cache on the server. | ||||||||||||||||||||||||||||
| Delegates to the corresponding `wh_Client_KeyUnwrapAndCache*` function. | ||||||||||||||||||||||||||||
| Returns the server-assigned cache slot ID in `*out_certId`. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| All functions accept an `enum wc_CipherType cipherType` parameter (e.g. | ||||||||||||||||||||||||||||
| `WC_CIPHER_AES_GCM`) to specify the wrapping cipher. The blocking variants | ||||||||||||||||||||||||||||
| call their respective Request/Response functions in a do-while-NOTREADY loop. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| These are pure convenience; a caller could use `wh_Client_KeyWrap*` / | ||||||||||||||||||||||||||||
| `wh_Client_KeyUnwrapAndExport*` / `wh_Client_KeyUnwrapAndCache*` directly if | ||||||||||||||||||||||||||||
| it needed custom metadata. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ### Server-side routing (the key change) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| #### `wh_Server_CertReadTrusted` (`src/wh_server_cert.c`) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Previously accepted only `whNvmId` and always read from NVM. Now accepts | ||||||||||||||||||||||||||||
| `whKeyId` and branches on the TYPE field: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||
| if WH_KEYID_TYPE(id) == WH_KEYTYPE_WRAPPED | ||||||||||||||||||||||||||||
| → wh_Server_KeystoreReadKey(server, id, &meta, cert, &sz) // cache path | ||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||
| → wh_Nvm_GetMetadata / wh_Nvm_Read // NVM path (unchanged) | ||||||||||||||||||||||||||||
|
Comment on lines
+175
to
+178
|
||||||||||||||||||||||||||||
| if WH_KEYID_TYPE(id) == WH_KEYTYPE_WRAPPED | |
| → wh_Server_KeystoreReadKey(server, id, &meta, cert, &sz) // cache path | |
| else | |
| → wh_Nvm_GetMetadata / wh_Nvm_Read // NVM path (unchanged) | |
| switch (WH_KEYID_TYPE(id)) | |
| case WH_KEYTYPE_WRAPPED: | |
| → wh_Server_KeystoreReadKey(server, id, &meta, cert, &sz) // cache path | |
| break | |
| case WH_KEYTYPE_NVM: | |
| → wh_Nvm_GetMetadata / wh_Nvm_Read // NVM path (unchanged) | |
| break | |
| default: | |
| → reject id / return error for unsupported key type |
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doc example calls wh_KeyId_TranslateFromClient(WH_KEYTYPE_NVM, ...) but the text says it should produce an internal key ID with TYPE=WH_KEYTYPE_WRAPPED. The first argument should reflect the wrapped key type (or match what the server code actually passes) to avoid confusing readers.
| `wh_KeyId_TranslateFromClient(WH_KEYTYPE_NVM, server->comm->client_id, req.id)` | |
| `wh_KeyId_TranslateFromClient(WH_KEYTYPE_WRAPPED, server->comm->client_id, req.id)` |
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section claims the handlers now use conditional NVM locking (and may skip locking when server->nvm == NULL), but the updated cert handlers in src/wh_server_cert.c still call WH_SERVER_NVM_LOCK(server) unconditionally even for the cache path. Please adjust the documentation or the code so the described locking behavior matches reality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: "funcitonality" should be "functionality".