diff --git a/docs/draft/auth.md b/docs/draft/auth.md new file mode 100644 index 00000000..077dd568 --- /dev/null +++ b/docs/draft/auth.md @@ -0,0 +1,488 @@ +# wolfHSM Authentication Manager — PR #270 Overview (v2) + +**PR:** wolfSSL/wolfHSM#270 — *authentication manager feature addition* +**Author:** @JacobBarthelmeh · **Target:** `main` · **State:** OPEN (as of 2026-04-16) +**Scope:** +6540 / −33 across 44 files · head: `fa54634b` + +This document is a fresh read of the PR (no reference to any prior notes in the tree). It is aimed at an engineer about to review the change. It walks the feature top-down: what it is, what was added, how the pieces fit together, and how an individual user's credentials and permissions are used during a normal request. + +--- + +## 1. TL;DR + +PR #270 introduces a **PKCS11-flavored Authentication/Authorization Manager** to wolfHSM. It provides: + +- **Login/logout** with two credential methods: **PIN** (SHA-256 hashed) or **X.509 certificate**. +- A **user database** managed via add/delete/get and set-permissions / set-credentials APIs. +- A **permission model** of (admin flag) + (per-group allow boolean) + (per-group bitmap of 256 allowed actions) + (a small per-user list of accessible key IDs — not yet wired into crypto paths). +- A **server-side request gate** that, on every incoming request, consults the Auth Manager and rejects messages the current session is not permitted to run. +- A **message group** `WH_MESSAGE_GROUP_AUTH = 0x0D00` with 7 new actions (login, logout, user add/delete/get, set-permissions, set-credentials), complete with endian/magic translation functions. +- A **pluggable backend**: everything goes through a `whAuthCb` callback vtable. A default in-memory backend lives in `src/wh_auth_base.c` (up to 5 users, credential storage up to 2 KiB per user, used by examples and tests). +- The feature is **opt-in**: entire subsystem is guarded by `WOLFHSM_CFG_ENABLE_AUTHENTICATION`. With it compiled in but no context configured (`server->auth == NULL`), the server logs a SECEVENT and processes all requests without any authorization check — preserving backwards compatibility. + +Design notes called out by the author: +- The "check key use" callback (`CheckKeyAuthorization`) is wired into the interface but **not yet invoked** on the key paths — it's a TODO placeholder. +- The base user list is in RAM, not NVM — deliberate for an initial cut. +- Logging of auth events (login success/failure, crypto actions) is another TODO, though authorization failures already log via `WH_LOG_ON_ERROR_F`. + +--- + +## 2. High-level architecture + +``` + ┌──────────────────────┐ + │ Client App │ + │ wh_Client_Auth*() │ client-side request/response helpers + └──────────┬───────────┘ + │ WH_MESSAGE_GROUP_AUTH (0x0D00) + │ + ┌──────────────▼────────────────────────────────────────────┐ + │ Server dispatch │ + │ wh_Server_HandleRequestMessage() (src/wh_server.c) │ + │ 1. Recv packet -> extract (group, action) │ + │ 2. wh_Auth_CheckRequestAuthorization(group, action) │ <-- the gate + │ 3. Dispatch by group │ + └──────────────┬─────────────────────────┬──────────────────┘ + │ │ + WH_MESSAGE_GROUP_AUTH any other group + │ │ + ▼ ▼ + ┌─────────────────────────┐ (NVM/key/crypto/SHE/etc. handlers; + │ wh_Server_HandleAuth- │ they do not re-check auth — the gate + │ Request() │ above has already vetted the call) + │ (src/wh_server_auth.c) │ + └───────┬─────────────────┘ + │ wh_Auth_Login / _Logout / _UserAdd / _UserDelete / + │ _UserGet / _UserSetPermissions / _UserSetCredentials + ▼ + ┌─────────────────────────┐ + │ Auth Manager core │ transport/protocol-agnostic wrappers + │ src/wh_auth.c │ that take the lock and delegate to cb + └───────┬─────────────────┘ + │ whAuthCb->Login / ->UserAdd / ... + ▼ + ┌─────────────────────────┐ + │ Pluggable backend │ + │ default: wh_auth_base │ in-memory user db, SHA-256 PIN hashing, + │ (src/wh_auth_base.c) │ optional wolfSSL cert verification + └─────────────────────────┘ +``` + +Key separation of concerns: + +1. `wh_auth.h` / `wh_auth.c` — **the "front end":** public API, session state, locking, policy decisions (the default group+action bitmap check). Always compiled when auth is on; does not depend on any specific user-store format. +2. `wh_auth_base.h` / `wh_auth_base.c` — **reference backend:** owns the user list, hashes PINs, verifies certificates, stores permissions. Can be swapped for a custom backend by registering a different `whAuthCb` vtable. +3. `wh_message_auth.h` / `wh_message_auth.c` — wire format and endian translation for all 7 auth messages (plus a flatten/unflatten pair for the permissions struct, which is too large and array-heavy for the usual `WH_T*()` helpers). +4. `wh_server_auth.c` / `wh_client_auth.c` — the message handlers on each side, each of which lives under both `WOLFHSM_CFG_ENABLE_{SERVER,CLIENT}` and `WOLFHSM_CFG_ENABLE_AUTHENTICATION` guards. + +--- + +## 3. Files touched (grouped) + +| Area | Files | +|------|-------| +| New public headers | `wolfhsm/wh_auth.h`, `wolfhsm/wh_auth_base.h`, `wolfhsm/wh_message_auth.h`, `wolfhsm/wh_server_auth.h` | +| Core & base impl | `src/wh_auth.c`, `src/wh_auth_base.c`, `src/wh_message_auth.c` | +| Server integration | `src/wh_server.c`, `src/wh_server_auth.c`, `wolfhsm/wh_server.h` | +| Client integration | `src/wh_client.c`, `src/wh_client_auth.c`, `wolfhsm/wh_client.h` | +| Error / message enums | `wolfhsm/wh_error.h` (3 new codes), `wolfhsm/wh_message.h` (new group + actions) | +| Examples | `examples/posix/wh_posix_server/wh_posix_server_cfg.c`, `wh_posix_server_cfg.h`, `wh_posix_server.c`; `examples/posix/wh_posix_client/Makefile`; `examples/posix/wh_posix_server/Makefile`; `examples/demo/client/wh_demo_client_auth.{c,h}`; `examples/demo/client/wh_demo_client_all.c` | +| Tests | `test/wh_test_auth.{c,h}` (1440 LOC), hook-ins in `test/wh_test.c`, `wh_test_clientserver.c`, `wh_test_crypto.c`, `wh_test_keywrap.c`, `wh_test_she.c`, `wh_test_log.c`, `wh_test_posix_threadsafe_stress.c`, `wh_test_common.h`, `test/Makefile` | +| Misc | `src/wh_server_she.c` (SHE tests now log in as admin), `src/wh_utils.c` + `wolfhsm/wh_utils.h` (new `wh_Utils_ForceZero` and `wh_Utils_ConstantCompare`), `port/posix/posix_transport_tls.c`, CI workflows, `docs/src/chapter09.md` (new docs chapter) | + +--- + +## 4. Data model (what an "individual user" looks like) + +### 4.1 Identity: `whUserId` + +`whUserId` is a `uint16_t`. Zero is reserved as `WH_USER_ID_INVALID`. The base backend assigns IDs 1..`WH_AUTH_BASE_MAX_USERS` (5 by default), where the ID is literally the 1-based slot in the static users array (`id = slot_index + 1`). + +### 4.2 Credentials: `whAuthMethod` + +```c +typedef enum { + WH_AUTH_METHOD_NONE = 0, + WH_AUTH_METHOD_PIN, // SHA-256 hashed when crypto is enabled + WH_AUTH_METHOD_CERTIFICATE, // wolfSSL cert verification; gated on WOLFHSM_CFG_CERTIFICATE_MANAGER +} whAuthMethod; +``` + +- PIN: the base backend stores the 32-byte SHA-256 of the PIN (falls back to a direct copy when `WOLFHSM_CFG_NO_CRYPTO` is set). Comparison uses `wh_Utils_ConstantCompare` — a new utility added by this PR. +- Certificate: the user's stored "credential" is a CA in DER; login presents a leaf cert, which the base backend feeds through `wolfSSL_CertManagerLoadCABuffer` + `wolfSSL_CertManagerVerifyBuffer`. + +### 4.3 Permissions: `whAuthPermissions` + +#### 4.3.1 The two-tier concept + +Every wolfHSM request on the wire is identified by a 16-bit `kind` that splits into: + +- **Group** (high byte): the category of operation. `WH_MESSAGE_GROUP_*` defines 13 groups today (`wolfhsm/wh_message.h:38-50`): `COMM=0x0100`, `NVM=0x0200`, `KEY=0x0300`, `CRYPTO=0x0400`, `IMAGE=0x0500`, `PKCS11=0x0600`, `SHE=0x0700`, `COUNTER=0x0800`, `CUSTOM=0x0A00`, `CRYPTO_DMA=0x0B00`, `CERT=0x0C00`, `AUTH=0x0D00`. +- **Action** (low byte): the specific operation within that group. Action enums are *group-local* — `KEY_CACHE` and `CRYPTO_SIGN` may both be value `0`, but they live in different groups so they're unambiguous in context. + +The auth manager mirrors that split in the user's `whAuthPermissions` struct as **two independent filters that both must pass** before a request is admitted to its handler: + +```c +typedef struct { + uint8_t groupPermissions[WH_NUMBER_OF_GROUPS + 1]; // boolean allow per group; last byte = admin flag + uint32_t actionPermissions[WH_NUMBER_OF_GROUPS][WH_AUTH_ACTION_WORDS]; // 256 bits per group (8 x uint32_t) + uint16_t keyIdCount; + uint32_t keyIds[WH_AUTH_MAX_KEY_IDS]; // small allowlist; default WH_AUTH_MAX_KEY_IDS = 2 +} whAuthPermissions; +``` + +#### 4.3.2 Filter 1 — group boolean + +`groupPermissions[groupIndex]` (where `groupIndex = (group >> 8) & 0xFF`) is a single byte: nonzero means "this user is allowed to talk to this group at all." If it's 0, the request is denied without ever looking at the bitmap. It's a fast reject path *and* a coarse on/off switch — useful for "this user only ever uses NVM, never crypto." + +The `+1` slot at the end (`groupPermissions[WH_NUMBER_OF_GROUPS]`) is reused as the **admin flag** — `WH_AUTH_IS_ADMIN(p)` reads it. Admin isn't a group; it's a separate capability that gates things like `UserAdd` of another admin and cross-user logout. + +#### 4.3.3 Filter 2 — action bitmap + +If the group passes, the gate then checks the per-group **256-bit bitmap** stored as 8 × `uint32_t`. The mapping is straightforward (`wolfhsm/wh_auth.h:79`): + +```c +wordIdx = action / 32 +bitMask = 1U << (action % 32) +allowed = actionPermissions[groupIndex][wordIdx] & bitMask +``` + +Wire actions are `uint16_t`, so 65 536 are theoretically possible — the model caps at 256 and rejects anything beyond. That's a deliberate trade: 256 bits per group keeps the struct flat and copy-friendly (the whole `whAuthPermissions` flattens to ~473 bytes for the wire) at the cost of an upper limit on actions per group. Today no group comes close. + +#### 4.3.4 Derived constants and byte shape + +- `WH_NUMBER_OF_GROUPS = (WH_MESSAGE_GROUP_MAX >> 8) + 1` — currently 14, since `WH_MESSAGE_GROUP_MAX = WH_MESSAGE_GROUP_AUTH = 0x0D00`. +- `WH_AUTH_ACTIONS_PER_GROUP = 256`, `WH_AUTH_ACTION_WORDS = 8`. + +Shape in bytes (exactly what gets flattened on the wire, `WH_FLAT_PERMISSIONS_LEN`): +``` +(WH_NUMBER_OF_GROUPS + 1) // group booleans + admin ++ 4 * WH_NUMBER_OF_GROUPS * WH_AUTH_ACTION_WORDS // action bitmap (per-group) ++ 2 // keyIdCount ++ 4 * WH_AUTH_MAX_KEY_IDS // keyIds += 15 + (4 * 14 * 8) + 2 + 8 = 473 bytes // with current defaults +``` + +#### 4.3.5 Helper macros + +The two filters compose through the macros in `wolfhsm/wh_auth.h:86-125`: + +| Macro | Group byte | Action bitmap | +|-------|------------|---------------| +| `WH_AUTH_IS_ADMIN(p)` | reads admin slot (byte index `WH_NUMBER_OF_GROUPS`) | — | +| `WH_AUTH_SET_IS_ADMIN(p, v)` | writes admin slot (`v ? 1 : 0`) | — | +| `WH_AUTH_SET_ALLOWED_GROUP(p, group)` | set to 1 | set all 256 bits | +| `WH_AUTH_SET_ALLOWED_ACTION(p, group, action)` | set to 1 | OR in one bit (existing bits preserved — Copilot flagged a mismatch with the header comment that says "only the given action bit") | +| `WH_AUTH_CLEAR_ALLOWED_GROUP(p, group)` | set to 0 | zero all bits | +| `WH_AUTH_CLEAR_ALLOWED_ACTION(p, group, action)` | left alone | clear one bit | + +So enabling a single action also implicitly enables its group, but disabling a single action leaves the group enabled (you can still use *other* actions in it). Disabling the group nukes everything. + +#### 4.3.6 Worked examples + +**Crypto-only signer:** group byte set for `CRYPTO`, only the `SIGN` action bit set in the CRYPTO bitmap, every other group byte = 0. Any NVM/KEY/IMAGE/etc. request hits filter 1 and is denied; any CRYPTO request other than `SIGN` passes filter 1 but fails filter 2. + +**Admin everything:** the example POSIX server seeds admin with `memset(&permissions, 0xFF, sizeof(permissions))` (`examples/posix/wh_posix_server/wh_posix_server_cfg.c:719`). That sets every group byte, every action bit, and the admin slot in one shot. + +#### 4.3.7 Exceptions to the bitmap + +The gate (`src/wh_auth.c:206`) hard-codes a few unconditional allows that bypass both filters: + +- Unauthenticated session → all of group `COMM`, plus `(AUTH, LOGIN)`. Without these, no one could ever open a connection or log in. +- Authenticated session → `(AUTH, LOGOUT)` always succeeds. You can always log yourself out regardless of permission state. + +Everything else flows through the two-tier filter described above, optionally followed by the backend's `cb->CheckRequestAuthorization` override (see §7.1) which sees the tentative verdict and can flip it either way. + +### 4.4 Session: `whAuthUser` / `whAuthContext` + +```c +typedef struct { + whUserId user_id; + char username[32]; + whAuthPermissions permissions; + bool is_active; +} whAuthUser; + +struct whAuthContext_t { + whAuthCb* cb; // backend vtable + whAuthUser user; // *the* currently-logged-in user for this connection + void* context; // opaque backend state +#ifdef WOLFHSM_CFG_THREADSAFE + whLock lock; +#endif +}; +``` + +A single `whAuthContext` holds **one** logged-in user at a time (src/wh_auth.c:139: "allowing only one user logged in to an open connection at a time"). A second login attempt while someone is already logged in returns success at the protocol level with `loggedIn=0` set — i.e. the call was processed fine, authentication simply didn't happen. + +--- + +## 5. The plugin contract: `whAuthCb` + +The core wraps every operation, acquires the lock, and then calls into this vtable: + +```c +typedef struct { + int (*Init)(void* ctx, const void* cfg); + int (*Cleanup)(void* ctx); + + int (*Login)(void* ctx, uint8_t client_id, whAuthMethod method, + const char* username, const void* auth_data, uint16_t auth_data_len, + whUserId* out_user_id, whAuthPermissions* out_permissions, + int* loggedIn); + int (*Logout)(void* ctx, whUserId current_user_id, whUserId user_id); + + /* Optional authorization-decision overrides */ + int (*CheckRequestAuthorization)(void* ctx, int err, uint16_t user_id, + uint16_t group, uint16_t action); + int (*CheckKeyAuthorization)(void* ctx, int err, uint16_t user_id, + uint32_t key_id, uint16_t action); + + /* User management */ + int (*UserAdd)(void* ctx, const char* username, whUserId* out_user_id, + whAuthPermissions permissions, whAuthMethod method, + const void* credentials, uint16_t credentials_len); + int (*UserDelete)(void* ctx, whUserId current_user_id, whUserId user_id); + int (*UserSetPermissions)(void* ctx, whUserId current_user_id, + whUserId user_id, whAuthPermissions permissions); + int (*UserGet)(void* ctx, const char* username, + whUserId* out_user_id, whAuthPermissions* out_permissions); + int (*UserSetCredentials)(void* ctx, whUserId user_id, whAuthMethod method, + const void* current_credentials, uint16_t current_credentials_len, + const void* new_credentials, uint16_t new_credentials_len); +} whAuthCb; +``` + +The two "Check*" callbacks are **overrides, not gates** — see §7 below for exactly how they're layered over the default decision. + +--- + +## 6. Default backend (`wh_auth_base.c`) + +- **Storage:** `static whAuthBase_User users[WH_AUTH_BASE_MAX_USERS]` (=5). Each slot has the public `whAuthUser`, the chosen method, and a `credentials[2048]` byte buffer (+ length). The author notes this is intentionally simple and not yet NVM-backed. +- **Thread safety:** explicitly documented (src/wh_auth_base.c:54) — the global array is protected by the auth context's lock which the core `wh_Auth_*` wrappers acquire before calling any backend entry. The backend itself does no locking. +- **PIN path:** `wh_Auth_BaseCheckPin` hashes the incoming PIN with `wc_Sha256Hash_ex` and compares to the stored digest using `wh_Utils_ConstantCompare`. Hash buffer is `wh_Utils_ForceZero`d on exit whether the compare succeeded or not. When `WOLFHSM_CFG_NO_CRYPTO` is set, the PIN is stored verbatim (bounded by `WH_AUTH_BASE_MAX_CREDENTIALS_LEN`). +- **Certificate path:** guarded by `WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO`. Uses a per-call `WOLFSSL_CERT_MANAGER` seeded with the user's stored DER as a CA and then verifies the supplied leaf. +- **Admin enforcement:** `wh_Auth_BaseUserDelete` and `wh_Auth_BaseUserSetPermissions` both require `current_user_id` (the caller session) to have the admin flag. `wh_Auth_BaseLogout` allows logging out someone *other* than yourself only if you're admin. +- **Set-credentials:** if the target user already has credentials, the old ones must be presented and match (constant-time compare, PIN hashed first); otherwise `current_credentials` must be NULL. PINs are rehashed before replacement, and intermediate hash buffers are force-zeroed. +- **User ID policy:** 1-based indexes into `users[]`; 0 reserved. Duplicate usernames are rejected by `wh_Auth_BaseUserAdd` with `WH_ERROR_BADARGS`. `keyIdCount` is clamped to `WH_AUTH_MAX_KEY_IDS` and unused `keyIds` entries are zeroed (done both on add and on set-permissions). + +--- + +## 7. Authorization — how the server enforces it on *every* request + +The integration point is **`wh_Server_HandleRequestMessage`** in `src/wh_server.c`. After a packet is received and the `(group, action)` are extracted, before the switch on `group`: + +```c +#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION + if (server->auth != NULL) { + rc = wh_Auth_CheckRequestAuthorization(server->auth, group, action); + if (rc != WH_ERROR_OK) { + int32_t error_code = (int32_t)WH_AUTH_PERMISSION_ERROR; + uint16_t resp_size = _FormatAuthErrorResponse(magic, group, action, + error_code, data); + do { rc = wh_CommServer_SendResponse(server->comm, magic, kind, + seq, resp_size, data); + } while (rc == WH_ERROR_NOTREADY); + WH_LOG_ON_ERROR_F(&server->log, WH_LOG_LEVEL_ERROR, + WH_AUTH_PERMISSION_ERROR, + "Authorization failed for (group=%d, action=%d, seq=%d)", + group, action, seq); + return rc; + } + } +#endif +``` + +Two deliberate design points here: + +1. **The check happens once per request**, up-front, on the front end — as explicitly requested by @bigbrett in review ("the actual authorization check … should be part of the generic 'front end' and not delegated to the back-end"). Backends only affect authorization through the optional override callback. +2. **When `server->auth == NULL`** (auth compiled in but not configured) the check is skipped entirely and a SECEVENT log line is emitted at init time to announce this. That keeps existing code/tests working without having to introduce logins everywhere. + +### 7.1 The default decision (`wh_Auth_CheckRequestAuthorization`) + +In `src/wh_auth.c` the flow inside the lock is: + +1. Read `user_id = context->user.user_id`. +2. **If no user is logged in** (`user_id == WH_USER_ID_INVALID`): + - Allow `WH_MESSAGE_GROUP_COMM` (so a client can still perform comm handshakes/echo/close). + - Allow `WH_MESSAGE_GROUP_AUTH` + `WH_MESSAGE_AUTH_ACTION_LOGIN` (so a client can actually log in). + - Deny everything else → `WH_ERROR_ACCESS`. +3. **If a user is logged in:** + - Always allow `WH_MESSAGE_GROUP_AUTH` + `WH_MESSAGE_AUTH_ACTION_LOGOUT` (you can always log yourself out). + - Otherwise, look up `groupIndex = (group >> 8) & 0xFF`: + - Bounds-check `groupIndex < WH_NUMBER_OF_GROUPS`. + - If `permissions.groupPermissions[groupIndex] == 0` → deny. + - If `action >= WH_AUTH_ACTIONS_PER_GROUP` → deny. + - Map `action → (wordIdx, bitMask)` via `WH_AUTH_ACTION_TO_WORD_AND_BITMASK`. Allow iff the bit is set in `permissions.actionPermissions[groupIndex][wordIdx]`. +4. **Override hook:** if `cb->CheckRequestAuthorization != NULL`, invoke it with the preliminary `rc`, the user id, and the (group, action). Its return becomes the final decision. This is what `test/wh_test_auth.c` exercises to confirm backends can see the result and flip it either direction. + +If this stage denies the request, the server synthesizes a per-group/per-action error response via `_FormatAuthErrorResponse` (new helper) so the client always gets a well-formed reply carrying `WH_AUTH_PERMISSION_ERROR`. The helper handles the three auth responses that are bigger than `SimpleResponse` (Login / UserAdd / UserGet), the oversized NVM ones, the Cert group, and falls back to writing just a translated `int32_t` rc for everything else. + +### 7.2 Key-level authorization — deferred + +`wh_Auth_CheckKeyAuthorization` and the `CheckKeyAuthorization` callback are defined and tested (presence-of-callback), but **no current request handler calls it**. The PR author called this out explicitly: "I added a callback function framework for checking authorization of key use based on key ID and user permissions but did not tie in that check yet." The reviewer should confirm no crypto/key handler was modified to call it — otherwise callers silently skip that layer today. + +### 7.3 Admin gating in auth operations + +Two additional checks live above the backend in `wh_Auth_UserAdd` (src/wh_auth.c): + +- `WH_AUTH_IS_ADMIN(permissions_to_assign) && !WH_AUTH_IS_ADMIN(current_session_permissions)` → `WH_AUTH_PERMISSION_ERROR`. That is, **a non-admin session can never promote another user to admin**. This is enforced in the core, not the backend, so any custom backend inherits it. + +The backend `wh_Auth_BaseUserDelete` and `wh_Auth_BaseUserSetPermissions` additionally require the caller to be admin. `wh_Auth_BaseLogout` requires admin for cross-user logouts. + +### 7.4 Auto-logout on disconnect + +The COMM group's `CLOSE` action handler (src/wh_server.c:270) now logs the current user out when the comm channel is torn down. This prevents a stale session from persisting across client reconnects on the same server. + +--- + +## 8. Wire protocol — the auth message group + +Added to `wolfhsm/wh_message.h`: + +```c +WH_MESSAGE_GROUP_AUTH = 0x0D00 +WH_MESSAGE_GROUP_MAX = 0x0D00 // bumped so WH_NUMBER_OF_GROUPS reflects it + +enum { + WH_MESSAGE_AUTH_ACTION_LOGIN, + WH_MESSAGE_AUTH_ACTION_LOGOUT, + WH_MESSAGE_AUTH_ACTION_USER_ADD, + WH_MESSAGE_AUTH_ACTION_USER_DELETE, + WH_MESSAGE_AUTH_ACTION_USER_GET, + WH_MESSAGE_AUTH_ACTION_USER_SET_PERMISSIONS, + WH_MESSAGE_AUTH_ACTION_USER_SET_CREDENTIALS, +}; +``` + +The 7 request/response pairs live in `wh_message_auth.h`. Three of them carry **variable-length payloads** after a fixed header (login auth data, user-add credentials, set-credentials' two credential buffers). Those use `Translate*Request(void* src_packet, uint16_t src_size, ...)` helpers that validate `src_size` against the header-plus-declared-payload length (returning `WH_ERROR_BUFFER_SIZE` on mismatch), and each type has its own cap: + +```c +WH_MESSAGE_AUTH_LOGIN_MAX_AUTH_DATA_LEN + = COMM_DATA_LEN - sizeof(LoginRequest) +WH_MESSAGE_AUTH_USERADD_MAX_CREDENTIALS_LEN + = COMM_DATA_LEN - sizeof(UserAddRequest) +WH_MESSAGE_AUTH_SETCREDS_MAX_CREDENTIALS_LEN + = (COMM_DATA_LEN - sizeof(UserSetCredentialsRequest)) / 2 +``` + +`whAuthPermissions` is large and contains nested arrays, so the PR adds `wh_MessageAuth_FlattenPermissions` / `_Unflatten…` to marshal it into a fixed-size little-endian byte buffer (`WH_FLAT_PERMISSIONS_LEN` bytes) that's embedded in the UserAdd, UserGet, and UserSetPermissions messages. Everything else uses the standard `WH_T16`/`WH_T32` magic-aware translation helpers. + +Responses either use a dedicated type (Login → user_id, UserAdd → user_id, UserGet → user_id + flat permissions) or the shared `whMessageAuth_SimpleResponse { int32_t rc; }`. + +--- + +## 9. Client API (`src/wh_client_auth.c`) + +Every action comes in the wolfHSM-standard three flavors — a non-blocking send, a non-blocking receive, and a blocking loop on `WH_ERROR_NOTREADY`: + +```c +/* One-shot helpers */ +int wh_Client_AuthLogin(whClientContext* c, whAuthMethod method, + const char* username, const void* auth_data, + uint16_t auth_data_len, + int32_t* out_rc, whUserId* out_user_id); +int wh_Client_AuthLogout(whClientContext* c, whUserId user_id, int32_t* out_rc); +int wh_Client_AuthUserAdd(whClientContext* c, const char* username, + whAuthPermissions permissions, whAuthMethod method, + const void* credentials, uint16_t credentials_len, + int32_t* out_rc, whUserId* out_user_id); +int wh_Client_AuthUserDelete(whClientContext*, whUserId, int32_t* out_rc); +int wh_Client_AuthUserGet(whClientContext*, const char* username, + int32_t* out_rc, whUserId* out_user_id, + whAuthPermissions* out_permissions); +int wh_Client_AuthUserSetPermissions(whClientContext*, whUserId, + whAuthPermissions, int32_t* out_rc); +int wh_Client_AuthUserSetCredentials(whClientContext*, whUserId, whAuthMethod, + const void* current, uint16_t current_len, + const void* new, uint16_t new_len, + int32_t* out_rc); +``` + +Client-side defensive behavior to notice during review: + +- **Username validity** (`_UserNameIsValid` in wh_client_auth.c) requires non-NULL, non-empty, `< WH_MESSAGE_AUTH_MAX_USERNAME_LEN` (32) chars. +- The client stages all credential-carrying requests in a **stack buffer of size `WOLFHSM_CFG_COMM_DATA_LEN`** and calls `wh_Utils_ForceZero(buffer, sizeof(buffer))` before returning. This is the client-side mirror of the zeroization the server does after processing. +- `wh_Client_AuthLoginResponse` tolerates a server that responds with a `SimpleResponse` instead of a `LoginResponse` — that's how the server signals `WH_AUTH_NOT_ENABLED` to older/simpler clients; the demo (`wh_demo_client_auth.c`) keys off this to skip the demo cleanly. +- Every response handler validates `(resp_group, resp_action, resp_size)` before trusting the buffer. This is defense-in-depth against a desynchronized server — important since responses are returned in the same memory the request was written to. + +--- + +## 10. A concrete end-to-end: "user logs in and does a crypto op" + +To make the per-request/per-user flow concrete, here's what happens when a client does a `Login` followed by, say, a cached-key crypto call. Assume the server has been seeded with an admin `admin/1234` and a non-admin `demo` with `CRYPTO` group access (the exact setup in the POSIX example and the demo): + +1. **Client sends `Login("demo", PIN="…")`.** + - `wh_Client_AuthLogin` packs `whMessageAuth_LoginRequest` + PIN bytes into the comm buffer and sends with `(group=AUTH, action=LOGIN)`. + - The buffer is `ForceZero`d before return. +2. **Server front end** (`wh_Server_HandleRequestMessage`) receives the packet, extracts `(AUTH, LOGIN)`, and calls `wh_Auth_CheckRequestAuthorization`. No one is logged in yet, but the gate explicitly whitelists `(AUTH, LOGIN)` — passes. +3. **`wh_Server_HandleAuthRequest`** is dispatched, which for `LOGIN`: + - Translates the header (endian/magic) via `wh_MessageAuth_TranslateLoginRequest`. + - Calls `wh_Auth_Login(server->auth, comm->client_id, method, username, auth_data, auth_data_len, &loggedIn)`. +4. **`wh_Auth_Login`** acquires the auth lock. If someone is already logged in on this context, it returns `WH_ERROR_OK` with `loggedIn=0` (the slot is "busy"). Otherwise it calls the backend `Login`. On success it stashes `user.user_id`, `user.permissions`, and `user.is_active = true` inside the `whAuthContext`. +5. **`wh_Auth_BaseLogin`** (PIN path) hashes the provided PIN with `wc_Sha256Hash_ex`, looks the username up in the static array, constant-time compares the digests, sets `loggedIn=1` and copies out the user id and permissions on match. The hash scratch buffer is `ForceZero`d on every exit path. +6. **Server sends `LoginResponse`** containing either the new `user_id` or `WH_AUTH_LOGIN_FAILED`. The request packet's `auth_data` region is `ForceZero`d before the server returns. +7. **Client receives**, pulls out `out_rc` and `out_user_id` for later `Logout`. +8. **Client now does a crypto call**, e.g. `wc_…` which goes through the crypto-callback layer and ultimately sends `(group=CRYPTO, action=)`. +9. **Server front end** runs `wh_Auth_CheckRequestAuthorization(CRYPTO, op)`. Inside: + - `user_id` is non-invalid. + - `groupIndex = (WH_MESSAGE_GROUP_CRYPTO >> 8) & 0xFF = 4`. + - `permissions.groupPermissions[4]` is 1 → proceed to action bitmap. + - `(wordIdx, bitMask)` is computed from the action enum; allowed iff the bit is set. + - If a `CheckRequestAuthorization` callback is registered, it gets the tentative verdict and can flip it. In the example server it is `NULL`, so the callback step is skipped. +10. **If allowed** — the normal crypto handler runs; **no additional auth check** is performed today, even when the operation names a specific `keyId`. That's the TODO: the `CheckKeyAuthorization` callback and per-user `keyIds` allowlist exist in the data model and public API but the PR does not wire them into the crypto path. +11. **If denied** — `_FormatAuthErrorResponse` writes a group-appropriate error response carrying `WH_AUTH_PERMISSION_ERROR` (-2301), `wh_CommServer_SendResponse` ships it, and `WH_LOG_ON_ERROR_F` logs "Authorization failed for (group=%d, action=%d, seq=%d)". The request never reaches the crypto handler. +12. **Eventually the client sends `Close` on the comm channel.** The server's COMM close handler detects a live user, calls `wh_Auth_Logout`, which clears the `user` field inside `whAuthContext`. This happens even if the client forgets to call `AuthLogout` explicitly. + +--- + +## 11. Noteworthy security posture + +Things that are present and worth confirming during review: + +- **Constant-time credential compare** (`wh_Utils_ConstantCompare`, new utility) used for PIN hashes and cert buffers in the base backend. +- **Force-zero of sensitive buffers** (`wh_Utils_ForceZero`, new utility) on both client and server: PIN hash scratch, request-packet credential regions after processing, response packets with credentials, entire staging buffer on the client before return, and the user array on base-backend cleanup. Several of the later commits in the PR were exactly to add more of these. +- **Single-session-per-connection** semantics reduce the attack surface for cross-user confusion inside one comm channel. +- **Admin promotion guard** sits in the generic front end (`wh_Auth_UserAdd`), so backends can't accidentally allow it even if their `UserAdd` doesn't check. +- **Lock discipline**: all `wh_Auth_*` wrappers take the lock before calling into the backend; `wh_Auth_BaseLogin` and friends document that they expect to be called under the lock. Reviewer should verify any new call sites honor this. +- **Graceful fallback responses**: clients that get `WH_AUTH_NOT_ENABLED` when auth isn't configured server-side still see a well-formed message, not a malformed/oversized frame. + +Things that are **explicit open items** (per PR body and code comments): + +- No NVM backing for the user list — the base backend is RAM-only and losses on reboot. +- `CheckKeyAuthorization` is wired but not called anywhere in the request-handling paths in this PR. +- Logging of login attempts (successes and failures) is a TODO — only authorization denials are logged today. +- The `WH_AUTH_SET_ALLOWED_ACTION` macro comment says "and only the given action bit," but the implementation ORs (Copilot raised this during review). Either the comment or the semantics should change. + +--- + +## 12. Review checklist (suggested focus areas) + +1. **Front-end gate placement** — confirm every server-side request path goes through `wh_Server_HandleRequestMessage` before reaching a group handler. In particular, check DMA, SHE, PKCS11, CERT, and custom handler dispatch paths; anything that inserts a second dispatch could bypass the gate. +2. **`server->auth == NULL` semantics** — tests should confirm (a) auth-compiled + no context lets all non-auth requests through and (b) auth requests in that state return `WH_AUTH_NOT_ENABLED`, not `WH_ERROR_BADARGS`. +3. **Cross-user logout semantics** — `wh_Auth_Logout` in the core still wipes local session memory only if `user_id == context->user.user_id`, but the backend `wh_Auth_BaseLogout` can also deactivate a different user if the caller is admin. That asymmetry is intentional but worth double-checking against the test cases. +4. **Message validation against `WOLFHSM_CFG_COMM_DATA_LEN`** — particularly UserSetCredentials, which packs *two* variable-length buffers back to back. The per-message caps exist (`WH_MESSAGE_AUTH_SETCREDS_MAX_CREDENTIALS_LEN`) but verify both client and server reject an aggregate-size overrun. +5. **Macro behavior vs docs** on `WH_AUTH_SET_ALLOWED_ACTION` (OR vs assign) — the project should decide intended semantics since callers (including the demo) rely on OR behavior. +6. **`keyIdCount` clamping + unused slot zeroing** is duplicated between `UserAdd` and `UserSetPermissions` — worth a small helper to keep these in sync. +7. **Thread-safety contract** — backend docs say "protected by the auth context lock"; confirm any future backend author can't easily step outside the lock by, e.g., calling `wh_Auth_BaseFindUser` directly. +8. **Auto-logout on close** — confirm tests cover a client that crashes mid-session and reconnects; `CLOSE` isn't the only path to disconnection. +9. **`CheckKeyAuthorization` TODO** — decide whether merging without at least a scaffolded call site in the key/crypto handlers is acceptable, or whether it should be added (even if defaulting to allow) before merge so customers don't build against an interface that changes behavior later. +10. **Force-zero coverage** — spot-check that every function that stages credentials or PIN digests on the stack `ForceZero`s before return, even on error paths. The commit log shows several late additions here, suggesting it's easy to miss. + +--- + +## 13. Build & try it locally + +Per `docs/src/chapter09.md`, enable the feature via `WOLFHSM_CFG_ENABLE_AUTHENTICATION` and use the `AUTH=1` Make flag in the POSIX examples/tests: + +```bash +# Tests +cd test && make clean && make -j AUTH=1 && make run + +# Example server + demo client +cd examples/posix/wh_posix_server && make AUTH=1 +cd examples/posix/wh_posix_client && make AUTH=1 +``` + +The example server seeds an `admin/1234` user with everything-allowed permissions at startup (see `wh_PosixServer_ExampleAuthConfig` in `examples/posix/wh_posix_server/wh_posix_server_cfg.c`). The demo client (`examples/demo/client/wh_demo_client_auth.c`) logs in as admin, adds `demo/1234` with USER_SET_CREDENTIALS permission, rotates the `demo` PIN to `5678`, verifies the old PIN fails and the new PIN works, then logs out. Running the demo against an auth-disabled server returns `WH_AUTH_NOT_ENABLED` and the demo prints "Authentication not enabled on server, skipping …" and returns `WH_ERROR_OK` — useful for CI matrix coverage without separate test binaries. diff --git a/docs/draft/certificates.md b/docs/draft/certificates.md new file mode 100644 index 00000000..873d2dd6 --- /dev/null +++ b/docs/draft/certificates.md @@ -0,0 +1,555 @@ +# Certificate Chain Validation in wolfHSM + +## 1. Overview + +wolfHSM provides a server-resident X.509 certificate manager that lets clients +provision trusted root anchors into NVM and then verify candidate certificate +chains against those anchors over the standard wolfHSM client/server protocol. +The chain walk, signature checks, anchor selection, and any custom verify +callbacks all run inside the trusted server environment; the client only ever +ships DER bytes and trust-anchor identifiers, never private key material or +root certificates that have been provisioned with the non-exportable flag. + +The feature set is layered. Each layer below adds capability without +invalidating the layer above it, and each is independently gated by a +compile-time configuration macro. + +| Capability | Macro | Notes | +|-----------------------------------------|----------------------------------------------------|-------| +| Trusted-root NVM CRUD + chain verify | `WOLFHSM_CFG_CERTIFICATE_MANAGER` | Base feature. Requires crypto. | +| Multi-root chain verify | (always available with the base feature) | Bounded by `WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS`. | +| Trusted CA verify-result cache | `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE` | Per-server cache by default. | +| Cross-client (global) verify cache | `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL` | Layered on top of the verify cache. | +| Cache leaf public key after verify | `WH_CERT_FLAGS_CACHE_LEAF_PUBKEY` request flag | Available on every verify variant. | +| Attribute-certificate (X.509 ACERT) | `WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT` | Single-root verify only. | +| DMA transport for large chains | `WOLFHSM_CFG_DMA` | Available on every cert API. | +| User-supplied verify callback | `whServerCertConfig.verifyCb` (server-side only) | Applied per cert manager. | + +The remaining sections walk the client API for each operation, then dive into +the multi-root and trusted-cache features in detail and describe the precise +semantics that result when both are enabled together. + +## 2. Build Configuration + +### Required + +- `WOLFHSM_CFG_CERTIFICATE_MANAGER` — enables every API in this document. + Requires `!WOLFHSM_CFG_NO_CRYPTO` (the implementation depends on + `WOLFSSL_CERT_MANAGER` and the wolfCrypt ASN.1 decoder). + +### Optional + +- `WOLFHSM_CFG_DMA` — enables the `*Dma*` variants that pass the candidate + chain by client address rather than copying it through the comm buffer. +- `WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT` — enables `wh_Client_CertVerifyAcert` + / `wh_Client_CertVerifyAcertDma`. Requires wolfSSL built with `WOLFSSL_ACERT` + and `WOLFSSL_ASN_TEMPLATE`. +- `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE` — enables the trusted CA verify cache + (Section 5). Pulls in `wh_Client_CertVerifyCacheClear` on the client. +- `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL` — relocates the verify cache + from the per-server context into the shared NVM context so it is reused + across every client connected to the server. Requires + `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE`. + +### Bounds + +- `WOLFHSM_CFG_MAX_CERT_SIZE` — maximum DER size of any single certificate + read from or written to NVM. Defaults to `WOLFHSM_CFG_COMM_DATA_LEN` when + DMA is off, `4096` when DMA is on. +- `WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS` — upper bound on the number of trusted + root NVM IDs that may be supplied to a single multi-root verify call. + Defaults to `8`. This bound also sizes the inline root-id array in the + multi-root DMA request and the per-slot root binding in the verify cachel + fails a static assert if the resulting DMA request struct would exceed + `WOLFHSM_CFG_COMM_DATA_LEN`. +- `WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT` — number of slots in the verify + cache (FIFO ring). Defaults to `16`. + +## 3. Common Concepts + +### 3.1 Trust anchors live in NVM + +Every trusted root is a regular NVM object identified by a `whNvmId`. The +client provisions roots with `wh_Client_CertAddTrusted`, removes them with +`wh_Client_CertEraseTrusted`, and reads them back with +`wh_Client_CertReadTrusted`. Verification operations take the NVM ID(s) of +the root(s) to anchor against — the root certificate bytes themselves are +never sent inline with a verify request. + +Roots respect normal NVM access and flag policy. A root provisioned with +`WH_NVM_FLAGS_NONEXPORTABLE` cannot be read back via +`wh_Client_CertReadTrusted` (the server returns `WH_ERROR_ACCESS`) but is +still usable as a verify anchor. + +### 3.2 Verification flags (`whCertFlags`) + +Defined in `wolfhsm/wh_common.h`: + +- `WH_CERT_FLAGS_NONE` — verify only. +- `WH_CERT_FLAGS_CACHE_LEAF_PUBKEY` — on a successful verify, extract the + leaf certificate's `SubjectPublicKeyInfo` and cache it in the server's + key cache so subsequent crypto operations can address it by `whKeyId`. + +### 3.3 Cached leaf key id + +Verify variants whose name contains `AndCacheLeafPubKey` take an `inout_keyId` +argument. On entry, supply either an explicit `whKeyId` or `WH_KEYID_ERASED` +to let the server pick a unique id; on success, the caller-side id is updated +with the value the server actually used. Failed verifies leave the prior id +contents undisturbed and do not populate the key cache. + +`cachedKeyFlags` carries the NVM usage flags applied to the cached key — +typically `WH_NVM_FLAGS_USAGE_VERIFY` for a leaf certificate's public key. + +### 3.4 Async (request/response) split + +Every verify and trusted-root mutation API has three forms: + +- A single blocking call (e.g. `wh_Client_CertVerify`). +- A non-blocking `*Request` call that returns as soon as the request is on + the wire. +- A non-blocking `*Response` call that returns `WH_ERROR_NOTREADY` until the + server has replied, then yields `out_rc`. + +The blocking forms loop on `WH_ERROR_NOTREADY` internally. Use the split +pair when the calling thread needs to remain responsive (for example, to +service a separate request). + +### 3.5 Return-code conventions + +All client functions return a wolfHSM transport-layer `int`: +`WH_ERROR_OK` if the request and response cycle completed, or a negative +error code if the comm layer itself failed. + +The server's verify result is returned separately via `out_rc`: + +| `out_rc` | Meaning | +|-------------------------|----------------------------------------------------------------------| +| `WH_ERROR_OK` (0) | Chain anchored successfully. | +| `WH_ERROR_CERT_VERIFY` | Chain did not anchor (signature, expiry, or path failure). | +| `WH_ERROR_NOTFOUND` | (Multi-root only) every supplied root id was absent from NVM. | +| `WH_ERROR_BADARGS` | Argument shape or wire-payload size violation. | +| `WH_ERROR_ACCESS` | Read-trusted on a non-exportable cert. | +| Other negative codes | Underlying NVM, transport, or cert-manager environment errors. | + +This separation lets callers distinguish a real trust failure +(`WH_ERROR_CERT_VERIFY`) from "the trust store itself is empty" +(`WH_ERROR_NOTFOUND`) and from infrastructure errors. + +## 4. Client API + +All prototypes below live in `wolfhsm/wh_client.h`. The `*Request` / +`*Response` split forms are omitted from the listing for brevity but exist +for every blocking entry point shown. + +### 4.1 Initialization + +```c +int wh_Client_CertInit(whClientContext* c, int32_t* out_rc); +``` + +Initializes the server's certificate manager subsystem. Required once per +server before any other cert call. When the trusted-cert verify cache is +enabled in per-client mode, `CertInit` clears the calling client's cache +(see Section 5.4). + +### 4.2 Trusted root provisioning + +```c +int wh_Client_CertAddTrusted(whClientContext* c, whNvmId id, + whNvmAccess access, whNvmFlags flags, + uint8_t* label, whNvmSize label_len, + const uint8_t* cert, uint32_t cert_len, + int32_t* out_rc); + +int wh_Client_CertEraseTrusted(whClientContext* c, whNvmId id, int32_t* out_rc); + +int wh_Client_CertReadTrusted(whClientContext* c, whNvmId id, uint8_t* cert, + uint32_t* cert_len, int32_t* out_rc); +``` + +`CertAddTrusted` writes a DER root certificate into NVM under the supplied +`whNvmId` with the given access and flag policy. `CertEraseTrusted` removes +it. `CertReadTrusted` reads it back, with `*cert_len` updated to the actual +stored size on success (or, on `WH_ERROR_BUFFER_SIZE`, the size needed). + +When the verify cache is enabled, both `AddTrusted` and `EraseTrusted` also +trigger a cache eviction for the affected root id (Section 5.5). + +DMA variants: + +```c +int wh_Client_CertAddTrustedDma(whClientContext* c, whNvmId id, + whNvmAccess access, whNvmFlags flags, + uint8_t* label, whNvmSize label_len, + const void* cert, uint32_t cert_len, + int32_t* out_rc); + +int wh_Client_CertReadTrustedDma(whClientContext* c, whNvmId id, void* cert, + uint32_t cert_len, int32_t* out_rc); +``` + +### 4.3 Single-root chain verify + +```c +int wh_Client_CertVerify(whClientContext* c, const uint8_t* cert, + uint32_t cert_len, whNvmId trustedRootNvmId, + int32_t* out_rc); +``` + +Walks the chain in `cert` (concatenated DER in leaf-last certificate order), +anchoring against the single root identified by `trustedRootNvmId`. The server +constructs a fresh `WOLFSSL_CERT_MANAGER` for the call, loads the root, walks +the chain via `wolfSSL_CertManagerVerifyBuffer`, and returns the result via +`out_rc`. + +DMA variant: + +```c +int wh_Client_CertVerifyDma(whClientContext* c, const void* cert, + uint32_t cert_len, whNvmId trustedRootNvmId, + int32_t* out_rc); +``` + +### 4.4 Single-root verify with leaf-key caching + +```c +int wh_Client_CertVerifyAndCacheLeafPubKey( + whClientContext* c, const uint8_t* cert, uint32_t cert_len, + whNvmId trustedRootNvmId, whNvmFlags cachedKeyFlags, whKeyId* inout_keyId, + int32_t* out_rc); +``` + +Same chain walk as `wh_Client_CertVerify`, plus on success the leaf +certificate's public key is copied into the server's key cache under +`*inout_keyId` (or a server-chosen id if the input was `WH_KEYID_ERASED`) +with `cachedKeyFlags` as its NVM usage policy. Subsequent crypto operations +can address the key by id. + +DMA variant: `wh_Client_CertVerifyDmaAndCacheLeafPubKey`. + +### 4.5 Multi-root chain verify + +```c +int wh_Client_CertVerifyMultiRoot(whClientContext* c, const uint8_t* cert, + uint32_t cert_len, + const whNvmId* trustedRootNvmIds, + uint16_t numRoots, int32_t* out_rc); +``` + +Identical to the single-root call, except the server loads up to +`numRoots` roots (`1 .. WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS`) into a single +cert manager and the chain succeeds if it anchors to *any* of them. See +Section 5 for the full semantics. + +DMA variant: `wh_Client_CertVerifyMultiRootDma`. + +### 4.6 Multi-root verify with leaf-key caching + +```c +int wh_Client_CertVerifyMultiRootAndCacheLeafPubKey( + whClientContext* c, const uint8_t* cert, uint32_t cert_len, + const whNvmId* trustedRootNvmIds, uint16_t numRoots, + whNvmFlags cachedKeyFlags, whKeyId* inout_keyId, int32_t* out_rc); +``` + +DMA variant: `wh_Client_CertVerifyMultiRootDmaAndCacheLeafPubKey`. + +### 4.7 Attribute certificate verify + +```c +int wh_Client_CertVerifyAcert(whClientContext* c, const void* cert, + uint32_t cert_len, whNvmId trustedRootNvmId, + int32_t* out_rc); +``` + +Verifies an X.509 attribute certificate's signature against the public key +of the trusted root identified by `trustedRootNvmId`. Available only when +the server is built with `WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT`. There is +no multi-root or leaf-cache variant — attribute certificates are signed +directly by an attribute authority and the call carries a single anchor. + +A signature mismatch is reported as `WH_ERROR_CERT_VERIFY` in `out_rc`, the +same convention as the standard verify path. + +DMA variant: `wh_Client_CertVerifyAcertDma`. + +### 4.8 Verify-cache management + +Available only when `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE` is enabled: + +```c +int wh_Client_CertVerifyCacheClear(whClientContext* c, int32_t* out_rc); +``` + +Drops every entry from the server's verify cache. In per-client mode this +clears only the calling client's cache; in global mode (Section 5.6) it +clears the shared cache for all clients. Subsequent verifies fall back to +running the full wolfSSL signature path until the cache is repopulated. + +## 5. The Multi-Root Feature + +### 5.1 Why it exists + +The single-root entry point couples each verify to exactly one trust anchor. +Callers needing to validate a chain against any of several acceptable roots +otherwise have to either fold every acceptable root under a single super-root +(operationally awkward when the root infrastructures are independent) or +loop over `wh_Client_CertVerify` per root, parsing the chain again each +attempt and inferring at the application layer whether a per-anchor failure +should trigger a retry against the next anchor. + +`wh_Client_CertVerifyMultiRoot` collapses both of those into a single +request: hand the server an array of up to `WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS` +NVM ids, the server loads each one as a CA into a single cert manager, and +the chain is walked exactly once. If it anchors to any of the supplied +roots the verify succeeds; otherwise it fails with `WH_ERROR_CERT_VERIFY`. + +### 5.2 Order independence + +The cert manager picks an issuer for each child cert by subject/issuer +matching during chain walk, not by load order. Listing root A before root B +does not "prefer" A. + +### 5.3 Mixed-failure semantics + +Multi-root distinguishes three failure modes via `out_rc`: + +| Outcome | `out_rc` | +|-----------------------------------------------------------|-------------------------| +| Chain anchors to ≥ 1 loaded root | `WH_ERROR_OK` | +| ≥ 1 anchor loaded; chain does not anchor to any of them | `WH_ERROR_CERT_VERIFY` | +| Every supplied root id is absent from NVM | `WH_ERROR_NOTFOUND` | +| Any non-absent failure reading or loading a supplied root | underlying error code | + +Roots that are absent from NVM are skipped silently — they do not abort the +operation and do not count against the chain's chance of anchoring. A read +or load failure on an *existing* root, by contrast, is treated as an +environment error and aborts the call. + +## 6. The Trusted Verify Cache + +### 6.1 Overview + +When `WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE` is enabled, the server keeps a +fixed-size FIFO ring of slots, each holding: + +- A SHA-256 hash of a successfully-verified DER-encoded **CA** certificate. +- The set of trusted root NVM ids that were loaded into the cert manager + when that cert was verified. + +On a subsequent verify, before the server invokes +`wolfSSL_CertManagerVerifyBuffer` for a CA cert in the chain, it hashes +that cert and looks the hash up in the cache. A hit short-circuits the +public-key signature check; the rest of the chain walk (CA decode, store +load for downstream certs, leaf pubkey extract) continues unchanged. + +Only CA certs are ever inserted. Leaves are deliberately excluded. + +The verify cache is never stored in NVM and does not persist across power +cycles. + +This feature is intended to provide a substantial performance enhancement by +eliminating multiple potentially redundant and expensive public key verification +operations, however it does so at the expense of security in some scenarios. If +deploying this feature in production it is paramount that the nuances regarding +the trust anchor consequences are fully understood and align with the threat +model of the application. **This feature should be used with caution and for most +scenarios is NOT recommended.** + +### 6.2 Internals + +The wolfHSM trusted certificate cache binds each entry to the *set* of +trusted roots that were actually loaded when the verify occured, and lookups +require the cached set to be a **subset of the caller's currently loaded set**. + +The soundness argument rests on the monotonicity of X.509 verification: adding +more trusted roots should never invalidate a previously successful verify, so a +chain that validated under set `S` still validates under any superset `T ⊇ S`. +A cache hit therefore implies the cached verify's anchor (whichever root in `S` +actually closed the chain) is currently trusted, regardless of which element of +`S` it was, since every element of `S` is known to be in `T`. + +### 6.3 Hits, misses, and recording the loaded set + +Crucially, the *loaded* set is recorded — not the caller-supplied set. If a +caller passes three roots but only two are present in NVM, the cache slot +records the two-element loaded set. Forwarding the three-element supplied +set instead would let a stale entry under the missing root match a verify +whose effective trust store does not contain that root. + +Insertion is deduplicated on exact `(set, hash)` match under the cache lock, so +concurrent inserts of the same verify collapse to a single slot. Two +entries with the same hash but different sets coexist: each is an +independent claim about a distinct verify, and dropping either could lose +hit coverage for callers whose loaded set is a superset of one but not the +other. + +The ring overwrites using a FIFO pattern once full. + +### 6.4 Cache lifecycle and eviction + +Three mutation paths interact with the cache: + +- **`wh_Client_CertAddTrusted`** evicts every cache slot whose stored set + contains the affected root id. `AddObject` supersedes any prior object at + that id, so cached verifies anchored at the previous root would otherwise + short-circuit a verify under the new (different) root resident at that id. +- **`wh_Client_CertEraseTrusted`** evicts the same way. Without this, a + later `AddTrusted` reusing the freed id would inherit phantom cache hits + from the now-departed root. +- **`wh_Client_CertVerifyCacheClear`** drops every slot. + +Eviction happens on success only. Otherwise, a failed `AddTrusted` or +`EraseTrusted` leaves the prior root and any cache entries bound to it in +place. + +### 6.5 Per-client vs global mode + +By default the cache lives in `whServerCertContext` and is per-server (and +therefore per-client connection). Each client connection sees its own slots +and its own hit rate; a verify under client A does not warm client B's +cache. + +`WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL` relocates the cache into the +shared NVM context, where every connected client shares one `whCertVerify­ +CacheContext`. Hits then apply across client boundaries: once any client +has verified a CA against root R, every client whose loaded root set +contains R hits the cache for that CA. + +Global mode adds a dedicated lock embedded in the cache so cache operations +do not serialize behind general NVM I/O. In per-client mode the cache +piggybacks on the NVM lock — adequate given the cache is private to one +server and `CertInit` resets it on each (re)connect. + +### 6.6 Interaction with the user-supplied verify callback + +Server builds may register a `VerifyCallback` via +`whServerCertConfig.verifyCb` (or replace it at runtime via +`wh_Server_CertSetVerifyCb`). The callback is installed on the per-request +cert manager and runs inside `wolfSSL_CertManagerVerifyBuffer`. **Verify +cache hits short-circuit that path and so deliberately do not invoke the +callback.** + +## 7. Multi-Root and the Verify Cache Together + +When both features are compiled in, the cache participates in both +single-root and multi-root verifies. The combination preserves the +single-root behavior exactly while extending hit semantics to the larger +trust-set landscape multi-root callers create. The interaction has three +corner cases worth being explicit about. + +### 7.1 Cache entries record which roots were actually used, not which were asked for + +A multi-root verify request lists the trusted roots the caller is willing +to trust, but some of those roots may not currently exist in NVM. The +server skips any missing roots and only loads the ones it finds, so the +trust store the chain is actually verified against can be smaller than the +list the caller supplied. + +When a successful verify produces a new cache entry, the entry remembers +that smaller, real trust store — not the original request. For example, a +caller that asks for eight roots but only has three present in NVM +produces a three-root cache entry, not an eight-root one. + +This matters because cache lookups use the subset rule: an entry hits only +when its recorded roots are all present in the looking-up caller's +currently-loaded set. Recording roots that were never actually loaded +would let a future verify hit an entry under a root that wasn't part of +the trust store when the cached chain originally validated, and the +subset rule's soundness argument would no longer hold. + +### 7.2 Single-root verifies populate the cache too — and produce the broadest entries + +The single-root path is implemented as a one-element multi-root call. +Successful single-root verifies therefore insert one-element entries +(`{R}`) — the narrowest possible set. Under the subset rule, those entries +are also the most reusable: any future multi-root call whose loaded set +contains `R` (e.g. `{R, R₂}`, `{R, R₂, R₃}`) hits. + +Multi-root entries with larger sets (`{R₁, R₂, R₃}`) have correspondingly +narrower reuse — only future verifies whose loaded set is a superset +(`{R₁, R₂, R₃}` itself, or `{R₁, R₂, R₃, R₄}`, etc.) will hit. They are +still useful: they capture verifies that pure single-root traffic would +not generate. + +A practical consequence: if a deployment runs both single-root traffic +against `R₁` and multi-root traffic against `{R₁, R₂}`, the single-root +verifies populate `{R₁}`-bound entries that the multi-root traffic also +hits, while the multi-root verifies populate `{R₁, R₂}`-bound entries that +do *not* serve future single-root `{R₁}` traffic. The cache is therefore +biased toward maximizing reuse from single-root callers. + +### 7.3 A single root rotation invalidates entries across both paths + +`AddTrusted` and `EraseTrusted` call `CertVerifyCache_EvictRoot(id)`, which +drops every slot whose recorded set *contains* `id`. This does the right +thing for both single- and multi-root populated entries: + +- A `{R₁}` entry is dropped on a rotation of `R₁` and is unaffected by + rotations of any other root — exactly what monotonicity demands. +- A `{R₁, R₂}` entry is dropped on a rotation of either `R₁` or `R₂`. The + original verify may have anchored at the rotated root, and the remaining + set is no longer a sound claim about which stores still validate the + chain. Stripping just the rotated id from the set would leave a slot + that falsely claims `{R_other}` validated this chain on its own. + +A multi-root caller's "live" cache footprint therefore depends on the +stability of every root in its supplied sets, not just the one that +ultimately anchored. This is intrinsic to the soundness argument — the +cache cannot identify which anchor closed any given chain after the fact — +and is the trade-off paid for cross-anchor cache reuse. + +### 7.4 Cache miss falls back to the regular multi-root path + +A miss does not change semantics relative to a no-cache build: the server +runs `wolfSSL_CertManagerVerifyBuffer` against the populated cert manager +just as it would have without the cache. There is no path by which a miss +weakens the verify; the cache is a pure performance optimization. + +### 7.5 Recommendations + +- Provision long-lived roots with stable NVM ids when targeting a high + cache hit rate. Frequent rotations will keep the cache cold. +- Prefer per-client mode (the default) when client trust stores diverge + significantly. Prefer global mode when most clients verify against the + same set of roots (e.g. fleet-uniform PKI). +- Single-root callers benefit from the cache without any additional design. + Multi-root callers benefit most when the supplied set is reasonably + stable across calls — the recorded set is what determines hit eligibility + for downstream traffic. + +## 8. Worked Example + +Provision two roots, verify a chain against either, and cache the leaf +public key for subsequent signing-key lookup: + +```c +whClientContext* c = /* ... */; +whNvmId rootIds[2] = { 100, 101 }; +int32_t rc; +whKeyId leafKeyId = WH_KEYID_ERASED; + +/* One-time provisioning (can also be done offline) */ +wh_Client_CertInit(c, &rc); +wh_Client_CertAddTrusted(c, rootIds[0], WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, + (uint8_t*)"primary", 7, + primary_root_der, primary_root_len, &rc); +wh_Client_CertAddTrusted(c, rootIds[1], WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, + (uint8_t*)"backup", 6, + backup_root_der, backup_root_len, &rc); + +/* Verify a chain against either root and cache the leaf public key */ +int ret = wh_Client_CertVerifyMultiRootAndCacheLeafPubKey( + c, chain_der, chain_len, rootIds, 2, + WH_NVM_FLAGS_USAGE_VERIFY, &leafKeyId, &rc); + +if (ret == WH_ERROR_OK && rc == WH_ERROR_OK) { + /* leafKeyId now refers to the leaf cert's public key in the server's + * key cache; subsequent crypto operations can use it by id. With the verify + * cache enabled, a repeat verify of this chain against this root set hits + * the cache for every CA in the chain and skips the wolfSSL signature path. + */ +} +``` + diff --git a/src/wh_client_cert.c b/src/wh_client_cert.c index 26b2f3ca..f0815701 100644 --- a/src/wh_client_cert.c +++ b/src/wh_client_cert.c @@ -669,6 +669,65 @@ int wh_Client_CertVerifyMultiRootAndCacheLeafPubKey( inout_keyId, out_rc); } +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE +int wh_Client_CertVerifyCacheClearRequest(whClientContext* c) +{ + if (c == NULL) { + return WH_ERROR_BADARGS; + } + return wh_Client_SendRequest(c, WH_MESSAGE_GROUP_CERT, + WH_MESSAGE_CERT_ACTION_VERIFY_CACHE_CLEAR, 0, + NULL); +} + +int wh_Client_CertVerifyCacheClearResponse(whClientContext* c, int32_t* out_rc) +{ + int rc; + uint16_t group; + uint16_t action; + uint16_t size; + whMessageCert_SimpleResponse resp; + + if (c == NULL) { + return WH_ERROR_BADARGS; + } + + rc = wh_Client_RecvResponse(c, &group, &action, &size, &resp); + if (rc == WH_ERROR_OK) { + if ((group != WH_MESSAGE_GROUP_CERT) || + (action != WH_MESSAGE_CERT_ACTION_VERIFY_CACHE_CLEAR) || + (size != sizeof(resp))) { + rc = WH_ERROR_ABORTED; + } + else if (out_rc != NULL) { + *out_rc = resp.rc; + } + } + return rc; +} + +int wh_Client_CertVerifyCacheClear(whClientContext* c, int32_t* out_rc) +{ + int rc = WH_ERROR_OK; + + if (c == NULL) { + return WH_ERROR_BADARGS; + } + + do { + rc = wh_Client_CertVerifyCacheClearRequest(c); + } while (rc == WH_ERROR_NOTREADY); + + if (rc == WH_ERROR_OK) { + do { + rc = wh_Client_CertVerifyCacheClearResponse(c, out_rc); + } while (rc == WH_ERROR_NOTREADY); + } + + return rc; +} +#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */ + #ifdef WOLFHSM_CFG_DMA int wh_Client_CertAddTrustedDmaRequest(whClientContext* c, whNvmId id, diff --git a/src/wh_nvm.c b/src/wh_nvm.c index 371e4e79..59557f0a 100644 --- a/src/wh_nvm.c +++ b/src/wh_nvm.c @@ -104,13 +104,30 @@ int wh_Nvm_Init(whNvmContext* context, const whNvmConfig* config) memset(&context->globalCache, 0, sizeof(context->globalCache)); #endif +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL + /* Initialize the global cert verify cache */ + memset(&context->globalCertVerifyCache, 0, + sizeof(context->globalCertVerifyCache)); +#endif + #ifdef WOLFHSM_CFG_THREADSAFE /* Initialize lock (NULL lockConfig = no-op locking) */ rc = wh_Lock_Init(&context->lock, config->lockConfig); if (rc != WH_ERROR_OK) { return rc; } +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL + /* Initialize the global cert verify cache lock. Distinct lock from the + * NVM lock so cert-cache traffic and NVM I/O don't serialize each other. + * NULL config => no-op locking, same as the NVM lock above. */ + rc = wh_Lock_Init(&context->globalCertVerifyCache.lock, + config->certVerifyCacheLockConfig); + if (rc != WH_ERROR_OK) { + (void)wh_Lock_Cleanup(&context->lock); + return rc; + } #endif +#endif /* WOLFHSM_CFG_THREADSAFE */ if (context->cb != NULL && context->cb->Init != NULL) { rc = context->cb->Init(context->context, config->config); @@ -118,6 +135,9 @@ int wh_Nvm_Init(whNvmContext* context, const whNvmConfig* config) context->cb = NULL; context->context = NULL; #ifdef WOLFHSM_CFG_THREADSAFE +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL + (void)wh_Lock_Cleanup(&context->globalCertVerifyCache.lock); +#endif (void)wh_Lock_Cleanup(&context->lock); #endif } @@ -140,6 +160,14 @@ int wh_Nvm_Cleanup(whNvmContext* context) memset(&context->globalCache, 0, sizeof(context->globalCache)); #endif +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL + /* Clear cache slots/writeIdx but keep the embedded lock intact until its + * own cleanup below. */ + memset(context->globalCertVerifyCache.slots, 0, + sizeof(context->globalCertVerifyCache.slots)); + context->globalCertVerifyCache.writeIdx = 0; +#endif + /* No callback? Return ABORTED */ if (context->cb->Cleanup == NULL) { rc = WH_ERROR_ABORTED; @@ -149,6 +177,9 @@ int wh_Nvm_Cleanup(whNvmContext* context) } #ifdef WOLFHSM_CFG_THREADSAFE +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL + (void)wh_Lock_Cleanup(&context->globalCertVerifyCache.lock); +#endif (void)wh_Lock_Cleanup(&context->lock); #endif diff --git a/src/wh_server.c b/src/wh_server.c index 943e583c..3e188cad 100644 --- a/src/wh_server.c +++ b/src/wh_server.c @@ -122,6 +122,14 @@ int wh_Server_Init(whServerContext* server, whServerConfig* config) } #endif /* WOLFHSM_CFG_DMA */ +#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) && !defined(WOLFHSM_CFG_NO_CRYPTO) + /* Register the user-supplied verify callback, if any. The cache (if + * compiled in) is already zero-initialized by the memset above. */ + if (config->certConfig != NULL) { + server->cert.verifyCb = config->certConfig->verifyCb; + } +#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */ + /* Log the server startup */ WH_LOG(&server->log, WH_LOG_LEVEL_INFO, "Server Initialized"); diff --git a/src/wh_server_cert.c b/src/wh_server_cert.c index 83b583d3..f8f54707 100644 --- a/src/wh_server_cert.c +++ b/src/wh_server_cert.c @@ -33,6 +33,7 @@ #include "wolfhsm/wh_error.h" #include "wolfhsm/wh_server.h" #include "wolfhsm/wh_server_cert.h" +#include "wolfhsm/wh_server_cert_cache.h" #include "wolfhsm/wh_server_nvm.h" #include "wolfhsm/wh_server_keystore.h" #include "wolfhsm/wh_message.h" @@ -41,6 +42,257 @@ #include "wolfssl/wolfcrypt/types.h" #include "wolfssl/ssl.h" #include "wolfssl/wolfcrypt/asn.h" +#include "wolfssl/wolfcrypt/sha256.h" + + +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE +/* Resolve the verify cache for this server. In per-client mode the cache + * lives on the server context; in global mode it lives on the shared NVM + * context. Returns NULL if either pointer is missing. */ +static whCertVerifyCacheContext* _GetVerifyCache(whServerContext* server) +{ +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL + if ((server == NULL) || (server->nvm == NULL)) { + return NULL; + } + return &server->nvm->globalCertVerifyCache; +#else + if (server == NULL) { + return NULL; + } + return &server->cert.cache; +#endif +} + +/* Lock helpers compile to no-ops when the cache has no embedded lock (i.e. + * outside global+threadsafe builds). */ +static int _LockVerifyCache(whCertVerifyCacheContext* cache) +{ +#if defined(WOLFHSM_CFG_THREADSAFE) && \ + defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL) + return wh_Lock_Acquire(&cache->lock); +#else + (void)cache; + return WH_ERROR_OK; +#endif +} + +static int _UnlockVerifyCache(whCertVerifyCacheContext* cache) +{ +#if defined(WOLFHSM_CFG_THREADSAFE) && \ + defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL) + return wh_Lock_Release(&cache->lock); +#else + (void)cache; + return WH_ERROR_OK; +#endif +} + +/* Returns 1 if every element of `subset` appears in `superset`. The arrays + * are unsorted but bounded by WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS, so the + * O(N*M) scan is fine. */ +static int _IsSubsetOf(const whNvmId* subset, uint16_t subsetCount, + const whNvmId* superset, uint16_t supersetCount) +{ + uint16_t i, j; + int found; + for (i = 0; i < subsetCount; i++) { + found = 0; + for (j = 0; j < supersetCount; j++) { + if (subset[i] == superset[j]) { + found = 1; + break; + } + } + if (!found) { + return 0; + } + } + return 1; +} + +/* Internal slot scan, must be called with the cache lock held. Hit if any + * committed slot's stored root set is a subset of the supplied root set + * AND its hash matches. */ +static int _LookupSubsetUnlocked(const whCertVerifyCacheContext* cache, + const whNvmId* rootNvmIds, uint16_t numRoots, + const uint8_t* hash) +{ + int i; + for (i = 0; i < WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT; i++) { + const whCertVerifyCacheSlot* slot = &cache->slots[i]; + if (slot->committed && + (memcmp(slot->hash, hash, WH_CERT_VERIFY_CACHE_HASH_LEN) == 0) && + _IsSubsetOf(slot->rootNvmIds, slot->numRoots, rootNvmIds, + numRoots)) { + return WH_ERROR_OK; + } + } + return WH_ERROR_NOTFOUND; +} + +/* Internal exact-match scan for insert dedup. Two sets of equal size are + * equal iff one is a subset of the other, so reuse _IsSubsetOf with a size + * check rather than sorting. */ +static int _HasExactSlotUnlocked(const whCertVerifyCacheContext* cache, + const whNvmId* rootNvmIds, uint16_t numRoots, + const uint8_t* hash) +{ + int i; + for (i = 0; i < WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT; i++) { + const whCertVerifyCacheSlot* slot = &cache->slots[i]; + if (slot->committed && (slot->numRoots == numRoots) && + (memcmp(slot->hash, hash, WH_CERT_VERIFY_CACHE_HASH_LEN) == 0) && + _IsSubsetOf(slot->rootNvmIds, slot->numRoots, rootNvmIds, + numRoots)) { + return 1; + } + } + return 0; +} + +int wh_Server_CertVerifyCache_Lookup(whServerContext* server, + const whNvmId* rootNvmIds, + uint16_t numRoots, const uint8_t* hash) +{ + whCertVerifyCacheContext* cache; + int rc; + int found; + + if ((server == NULL) || (hash == NULL) || (rootNvmIds == NULL) || + (numRoots == 0)) { + return WH_ERROR_BADARGS; + } + cache = _GetVerifyCache(server); + if (cache == NULL) { + return WH_ERROR_BADARGS; + } + + rc = _LockVerifyCache(cache); + if (rc != WH_ERROR_OK) { + return rc; + } + found = _LookupSubsetUnlocked(cache, rootNvmIds, numRoots, hash); + (void)_UnlockVerifyCache(cache); + return found; +} + +void wh_Server_CertVerifyCache_Insert(whServerContext* server, + const whNvmId* rootNvmIds, + uint16_t numRoots, const uint8_t* hash) +{ + whCertVerifyCacheContext* cache; + whCertVerifyCacheSlot* slot; + uint16_t idx; + uint16_t k; + int rc; + + if ((server == NULL) || (hash == NULL) || (rootNvmIds == NULL) || + (numRoots == 0) || (numRoots > WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS)) { + return; + } + cache = _GetVerifyCache(server); + if (cache == NULL) { + return; + } + + rc = _LockVerifyCache(cache); + if (rc != WH_ERROR_OK) { + return; + } + /* Dedup on exact (set, hash) match under the lock so concurrent inserts + * of the same verify collapse to a single slot. Differing-set entries + * for the same hash coexist: each is an independent claim about a + * distinct verify, and dropping either could lose hit coverage. */ + if (!_HasExactSlotUnlocked(cache, rootNvmIds, numRoots, hash)) { + idx = cache->writeIdx; + slot = &cache->slots[idx]; + slot->numRoots = (uint8_t)numRoots; + for (k = 0; k < numRoots; k++) { + slot->rootNvmIds[k] = rootNvmIds[k]; + } + memcpy(slot->hash, hash, WH_CERT_VERIFY_CACHE_HASH_LEN); + slot->committed = 1; + cache->writeIdx = + (uint16_t)((idx + 1) % WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT); + } + (void)_UnlockVerifyCache(cache); +} + +void wh_Server_CertVerifyCache_Clear(whServerContext* server) +{ + whCertVerifyCacheContext* cache; + int rc; + + if (server == NULL) { + return; + } + cache = _GetVerifyCache(server); + if (cache == NULL) { + return; + } + + rc = _LockVerifyCache(cache); + if (rc != WH_ERROR_OK) { + return; + } + /* Clear payload only; the embedded lock (when present) must survive a + * Clear, otherwise the next operation would acquire an uninitialized + * lock. */ + memset(cache->slots, 0, sizeof(cache->slots)); + cache->writeIdx = 0; + (void)_UnlockVerifyCache(cache); +} + +void wh_Server_CertVerifyCache_EvictRoot(whServerContext* server, + whNvmId rootNvmId) +{ + whCertVerifyCacheContext* cache; + int rc; + int i; + + if (server == NULL) { + return; + } + cache = _GetVerifyCache(server); + if (cache == NULL) { + return; + } + + rc = _LockVerifyCache(cache); + if (rc != WH_ERROR_OK) { + return; + } + /* Drop any slot whose stored root set contains the evicted root. We + * cannot safely strip the root from the set and keep the entry: the + * original verify may have anchored at the now-departed root, so the + * remaining set is no longer a sound claim. writeIdx is left alone: + * the FIFO ring is sparse but still well-formed, and pruning here + * would otherwise need to compact entries belonging to other roots. */ + for (i = 0; i < WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT; i++) { + whCertVerifyCacheSlot* slot = &cache->slots[i]; + if (slot->committed) { + uint16_t k; + for (k = 0; k < slot->numRoots; k++) { + if (slot->rootNvmIds[k] == rootNvmId) { + memset(slot, 0, sizeof(*slot)); + break; + } + } + } + } + (void)_UnlockVerifyCache(cache); +} +#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */ + +int wh_Server_CertSetVerifyCb(whServerContext* server, VerifyCallback cb) +{ + if (server == NULL) { + return WH_ERROR_BADARGS; + } + server->cert.verifyCb = cb; + return WH_ERROR_OK; +} /* Replicates GetSequence, which is WOLFSSL_LOCAL. */ @@ -61,20 +313,28 @@ static int DerNextSequence(const uint8_t* input, uint32_t maxIdx, } -static int _verifyChainAgainstCmStore(whServerContext* server, - WOLFSSL_CERT_MANAGER* cm, - const uint8_t* chain, uint32_t chain_len, - whCertFlags flags, - whNvmFlags cachedKeyFlags, - whKeyId* inout_keyId) +static int +_verifyChainAgainstCmStore(whServerContext* server, WOLFSSL_CERT_MANAGER* cm, + const uint8_t* chain, uint32_t chain_len, + const whNvmId* trustedRootNvmIds, uint16_t numRoots, + whCertFlags flags, whNvmFlags cachedKeyFlags, + whKeyId* inout_keyId) { int rc = 0; const uint8_t* cert_ptr = chain; uint32_t remaining_len = chain_len; int cert_len = 0; word32 idx = 0; +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + uint8_t certHash[WH_CERT_VERIFY_CACHE_HASH_LEN]; + int hashed = 0; +#else + (void)trustedRootNvmIds; + (void)numRoots; +#endif - if (cm == NULL || chain == NULL || chain_len == 0) { + if (cm == NULL || chain == NULL || chain_len == 0 || + trustedRootNvmIds == NULL || numRoots == 0) { return WH_ERROR_BADARGS; } @@ -82,6 +342,9 @@ static int _verifyChainAgainstCmStore(whServerContext* server, while (remaining_len > 0) { /* Reset index for each certificate */ idx = 0; +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + hashed = 0; +#endif /* Get the length of the current certificate */ rc = DerNextSequence(cert_ptr, remaining_len, &idx, &cert_len); @@ -94,9 +357,37 @@ static int _verifyChainAgainstCmStore(whServerContext* server, return WH_ERROR_ABORTED; } - /* Verify the current certificate */ - rc = wolfSSL_CertManagerVerifyBuffer(cm, cert_ptr, cert_len + idx, - WOLFSSL_FILETYPE_ASN1); +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + /* Hash the DER cert and check the verify cache. A hit short-circuits + * the public-key signature check; the cert is otherwise treated as if + * it had verified normally so the rest of the loop (CA decode, store + * load, leaf pubkey extract) continues unchanged. */ + rc = wc_Sha256Hash_ex(cert_ptr, (word32)(cert_len + idx), certHash, + NULL, server->devId); + if (rc != 0) { + return rc; + } + hashed = 1; + { + int hit = (wh_Server_CertVerifyCache_Lookup( + server, trustedRootNvmIds, numRoots, certHash) == + WH_ERROR_OK); + if (hit) { + rc = WOLFSSL_SUCCESS; + } + else { + /* Verify the current certificate */ + rc = wolfSSL_CertManagerVerifyBuffer( + cm, cert_ptr, cert_len + idx, WOLFSSL_FILETYPE_ASN1); + } + } +#else + { + /* Verify the current certificate */ + rc = wolfSSL_CertManagerVerifyBuffer(cm, cert_ptr, cert_len + idx, + WOLFSSL_FILETYPE_ASN1); + } +#endif /* If this is not the leaf certificate and it's trusted, add it to the @@ -169,6 +460,29 @@ static int _verifyChainAgainstCmStore(whServerContext* server, return rc; } } +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + /* Insert only CA certs into the verify cache. Leaves are not + * cached: a cache hit on a leaf during a future "leaf alone" + * verify would short-circuit the wolfSSL signature check that + * would otherwise have failed (the leaf's issuer is not in the + * cert manager when the leaf is supplied without its + * intermediates). CA caching is sound because the chain walk + * loads each verified CA into the cert manager before the next + * cert is processed. + * + * The slot's binding is the loaded root set passed in. Under + * subset-lookup semantics, a future verify hits this entry + * only when its loaded set is a superset, which by X.509 + * verify monotonicity guarantees the cached chain still + * validates. Single-root callers produce one-element entries + * (broadest reuse); multi-root callers produce wider entries + * that are still useful when later traffic presents at least + * the same roots. */ + if (hashed && dc.isCA) { + wh_Server_CertVerifyCache_Insert(server, trustedRootNvmIds, + numRoots, certHash); + } +#endif wc_FreeDecodedCert(&dc); } else { @@ -190,7 +504,18 @@ int wh_Server_CertInit(whServerContext* server) #ifdef DEBUG_WOLFSSL wolfSSL_Debugging_ON(); #endif +#if defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE) && \ + !defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL) + /* Per-client cache is owned by the server context and zeroed on each + * server init. Under _GLOBAL the cache lives in the NVM context and is + * initialized exactly once in wh_Nvm_Init — clearing it here would wipe + * entries populated by other clients. */ + if (server != NULL) { + wh_Server_CertVerifyCache_Clear(server); + } +#else (void)server; +#endif return WH_ERROR_OK; } @@ -225,6 +550,17 @@ int wh_Server_CertAddTrusted(whServerContext* server, whNvmId id, rc = wh_Nvm_AddObject(server->nvm, &metadata, cert_len, cert); +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + /* Cache entries are bound to the trusted root by NVM ID. AddObject + * supersedes any prior object at this ID, so cached verifies anchored at + * the previous root must be evicted lest they short-circuit a verify + * under the new (different) root. Evict on success only — a failed add + * leaves the prior root in place. */ + if (rc == WH_ERROR_OK) { + wh_Server_CertVerifyCache_EvictRoot(server, id); + } +#endif + return rc; } @@ -241,6 +577,15 @@ int wh_Server_CertEraseTrusted(whServerContext* server, whNvmId id) id_list[0] = id; rc = wh_Nvm_DestroyObjects(server->nvm, 1, id_list); +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + /* See AddTrusted: stale cache entries against the now-erased root must + * not survive, otherwise a future AddTrusted at the same ID would inherit + * a phantom cache hit. Evict on success only. */ + if (rc == WH_ERROR_OK) { + wh_Server_CertVerifyCache_EvictRoot(server, id); + } +#endif + return rc; } @@ -286,9 +631,14 @@ int wh_Server_CertVerifyMultiRoot(whServerContext* server, const uint8_t* cert, WOLFSSL_CERT_MANAGER* cm = NULL; uint8_t root_cert[WOLFHSM_CFG_MAX_CERT_SIZE]; uint32_t root_cert_len; - int rc = WH_ERROR_OK; - int anchorsLoaded = 0; - uint16_t i; + int rc = WH_ERROR_OK; + /* Track only the roots that were actually loaded into the cert manager. + * Forwarding the full caller-supplied set into the cache lookup would let + * a stale entry under a missing root match a verify whose effective trust + * store does not contain that root. */ + whNvmId loadedRootNvmIds[WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS]; + uint16_t loadedRootCount = 0; + uint16_t i; if ((server == NULL) || (cert == NULL) || (cert_len == 0) || (trustedRootNvmIds == NULL) || (numRoots == 0) || @@ -308,6 +658,13 @@ int wh_Server_CertVerifyMultiRoot(whServerContext* server, const uint8_t* cert, return WH_ERROR_ABORTED; } + /* Apply the user-supplied verify callback, if registered. wolfSSL invokes + * it during wolfSSL_CertManagerVerifyBuffer; cache hits short-circuit that + * path and so deliberately do not invoke the callback. */ + if (server->cert.verifyCb != NULL) { + wolfSSL_CertManagerSetVerify(cm, server->cert.verifyCb); + } + /* Load each root anchor. Absent roots are silently skipped; any other * read or load failure is fatal and reported. */ for (i = 0; i < numRoots; i++) { @@ -330,17 +687,20 @@ int wh_Server_CertVerifyMultiRoot(whServerContext* server, const uint8_t* cert, (void)wolfSSL_CertManagerFree(cm); return WH_ERROR_ABORTED; } - anchorsLoaded++; + loadedRootNvmIds[loadedRootCount++] = trustedRootNvmIds[i]; } /* If no anchors were loaded, the trust store is empty */ - if (anchorsLoaded == 0) { + if (loadedRootCount == 0) { (void)wolfSSL_CertManagerFree(cm); return WH_ERROR_NOTFOUND; } - /* Verify the chain against the populated trust store */ - rc = _verifyChainAgainstCmStore(server, cm, cert, cert_len, flags, + /* Verify the chain against the populated trust store. Pass only the + * loaded root set so cache lookups cannot match entries bound to a root + * that is not actually in cm. */ + rc = _verifyChainAgainstCmStore(server, cm, cert, cert_len, + loadedRootNvmIds, loadedRootCount, flags, cachedKeyFlags, inout_keyId); if (rc != WH_ERROR_OK) { rc = WH_ERROR_CERT_VERIFY; @@ -681,6 +1041,33 @@ int wh_Server_HandleCertRequest(whServerContext* server, uint16_t magic, *out_resp_size = sizeof(resp); }; break; +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + case WH_MESSAGE_CERT_ACTION_VERIFY_CACHE_CLEAR: { + whMessageCert_SimpleResponse resp = {0}; + +#ifndef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL + /* Per-client cache piggybacks on the NVM lock for serialization. + * Under _GLOBAL the cache has its own lock acquired internally by + * wh_Server_CertVerifyCache_Clear, so the NVM lock isn't needed + * (and acquiring it would needlessly block NVM I/O on cache + * clears). */ + rc = WH_SERVER_NVM_LOCK(server); + if (rc == WH_ERROR_OK) { + wh_Server_CertVerifyCache_Clear(server); + (void)WH_SERVER_NVM_UNLOCK(server); + } +#else + wh_Server_CertVerifyCache_Clear(server); + rc = WH_ERROR_OK; +#endif + resp.rc = rc; + + wh_MessageCert_TranslateSimpleResponse( + magic, &resp, (whMessageCert_SimpleResponse*)resp_packet); + *out_resp_size = sizeof(resp); + }; break; +#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */ + #ifdef WOLFHSM_CFG_DMA case WH_MESSAGE_CERT_ACTION_ADDTRUSTED_DMA: { whMessageCert_AddTrustedDmaRequest req = {0}; diff --git a/test/Makefile b/test/Makefile index 27f25921..ddfa7841 100644 --- a/test/Makefile +++ b/test/Makefile @@ -165,6 +165,18 @@ ifeq ($(AUTH),1) DEF += -DWOLFHSM_CFG_ENABLE_AUTHENTICATION endif +# Support trusted-cert verify-result cache +ifeq ($(CERT_VERIFY_CACHE),1) + DEF += -DWOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE +endif + +# Use the cross-client (global) variant of the trusted-cert verify cache. +# Implies CERT_VERIFY_CACHE. +ifeq ($(CERT_VERIFY_CACHE_GLOBAL),1) + DEF += -DWOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + DEF += -DWOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL +endif + ## Project defines # Option to build wolfcrypt tests ifeq ($(TESTWOLFCRYPT),1) @@ -295,8 +307,13 @@ $(BUILD_DIR)/%.o: %.s @echo "Compiling ASM file: $(notdir $<)" $(CMD_ECHO) $(AS) $(ASFLAGS) $(DEF) $(INC) -c -o $@ $< -# Add additional flag here to avoid pragma -$(BUILD_DIR)/wh_test_check_struct_padding.o: CFLAGS+=-Wpadded -DWOLFHSM_CFG_NO_CRYPTO +# Wire-format struct-padding audit. -Wpadded turns spurious padding into a +# build error. WH_PADDING_CHECK is an internal sentinel honored by +# wh_settings.h that suppresses external dependencies (wolfSSL etc.) so the +# audit doesn't drag in third-party source whose layout could perturb the +# result. Distinct from WOLFHSM_CFG_NO_CRYPTO so it doesn't disable user +# features (e.g. the cert verify cache). +$(BUILD_DIR)/wh_test_check_struct_padding.o: CFLAGS+=-Wpadded -DWH_PADDING_CHECK $(BUILD_DIR)/%.o: %.c @echo "Compiling C file: $(notdir $<)" diff --git a/test/wh_test_cert.c b/test/wh_test_cert.c index 80175ffa..222f4c3b 100644 --- a/test/wh_test_cert.c +++ b/test/wh_test_cert.c @@ -121,7 +121,9 @@ int whTest_CertServerCfg(whServerConfig* serverCfg) server, RAW_CERT_CHAIN_B, RAW_CERT_CHAIN_B_len, rootCertB, WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); - /* attempt to verify invalid chains, should fail */ + /* attempt to verify invalid chains, should fail. Cache entries are scoped + * to the trusted root NVM ID, so prior positive verifies under the true + * root cannot bypass these cross-root checks. */ WH_TEST_PRINT("Attempting to verify invalid certificate chains...\n"); WH_TEST_ASSERT_RETURN(WH_ERROR_CERT_VERIFY == wh_Server_CertVerify(server, RAW_CERT_CHAIN_A, @@ -253,6 +255,331 @@ int whTest_CertServerCfg(whServerConfig* serverCfg) WH_TEST_PRINT("Test completed successfully\n"); return rc; } + +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE +/* Exercises the trusted-cert verify cache directly through the server API: + * - repeat-verify of the same chain under the same root stays successful + * - cache entries are bound to the trusted root NVM ID: chain A under root B + * must fail even after chain A has been cached by a verify under root A + * (regression test against cross-root cache bypass) + * - clearing the cache leaves the cross-root case still failing */ +static int whTest_CertServerVerifyCache(whServerConfig* serverCfg) +{ + whServerContext server[1] = {0}; + const whNvmId rootCertA = 1; + const whNvmId rootCertB = 2; + + WH_TEST_PRINT("=== Server cert verify-cache test ===\n"); + + WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, serverCfg)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server)); + + WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted( + server, rootCertA, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONMODIFIABLE, NULL, + 0, ROOT_A_CERT, ROOT_A_CERT_len)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted( + server, rootCertB, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONMODIFIABLE, NULL, + 0, ROOT_B_CERT, ROOT_B_CERT_len)); + + /* 1. Repeat-verify hit: verify chain A twice under root A; both succeed. */ + WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify( + server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify( + server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); + + /* 2. Cache is bound to root NVM ID: chain A under root B must fail even + * though every cert in chain A was cached under root A by step 1. The + * cache hit must not let an unsigned-by-rootB chain through. */ + WH_TEST_ASSERT_RETURN(WH_ERROR_CERT_VERIFY == + wh_Server_CertVerify(server, RAW_CERT_CHAIN_A, + RAW_CERT_CHAIN_A_len, rootCertB, + WH_CERT_FLAGS_NONE, + WH_NVM_FLAGS_USAGE_ANY, NULL)); + + /* 3. Clear: the same cross-root verify still fails cold. */ + wh_Server_CertVerifyCache_Clear(server); + WH_TEST_ASSERT_RETURN(WH_ERROR_CERT_VERIFY == + wh_Server_CertVerify(server, RAW_CERT_CHAIN_A, + RAW_CERT_CHAIN_A_len, rootCertB, + WH_CERT_FLAGS_NONE, + WH_NVM_FLAGS_USAGE_ANY, NULL)); + + WH_TEST_RETURN_ON_FAIL(wh_Server_CertEraseTrusted(server, rootCertA)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertEraseTrusted(server, rootCertB)); + WH_TEST_PRINT("Server cert verify-cache test PASSED\n"); + return WH_ERROR_OK; +} +#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */ + +#if defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE) && \ + defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL) +/* Counts callback invocations to detect cross-client cache hits. The verify + * callback fires only on a cold verify; a global-cache hit short-circuits the + * wolfSSL verify path and bypasses the callback. Two server contexts that + * share the same NVM context must share the cache, so the second context's + * verify of an already-cached chain must NOT increment this counter. */ +static int s_globalCacheCb_count = 0; +static int whTest_globalCacheVerifyCb(int preverify, + WOLFSSL_X509_STORE_CTX* store) +{ + (void)store; + s_globalCacheCb_count++; + /* Mirror wolfSSL's verdict so cross-root verifies still fail. Returning a + * hard 1 would mask signature mismatches and break the cross-root + * regression check below. */ + return preverify; +} + +/* Cross-client cache hit test. Two whServerContext instances, both backed by + * the single whNvmContext owned by the test driver, must share the trusted + * cert verify cache: a chain verified on serverA must short-circuit when + * verified again on serverB. */ +static int whTest_CertServerVerifyCacheGlobalShared(whServerConfig* serverCfg) +{ + whServerContext serverA[1] = {0}; + whServerContext serverB[1] = {0}; + const whNvmId rootCertA = 1; + const whNvmId rootCertB = 2; + int beforeCount; + + WH_TEST_PRINT( + "=== Server cert verify-cache global cross-client test ===\n"); + + /* Two independent server contexts, both pointing at the same NVM + * context via serverCfg. The cache lives on the NVM context in global + * mode, so both servers see the same slots. */ + WH_TEST_RETURN_ON_FAIL(wh_Server_Init(serverA, serverCfg)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(serverA)); + WH_TEST_RETURN_ON_FAIL(wh_Server_Init(serverB, serverCfg)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(serverB)); + + /* Register the same counting callback on both servers so we can detect + * which verify path actually executed wolfSSL's signature check vs. + * which one short-circuited via the global cache. */ + WH_TEST_RETURN_ON_FAIL( + wh_Server_CertSetVerifyCb(serverA, whTest_globalCacheVerifyCb)); + WH_TEST_RETURN_ON_FAIL( + wh_Server_CertSetVerifyCb(serverB, whTest_globalCacheVerifyCb)); + + /* Trust both roots so the cross-root regression below has somewhere to + * land. */ + WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted( + serverA, rootCertA, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONMODIFIABLE, NULL, + 0, ROOT_A_CERT, ROOT_A_CERT_len)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted( + serverA, rootCertB, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONMODIFIABLE, NULL, + 0, ROOT_B_CERT, ROOT_B_CERT_len)); + + /* Make sure we start cold even if a prior test populated the global + * cache. wh_Server_CertInit no longer clears under _GLOBAL. */ + wh_Server_CertVerifyCache_Clear(serverA); + + /* 1. Cold verify on A populates the global cache. */ + s_globalCacheCb_count = 0; + WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify( + serverA, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); + WH_TEST_ASSERT_RETURN(s_globalCacheCb_count > 0); + + /* 2. Same chain re-verified on B hits the cache populated by A for the + * CA certs — those callback invocations are skipped. The leaf is not + * cached (caching it would let an isolated "leaf alone" verify falsely + * succeed via cache hit), so the leaf's callback still fires. The + * re-verify therefore invokes the callback fewer times than the cold + * verify but still at least once. */ + beforeCount = s_globalCacheCb_count; + WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify( + serverB, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); + WH_TEST_ASSERT_RETURN(s_globalCacheCb_count > beforeCount); + WH_TEST_ASSERT_RETURN(s_globalCacheCb_count - beforeCount < beforeCount); + + /* 3. Cross-root: chain A under rootB must still fail on B even though + * chain A was cached under rootA. The cache is keyed on (root, hash); + * a hit under one root must not satisfy a verify under another. */ + WH_TEST_ASSERT_RETURN(WH_ERROR_CERT_VERIFY == + wh_Server_CertVerify(serverB, RAW_CERT_CHAIN_A, + RAW_CERT_CHAIN_A_len, rootCertB, + WH_CERT_FLAGS_NONE, + WH_NVM_FLAGS_USAGE_ANY, NULL)); + + /* 4. Clear via serverA wipes the shared cache; serverB now cold-verifies + * again and the callback fires. */ + wh_Server_CertVerifyCache_Clear(serverA); + beforeCount = s_globalCacheCb_count; + WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify( + serverB, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); + WH_TEST_ASSERT_RETURN(s_globalCacheCb_count > beforeCount); + + /* Reset cache so subsequent tests in the driver get a clean slate. */ + wh_Server_CertVerifyCache_Clear(serverA); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertEraseTrusted(serverA, rootCertA)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertEraseTrusted(serverA, rootCertB)); + WH_TEST_PRINT("Server cert verify-cache global cross-client test PASSED\n"); + return WH_ERROR_OK; +} +#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE && \ + WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL */ + +/* State for the user-injectable verify callback test */ +static int s_verifyCb_count = 0; +static int s_verifyCb_lastPreverify = -1; +static int s_verifyCb_returnVal = 1; + +static int whTest_recordingVerifyCb(int preverify, + WOLFSSL_X509_STORE_CTX* store) +{ + (void)store; + s_verifyCb_count++; + s_verifyCb_lastPreverify = preverify; + return s_verifyCb_returnVal; +} + +/* Exercises the user-injectable verify callback configured through + * whServerCertConfig. Confirms: + * - the callback is invoked during chain verification with preverify=1 + * - returning zero from the callback fails the verify + * - cache hits on CA certs bypass the callback (when the verify cache + * is enabled). Leaf certs are intentionally not cached, so the leaf's + * signature is re-verified (and the callback re-invoked) on every + * verify call. */ +static int whTest_CertServerVerifyCallback(whServerConfig* serverCfg) +{ + int rc; + whServerContext server[1] = {0}; + whServerCertConfig certCfg = {.verifyCb = whTest_recordingVerifyCb}; + whServerCertConfig* savedCertConfig; + const whNvmId rootCertA = 1; + + WH_TEST_PRINT("=== Server cert verify-callback test ===\n"); + + /* Inject our cert config; restore on exit. */ + savedCertConfig = serverCfg->certConfig; + serverCfg->certConfig = &certCfg; + + s_verifyCb_count = 0; + s_verifyCb_lastPreverify = -1; + s_verifyCb_returnVal = 1; + + WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, serverCfg)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server)); + + WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted( + server, rootCertA, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONMODIFIABLE, NULL, + 0, ROOT_A_CERT, ROOT_A_CERT_len)); + + /* 1. Callback is invoked on a successful verify with preverify=1. */ + WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify( + server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); + WH_TEST_ASSERT_RETURN(s_verifyCb_count > 0); + WH_TEST_ASSERT_RETURN(s_verifyCb_lastPreverify == 1); + +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + { + /* 2. Cache hits on CA certs bypass the callback. Leaves are not + * cached (caching them would let an isolated "leaf alone" verify + * falsely succeed via cache hit), so the leaf's callback fires on + * every re-verify. The re-verify therefore invokes the callback + * fewer times than the cold verify but still at least once. */ + int firstRunCount = s_verifyCb_count; + WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify( + server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); + WH_TEST_ASSERT_RETURN(s_verifyCb_count > firstRunCount); + WH_TEST_ASSERT_RETURN(s_verifyCb_count - firstRunCount < firstRunCount); + + /* Clear cache so the next verify re-enters wolfSSL and the cb. */ + wh_Server_CertVerifyCache_Clear(server); + } +#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */ + + /* 3. Returning zero from the callback forces verify failure. */ + s_verifyCb_returnVal = 0; + s_verifyCb_count = 0; + WH_TEST_ASSERT_RETURN(WH_ERROR_CERT_VERIFY == + wh_Server_CertVerify(server, RAW_CERT_CHAIN_A, + RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, + WH_NVM_FLAGS_USAGE_ANY, NULL)); + WH_TEST_ASSERT_RETURN(s_verifyCb_count > 0); + + WH_TEST_RETURN_ON_FAIL(wh_Server_CertEraseTrusted(server, rootCertA)); + serverCfg->certConfig = savedCertConfig; + rc = WH_ERROR_OK; + WH_TEST_PRINT("Server cert verify-callback test PASSED\n"); + return rc; +} + +/* Exercises wh_Server_CertSetVerifyCb: register, replace, and unregister the + * verify callback after the server is already initialized (i.e. without + * supplying it via whServerCertConfig). */ +static int whTest_CertServerVerifyCallbackRuntime(whServerConfig* serverCfg) +{ + int rc; + whServerContext server[1] = {0}; + whServerCertConfig* savedCertConfig; + const whNvmId rootCertA = 1; + + WH_TEST_PRINT("=== Server cert verify-callback runtime test ===\n"); + + /* Force NULL certConfig so registration must come from the runtime API. */ + savedCertConfig = serverCfg->certConfig; + serverCfg->certConfig = NULL; + + s_verifyCb_count = 0; + s_verifyCb_lastPreverify = -1; + s_verifyCb_returnVal = 1; + + WH_TEST_RETURN_ON_FAIL(wh_Server_Init(server, serverCfg)); + WH_TEST_RETURN_ON_FAIL(wh_Server_CertInit(server)); + + WH_TEST_RETURN_ON_FAIL(wh_Server_CertAddTrusted( + server, rootCertA, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONMODIFIABLE, NULL, + 0, ROOT_A_CERT, ROOT_A_CERT_len)); + + /* 1. No callback registered: verify succeeds, counter stays 0. */ + WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify( + server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); + WH_TEST_ASSERT_RETURN(s_verifyCb_count == 0); + + /* 2. Register at runtime; cb must fire on the next cold verify. */ + WH_TEST_RETURN_ON_FAIL( + wh_Server_CertSetVerifyCb(server, whTest_recordingVerifyCb)); +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + wh_Server_CertVerifyCache_Clear(server); +#endif + WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify( + server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); + WH_TEST_ASSERT_RETURN(s_verifyCb_count > 0); + WH_TEST_ASSERT_RETURN(s_verifyCb_lastPreverify == 1); + + /* 3. Unregister at runtime; verify still succeeds, counter stays 0. */ + s_verifyCb_count = 0; + WH_TEST_RETURN_ON_FAIL(wh_Server_CertSetVerifyCb(server, NULL)); +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + wh_Server_CertVerifyCache_Clear(server); +#endif + WH_TEST_RETURN_ON_FAIL(wh_Server_CertVerify( + server, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertA, + WH_CERT_FLAGS_NONE, WH_NVM_FLAGS_USAGE_ANY, NULL)); + WH_TEST_ASSERT_RETURN(s_verifyCb_count == 0); + + /* 4. NULL server is rejected. */ + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + wh_Server_CertSetVerifyCb(NULL, NULL)); + + WH_TEST_RETURN_ON_FAIL(wh_Server_CertEraseTrusted(server, rootCertA)); + serverCfg->certConfig = savedCertConfig; + rc = WH_ERROR_OK; + WH_TEST_PRINT("Server cert verify-callback runtime test PASSED\n"); + return rc; +} #endif /* WOLFHSM_CFG_ENABLE_SERVER */ #ifdef WOLFHSM_CFG_ENABLE_CLIENT @@ -323,7 +650,9 @@ int whTest_CertClient(whClientContext* client) client, RAW_CERT_CHAIN_B, RAW_CERT_CHAIN_B_len, rootCertB_id, &out_rc)); WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); - /* attempt to verify invalid chains, should fail */ + /* attempt to verify invalid chains, should fail. Cache entries are scoped + * to the trusted root NVM ID, so prior positive verifies under the true + * root cannot bypass these cross-root checks. */ WH_TEST_PRINT("Attempting to verify invalid certificate chains...\n"); WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerify( client, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertB_id, &out_rc)); @@ -497,6 +826,64 @@ int whTest_CertClient(whClientContext* client) /* Test non-exportable flag enforcement */ WH_TEST_RETURN_ON_FAIL(whTest_CertNonExportable(client)); +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + /* Verify-cache scenarios over the full client/server RPC: cross-root + * recognition, the clear RPC, and re-verify behavior after clear. */ + { + whNvmId rootCertA_id_c = 1; + whNvmId rootCertB_id_c = 2; + + WH_TEST_PRINT("=== Client cert verify-cache test ===\n"); + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CertAddTrusted(client, rootCertA_id_c, WH_NVM_ACCESS_ANY, + WH_NVM_FLAGS_NONMODIFIABLE, NULL, 0, + ROOT_A_CERT, ROOT_A_CERT_len, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL( + wh_Client_CertAddTrusted(client, rootCertB_id_c, WH_NVM_ACCESS_ANY, + WH_NVM_FLAGS_NONMODIFIABLE, NULL, 0, + ROOT_B_CERT, ROOT_B_CERT_len, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Start from a known-empty cache. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerifyCacheClear(client, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Warm the cache by verifying chain A under its true root. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerify(client, RAW_CERT_CHAIN_A, + RAW_CERT_CHAIN_A_len, + rootCertA_id_c, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + + /* Cache entries are bound to the trusted root NVM ID: chain A under + * root B fails even though every cert in chain A is cached under + * root A. The cache hit must not bypass the cross-root check. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerify(client, RAW_CERT_CHAIN_A, + RAW_CERT_CHAIN_A_len, + rootCertB_id_c, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_CERT_VERIFY); + + /* After clear, the cross-root verify still fails cold. Exercises the + * clear RPC path. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerifyCacheClear(client, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerify(client, RAW_CERT_CHAIN_A, + RAW_CERT_CHAIN_A_len, + rootCertB_id_c, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_CERT_VERIFY); + + /* Cleanup */ + WH_TEST_RETURN_ON_FAIL( + wh_Client_CertEraseTrusted(client, rootCertA_id_c, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_RETURN_ON_FAIL( + wh_Client_CertEraseTrusted(client, rootCertB_id_c, &out_rc)); + WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); + WH_TEST_PRINT("Client cert verify-cache test PASSED\n"); + } +#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */ + WH_TEST_PRINT("Certificate client test completed successfully\n"); return rc; @@ -635,7 +1022,9 @@ int whTest_CertClientDma_ClientServerTestInternal(whClientContext* client) client, RAW_CERT_CHAIN_B, RAW_CERT_CHAIN_B_len, rootCertB_id, &out_rc)); WH_TEST_ASSERT_RETURN(out_rc == WH_ERROR_OK); - /* attempt to verify invalid chains, should fail */ + /* attempt to verify invalid chains, should fail. Cache entries are scoped + * to the trusted root NVM ID, so prior positive verifies under the true + * root cannot bypass these cross-root checks. */ WH_TEST_PRINT("Attempting to verify invalid certificate chains...\n"); WH_TEST_RETURN_ON_FAIL(wh_Client_CertVerifyDma( client, RAW_CERT_CHAIN_A, RAW_CERT_CHAIN_A_len, rootCertB_id, &out_rc)); @@ -998,6 +1387,40 @@ int whTest_CertRamSim(whTestNvmBackendType nvmType) WH_ERROR_PRINT("Certificate server config tests failed: %d\n", rc); } +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + if (rc == WH_ERROR_OK) { + rc = whTest_CertServerVerifyCache(s_conf); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("Cert verify-cache tests failed: %d\n", rc); + } + } +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL + if (rc == WH_ERROR_OK) { + rc = whTest_CertServerVerifyCacheGlobalShared(s_conf); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("Cert verify-cache global cross-client tests " + "failed: %d\n", + rc); + } + } +#endif +#endif + + if (rc == WH_ERROR_OK) { + rc = whTest_CertServerVerifyCallback(s_conf); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("Cert verify-callback tests failed: %d\n", rc); + } + } + + if (rc == WH_ERROR_OK) { + rc = whTest_CertServerVerifyCallbackRuntime(s_conf); + if (rc != WH_ERROR_OK) { + WH_ERROR_PRINT("Cert verify-callback runtime tests failed: %d\n", + rc); + } + } + /* Cleanup NVM */ wh_Nvm_Cleanup(nvm); #ifndef WOLFHSM_CFG_NO_CRYPTO diff --git a/test/wh_test_lock.c b/test/wh_test_lock.c index 6ba42c18..7dd7225d 100644 --- a/test/wh_test_lock.c +++ b/test/wh_test_lock.c @@ -174,9 +174,12 @@ static int testNvmRamSimWithLock(whLockConfig* lockConfig) .config = flashCfg, }; - /* NVM context with lock */ - whNvmContext nvm = {0}; - whNvmConfig nvmCfg; + /* NVM context with lock. Zero-init nvmCfg so any conditionally-compiled + * fields (e.g. certVerifyCacheLockConfig under + * WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL) start as NULL = no-op + * locking, rather than as indeterminate stack garbage. */ + whNvmContext nvm = {0}; + whNvmConfig nvmCfg = {0}; whNvmMetadata meta; uint8_t testData[] = "Hello, NVM with lock!"; diff --git a/wolfhsm/wh_client.h b/wolfhsm/wh_client.h index ca4ece9e..a911e339 100644 --- a/wolfhsm/wh_client.h +++ b/wolfhsm/wh_client.h @@ -2674,6 +2674,38 @@ int wh_Client_CertVerifyMultiRootAndCacheLeafPubKey( const whNvmId* trustedRootNvmIds, uint16_t numRoots, whNvmFlags cachedKeyFlags, whKeyId* inout_keyId, int32_t* out_rc); +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE +/** + * @brief Send a request to clear the server's trusted certificate verify cache. + * + * Subsequent verification of any certificate will re-run the public-key + * signature check until that cert is verified again and re-cached. + * + * @param[in] c Pointer to the client context. + * @return int Returns 0 on success, or a negative error code on failure. + */ +int wh_Client_CertVerifyCacheClearRequest(whClientContext* c); + +/** + * @brief Receive the response to a verify-cache clear request. + * + * @param[in] c Pointer to the client context. + * @param[out] out_rc Pointer to store the response code from the server. + * @return int Returns 0 on success, or a negative error code on failure. + */ +int wh_Client_CertVerifyCacheClearResponse(whClientContext* c, int32_t* out_rc); + +/** + * @brief Synchronous helper to clear the server's trusted certificate verify + * cache. + * + * @param[in] c Pointer to the client context. + * @param[out] out_rc Pointer to store the response code from the server. + * @return int Returns 0 on success, or a negative error code on failure. + */ +int wh_Client_CertVerifyCacheClear(whClientContext* c, int32_t* out_rc); +#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */ + #ifdef WOLFHSM_CFG_DMA diff --git a/wolfhsm/wh_message_cert.h b/wolfhsm/wh_message_cert.h index 8dbb0588..e0db2bbf 100644 --- a/wolfhsm/wh_message_cert.h +++ b/wolfhsm/wh_message_cert.h @@ -31,7 +31,6 @@ #include "wolfhsm/wh_common.h" #include "wolfhsm/wh_comm.h" #include "wolfhsm/wh_message.h" -#include "wolfhsm/wh_nvm.h" #include "wolfhsm/wh_utils.h" enum WH_MESSAGE_CERT_ACTION_ENUM { @@ -41,6 +40,7 @@ enum WH_MESSAGE_CERT_ACTION_ENUM { WH_MESSAGE_CERT_ACTION_READTRUSTED = 0x4, WH_MESSAGE_CERT_ACTION_VERIFY = 0x5, WH_MESSAGE_CERT_ACTION_VERIFY_MULTI_ROOT = 0x6, + WH_MESSAGE_CERT_ACTION_VERIFY_CACHE_CLEAR = 0x7, WH_MESSAGE_CERT_ACTION_ADDTRUSTED_DMA = 0x22, WH_MESSAGE_CERT_ACTION_READTRUSTED_DMA = 0x24, WH_MESSAGE_CERT_ACTION_VERIFY_DMA = 0x25, diff --git a/wolfhsm/wh_message_crypto.h b/wolfhsm/wh_message_crypto.h index ac341774..a5a3ef8d 100644 --- a/wolfhsm/wh_message_crypto.h +++ b/wolfhsm/wh_message_crypto.h @@ -149,14 +149,14 @@ int wh_MessageCrypto_TranslateRngResponse( /* * AES */ -/* AES CTR Request */ +/* AES CTR Request - fields ordered by size to keep padding trailing */ typedef struct { uint32_t enc; /* 1 for encrypt, 0 for decrypt */ uint32_t keyLen; /* Length of key in bytes */ uint32_t sz; /* Size of input data */ - uint16_t keyId; /* Key ID if using stored key */ uint32_t left; /* unused bytes left from last call */ - uint8_t WH_PAD[2]; /* Padding for alignment */ + uint16_t keyId; /* Key ID if using stored key */ + uint8_t WH_PAD[2]; /* Data follows: * uint8_t in[sz] * uint8_t key[keyLen] @@ -1068,13 +1068,16 @@ typedef struct { uint32_t inSz; } whMessageCrypto_Sha512DmaRequest; -/* SHA2 DMA Response - carries updated state or final hash inline */ +/* SHA2 DMA Response - carries updated state or final hash inline. + * Fields ordered by size (8-byte-aligned struct first) to keep padding + * trailing. */ typedef struct { + whMessageCrypto_DmaAddrStatus + dmaAddrStatus; /* 8-byte aligned, place first */ + uint8_t hash[64]; /* big enough for all SHA2 variants */ uint32_t hiLen; uint32_t loLen; - uint8_t hash[64]; /* big enough for all SHA2 variants */ uint32_t hashType; - whMessageCrypto_DmaAddrStatus dmaAddrStatus; uint8_t WH_PAD[4]; } whMessageCrypto_Sha2DmaResponse; @@ -1138,6 +1141,7 @@ typedef struct { typedef struct { whMessageCrypto_DmaAddrStatus dmaAddrStatus; uint32_t outSz; + uint8_t WH_PAD[4]; /* Round struct to 8-byte alignment */ } whMessageCrypto_AesEcbDmaResponse; /* AES-ECB DMA translation functions */ @@ -1168,6 +1172,7 @@ typedef struct { typedef struct { whMessageCrypto_DmaAddrStatus dmaAddrStatus; uint32_t outSz; + uint8_t WH_PAD[4]; /* Round struct to 8-byte alignment */ /* Trailing data: uint8_t iv[AES_IV_SIZE] */ } whMessageCrypto_AesCbcDmaResponse; diff --git a/wolfhsm/wh_message_nvm.h b/wolfhsm/wh_message_nvm.h index bf5a706f..023158dd 100644 --- a/wolfhsm/wh_message_nvm.h +++ b/wolfhsm/wh_message_nvm.h @@ -32,7 +32,6 @@ #include "wolfhsm/wh_common.h" #include "wolfhsm/wh_comm.h" #include "wolfhsm/wh_message.h" -#include "wolfhsm/wh_nvm.h" enum WH_MESSAGE_NVM_ACTION_ENUM { WH_MESSAGE_NVM_ACTION_INIT = 0x1, diff --git a/wolfhsm/wh_nvm.h b/wolfhsm/wh_nvm.h index e41f7f7c..91506f14 100644 --- a/wolfhsm/wh_nvm.h +++ b/wolfhsm/wh_nvm.h @@ -60,6 +60,9 @@ #include "wolfhsm/wh_common.h" /* For whNvm types */ #include "wolfhsm/wh_keycache.h" /* For whKeyCacheContext */ #include "wolfhsm/wh_lock.h" +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL +#include "wolfhsm/wh_server_cert_cache.h" /* For whCertVerifyCacheContext */ +#endif /** * @brief NVM backend callback table. @@ -137,6 +140,12 @@ typedef struct whNvmContext_t { #if !defined(WOLFHSM_CFG_NO_CRYPTO) && defined(WOLFHSM_CFG_GLOBAL_KEYS) whKeyCacheContext globalCache; /**< Global key cache (shared keys) */ #endif +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL + whCertVerifyCacheContext globalCertVerifyCache; /**< Global cross-client + * trusted cert verify + * cache. Carries its own + * dedicated lock. */ +#endif #ifdef WOLFHSM_CFG_THREADSAFE whLock lock; /**< Lock for serializing NVM and global cache operations */ #endif @@ -154,6 +163,16 @@ typedef struct whNvmConfig_t { #ifdef WOLFHSM_CFG_THREADSAFE whLockConfig* lockConfig; /**< Lock configuration (NULL for no-op locking) */ +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL + whLockConfig* certVerifyCacheLockConfig; /**< Lock config for the global + * cert verify cache. Independent + * from lockConfig — pass a + * separate platform context (e.g. + * a distinct posixLockContext) so + * the two locks back distinct + * mutexes. NULL for no-op + * locking. */ +#endif #endif } whNvmConfig; diff --git a/wolfhsm/wh_server.h b/wolfhsm/wh_server.h index f57cf2d7..e332434a 100644 --- a/wolfhsm/wh_server.h +++ b/wolfhsm/wh_server.h @@ -39,6 +39,7 @@ typedef struct whServerContext_t whServerContext; #include "wolfhsm/wh_common.h" #include "wolfhsm/wh_comm.h" #include "wolfhsm/wh_keycache.h" +#include "wolfhsm/wh_server_cert_cache.h" #include "wolfhsm/wh_nvm.h" #ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION #include "wolfhsm/wh_auth.h" @@ -160,6 +161,9 @@ typedef struct whServerConfig_t { #ifdef WOLFHSM_CFG_LOGGING whLogConfig* logConfig; #endif /* WOLFHSM_CFG_LOGGING */ +#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) && !defined(WOLFHSM_CFG_NO_CRYPTO) + whServerCertConfig* certConfig; /* optional; NULL = no verify callback */ +#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */ } whServerConfig; @@ -186,6 +190,9 @@ struct whServerContext_t { #ifdef WOLFHSM_CFG_LOGGING whLogContext log; #endif /* WOLFHSM_CFG_LOGGING */ +#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) && !defined(WOLFHSM_CFG_NO_CRYPTO) + whServerCertContext cert; /* verify callback + verify cache */ +#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */ }; diff --git a/wolfhsm/wh_server_cert.h b/wolfhsm/wh_server_cert.h index 7b939665..7c56fe53 100644 --- a/wolfhsm/wh_server_cert.h +++ b/wolfhsm/wh_server_cert.h @@ -131,6 +131,26 @@ int wh_Server_CertVerifyMultiRoot(whServerContext* server, const uint8_t* cert, whNvmFlags cachedKeyFlags, whKeyId* inout_keyId); +#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) && !defined(WOLFHSM_CFG_NO_CRYPTO) +/** + * @brief Register a verify callback at runtime. + * + * Replaces the callback previously set via whServerCertConfig.verifyCb (or by + * a prior call to this function). Pass NULL to unregister. + * + * The callback is applied to the per-request WOLFSSL_CERT_MANAGER created by + * wh_Server_CertVerify, so it participates in chain verification the same way + * a callback registered with wolfSSL_CertManagerSetVerify would. Verify-cache + * hits (when WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE is enabled) bypass the + * callback because they bypass wolfSSL's verify path entirely. + * + * @param server The server context. + * @param cb The callback to register, or NULL to unregister. + * @return WH_ERROR_OK on success, WH_ERROR_BADARGS if server is NULL. + */ +int wh_Server_CertSetVerifyCb(whServerContext* server, VerifyCallback cb); +#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */ + #if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT) /** * @brief Verifies an attribute certificate against a trusted root certificate diff --git a/wolfhsm/wh_server_cert_cache.h b/wolfhsm/wh_server_cert_cache.h new file mode 100644 index 00000000..49216198 --- /dev/null +++ b/wolfhsm/wh_server_cert_cache.h @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2025 wolfSSL Inc. + * + * This file is part of wolfHSM. + * + * wolfHSM is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfHSM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfHSM. If not, see . + */ + +/* + * wolfhsm/wh_server_cert_cache.h + * + * Server-side cert subsystem types embedded in whServerContext: + * - whServerCertContext / whServerCertConfig: hold the user-injectable + * verify callback and (optionally) the trusted-cert verify cache. + * - whCertVerifyCacheContext: trusted-cert verify-result cache. Records + * SHA-256 hashes of DER-encoded CA certificates that have already been + * successfully verified, scoped to the set of trusted-root NVM IDs + * that were loaded when the verify ran. Hits apply across clients + * but require the cached root set to be a subset of the caller's + * currently-loaded root set. + * + * Only CA certs are inserted. Caching a leaf would let a future + * "leaf alone" verify falsely succeed via cache hit, because the + * cache hit bypasses the wolfSSL signature check that would otherwise + * have failed (the leaf's issuer is not in the cert manager when the + * leaf is supplied without its intermediates). CA caching is sound + * because the chain walk loads each verified CA into the cert manager + * before the next cert is processed. + * + * Soundness of the subset rule rests on X.509 verify monotonicity: + * adding more trusted roots can never invalidate a previously + * successful verify, so a chain that validated under set S still + * validates under any superset T ⊇ S. A cache hit therefore implies + * the cached verify's anchor (whichever root in S actually closed + * the chain) is currently trusted, regardless of which element of S + * it was — every element of S is in T by hypothesis. + * + * Both single-root and multi-root verifies populate the cache. + * Single-root entries have one-element sets (maximum reuse, since + * any later caller whose loaded set contains that root will hit). + * Multi-root entries have larger sets (narrower reuse — only later + * callers whose loaded set is a superset will hit) but capture + * verifies that pure single-root traffic would not generate. + * + * Lives in its own header to avoid circular dependencies between wh_server.h + * and wh_server_cert.h. + */ + +#ifndef WOLFHSM_WH_SERVER_CERT_CACHE_H_ +#define WOLFHSM_WH_SERVER_CERT_CACHE_H_ + +/* Pick up compile-time configuration */ +#include "wolfhsm/wh_settings.h" + +#if defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) && !defined(WOLFHSM_CFG_NO_CRYPTO) + +#include + +#include "wolfhsm/wh_common.h" /* for whNvmId */ +#include "wolfhsm/wh_lock.h" /* for whLock (global cache lock) */ + +#include "wolfssl/ssl.h" /* for VerifyCallback */ + +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE + +#ifndef WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT +#define WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT 16 +#endif + +#define WH_CERT_VERIFY_CACHE_HASH_LEN 32 /* SHA-256 digest size */ + +typedef struct whCertVerifyCacheSlot { + uint8_t committed; /* 0 = empty, 1 = valid */ + uint8_t numRoots; /* count of valid entries in rootNvmIds */ + uint8_t WH_PAD[2]; + /* Set of trusted root NVM IDs loaded when this cert was verified. A + * lookup hits when this set is a subset of the caller's currently + * loaded set (verify monotonicity makes the over-approximation safe). */ + whNvmId rootNvmIds[WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS]; + uint8_t hash[WH_CERT_VERIFY_CACHE_HASH_LEN]; +} whCertVerifyCacheSlot; + +typedef struct whCertVerifyCacheContext { + whCertVerifyCacheSlot slots[WOLFHSM_CFG_CERT_VERIFY_CACHE_COUNT]; + uint16_t writeIdx; /* FIFO ring write position */ + uint8_t WH_PAD[6]; +#if defined(WOLFHSM_CFG_THREADSAFE) && \ + defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL) + /* Dedicated lock for the global verify cache. Independent from the NVM + * lock so cert-cache operations do not serialize behind NVM I/O. Only + * present when the cache lives in the shared NVM context. */ + whLock lock; +#endif +} whCertVerifyCacheContext; + +#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */ + +/* Per-server cert subsystem config, supplied via whServerConfig.certConfig. + * The verify callback signature matches wolfSSL's VerifyCallback so the same + * callback registered with wolfSSL_CertManagerSetVerify can be used here. */ +typedef struct { + VerifyCallback verifyCb; /* user-supplied; NULL = no callback */ +} whServerCertConfig; + +/* Per-server cert subsystem context, embedded by value in whServerContext. + * Holds the registered verify callback and (optionally) the per-client verify + * cache. When WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL is defined the cache + * is relocated into the shared whNvmContext, so the per-client copy is + * omitted. */ +typedef struct { + VerifyCallback verifyCb; +#if defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE) && \ + !defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL) + whCertVerifyCacheContext cache; +#endif +} whServerCertContext; + +/* Forward declaration to avoid pulling in wh_server.h */ +struct whServerContext_t; + +#ifdef WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE +/** + * @brief Look up a cert hash in the verify cache against a set of loaded + * trusted roots. + * + * Hits when there exists a committed slot whose stored root set is a + * subset of the supplied set and whose hash matches. By verify + * monotonicity, a previously successful verify under the slot's set + * remains valid under any superset, so the hit is sound. + * + * @param server The server context. + * @param rootNvmIds Array of trusted root NVM IDs currently loaded + * (presented set). + * @param numRoots Number of entries in rootNvmIds (must be > 0). + * @param hash Pointer to a SHA-256 (32-byte) digest of the DER cert. + * @return WH_ERROR_OK on hit, WH_ERROR_NOTFOUND on miss, + * WH_ERROR_BADARGS on invalid arguments. + */ +int wh_Server_CertVerifyCache_Lookup(struct whServerContext_t* server, + const whNvmId* rootNvmIds, + uint16_t numRoots, const uint8_t* hash); + +/** + * @brief Insert a cert hash into the verify cache, recording the supplied + * root set as the entry's binding. + * + * No-op if a slot with the same hash and the same root set already + * exists. Uses FIFO ring overwrite when full. + * + * The supplied set must be the set of roots actually loaded into the + * cert manager at the time of the verify (post-filtering of any roots + * absent from NVM); recording roots that were not actually loaded would + * widen the entry's required-trust set without justification. + * + * @param server The server context. + * @param rootNvmIds Array of trusted root NVM IDs loaded for the verify. + * @param numRoots Number of entries in rootNvmIds (must be > 0 and + * <= WOLFHSM_CFG_CERT_MAX_VERIFY_ROOTS). + * @param hash Pointer to a SHA-256 (32-byte) digest of the DER cert. + */ +void wh_Server_CertVerifyCache_Insert(struct whServerContext_t* server, + const whNvmId* rootNvmIds, + uint16_t numRoots, const uint8_t* hash); + +/** + * @brief Clear all entries from the verify cache. + * + * In per-client mode (default) clears this server's cache only. In global + * mode (WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL) clears the shared + * cache for every connected client. + * + * @param server The server context. + */ +void wh_Server_CertVerifyCache_Clear(struct whServerContext_t* server); + +/** + * @brief Evict every cache entry whose stored root set contains the + * supplied trusted-root NVM ID. + * + * Must be invoked whenever the trusted root at rootNvmId changes (add or + * erase). Without this, re-using a freed ID for a different root would + * let stale cache hits short-circuit verifies under a trust anchor that + * is no longer present at that ID. + * + * Entries whose stored set contains the evicted root are dropped + * entirely rather than stripped of that one root, because the original + * verify may have been anchored at the now-departed root. + * + * @param server The server context. + * @param rootNvmId NVM ID of the trusted root whose cache entries to drop. + */ +void wh_Server_CertVerifyCache_EvictRoot(struct whServerContext_t* server, + whNvmId rootNvmId); +#endif /* WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE */ + +#endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER && !WOLFHSM_CFG_NO_CRYPTO */ + +#endif /* !WOLFHSM_WH_SERVER_CERT_CACHE_H_ */ diff --git a/wolfhsm/wh_settings.h b/wolfhsm/wh_settings.h index 18d23bd5..47b656a6 100644 --- a/wolfhsm/wh_settings.h +++ b/wolfhsm/wh_settings.h @@ -169,7 +169,13 @@ #include -#ifndef WOLFHSM_CFG_NO_CRYPTO +/* WH_PADDING_CHECK is an internal sentinel set only by the wire-format + * struct-padding audit (test/wh_test_check_struct_padding.c). It suppresses + * external dependencies (wolfSSL headers, etc.) so that audit can compile + * without dragging in third-party source whose layout could perturb -Wpadded. + * It is NOT a public configuration flag — do not use it from application + * code. */ +#if !defined(WOLFHSM_CFG_NO_CRYPTO) && !defined(WH_PADDING_CHECK) #ifdef WOLFSSL_USER_SETTINGS #include "user_settings.h" #else @@ -181,7 +187,7 @@ #if defined(WOLFHSM_CFG_DEBUG) || defined(WOLFHSM_CFG_DEBUG_VERBOSE) #define WOLFHSM_CFG_HEXDUMP #endif -#endif /* !WOLFHSM_CFG_NO_CRYPTO */ +#endif /* !WOLFHSM_CFG_NO_CRYPTO && !WH_PADDING_CHECK */ /* Platform system time access */ #if !defined WOLFHSM_CFG_NO_SYS_TIME && !defined(WOLFHSM_CFG_PORT_GETTIME) @@ -388,7 +394,9 @@ #endif /** Configuration checks */ -#ifndef WOLFHSM_CFG_NO_CRYPTO +/* Skipped under WH_PADDING_CHECK because the wolfSSL feature macros + * referenced below are only defined when wolfssl/options.h is pulled. */ +#if !defined(WOLFHSM_CFG_NO_CRYPTO) && !defined(WH_PADDING_CHECK) /* Crypto Cb is mandatory */ #ifndef WOLF_CRYPTO_CB #error "wolfHSM requires wolfCrypt built with WOLF_CRYPTO_CB" @@ -439,27 +447,55 @@ #endif /* !WOLFSSL_ACERT || !WOLFSSL_ASN_TEMPLATE */ #endif /* WOLFHSM_CFG_CERTIFICATE_MANAGER_ACERT */ -#endif /* !WOLFHSM_CFG_NO_CRYPTO */ +#endif /* !WOLFHSM_CFG_NO_CRYPTO && !WH_PADDING_CHECK */ #if defined(WOLFHSM_CFG_NO_CRYPTO) && defined(WOLFHSM_CFG_KEYWRAP) #error "WOLFHSM_CFG_KEYWRAP is incompatible with WOLFHSM_CFG_NO_CRYPTO" #endif +/* Trusted cert verify cache requires the certificate manager and crypto. + * Enforce here so downstream code can gate on + * WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE alone instead of repeating the full + * dependency chain at every site. */ +#if defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE) && \ + !defined(WOLFHSM_CFG_CERTIFICATE_MANAGER) +#error \ + "WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE requires WOLFHSM_CFG_CERTIFICATE_MANAGER" +#endif + +#if defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE) && \ + defined(WOLFHSM_CFG_NO_CRYPTO) +#error \ + "WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE is incompatible with WOLFHSM_CFG_NO_CRYPTO" +#endif + +/* The global cross-client verify cache is a layered option on top of the + * per-client cache. Enforce the dependency so downstream code can gate on + * WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL alone. */ +#if defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL) && \ + !defined(WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE) +#error \ + "WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE_GLOBAL requires WOLFHSM_CFG_CERTIFICATE_VERIFY_CACHE" +#endif + /** Cache flushing and memory fencing synchronization primitives */ /* Create a full sequential memory fence to ensure compiler memory ordering */ #ifndef XMEMFENCE - #ifndef WOLFHSM_CFG_NO_CRYPTO - #include "wolfssl/wolfcrypt/wc_port.h" - #define XMEMFENCE() XFENCE() - #else - #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && !defined(__STDC_NO_ATOMICS__) - #include - #define XMEMFENCE() atomic_thread_fence(memory_order_seq_cst) - #elif defined(__GNUC__) || defined(__clang__) - #define XMEMFENCE() __atomic_thread_fence(__ATOMIC_SEQ_CST) - #else - /* PPC32: __asm__ volatile ("sync" : : : "memory") */ - #define XMEMFENCE() do { } while (0) +#if !defined(WOLFHSM_CFG_NO_CRYPTO) && !defined(WH_PADDING_CHECK) +#include "wolfssl/wolfcrypt/wc_port.h" +#define XMEMFENCE() XFENCE() +#else +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ + !defined(__STDC_NO_ATOMICS__) +#include +#define XMEMFENCE() atomic_thread_fence(memory_order_seq_cst) +#elif defined(__GNUC__) || defined(__clang__) +#define XMEMFENCE() __atomic_thread_fence(__ATOMIC_SEQ_CST) +#else +/* PPC32: __asm__ volatile ("sync" : : : "memory") */ +#define XMEMFENCE() \ + do { \ + } while (0) #warning "wolfHSM memory transports should have a functional XMEMFENCE" #endif #endif