|
| 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