Skip to content

Commit 5ee1a25

Browse files
committed
Modify cert layer to support wrapped/cached certs
1 parent 0507f33 commit 5ee1a25

File tree

6 files changed

+1017
-52
lines changed

6 files changed

+1017
-52
lines changed

docs/draft/wrapped-certs.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# Wrapped Cached Certificates
2+
3+
## Overview
4+
5+
This documents the "wrapped certificate" use case, showing how to leverage the
6+
certificate manager to use trusted root certificates that live in the server's
7+
**keystore cache** (RAM) after being unwrapped via keywrap funcitonality,
8+
rather than exclusively in **NVM** (flash). A root certificate is wrapped
9+
(AES-GCM encrypted) by the server, handed back to the client as an opaque blob,
10+
and later unwrapped into the server's key cache on demand. Once cached, it can
11+
be used in all certificate verification paths — standard, DMA, and ACERT —
12+
exactly like an NVM-resident root certificate.
13+
14+
This is useful when a client needs to use a trusted root for verification but
15+
does not want to (or cannot) commit it to NVM. The wrapped blob can be stored
16+
cheaply on the client side, while the server only holds the unwrapped plaintext
17+
in its volatile cache for as long as it is needed.
18+
19+
## High-Level Usage
20+
21+
The lifecycle has three stages: **wrap**, **unwrap-and-cache**, and **use**.
22+
23+
### 1. Provision a wrapping key (KEK)
24+
25+
Before wrapping anything the server needs an AES-256 key to use as the
26+
key-encryption key. Cache it on the server with the `WH_NVM_FLAGS_USAGE_WRAP`
27+
flag:
28+
29+
```c
30+
whKeyId kekId = 10;
31+
uint8_t kek[32] = { /* 256-bit AES key */ };
32+
33+
wh_Client_KeyCache(client,
34+
WH_NVM_FLAGS_USAGE_WRAP, NULL, 0,
35+
kek, sizeof(kek), &kekId);
36+
```
37+
38+
The KEK is now sitting in the server's `localCache` (or `globalCache` if marked
39+
global), indexed by `kekId`.
40+
41+
### 2. Wrap the certificate
42+
43+
Call `wh_Client_CertWrap` with the raw certificate DER and the KEK's ID. The
44+
server encrypts the certificate using AES-GCM and returns the wrapped blob:
45+
46+
```c
47+
uint8_t wrappedCert[2048];
48+
uint16_t wrappedCertSz = sizeof(wrappedCert);
49+
50+
/* Build metadata: id embeds TYPE=WRAPPED and the client's USER id;
51+
* caller controls flags, access, and optionally label */
52+
whNvmMetadata certMeta = {0};
53+
certMeta.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(
54+
client->comm->client_id, 5);
55+
certMeta.flags = WH_NVM_FLAGS_USAGE_ANY;
56+
certMeta.access = WH_NVM_ACCESS_ANY;
57+
58+
wh_Client_CertWrap(client, WC_CIPHER_AES_GCM, kekId,
59+
rootCaCert, rootCaCertLen,
60+
&certMeta,
61+
wrappedCert, &wrappedCertSz);
62+
```
63+
64+
After this call:
65+
66+
| Data | Location |
67+
|---|---|
68+
| KEK | Server key cache (`localCache[kekId]`) |
69+
| Wrapped cert blob (ciphertext + GCM tag + IV + metadata) | Client memory (`wrappedCert` buffer) |
70+
| Raw certificate | Nowhere on the server — only the client supplied it transiently |
71+
72+
The client can now persist `wrappedCert` to its own storage (file, flash,
73+
external memory, etc.).
74+
75+
### 3. Unwrap and cache the certificate on the server
76+
77+
When the client needs the root for verification, it pushes the wrapped blob back
78+
to the server:
79+
80+
```c
81+
whKeyId cachedCertId = WH_KEYID_ERASED;
82+
83+
wh_Client_CertUnwrapAndCache(client, WC_CIPHER_AES_GCM, kekId,
84+
wrappedCert, wrappedCertSz,
85+
&cachedCertId);
86+
```
87+
88+
The server decrypts the blob using the KEK, verifies the GCM authentication
89+
tag, and places the plaintext certificate into its key cache. The returned
90+
`cachedCertId` is the server-internal key ID (with `TYPE=WH_KEYTYPE_WRAPPED`
91+
already encoded).
92+
93+
After this call:
94+
95+
| Data | Location |
96+
|---|---|
97+
| KEK | Server key cache |
98+
| Plaintext certificate | Server key cache (`localCache[cachedCertId]`) |
99+
| Wrapped cert blob | Still in client memory (unchanged) |
100+
101+
### 4. Use the cached cert for verification
102+
103+
Pass the cached cert's ID — decorated with the wrapped flag — as the trusted
104+
root to any verify API:
105+
106+
```c
107+
int32_t verifyResult;
108+
109+
wh_Client_CertVerify(client,
110+
intermediateCert, intermediateCertLen,
111+
WH_CLIENT_KEYID_MAKE_WRAPPED(cachedCertId),
112+
&verifyResult);
113+
```
114+
115+
`WH_CLIENT_KEYID_MAKE_WRAPPED(cachedCertId)` sets bit 9
116+
(`WH_KEYID_CLIENT_WRAPPED_FLAG = 0x0200`) on the ID the client sends to the
117+
server. This is the signal that tells the server "this root cert is in the
118+
cache, not in NVM."
119+
120+
The same pattern works for:
121+
- `wh_Client_CertVerifyDma` (DMA path)
122+
- `wh_Client_CertReadTrusted` / `wh_Client_CertReadTrustedDma` (read-back)
123+
- `wh_Client_CertVerifyAcert` / `wh_Client_CertVerifyAcertDma` (attribute certs)
124+
125+
### 5. Cleanup
126+
127+
Evict the cached cert and KEK when done:
128+
129+
```c
130+
wh_Client_KeyEvict(client, WH_CLIENT_KEYID_MAKE_WRAPPED(cachedCertId));
131+
wh_Client_KeyEvict(client, kekId);
132+
```
133+
134+
## Low-Level Implementation Details
135+
136+
### Client-side functions
137+
138+
Nine thin wrappers in `src/wh_client_cert.c` (guarded by
139+
`WOLFHSM_CFG_KEYWRAP`), mirroring the Key wrap/unwrap API:
140+
141+
- **`wh_Client_CertWrap`** / **`wh_Client_CertWrapRequest`** /
142+
**`wh_Client_CertWrapResponse`** — Wrap a certificate. Accepts a
143+
caller-provided `whNvmMetadata*` (with `id`, `flags`, `access`, and
144+
optionally `label` set by the caller), sets `meta->len = certSz`, then
145+
delegates to the corresponding `wh_Client_KeyWrap*` function. The metadata's
146+
`id` field must have `TYPE=WH_KEYTYPE_WRAPPED` encoded via
147+
`WH_CLIENT_KEYID_MAKE_WRAPPED_META`.
148+
149+
- **`wh_Client_CertUnwrapAndExport`** / **`wh_Client_CertUnwrapAndExportRequest`** /
150+
**`wh_Client_CertUnwrapAndExportResponse`** — Unwrap a wrapped certificate
151+
and export both the plaintext certificate and its metadata back to the client.
152+
Delegates to the corresponding `wh_Client_KeyUnwrapAndExport*` function.
153+
154+
- **`wh_Client_CertUnwrapAndCache`** / **`wh_Client_CertUnwrapAndCacheRequest`** /
155+
**`wh_Client_CertUnwrapAndCacheResponse`** — Unwrap and cache on the server.
156+
Delegates to the corresponding `wh_Client_KeyUnwrapAndCache*` function.
157+
Returns the server-assigned cache slot ID in `*out_certId`.
158+
159+
All functions accept an `enum wc_CipherType cipherType` parameter (e.g.
160+
`WC_CIPHER_AES_GCM`) to specify the wrapping cipher. The blocking variants
161+
call their respective Request/Response functions in a do-while-NOTREADY loop.
162+
163+
These are pure convenience; a caller could use `wh_Client_KeyWrap*` /
164+
`wh_Client_KeyUnwrapAndExport*` / `wh_Client_KeyUnwrapAndCache*` directly if
165+
it needed custom metadata.
166+
167+
### Server-side routing (the key change)
168+
169+
#### `wh_Server_CertReadTrusted` (`src/wh_server_cert.c`)
170+
171+
Previously accepted only `whNvmId` and always read from NVM. Now accepts
172+
`whKeyId` and branches on the TYPE field:
173+
174+
```
175+
if WH_KEYID_TYPE(id) == WH_KEYTYPE_WRAPPED
176+
→ wh_Server_KeystoreReadKey(server, id, &meta, cert, &sz) // cache path
177+
else
178+
→ wh_Nvm_GetMetadata / wh_Nvm_Read // NVM path (unchanged)
179+
```
180+
181+
`wh_Server_KeystoreReadKey` looks up the key in the server's `localCache` (or
182+
`globalCache` if global keys are enabled and the USER field is 0). It copies
183+
both the metadata and the raw data into the caller's buffers.
184+
185+
#### `wh_Server_CertVerify` / `wh_Server_CertVerifyAcert`
186+
187+
Signature changed from `whNvmId trustedRootNvmId` to `whKeyId trustedRootId`.
188+
Internally they just call `wh_Server_CertReadTrusted`, which now handles the
189+
routing.
190+
191+
#### Request handlers in `wh_Server_HandleCertRequest`
192+
193+
Every handler that accepts a trusted root ID (`READTRUSTED`, `VERIFY`,
194+
`READTRUSTED_DMA`, `VERIFY_DMA`, `VERIFY_ACERT`, `VERIFY_ACERT_DMA`) was
195+
updated with the same pattern:
196+
197+
1. **Translate the client ID**: If the incoming `req.id` (or
198+
`req.trustedRootNvmId`) has `WH_KEYID_CLIENT_WRAPPED_FLAG` set, call
199+
`wh_KeyId_TranslateFromClient(WH_KEYTYPE_NVM, server->comm->client_id, req.id)`
200+
to produce a full server-internal key ID with `TYPE=WH_KEYTYPE_WRAPPED`,
201+
`USER=client_id`, and the bare key `ID` in the low byte.
202+
203+
2. **Branch on key type** for the read/verify:
204+
- **Cache path** (`WH_KEYID_TYPE(certId) == WH_KEYTYPE_WRAPPED`): Calls
205+
`wh_Server_KeystoreReadKey` to fetch the cert from the cache. Checks
206+
`WH_NVM_FLAGS_NONEXPORTABLE` on the metadata for read-back requests.
207+
- **NVM path** (original, `WH_KEYID_TYPE != WH_KEYTYPE_WRAPPED`): Unchanged
208+
behavior — reads from flash via `wh_Nvm_GetMetadata` / `wh_Nvm_Read`.
209+
210+
### Key ID encoding walkthrough
211+
212+
Consider a client with `client_id = 1` wrapping a cert with bare ID `5`:
213+
214+
| Stage | Value | Encoding |
215+
|---|---|---|
216+
| `WH_CLIENT_KEYID_MAKE_WRAPPED_META(1, 5)` | `0x4105` | TYPE=4 (WRAPPED), USER=1, ID=5 — stored *inside* the wrapped blob metadata |
217+
| Server returns `cachedCertId` after unwrap | `0x4105` | Same — the server preserved the metadata ID |
218+
| Client sends `WH_CLIENT_KEYID_MAKE_WRAPPED(0x4105)` | `0x4305` | Bit 9 (0x0200) set as client flag |
219+
| Server calls `wh_KeyId_TranslateFromClient(...)` | `0x4105` | Flag stripped, TYPE=WRAPPED confirmed, USER=1, ID=5 |
220+
| `WH_KEYID_TYPE(0x4105)` | `4` | Equals `WH_KEYTYPE_WRAPPED` (4) → routes to cache |
221+
222+
### Data stored at each point
223+
224+
| Point in flow | Server key cache | Server NVM | Client memory |
225+
|---|---|---|---|
226+
| After `KeyCache` (KEK) | KEK at `kekId` | — | — |
227+
| After `CertWrap` | KEK at `kekId` | — | Wrapped blob (ciphertext + tag + IV + metadata) |
228+
| After `CertUnwrapAndCache` | KEK at `kekId`, plaintext cert at `cachedCertId` | — | Wrapped blob (unchanged) |
229+
| During `CertVerify` | KEK, plaintext cert (read into stack buffer `root_cert[WOLFHSM_CFG_MAX_CERT_SIZE]` by `CertReadTrusted`) | — | — |
230+
| After `KeyEvict` (cert) | KEK at `kekId` | — | Wrapped blob |
231+
| After `KeyEvict` (KEK) | — | — | Wrapped blob |
232+
233+
## Interaction with Locking and Thread Safety
234+
235+
### The NVM lock (`WH_SERVER_NVM_LOCK` / `WH_SERVER_NVM_UNLOCK`)
236+
237+
When `WOLFHSM_CFG_THREADSAFE` is defined, `WH_SERVER_NVM_LOCK(server)` calls
238+
`wh_Server_NvmLock(server)`, which acquires a mutex protecting NVM state. When
239+
not threadsafe, the macros expand to `(WH_ERROR_OK)` (no-ops).
240+
241+
The existing (pre-branch) code unconditionally called `WH_SERVER_NVM_LOCK`
242+
around every cert read/verify handler, because the cert always came from NVM.
243+
244+
### What changes for cached certs
245+
246+
Cached certs do not touch NVM at all — they are read from the in-memory key
247+
cache via `wh_Server_KeystoreReadKey`. However, the NVM lock is still
248+
unconditionally acquired around both cache and NVM paths. This is conservative
249+
but correct: the key cache (`localCache` / `globalCache`) does not have its own
250+
lock, so the NVM lock serves as the coarse serialization mechanism for all
251+
server-side storage operations (both NVM and cache) when
252+
`WOLFHSM_CFG_THREADSAFE` is enabled.
253+
254+
The pattern used in every updated handler is:
255+
256+
```c
257+
rc = WH_SERVER_NVM_LOCK(server);
258+
if (rc == WH_ERROR_OK) {
259+
if (req.id & WH_KEYID_CLIENT_WRAPPED_FLAG) {
260+
/* Cache path: translate and read from keystore cache */
261+
whKeyId certId = wh_KeyId_TranslateFromClient(
262+
WH_KEYTYPE_WRAPPED, server->comm->client_id, req.id);
263+
rc = wh_Server_KeystoreReadKey(server, certId, &meta, cert_data, &cert_len);
264+
/* ... exportability check for read-back requests ... */
265+
} else {
266+
/* NVM path (unchanged) */
267+
rc = wh_Nvm_GetMetadata(server->nvm, req.id, &meta);
268+
/* ... NVM reads ... */
269+
}
270+
(void)WH_SERVER_NVM_UNLOCK(server);
271+
}
272+
```
273+
274+
Key points:
275+
276+
- **Both paths hold the NVM lock**: The lock is always acquired before
277+
branching. While the cache read itself doesn't strictly need NVM protection,
278+
holding the lock ensures serialization with any concurrent operations that
279+
access the `localCache` array on other threads.
280+
281+
- **NVM path**: Unchanged — same behavior as before this branch.
282+
283+
### Backward compatibility
284+
285+
- All existing NVM-based certificate operations continue to work identically.
286+
The routing branch only activates when the key type is `WH_KEYTYPE_WRAPPED`.
287+
- The `wh_Server_CertReadTrusted` and `wh_Server_CertVerify` function
288+
signatures changed from `whNvmId` to `whKeyId`. Since `whNvmId` and `whKeyId`
289+
are both `uint16_t`, this is ABI-compatible. Any existing callers passing a
290+
plain NVM ID (with TYPE=0) will hit the NVM path as before.

0 commit comments

Comments
 (0)