From 2324dc15130c08add911706d85c81c38f50ae924 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Mon, 22 Jun 2026 18:39:33 -0700 Subject: [PATCH 1/5] feat: add sbom target to Makefile Adds sbom target that calls gen-sbom to produce CycloneDX and SPDX output files. Parses version from ChangeLog.md. Sources enumerated from src/*.c. Requires WOLFSSL_DIR pointing to wolfssl tree with the feat/sbom-embedded branch (includes gen-sbom). --- Makefile | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Makefile b/Makefile index 6d4763e3f..105976e19 100644 --- a/Makefile +++ b/Makefile @@ -56,3 +56,48 @@ clean: make -C benchmark clean make -C tools clean make -C examples clean + +# ---- SBOM generation ---- +CC ?= cc +WOLFSSL_DIR ?= ../../wolfssl +VERSION := $(shell sed -n 's/^# wolfHSM Release v\([0-9][0-9.]*\).*/\1/p' ChangeLog.md | head -1) +SRCS := $(wildcard src/*.c) +SBOM_CDX := wolfhsm-$(VERSION).cdx.json +SBOM_SPDX := wolfhsm-$(VERSION).spdx.json + +.PHONY: sbom + +sbom: + @if [ -z "$(VERSION)" ]; then \ + echo "ERROR: could not parse version from ChangeLog.md." >&2; \ + exit 1; \ + fi + @if [ -z "$(WOLFSSL_DIR)" ] || [ ! -d "$(WOLFSSL_DIR)" ]; then \ + echo "ERROR: WOLFSSL_DIR=$(WOLFSSL_DIR) is not a directory." >&2; \ + echo " Set WOLFSSL_DIR to your wolfssl source tree." >&2; \ + exit 1; \ + fi + @if [ ! -f "$(WOLFSSL_DIR)/scripts/gen-sbom" ]; then \ + echo "ERROR: $(WOLFSSL_DIR)/scripts/gen-sbom not found." >&2; \ + echo " Use a wolfSSL tree that includes SBOM support." >&2; \ + exit 1; \ + fi + @echo "wolfHSM version: $(VERSION)" + @echo "Sources: $(words $(SRCS)) .c files in src/" + @_defines=$$(mktemp /tmp/wolfhsm-defines.XXXXXX) && \ + trap 'rm -f "$$_defines"' EXIT && \ + if ! $(CC) -dM -E -I. -I$(WOLFSSL_DIR) -x c /dev/null >"$$_defines" 2>/dev/null; then \ + echo "ERROR: $(CC) -dM -E failed." >&2; exit 1; \ + fi && \ + _py=$$(command -v python3 2>/dev/null || command -v python 2>/dev/null) && \ + [ -n "$$_py" ] || { echo "ERROR: python3 not found." >&2; exit 1; } && \ + "$$_py" $(WOLFSSL_DIR)/scripts/gen-sbom \ + --name wolfhsm \ + --version $(VERSION) \ + --supplier "wolfSSL Inc." \ + --license-file LICENSING \ + --options-h "$$_defines" \ + --srcs $(SRCS) \ + --cdx-out $(SBOM_CDX) \ + --spdx-out $(SBOM_SPDX) + @echo "Done: $(SBOM_CDX) $(SBOM_SPDX)" From 6b7ee110df9ddc39ae20a7f626a722b1406cb22c Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Tue, 23 Jun 2026 17:40:28 -0700 Subject: [PATCH 2/5] docs: add SBOM/EU CRA Compliance section to README --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 653da9105..381a77513 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,28 @@ please refer to the following resources. - [wolfHSM Manual](https://www.wolfssl.com/documentation/manuals/wolfhsm/index.html) - [wolfHSM API Reference](https://www.wolfssl.com/documentation/manuals/wolfhsm/appendix01.html) - [wolfHSM Examples](https://github.com/wolfSSL/wolfHSM/tree/main/examples) + +## SBOM / EU CRA Compliance + +wolfHSM generates a Software Bill of Materials (SBOM) in CycloneDX 1.6 and +SPDX 2.3 formats to support compliance with the EU Cyber Resilience Act (CRA). + +wolfHSM uses a custom build system; invoke `gen-sbom` from the wolfssl source +tree directly: + +```sh +python3 $WOLFSSL_DIR/scripts/gen-sbom \ + --name wolfhsm \ + --version $(head -1 $WOLFHSM_DIR/ChangeLog.md | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') \ + --supplier "wolfSSL Inc." \ + --options-h $WOLFSSL_DIR/include/wolfssl/options.h \ + --srcs $WOLFHSM_DIR/src/*.c +``` + +`WOLFSSL_DIR` must point to a wolfssl source tree containing `scripts/gen-sbom` +(branch `feat/sbom-embedded`, or `master` once wolfSSL/wolfssl#10343 merges). +`WOLFHSM_DIR` is the root of the wolfHSM source tree. + +Requires `python3` and `pyspdxtools` (`pip install spdx-tools`). + +For further CRA guidance see [wolfssl/doc/CRA.md](https://github.com/wolfSSL/wolfssl/blob/master/doc/CRA.md). From e4b910db722614857906db9873fddbbb28788ff7 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Fri, 10 Jul 2026 12:56:42 -0700 Subject: [PATCH 3/5] fix(sbom): graceful gen-sbom errors, portable recipe --- Makefile | 25 ++++++++++++++----------- README.md | 32 ++++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 105976e19..f3a6bd010 100644 --- a/Makefile +++ b/Makefile @@ -59,11 +59,11 @@ clean: # ---- SBOM generation ---- CC ?= cc -WOLFSSL_DIR ?= ../../wolfssl -VERSION := $(shell sed -n 's/^# wolfHSM Release v\([0-9][0-9.]*\).*/\1/p' ChangeLog.md | head -1) -SRCS := $(wildcard src/*.c) -SBOM_CDX := wolfhsm-$(VERSION).cdx.json -SBOM_SPDX := wolfhsm-$(VERSION).spdx.json +WOLFSSL_DIR ?= ../wolfssl +VERSION = $(shell sed -n 's/^# wolfHSM Release v\([0-9][0-9.]*\).*/\1/p' ChangeLog.md | head -1) +SRCS := $(sort $(wildcard src/*.c)) +SBOM_CDX = wolfhsm-$(VERSION).cdx.json +SBOM_SPDX = wolfhsm-$(VERSION).spdx.json .PHONY: sbom @@ -79,19 +79,22 @@ sbom: fi @if [ ! -f "$(WOLFSSL_DIR)/scripts/gen-sbom" ]; then \ echo "ERROR: $(WOLFSSL_DIR)/scripts/gen-sbom not found." >&2; \ - echo " Use a wolfSSL tree that includes SBOM support." >&2; \ + echo " The sbom target needs a wolfSSL source tree that includes" >&2; \ + echo " scripts/gen-sbom (wolfSSL PR #10343, pending a future release)." >&2; \ + echo " Set WOLFSSL_DIR to such a tree." >&2; \ exit 1; \ fi @echo "wolfHSM version: $(VERSION)" @echo "Sources: $(words $(SRCS)) .c files in src/" - @_defines=$$(mktemp /tmp/wolfhsm-defines.XXXXXX) && \ - trap 'rm -f "$$_defines"' EXIT && \ + @_defines=$$(mktemp "$${TMPDIR:-/tmp}/wolfhsm-defines.XXXXXX") && \ + trap 'rm -f "$$_defines"' 0 && \ if ! $(CC) -dM -E -I. -I$(WOLFSSL_DIR) -x c /dev/null >"$$_defines" 2>/dev/null; then \ echo "ERROR: $(CC) -dM -E failed." >&2; exit 1; \ fi && \ - _py=$$(command -v python3 2>/dev/null || command -v python 2>/dev/null) && \ - [ -n "$$_py" ] || { echo "ERROR: python3 not found." >&2; exit 1; } && \ - "$$_py" $(WOLFSSL_DIR)/scripts/gen-sbom \ + if ! command -v python3 >/dev/null 2>&1; then \ + echo "ERROR: python3 not found." >&2; exit 1; \ + fi && \ + python3 $(WOLFSSL_DIR)/scripts/gen-sbom \ --name wolfhsm \ --version $(VERSION) \ --supplier "wolfSSL Inc." \ diff --git a/README.md b/README.md index 381a77513..5dcda024d 100644 --- a/README.md +++ b/README.md @@ -31,22 +31,34 @@ please refer to the following resources. wolfHSM generates a Software Bill of Materials (SBOM) in CycloneDX 1.6 and SPDX 2.3 formats to support compliance with the EU Cyber Resilience Act (CRA). -wolfHSM uses a custom build system; invoke `gen-sbom` from the wolfssl source -tree directly: +Generate both SBOMs with the `sbom` Makefile target: + +```sh +make sbom WOLFSSL_DIR=../wolfssl +``` + +This parses the version from `ChangeLog.md`, collects `src/*.c`, and writes +`wolfhsm-.cdx.json` and `wolfhsm-.spdx.json`. + +`WOLFSSL_DIR` must point to a wolfssl source tree containing `scripts/gen-sbom`, +which ships in wolfSSL PR #10343 (pending a future wolfSSL release). If the +script is absent the target fails with a message telling you what is missing. + +Requires `python3` and `pyspdxtools` (`pip install spdx-tools`). + +To invoke `gen-sbom` directly instead of through the target, run the same +command it runs: ```sh python3 $WOLFSSL_DIR/scripts/gen-sbom \ --name wolfhsm \ - --version $(head -1 $WOLFHSM_DIR/ChangeLog.md | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') \ + --version $(sed -n 's/^# wolfHSM Release v\([0-9][0-9.]*\).*/\1/p' ChangeLog.md | head -1) \ --supplier "wolfSSL Inc." \ + --license-file LICENSING \ --options-h $WOLFSSL_DIR/include/wolfssl/options.h \ - --srcs $WOLFHSM_DIR/src/*.c + --srcs src/*.c \ + --cdx-out wolfhsm.cdx.json \ + --spdx-out wolfhsm.spdx.json ``` -`WOLFSSL_DIR` must point to a wolfssl source tree containing `scripts/gen-sbom` -(branch `feat/sbom-embedded`, or `master` once wolfSSL/wolfssl#10343 merges). -`WOLFHSM_DIR` is the root of the wolfHSM source tree. - -Requires `python3` and `pyspdxtools` (`pip install spdx-tools`). - For further CRA guidance see [wolfssl/doc/CRA.md](https://github.com/wolfSSL/wolfssl/blob/master/doc/CRA.md). From 4c0af52c4b41983ae20eff4f895a34eb26c2e042 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Fri, 10 Jul 2026 13:27:39 -0700 Subject: [PATCH 4/5] fix: keep '#' out of make-level shell call --- .github/workflows/wolfboot-tz-integration.yml | 162 ++ Makefile | 2 +- port/armv8m-tz/README.md | 44 + port/armv8m-tz/wh_transport_nsc.c | 224 +++ port/armv8m-tz/wh_transport_nsc.h | 117 ++ port/stmicro/stm32h5-tz/README.md | 12 + src/wh_hwkeystore.c | 146 ++ test-refactor/client-server/wh_test_auth.c | 1121 +++++++++++ test-refactor/client-server/wh_test_counter.c | 173 ++ .../client-server/wh_test_crypto_lms.c | 460 +++++ .../client-server/wh_test_crypto_xmss.c | 448 +++++ test-refactor/client-server/wh_test_nvm_ops.c | 465 +++++ test-refactor/client-server/wh_test_she.c | 552 ++++++ test-refactor/misc/wh_test_client_devid.c | 313 ++++ test-refactor/misc/wh_test_hwkeystore.c | 664 +++++++ test-refactor/misc/wh_test_multiclient.c | 1658 +++++++++++++++++ .../posix/wh_test_flash_fault_inject.c | 183 ++ .../posix/wh_test_flash_fault_inject.h | 80 + .../server/wh_test_hwkeystore_server.c | 345 ++++ test-refactor/server/wh_test_she_server.c | 482 +++++ wolfhsm/wh_hwkeystore.h | 183 ++ 21 files changed, 7833 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/wolfboot-tz-integration.yml create mode 100644 port/armv8m-tz/README.md create mode 100644 port/armv8m-tz/wh_transport_nsc.c create mode 100644 port/armv8m-tz/wh_transport_nsc.h create mode 100644 port/stmicro/stm32h5-tz/README.md create mode 100644 src/wh_hwkeystore.c create mode 100644 test-refactor/client-server/wh_test_auth.c create mode 100644 test-refactor/client-server/wh_test_counter.c create mode 100644 test-refactor/client-server/wh_test_crypto_lms.c create mode 100644 test-refactor/client-server/wh_test_crypto_xmss.c create mode 100644 test-refactor/client-server/wh_test_nvm_ops.c create mode 100644 test-refactor/client-server/wh_test_she.c create mode 100644 test-refactor/misc/wh_test_client_devid.c create mode 100644 test-refactor/misc/wh_test_hwkeystore.c create mode 100644 test-refactor/misc/wh_test_multiclient.c create mode 100644 test-refactor/posix/wh_test_flash_fault_inject.c create mode 100644 test-refactor/posix/wh_test_flash_fault_inject.h create mode 100644 test-refactor/server/wh_test_hwkeystore_server.c create mode 100644 test-refactor/server/wh_test_she_server.c create mode 100644 wolfhsm/wh_hwkeystore.h diff --git a/.github/workflows/wolfboot-tz-integration.yml b/.github/workflows/wolfboot-tz-integration.yml new file mode 100644 index 000000000..363986fb5 --- /dev/null +++ b/.github/workflows/wolfboot-tz-integration.yml @@ -0,0 +1,162 @@ +# Nightly integration: build wolfBoot's STM32H5 TrustZone demo against +# the latest wolfHSM main and run it under the m33mu emulator. This +# catches drift between wolfHSM and wolfBoot main (in either direction) +# that the pinned-submodule per-PR builds do not. The full client suite +# runs software RSA and ECC, which is slow under the emulator, so this +# is a nightly job rather than a per-PR gate. +# +# On failure it opens (or updates) a tracking issue assigned to +# wolfSSL-Bot. + +name: wolfBoot TrustZone integration nightly (m33mu) + +on: + schedule: + # 07:00 UTC daily + - cron: '0 7 * * *' + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + wolfboot-stm32h5-tz-wolfhsm: + runs-on: ubuntu-latest + timeout-minutes: 60 + container: + image: ghcr.io/wolfssl/wolfboot-ci-m33mu:latest + + steps: + - name: Checkout wolfHSM (this repo, main) + uses: actions/checkout@v4 + with: + path: wolfHSM + + - name: Checkout wolfBoot master + uses: actions/checkout@v4 + with: + repository: wolfSSL/wolfBoot + path: wolfBoot + # Skip lib/wolfHSM submodule init - we replace it with this + # checkout. Other submodules still need to come down. + submodules: false + + - name: Initialise wolfBoot submodules (except lib/wolfHSM) + working-directory: wolfBoot + run: | + git config -f .gitmodules submodule.lib/wolfHSM.update none + git submodule update --init --recursive + + - name: Point lib/wolfHSM at this checkout + working-directory: wolfBoot + run: | + rm -rf lib/wolfHSM + ln -s "$GITHUB_WORKSPACE/wolfHSM" lib/wolfHSM + ls -l lib/wolfHSM + + - name: Build wolfBoot STM32H5 TZ demo + working-directory: wolfBoot/port/stmicro/stm32h5-tz-wolfhsm + run: make + + - name: Run wolfHSM whTest_ClientConfig under m33mu + working-directory: wolfBoot/port/stmicro/stm32h5-tz-wolfhsm + run: | + set -o pipefail + mkdir -p /tmp/m33mu-wolfhsm-persist + cp out/wolfboot.bin out/image_v1_signed.bin /tmp/m33mu-wolfhsm-persist/ + cd /tmp/m33mu-wolfhsm-persist + # m33mu timeout below the 60 min job timeout so a hung run + # still produces a log. Software RSA keygen dominates. + m33mu wolfboot.bin image_v1_signed.bin:0x60000 \ + --persist --uart-stdout --timeout 3000 \ + --expect-bkpt 0x7d --quit-on-faults \ + 2>&1 | tee /tmp/m33mu-wolfhsm.log + + - name: Verify wolfHSM whTest_ClientConfig + run: | + grep -q "wolfHSM whTest_ClientConfig PASSED" /tmp/m33mu-wolfhsm.log + grep -q "RNG DEVID=0x5748534D SUCCESS" /tmp/m33mu-wolfhsm.log + grep -q "AES GCM DEVID=0x5748534D SUCCESS" /tmp/m33mu-wolfhsm.log + grep -q "RSA SUCCESS" /tmp/m33mu-wolfhsm.log + grep -q "ECC ephemeral ECDH SUCCESS" /tmp/m33mu-wolfhsm.log + grep -q "SHA256 DEVID=0x5748534D SUCCESS" /tmp/m33mu-wolfhsm.log + grep -q "HKDF SUCCESS" /tmp/m33mu-wolfhsm.log + grep -q "\\[BKPT\\] imm=0x7d" /tmp/m33mu-wolfhsm.log + grep -q "\\[EXPECT BKPT\\] Success" /tmp/m33mu-wolfhsm.log + + - name: Archive emulator log on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: m33mu-wolfhsm.log + path: /tmp/m33mu-wolfhsm.log + + - name: Open or update tracking issue on failure + if: failure() + uses: actions/github-script@v7 + with: + script: | + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const marker = 'nightly-tz-integration'; + const title = 'Nightly wolfBoot TrustZone (m33mu) integration failed'; + const body = [ + 'The nightly wolfBoot STM32H5 TrustZone integration run failed.', + '', + `Run: ${runUrl}`, + '', + 'This builds wolfBoot main against wolfHSM main and runs the', + 'STM32H5 demo under m33mu. A failure usually means a regression', + 'in the NSC transport, the wolfBoot port, or a wolfHSM/wolfBoot', + 'API drift. The emulator log is attached to the run as an artifact.', + ].join('\n'); + + // Reuse one open tracking issue instead of opening a new one + // every night. + const existing = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: marker, + }); + + if (existing.data.length > 0) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.data[0].number, + body: `Still failing as of ${runUrl}`, + }); + return; + } + + // Best-effort label create (ignore if it already exists). + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: marker, + color: 'b60205', + }); + } catch (e) { /* label exists */ } + + try { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + labels: [marker], + assignees: ['wolfSSL-Bot'], + }); + } catch (e) { + // Assignee may not be assignable; retry without it so the + // issue still gets filed. + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + labels: [marker], + }); + } diff --git a/Makefile b/Makefile index f3a6bd010..0d558be4a 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ clean: # ---- SBOM generation ---- CC ?= cc WOLFSSL_DIR ?= ../wolfssl -VERSION = $(shell sed -n 's/^# wolfHSM Release v\([0-9][0-9.]*\).*/\1/p' ChangeLog.md | head -1) +VERSION = $(shell sed -n 's/^. wolfHSM Release v//p' ChangeLog.md | head -1 | cut -d' ' -f1) SRCS := $(sort $(wildcard src/*.c)) SBOM_CDX = wolfhsm-$(VERSION).cdx.json SBOM_SPDX = wolfhsm-$(VERSION).spdx.json diff --git a/port/armv8m-tz/README.md b/port/armv8m-tz/README.md new file mode 100644 index 000000000..040684282 --- /dev/null +++ b/port/armv8m-tz/README.md @@ -0,0 +1,44 @@ +# ARMv8-M TrustZone NSC bridge transport + +A synchronous wolfHSM transport for ARMv8-M TrustZone targets that +bridges a non-secure client to a secure-world server through a single +Non-Secure Callable (NSC) veneer. See `wh_transport_nsc.h` for the C +API. + +## How it works + +Client and server share a single secure-callable function. The +non-secure side packs a wolfHSM request into a buffer, calls the +veneer, and waits for the veneer to return with the response written +into a second buffer. The secure side runs the wolfHSM server in the +same process; there is no separate task, no IRQ, and no shared-memory +ring — just a function call across the security boundary. + +## Host-side veneer contract + +The integrator provides one function with this exact shape: + +```c +int wcs_wolfhsm_transmit(const uint8_t *cmd, uint32_t cmdSz, + uint8_t *rsp, uint32_t *rspSz); +``` + +declared `cmse_nonsecure_entry` from the secure side. On entry, +`cmd[0..cmdSz)` holds the request and `*rspSz` is the maximum response +size the client can accept; on return the function writes the +response into `rsp[0..*rspSz)` and updates `*rspSz` to the actual +size. Return value follows wolfHSM's `WH_ERROR_*` convention. + +The veneer must not block on anything outside the secure server; the +non-secure side treats it as a synchronous call. + +## Known integrations + +- **wolfBoot STM32H5 demo app** at + [`port/stmicro/stm32h5-tz-wolfhsm/`](https://github.com/wolfSSL/wolfBoot/tree/main/port/stmicro/stm32h5-tz-wolfhsm) + in wolfBoot. Reference integration on a NUCLEO-H563ZI; + verified end-to-end on real silicon and on the m33mu emulator. + +To add a new integration, copy the veneer skeleton from wolfBoot's +demo, wire `wh_transport_nsc.h` into the non-secure client init, and +write a `whServerCb` table for the secure server. diff --git a/port/armv8m-tz/wh_transport_nsc.c b/port/armv8m-tz/wh_transport_nsc.c new file mode 100644 index 000000000..9f9dd5a4e --- /dev/null +++ b/port/armv8m-tz/wh_transport_nsc.c @@ -0,0 +1,224 @@ +/* + * port/armv8m-tz/wh_transport_nsc.c + * + * Copyright (C) 2026 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 . + */ + +#include "wolfhsm/wh_settings.h" + +#ifdef WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC + +#include +#include + +#include "wolfhsm/wh_comm.h" +#include "wolfhsm/wh_error.h" +#include "wh_transport_nsc.h" + +/* + * Resolved on the non-secure side via the wolfBoot --cmse-implib import + * library; on the secure side the same symbol is provided by the host's + * NSC veneer (wolfBoot's src/wolfhsm_callable.c). The server callbacks + * below never call this; --gc-sections strips client-side code from the + * secure image. + */ +extern int wcs_wolfhsm_transmit(const uint8_t* cmd, uint32_t cmdSz, + uint8_t* rsp, uint32_t* rspSz); + + +/* ============================================================ + * Non-secure (client) callbacks + * ============================================================ */ + +int wh_TransportNsc_ClientInit(void* context, const void* config, + whCommSetConnectedCb connectcb, + void* connectcb_arg) +{ + whTransportNscClientContext* ctx = (whTransportNscClientContext*)context; + + (void)config; + + if (ctx == NULL) { + return WH_ERROR_BADARGS; + } + + memset(ctx, 0, sizeof(*ctx)); + ctx->initialized = 1; + + /* Synchronous bridge: the secure side is always reachable once linked. */ + if (connectcb != NULL) { + connectcb(connectcb_arg, WH_COMM_CONNECTED); + } + return WH_ERROR_OK; +} + +int wh_TransportNsc_ClientSend(void* context, uint16_t size, const void* data) +{ + whTransportNscClientContext* ctx = (whTransportNscClientContext*)context; + uint32_t rspSz; + int rc; + + if (ctx == NULL || data == NULL || ctx->initialized == 0U) { + return WH_ERROR_BADARGS; + } + if (size == 0U || size > WH_TRANSPORT_NSC_BUFFER_SIZE) { + return WH_ERROR_BADARGS; + } + /* prior response must be consumed before next Send */ + if (ctx->last_rsp_size != 0U) { + return WH_ERROR_NOTREADY; + } + + rspSz = (uint32_t)WH_TRANSPORT_NSC_BUFFER_SIZE; + rc = wcs_wolfhsm_transmit((const uint8_t*)data, (uint32_t)size, + ctx->rsp_buf, &rspSz); + if (rc != 0) { + ctx->last_rsp_size = 0; + /* propagate known wolfHSM error codes, collapse unknowns */ + if (rc == WH_ERROR_BADARGS || rc == WH_ERROR_NOTREADY || + rc == WH_ERROR_ABORTED) { + return rc; + } + return WH_ERROR_ABORTED; + } + if (rspSz == 0U || rspSz > (uint32_t)WH_TRANSPORT_NSC_BUFFER_SIZE) { + ctx->last_rsp_size = 0; + return WH_ERROR_ABORTED; + } + + ctx->last_rsp_size = (uint16_t)rspSz; + return WH_ERROR_OK; +} + +int wh_TransportNsc_ClientRecv(void* context, uint16_t* out_size, void* data) +{ + whTransportNscClientContext* ctx = (whTransportNscClientContext*)context; + + if (ctx == NULL || out_size == NULL || data == NULL || + ctx->initialized == 0U) { + return WH_ERROR_BADARGS; + } + if (ctx->last_rsp_size == 0U) { + return WH_ERROR_NOTREADY; + } + /* out_size is in/out capacity; reject truncation, keep cached response */ + if (*out_size < ctx->last_rsp_size) { + return WH_ERROR_BADARGS; + } + + memcpy(data, ctx->rsp_buf, ctx->last_rsp_size); + *out_size = ctx->last_rsp_size; + ctx->last_rsp_size = 0; + return WH_ERROR_OK; +} + +int wh_TransportNsc_ClientCleanup(void* context) +{ + whTransportNscClientContext* ctx = (whTransportNscClientContext*)context; + if (ctx == NULL) { + return WH_ERROR_BADARGS; + } + ctx->initialized = 0; + return WH_ERROR_OK; +} + +/* ============================================================ + * Secure-side (server) callbacks + * + * The host's NSC veneer populates req_buf/req_size/rsp_buf/rsp_capacity + * and sets request_pending = 1 before calling wh_Server_HandleRequestMessage. + * Recv hands the request to the dispatcher; Send writes the response back + * into rsp_buf and stores its size for the veneer to read. + * ============================================================ */ + +int wh_TransportNsc_ServerInit(void* context, const void* config, + whCommSetConnectedCb connectcb, + void* connectcb_arg) +{ + whTransportNscServerContext* ctx = (whTransportNscServerContext*)context; + + (void)config; + + if (ctx == NULL) { + return WH_ERROR_BADARGS; + } + + memset(ctx, 0, sizeof(*ctx)); + + if (connectcb != NULL) { + connectcb(connectcb_arg, WH_COMM_CONNECTED); + } + return WH_ERROR_OK; +} + +int wh_TransportNsc_ServerRecv(void* context, uint16_t* inout_size, void* data) +{ + whTransportNscServerContext* ctx = (whTransportNscServerContext*)context; + + if (ctx == NULL || inout_size == NULL || data == NULL) { + return WH_ERROR_BADARGS; + } + if (!ctx->request_pending || ctx->req_buf == NULL || ctx->req_size == 0U) { + return WH_ERROR_NOTREADY; + } + /* clear stale rsp_size up-front so every exit path leaves a clean state */ + ctx->rsp_size = 0; + + if (ctx->req_size > *inout_size) { + ctx->request_pending = 0; + return WH_ERROR_ABORTED; + } + + memcpy(data, ctx->req_buf, ctx->req_size); + *inout_size = ctx->req_size; + ctx->request_pending = 0; + return WH_ERROR_OK; +} + +int wh_TransportNsc_ServerSend(void* context, uint16_t size, const void* data) +{ + /* veneer is responsible for Recv/Send pairing; Send does not enforce it */ + whTransportNscServerContext* ctx = (whTransportNscServerContext*)context; + + if (ctx == NULL || data == NULL) { + return WH_ERROR_BADARGS; + } + if (size == 0U || size > ctx->rsp_capacity) { + return WH_ERROR_BADARGS; + } + if (ctx->rsp_buf == NULL) { + return WH_ERROR_ABORTED; + } + + memcpy(ctx->rsp_buf, data, size); + ctx->rsp_size = size; + return WH_ERROR_OK; +} + +int wh_TransportNsc_ServerCleanup(void* context) +{ + whTransportNscServerContext* ctx = (whTransportNscServerContext*)context; + if (ctx == NULL) { + return WH_ERROR_BADARGS; + } + /* clear stale NS pointers so they cannot survive reinit */ + memset(ctx, 0, sizeof(*ctx)); + return WH_ERROR_OK; +} + +#endif /* WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC */ diff --git a/port/armv8m-tz/wh_transport_nsc.h b/port/armv8m-tz/wh_transport_nsc.h new file mode 100644 index 000000000..7e5e702e6 --- /dev/null +++ b/port/armv8m-tz/wh_transport_nsc.h @@ -0,0 +1,117 @@ +/* + * port/armv8m-tz/wh_transport_nsc.h + * + * Copyright (C) 2026 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 . + */ + +/* + * Synchronous TrustZone NSC bridge transport for wolfHSM. + * + * The non-secure (client) side calls a single ARMv8-M Cortex-M + * cmse_nonsecure_entry veneer (`wcs_wolfhsm_transmit`) provided by the + * secure-side host. The veneer hands the request to the secure-side + * server context, runs `wh_Server_HandleRequestMessage` once inline, + * and returns the response in the same call. There is no polling, + * notify counter, or async producer/consumer — Send delivers the + * response, Recv just hands it back. + * + * The transport is target-agnostic across ARMv8-M TrustZone parts; + * the target-specific NSC veneer is provided by the host. + */ + +#ifndef WH_TRANSPORT_NSC_H_ +#define WH_TRANSPORT_NSC_H_ + +#include "wolfhsm/wh_settings.h" + +#ifdef WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC + +#include +#include "wolfhsm/wh_comm.h" + +#define WH_TRANSPORT_NSC_BUFFER_SIZE WH_COMM_MTU + +/* + * Non-secure (client) context. Owns the response buffer in NS .bss. + * Not internally thread-safe. + */ +typedef struct { + uint8_t rsp_buf[WH_TRANSPORT_NSC_BUFFER_SIZE]; + uint16_t last_rsp_size; + uint8_t initialized; + uint8_t WH_PAD[5]; /* trailing slack */ +} whTransportNscClientContext; + +/* Empty config; Init accepts NULL since there is nothing to read. */ +typedef struct { + uint8_t WH_PAD[1]; +} whTransportNscClientConfig; + +/* + * Secure-side server context. Populated by the NSC veneer per call: + * before invoking `wh_Server_HandleRequestMessage` the host sets + * req_buf/req_size/rsp_buf/rsp_capacity; after the dispatcher returns, + * the host reads rsp_size to pass back to the non-secure caller. + */ +typedef struct { + const uint8_t* req_buf; + uint8_t* rsp_buf; + uint16_t req_size; + uint16_t rsp_capacity; + uint16_t rsp_size; /* set by Send, read by veneer */ + uint8_t request_pending; /* set by veneer, cleared by Recv */ + uint8_t WH_PAD[1]; +} whTransportNscServerContext; + +typedef struct { + uint8_t WH_PAD[1]; +} whTransportNscServerConfig; + +int wh_TransportNsc_ClientInit(void* c, const void* cf, + whCommSetConnectedCb connectcb, + void* connectcb_arg); +int wh_TransportNsc_ClientSend(void* c, uint16_t len, const void* data); +int wh_TransportNsc_ClientRecv(void* c, uint16_t* out_len, void* data); +int wh_TransportNsc_ClientCleanup(void* c); + +int wh_TransportNsc_ServerInit(void* c, const void* cf, + whCommSetConnectedCb connectcb, + void* connectcb_arg); +int wh_TransportNsc_ServerRecv(void* c, uint16_t* out_len, void* data); +int wh_TransportNsc_ServerSend(void* c, uint16_t len, const void* data); +int wh_TransportNsc_ServerCleanup(void* c); + +#define WH_TRANSPORT_NSC_CLIENT_CB \ + { \ + .Init = wh_TransportNsc_ClientInit, \ + .Send = wh_TransportNsc_ClientSend, \ + .Recv = wh_TransportNsc_ClientRecv, \ + .Cleanup = wh_TransportNsc_ClientCleanup, \ + } + +#define WH_TRANSPORT_NSC_SERVER_CB \ + { \ + .Init = wh_TransportNsc_ServerInit, \ + .Recv = wh_TransportNsc_ServerRecv, \ + .Send = wh_TransportNsc_ServerSend, \ + .Cleanup = wh_TransportNsc_ServerCleanup, \ + } + +#endif /* WOLFHSM_CFG_PORT_ARMV8M_TZ_NSC */ + +#endif /* WH_TRANSPORT_NSC_H_ */ diff --git a/port/stmicro/stm32h5-tz/README.md b/port/stmicro/stm32h5-tz/README.md new file mode 100644 index 000000000..7c581b255 --- /dev/null +++ b/port/stmicro/stm32h5-tz/README.md @@ -0,0 +1,12 @@ +# STM32H5 TrustZone port + +The STM32H5 wolfHSM TrustZone port (HAL adapter, secure-side server, +flash/NVM backend, non-secure demo app, linker layout, and test +harness) is maintained in +[wolfBoot](https://github.com/wolfSSL/wolfBoot) under +`port/stmicro/stm32h5-tz-wolfhsm/` and `test-app/wcs/`. + +It consumes the generic ARMv8-M TrustZone NSC bridge transport in +`port/armv8m-tz/` of this repository. See +[`port/armv8m-tz/README.md`](../../armv8m-tz/README.md) for the +transport description. diff --git a/src/wh_hwkeystore.c b/src/wh_hwkeystore.c new file mode 100644 index 000000000..69f1b04ae --- /dev/null +++ b/src/wh_hwkeystore.c @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * src/wh_hwkeystore.c + * + * Hardware keystore front-end implementation. Dispatches to a backend callback + * table (whHwKeystoreCb) with argument validation and lock serialization. See + * wolfhsm/wh_hwkeystore.h for the module description. + */ + +/* Pick up compile-time configuration */ +#include "wolfhsm/wh_settings.h" + +#ifdef WOLFHSM_CFG_HWKEYSTORE + +#include +#include +#include + +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_hwkeystore.h" + +int wh_HwKeystore_Init(whHwKeystoreContext* context, + const whHwKeystoreConfig* config) +{ + if ((context == NULL) || (config == NULL) || (config->cb == NULL) || + (config->cb->GetKey == NULL)) { + return WH_ERROR_BADARGS; + } + + memset(context, 0, sizeof(*context)); + context->cb = config->cb; + context->context = config->context; + + /* Initialize the lock before the backend so that, on a backend Init + * failure, teardown is a simple lock cleanup with no backend state to + * undo. This keeps init and cleanup symmetric: wh_HwKeystore_Cleanup tears + * down the backend then the lock, the reverse of the order here */ +#ifdef WOLFHSM_CFG_THREADSAFE + { + int rc = wh_Lock_Init(&context->lock, config->lockConfig); + if (rc != WH_ERROR_OK) { + memset(context, 0, sizeof(*context)); + return rc; + } + } +#endif /* WOLFHSM_CFG_THREADSAFE */ + + /* Initialize the backend if it provides an Init callback. Done last so it + * is the only fallible step after the lock: on failure, undo the lock so a + * successful backend Init is never left without a paired Cleanup */ + if (context->cb->Init != NULL) { + int rc = context->cb->Init(context->context, config->config); + if (rc != WH_ERROR_OK) { +#ifdef WOLFHSM_CFG_THREADSAFE + (void)wh_Lock_Cleanup(&context->lock); +#endif /* WOLFHSM_CFG_THREADSAFE */ + memset(context, 0, sizeof(*context)); + return rc; + } + } + + context->initialized = 1; + return WH_ERROR_OK; +} + +int wh_HwKeystore_Cleanup(whHwKeystoreContext* context) +{ + int rc = WH_ERROR_OK; + + if (context == NULL) { + return WH_ERROR_BADARGS; + } + + /* Capture the backend teardown result but always finish releasing the lock + * and zeroing the context, then surface the backend error. */ + if ((context->cb != NULL) && (context->cb->Cleanup != NULL)) { + rc = context->cb->Cleanup(context->context); + } + +#ifdef WOLFHSM_CFG_THREADSAFE + { + /* Only let a lock-release failure surface when the backend cleanup + * itself succeeded, so a real backend error is not hidden */ + int relRc = wh_Lock_Cleanup(&context->lock); + if (rc == WH_ERROR_OK) { + rc = relRc; + } + } +#endif /* WOLFHSM_CFG_THREADSAFE */ + + memset(context, 0, sizeof(*context)); + return rc; +} + +int wh_HwKeystore_GetKey(whHwKeystoreContext* context, whKeyId keyId, + uint8_t* out, uint16_t* inout_len) +{ + int rc; + + if ((context == NULL) || (context->initialized == 0) || + (context->cb == NULL) || (context->cb->GetKey == NULL) || + (out == NULL) || (inout_len == NULL)) { + return WH_ERROR_BADARGS; + } + +#ifdef WOLFHSM_CFG_THREADSAFE + rc = wh_Lock_Acquire(&context->lock); + if (rc != WH_ERROR_OK) { + return rc; + } +#endif /* WOLFHSM_CFG_THREADSAFE */ + + rc = context->cb->GetKey(context->context, keyId, out, inout_len); + +#ifdef WOLFHSM_CFG_THREADSAFE + { + /* Preserve the backend result; only surface a release failure when the + * GetKey call itself succeeded so a real lock error is not hidden */ + int relRc = wh_Lock_Release(&context->lock); + if (rc == WH_ERROR_OK) { + rc = relRc; + } + } +#endif /* WOLFHSM_CFG_THREADSAFE */ + + return rc; +} + +#endif /* WOLFHSM_CFG_HWKEYSTORE */ diff --git a/test-refactor/client-server/wh_test_auth.c b/test-refactor/client-server/wh_test_auth.c new file mode 100644 index 000000000..34eb55dac --- /dev/null +++ b/test-refactor/client-server/wh_test_auth.c @@ -0,0 +1,1121 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/client-server/wh_test_auth.c + * + * Client-side authentication tests. The POSIX server is configured + * with an auth context plus an admin user, and the client logs in as + * admin at connect (see posix/wh_test_posix_server.c and + * wh_test_posix_client.c). Each test here changes the login session to + * exercise a behavior, then restores the admin baseline so the next + * client test stays authorized (the auth gate denies un-logged-in + * requests). The bracketing is done once in _whTest_Auth_RunBracketed. + */ + +#include "wolfhsm/wh_settings.h" + +#if defined(WOLFHSM_CFG_ENABLE_AUTHENTICATION) && \ + defined(WOLFHSM_CFG_ENABLE_CLIENT) && defined(WOLFHSM_CFG_ENABLE_SERVER) + +#include +#include + +#include "wolfhsm/wh_common.h" +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_client.h" +#include "wolfhsm/wh_auth.h" +#include "wolfhsm/wh_message.h" +#include "wolfhsm/wh_message_auth.h" + +#include "wh_test_common.h" + +#ifndef TEST_ADMIN_USERNAME +#define TEST_ADMIN_USERNAME "admin" +#endif +#ifndef TEST_ADMIN_PIN +#define TEST_ADMIN_PIN "1234" +#endif + + +/* ============================================================================ + * Client op helpers (blocking request/response against the running server) + * ============================================================================ + */ + +static int _whTest_Auth_LoginOp(whClientContext* client, whAuthMethod method, + const char* username, const void* auth_data, + uint16_t auth_data_len, int32_t* out_rc, + whUserId* out_user_id) +{ + return wh_Client_AuthLogin(client, method, username, auth_data, + auth_data_len, out_rc, out_user_id); +} + +static int _whTest_Auth_LogoutOp(whClientContext* client, whUserId user_id, + int32_t* out_rc) +{ + return wh_Client_AuthLogout(client, user_id, out_rc); +} + +static int _whTest_Auth_UserAddOp(whClientContext* client, const char* username, + whAuthPermissions permissions, + whAuthMethod method, const void* credentials, + uint16_t credentials_len, int32_t* out_rc, + whUserId* out_user_id) +{ + return wh_Client_AuthUserAdd(client, username, permissions, method, + credentials, credentials_len, out_rc, + out_user_id); +} + +static int _whTest_Auth_UserDeleteOp(whClientContext* client, whUserId user_id, + int32_t* out_rc) +{ + return wh_Client_AuthUserDelete(client, user_id, out_rc); +} + +static int _whTest_Auth_UserSetPermsOp(whClientContext* client, + whUserId user_id, + whAuthPermissions permissions, + int32_t* out_rc) +{ + return wh_Client_AuthUserSetPermissions(client, user_id, permissions, + out_rc); +} + +static int _whTest_Auth_UserSetCredsOp( + whClientContext* client, whUserId user_id, whAuthMethod method, + const void* current_credentials, uint16_t current_credentials_len, + const void* new_credentials, uint16_t new_credentials_len, int32_t* out_rc) +{ + return wh_Client_AuthUserSetCredentials( + client, user_id, method, current_credentials, current_credentials_len, + new_credentials, new_credentials_len, out_rc); +} + +static int _whTest_Auth_UserGetOp(whClientContext* client, const char* username, + int32_t* out_rc, whUserId* out_user_id, + whAuthPermissions* out_permissions) +{ + return wh_Client_AuthUserGet(client, username, out_rc, out_user_id, + out_permissions); +} + +static void _whTest_Auth_DeleteUserByName(whClientContext* client, + const char* username) +{ + int32_t server_rc = 0; + whUserId user_id = WH_USER_ID_INVALID; + whAuthPermissions perms; + + memset(&perms, 0, sizeof(perms)); + _whTest_Auth_UserGetOp(client, username, &server_rc, &user_id, &perms); + if (server_rc == WH_ERROR_OK && user_id != WH_USER_ID_INVALID) { + _whTest_Auth_UserDeleteOp(client, user_id, &server_rc); + } +} + + +/* ============================================================================ + * Session bracketing + * ============================================================================ + */ + +typedef int (*whTestAuthImplFn)(whClientContext*); + +/* Admin's server-assigned user id, needed to log the baseline session + * out (logout requires the real id, not WH_USER_ID_INVALID). Discovered + * once from the live admin session, which is the baseline at bracket + * entry. */ +static whUserId _adminUserId = WH_USER_ID_INVALID; + +/* + * Run an auth test body with the session bracketed: start from a clean + * logged-out state (the bodies assume logged-out, but the connect-time + * baseline is the admin session), and on return restore the admin + * baseline so the following client tests stay authorized. + */ +static int _whTest_Auth_RunBracketed(whClientContext* client, + whTestAuthImplFn fn) +{ + int ret; + int32_t rc = 0; + whUserId uid = WH_USER_ID_INVALID; + + if (client == NULL) { + return WH_ERROR_BADARGS; + } + + /* Discover admin's id once (admin is logged in at entry). */ + if (_adminUserId == WH_USER_ID_INVALID) { + whAuthPermissions perms; + memset(&perms, 0, sizeof(perms)); + (void)wh_Client_AuthUserGet(client, TEST_ADMIN_USERNAME, &rc, + &_adminUserId, &perms); + } + /* Drop the admin baseline so the body starts logged-out. */ + if (_adminUserId != WH_USER_ID_INVALID) { + (void)wh_Client_AuthLogout(client, _adminUserId, &rc); + } + + ret = fn(client); + + /* Restore the admin baseline for the next test. */ + (void)wh_Client_AuthLogin(client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, + TEST_ADMIN_PIN, (uint16_t)strlen(TEST_ADMIN_PIN), + &rc, &uid); + if (rc == WH_ERROR_OK && uid != WH_USER_ID_INVALID) { + _adminUserId = uid; + } + return ret; +} + + +/* ============================================================================ + * Test bodies (assume a logged-out start; bracketed by the wrappers below) + * ============================================================================ + */ + +/* Logout Tests */ +static int _whTest_AuthLogout_impl(whClientContext* client) +{ + int32_t server_rc; + whUserId user_id; + int32_t login_rc; + whAuthPermissions out_perms; + + /* Test 2: Logout after login */ + WH_TEST_PRINT(" Test: Logout after login\n"); + /* First login */ + memset(&out_perms, 0, sizeof(out_perms)); + login_rc = 0; + user_id = WH_USER_ID_INVALID; + WH_TEST_ASSERT_RETURN(client->comm != NULL); + WH_TEST_ASSERT_RETURN(client->comm->initialized == 1); + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, + TEST_ADMIN_PIN, 4, &login_rc, &user_id)); + WH_TEST_ASSERT_RETURN(login_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id != WH_USER_ID_INVALID); + + /* Then logout */ + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LogoutOp(client, user_id, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + WH_TEST_PRINT(" Test: Logout before login\n"); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LogoutOp(client, user_id, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + /* Test 3: Logout with invalid user id */ + WH_TEST_PRINT( + " Test: Logout attempt with invalid user ID (should fail)\n"); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_LogoutOp(client, WH_USER_ID_INVALID, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + return WH_TEST_SUCCESS; +} + +/* Login Tests */ +static int _whTest_AuthLogin_impl(whClientContext* client) +{ + int32_t server_rc; + whUserId user_id; + + /* Test 1: Login with invalid credentials */ + WH_TEST_PRINT(" Test: Login with invalid credentials\n"); + server_rc = 0; + user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, + TEST_ADMIN_USERNAME, "wrong", 5, + &server_rc, &user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_AUTH_LOGIN_FAILED || + server_rc != WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id == WH_USER_ID_INVALID); + + /* Test 2: Login with valid credentials */ + WH_TEST_PRINT(" Test: Login with valid credentials\n"); + server_rc = 0; + user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, + TEST_ADMIN_PIN, 4, &server_rc, &user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id != WH_USER_ID_INVALID); + + /* Logout for next test */ + _whTest_Auth_LogoutOp(client, user_id, &server_rc); + + /* Test 3: Login with invalid username */ + WH_TEST_PRINT(" Test: Login with invalid username\n"); + server_rc = 0; + user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, + "nonexistent", TEST_ADMIN_PIN, + 4, &server_rc, &user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_AUTH_LOGIN_FAILED || + server_rc != WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id == WH_USER_ID_INVALID); + + /* Test 4: Login if already logged in */ + WH_TEST_PRINT(" Test: Login if already logged in\n"); + server_rc = 0; + user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, + TEST_ADMIN_PIN, 4, &server_rc, &user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id != WH_USER_ID_INVALID); + + /* Try to login again without logout */ + server_rc = 0; + whUserId user_id2 = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, + TEST_ADMIN_PIN, 4, &server_rc, &user_id2)); + WH_TEST_ASSERT_RETURN(server_rc == WH_AUTH_LOGIN_FAILED || + server_rc != WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id2 == WH_USER_ID_INVALID); + + /* Cleanup */ + _whTest_Auth_LogoutOp(client, user_id, &server_rc); + + return WH_TEST_SUCCESS; +} + +/* Add User Tests */ +static int _whTest_AuthAddUser_impl(whClientContext* client) +{ + int32_t server_rc; + whUserId user_id; + whAuthPermissions perms; + char long_username[34]; /* 33 chars + null terminator */ + int rc; + + /* Login as admin first */ + whAuthPermissions admin_perms; + memset(&admin_perms, 0, sizeof(admin_perms)); + server_rc = 0; + whUserId admin_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, + TEST_ADMIN_PIN, 4, &server_rc, &admin_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Test 1: Add user with invalid username (too long) */ + WH_TEST_PRINT(" Test: Add user with invalid username (too long)\n"); + memset(long_username, 'a', 33); + long_username[33] = '\0'; + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + user_id = WH_USER_ID_INVALID; + + /* Expect client-side rejection due to username length */ + rc = wh_Client_AuthUserAddRequest(client, long_username, perms, + WH_AUTH_METHOD_PIN, "test", 4); + WH_TEST_ASSERT_RETURN(rc != WH_ERROR_OK || server_rc != WH_ERROR_OK || + user_id == WH_USER_ID_INVALID); + + /* Test 2: Add user with invalid permissions (keyIdCount > max) */ + WH_TEST_PRINT(" Test: Add user with invalid permissions\n"); + memset(&perms, 0, sizeof(perms)); + perms.keyIdCount = WH_AUTH_MAX_KEY_IDS + 1; /* Invalid: exceeds max */ + server_rc = 0; + user_id = WH_USER_ID_INVALID; + _whTest_Auth_UserAddOp(client, "testuser1", perms, WH_AUTH_METHOD_PIN, + "test", 4, &server_rc, &user_id); + /* Should clamp or reject invalid keyIdCount */ + if (server_rc == WH_ERROR_OK) { + /* If it succeeds, keyIdCount should be clamped */ + WH_TEST_ASSERT_RETURN(user_id != WH_USER_ID_INVALID); + } + + /* Test 3: Add user if already exists */ + WH_TEST_PRINT(" Test: Add user if already exists\n"); + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + user_id = WH_USER_ID_INVALID; + _whTest_Auth_UserAddOp(client, "testuser2", perms, WH_AUTH_METHOD_PIN, + "test", 4, &server_rc, &user_id); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id != WH_USER_ID_INVALID); + + /* Try to add same user again - should fail duplicate username */ + whUserId user_id2 = WH_USER_ID_INVALID; + server_rc = 0; + _whTest_Auth_UserAddOp(client, "testuser2", perms, WH_AUTH_METHOD_PIN, + "test", 4, &server_rc, &user_id2); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id2 == WH_USER_ID_INVALID); + + /* Test 4: Non-admin cannot add admin user */ + WH_TEST_PRINT(" Test: Non-admin cannot add admin user\n"); + { + whAuthPermissions nonadmin_add_perms; + + memset(&nonadmin_add_perms, 0, sizeof(nonadmin_add_perms)); + WH_AUTH_SET_ALLOWED_ACTION(nonadmin_add_perms, WH_MESSAGE_GROUP_AUTH, + WH_MESSAGE_AUTH_ACTION_USER_ADD); + WH_AUTH_SET_IS_ADMIN(nonadmin_add_perms, 0); + + server_rc = 0; + user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp( + client, "addadmin_testuser", nonadmin_add_perms, WH_AUTH_METHOD_PIN, + "pass", 4, &server_rc, &user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id != WH_USER_ID_INVALID); + + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, + "addadmin_testuser", "pass", + 4, &server_rc, &user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Non-admin can add other non-admin users */ + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + user_id2 = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp( + client, "other_nonadmin", perms, WH_AUTH_METHOD_PIN, "test", 4, + &server_rc, &user_id2)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id2 != WH_USER_ID_INVALID); + + /* Non-admin cannot add admin user */ + memset(&perms, 0xFF, sizeof(perms)); + perms.keyIdCount = 0; + server_rc = 0; + user_id2 = WH_USER_ID_INVALID; + _whTest_Auth_UserAddOp(client, "wouldbe_admin", perms, + WH_AUTH_METHOD_PIN, "test", 4, &server_rc, + &user_id2); + WH_TEST_ASSERT_RETURN(server_rc == WH_AUTH_PERMISSION_ERROR); + WH_TEST_ASSERT_RETURN(user_id2 == WH_USER_ID_INVALID); + } + + _whTest_Auth_LogoutOp(client, user_id, &server_rc); + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, + TEST_ADMIN_PIN, 4, &server_rc, &admin_id)); + + /* Cleanup */ + server_rc = 0; + _whTest_Auth_DeleteUserByName(client, "testuser1"); + _whTest_Auth_DeleteUserByName(client, "testuser2"); + _whTest_Auth_DeleteUserByName(client, "addadmin_testuser"); + _whTest_Auth_DeleteUserByName(client, "other_nonadmin"); + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + + return WH_TEST_SUCCESS; +} + +/* Delete User Tests */ +static int _whTest_AuthDeleteUser_impl(whClientContext* client) +{ + int32_t server_rc; + whAuthPermissions admin_perms; + whUserId admin_id = WH_USER_ID_INVALID; + whAuthPermissions perms; + whAuthPermissions out_perms; + whUserId delete_user_id = WH_USER_ID_INVALID; + + /* Login as admin to perform delete operations */ + memset(&admin_perms, 0, sizeof(admin_perms)); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp( + client, WH_AUTH_METHOD_PIN, "admin", "1234", 4, &server_rc, &admin_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Test 1: Delete user with invalid user id */ + WH_TEST_PRINT(" Test: Delete user with invalid user ID\n"); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_UserDeleteOp(client, WH_USER_ID_INVALID, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + /* Test 2: Delete user that does not exist */ + WH_TEST_PRINT(" Test: Delete user that does not exist\n"); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserDeleteOp(client, 999, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_NOTFOUND || + server_rc != WH_ERROR_OK); + + /* Test 2b: Delete existing user (success path) */ + WH_TEST_PRINT(" Test: Delete existing user\n"); + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp(client, "deleteuser", perms, + WH_AUTH_METHOD_PIN, "pass", 4, + &server_rc, &delete_user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(delete_user_id != WH_USER_ID_INVALID); + + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserGetOp( + client, "deleteuser", &server_rc, &delete_user_id, &out_perms)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + server_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_UserDeleteOp(client, delete_user_id, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + server_rc = 0; + delete_user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserGetOp( + client, "deleteuser", &server_rc, &delete_user_id, &out_perms)); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK || + delete_user_id == WH_USER_ID_INVALID); + + /* Test 3: Non-admin user trying to delete another user */ + WH_TEST_PRINT(" Test: Non-admin user trying to delete another user\n"); + { + whUserId nonadmin_id = WH_USER_ID_INVALID; + whUserId target_id = WH_USER_ID_INVALID; + whAuthPermissions nonadmin_perms; + + /* non-admin user with all auth group actions (includes delete) */ + memset(&nonadmin_perms, 0, sizeof(nonadmin_perms)); + WH_AUTH_SET_ALLOWED_GROUP(nonadmin_perms, WH_MESSAGE_GROUP_AUTH); + WH_AUTH_SET_IS_ADMIN(nonadmin_perms, 0); + + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp( + client, "nonadmin", nonadmin_perms, WH_AUTH_METHOD_PIN, "pass", 4, + &server_rc, &nonadmin_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(nonadmin_id != WH_USER_ID_INVALID); + + /* Create a target user to try to delete */ + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp( + client, "targetuser", perms, WH_AUTH_METHOD_PIN, "pass", 4, + &server_rc, &target_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(target_id != WH_USER_ID_INVALID); + + /* Logout admin and login as non-admin user */ + server_rc = 0; + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, + "nonadmin", "pass", 4, + &server_rc, &nonadmin_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Try to delete the target user as non-admin - should fail */ + server_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_UserDeleteOp(client, target_id, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + WH_TEST_PRINT(" Non-admin delete attempt correctly denied\n"); + + /* Logout non-admin and login as admin to cleanup */ + server_rc = 0; + _whTest_Auth_LogoutOp(client, nonadmin_id, &server_rc); + + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, + "admin", "1234", 4, + &server_rc, &admin_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Cleanup - delete both test users */ + _whTest_Auth_UserDeleteOp(client, nonadmin_id, &server_rc); + _whTest_Auth_UserDeleteOp(client, target_id, &server_rc); + } + + /* Test 4: Delete user when not logged in */ + WH_TEST_PRINT(" Test: Delete user when not logged in\n"); + /* Ensure we're logged out */ + server_rc = 0; + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + + /* Try to delete without being logged in */ + server_rc = 0; + _whTest_Auth_UserDeleteOp(client, 1, &server_rc); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + return WH_TEST_SUCCESS; +} + +/* Set User Permissions Tests */ +static int _whTest_AuthSetPermissions_impl(whClientContext* client) +{ + int32_t server_rc; + whUserId user_id; + whAuthPermissions perms, new_perms; + whAuthPermissions fetched_perms; + whUserId fetched_user_id = WH_USER_ID_INVALID; + int32_t get_rc = 0; + + /* Login as admin first */ + whAuthPermissions admin_perms; + memset(&admin_perms, 0, sizeof(admin_perms)); + server_rc = 0; + whUserId admin_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp( + client, WH_AUTH_METHOD_PIN, "admin", "1234", 4, &server_rc, &admin_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Create a test user first */ + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp(client, "testuser3", perms, + WH_AUTH_METHOD_PIN, "test", 4, + &server_rc, &user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id != WH_USER_ID_INVALID); + + /* Test 1: Set user permissions with invalid user id */ + WH_TEST_PRINT(" Test: Set user permissions with invalid user ID\n"); + memset(&new_perms, 0xFF, sizeof(new_perms)); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserSetPermsOp( + client, WH_USER_ID_INVALID, new_perms, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + /* Test 2: Set user permissions with invalid permissions */ + WH_TEST_PRINT(" Test: Set user permissions with invalid permissions\n"); + memset(&new_perms, 0, sizeof(new_perms)); + new_perms.keyIdCount = WH_AUTH_MAX_KEY_IDS + 1; /* Invalid */ + server_rc = 0; + _whTest_Auth_UserSetPermsOp(client, user_id, new_perms, &server_rc); + /* Should clamp or reject invalid keyIdCount */ + if (server_rc == WH_ERROR_OK) { + /* If it succeeds, keyIdCount should be clamped */ + } + + /* Test 2b: Set user permissions success path */ + WH_TEST_PRINT(" Test: Set user permissions success\n"); + memset(&new_perms, 0, sizeof(new_perms)); + WH_AUTH_SET_ALLOWED_ACTION(new_perms, WH_MESSAGE_GROUP_AUTH, + WH_MESSAGE_AUTH_ACTION_USER_ADD); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_UserSetPermsOp(client, user_id, new_perms, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + memset(&fetched_perms, 0, sizeof(fetched_perms)); + fetched_user_id = WH_USER_ID_INVALID; + get_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserGetOp( + client, "testuser3", &get_rc, &fetched_user_id, &fetched_perms)); + WH_TEST_ASSERT_RETURN(get_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(fetched_user_id == user_id); + { + /* Compare group permission and all action permission words */ + int groupIndex = (WH_MESSAGE_GROUP_AUTH >> 8) & 0xFF; + int j; + int permissions_match = 1; + WH_TEST_ASSERT_RETURN(fetched_perms.groupPermissions[groupIndex] == + new_perms.groupPermissions[groupIndex]); + for (j = 0; j < WH_AUTH_ACTION_WORDS; j++) { + if (fetched_perms.actionPermissions[groupIndex][j] != + new_perms.actionPermissions[groupIndex][j]) { + permissions_match = 0; + break; + } + } + WH_TEST_ASSERT_RETURN(permissions_match); + } + + /* Test 3: Set user permissions for non-existent user */ + WH_TEST_PRINT(" Test: Set user permissions for non-existent user\n"); + memset(&new_perms, 0, sizeof(new_perms)); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_UserSetPermsOp(client, 999, new_perms, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_NOTFOUND || + server_rc != WH_ERROR_OK); + + /* Test 4: Set user permissions when not logged in */ + WH_TEST_PRINT(" Test: Set user permissions when not logged in\n"); + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + + memset(&new_perms, 0, sizeof(new_perms)); + server_rc = 0; + _whTest_Auth_UserSetPermsOp(client, user_id, new_perms, &server_rc); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + /* Cleanup */ + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp( + client, WH_AUTH_METHOD_PIN, "admin", "1234", 4, &server_rc, &admin_id)); + _whTest_Auth_DeleteUserByName(client, "testuser3"); + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + + return WH_TEST_SUCCESS; +} + +/* Set User Credentials Tests */ +static int _whTest_AuthSetCredentials_impl(whClientContext* client) +{ + int32_t server_rc; + whUserId user_id; + whAuthPermissions perms; + + /* Login as admin first */ + whAuthPermissions admin_perms; + memset(&admin_perms, 0, sizeof(admin_perms)); + server_rc = 0; + whUserId admin_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp( + client, WH_AUTH_METHOD_PIN, "admin", "1234", 4, &server_rc, &admin_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Create a test user first */ + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp(client, "testuser4", perms, + WH_AUTH_METHOD_PIN, "test", 4, + &server_rc, &user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id != WH_USER_ID_INVALID); + + /* Test 1: Set user credentials with invalid user id */ + WH_TEST_PRINT(" Test: Set user credentials with invalid user ID\n"); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserSetCredsOp( + client, WH_USER_ID_INVALID, WH_AUTH_METHOD_PIN, "test", 4, "newpass", 7, + &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + /* Test 2: Set user credentials with invalid method */ + WH_TEST_PRINT(" Test: Set user credentials with invalid method\n"); + server_rc = 0; + _whTest_Auth_UserSetCredsOp(client, user_id, WH_AUTH_METHOD_NONE, "test", 4, + "newpass", 7, &server_rc); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + /* Test 3: Set user credentials for non-existent user */ + WH_TEST_PRINT(" Test: Set user credentials for non-existent user\n"); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserSetCredsOp( + client, 999, WH_AUTH_METHOD_PIN, NULL, 0, "newpass", 7, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_NOTFOUND || + server_rc != WH_ERROR_OK); + + WH_TEST_PRINT(" Test: Admin setting credentials for non-admin user\n"); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_UserSetCredsOp(client, user_id, WH_AUTH_METHOD_PIN, "test", + 4, "newpass", 7, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Verify new credentials work */ + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + memset(&admin_perms, 0, sizeof(admin_perms)); + server_rc = 0; + whUserId test_user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, + "testuser4", "newpass", 7, + &server_rc, &test_user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(test_user_id == user_id); + + /* Cleanup: remove the test user (logged in as admin so the delete + * is authorized). */ + _whTest_Auth_LogoutOp(client, test_user_id, &server_rc); + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp( + client, WH_AUTH_METHOD_PIN, "admin", "1234", 4, &server_rc, &admin_id)); + _whTest_Auth_DeleteUserByName(client, "testuser4"); + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + + return WH_TEST_SUCCESS; +} + +/* Authorization Checks Tests */ +static int _whTest_AuthRequestAuthorization_impl(whClientContext* client) +{ + int32_t server_rc; + whUserId user_id; + whUserId temp_id3 = WH_USER_ID_INVALID; + whAuthPermissions perms; + + /* Test 1: Operation when not logged in and not allowed */ + WH_TEST_PRINT(" Test: Operation when not logged in and not allowed\n"); + /* Ensure logged out */ + server_rc = 0; + _whTest_Auth_LogoutOp(client, WH_USER_ID_INVALID, &server_rc); + + /* Try an operation that requires auth (e.g., add user) */ + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + whUserId temp_id = WH_USER_ID_INVALID; + _whTest_Auth_UserAddOp(client, "testuser5", perms, WH_AUTH_METHOD_PIN, + "test", 4, &server_rc, &temp_id); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + /* Test 2: Operation when logged in and allowed */ + WH_TEST_PRINT(" Test: Operation when logged in and allowed\n"); + /* Login as admin */ + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + whUserId admin_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp( + client, WH_AUTH_METHOD_PIN, "admin", "1234", 4, &server_rc, &admin_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Retry operation after login (admin should be allowed) */ + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp(client, "testuser6", perms, + WH_AUTH_METHOD_PIN, "test", 4, + &server_rc, &user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(user_id != WH_USER_ID_INVALID); + + /* Test 3: Operation when logged in and not allowed */ + WH_TEST_PRINT(" Test: Operation when logged in and not allowed\n"); + /* Create a user with auth group but not USER_ADD action */ + memset(&perms, 0, sizeof(perms)); + WH_AUTH_SET_ALLOWED_GROUP(perms, WH_MESSAGE_GROUP_AUTH); + WH_AUTH_CLEAR_ALLOWED_ACTION(perms, WH_MESSAGE_GROUP_AUTH, + WH_MESSAGE_AUTH_ACTION_USER_ADD); + server_rc = 0; + whUserId limited_user_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_UserAddOp(client, "limiteduser", perms, WH_AUTH_METHOD_PIN, + "pass", 4, &server_rc, &limited_user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Logout admin and login as limited user */ + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + whUserId logged_in_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, + "limiteduser", "pass", 4, + &server_rc, &logged_in_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Try an operation that requires permissions */ + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + whUserId temp_id2 = WH_USER_ID_INVALID; + _whTest_Auth_UserAddOp(client, "testuser7", perms, WH_AUTH_METHOD_PIN, + "test", 4, &server_rc, &temp_id2); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + /* Test 3b: User with auth group cleared cannot add + * (WH_AUTH_CLEAR_ALLOWED_GROUP) */ + WH_TEST_PRINT(" Test: User with no auth group cannot add\n"); + _whTest_Auth_LogoutOp(client, logged_in_id, &server_rc); + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp( + client, WH_AUTH_METHOD_PIN, "admin", "1234", 4, &server_rc, &admin_id)); + memset(&perms, 0xFF, sizeof(perms)); + perms.keyIdCount = 0; + WH_AUTH_CLEAR_ALLOWED_GROUP(perms, WH_MESSAGE_GROUP_AUTH); + WH_AUTH_SET_IS_ADMIN(perms, 0); + server_rc = 0; + whUserId noauth_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_UserAddOp(client, "noauthuser", perms, + WH_AUTH_METHOD_PIN, "pass", 4, + &server_rc, &noauth_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, + "noauthuser", "pass", 4, + &server_rc, &logged_in_id)); + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + temp_id2 = WH_USER_ID_INVALID; + _whTest_Auth_UserAddOp(client, "testuser7b", perms, WH_AUTH_METHOD_PIN, + "test", 4, &server_rc, &temp_id2); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + _whTest_Auth_LogoutOp(client, logged_in_id, &server_rc); + + /* Test 4: Logged in as different user and allowed */ + WH_TEST_PRINT(" Test: Logged in as different user and allowed\n"); + _whTest_Auth_LogoutOp(client, logged_in_id, &server_rc); + + server_rc = 0; + whUserId allowed_user_id = WH_USER_ID_INVALID; + /* Login as admin to create allowed user */ + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp( + client, WH_AUTH_METHOD_PIN, "admin", "1234", 4, &server_rc, &admin_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + memset(&perms, 0, sizeof(perms)); + WH_AUTH_SET_ALLOWED_ACTION(perms, WH_MESSAGE_GROUP_AUTH, + WH_MESSAGE_AUTH_ACTION_USER_ADD); + /* Free a slot so the adds below stay within WH_AUTH_BASE_MAX_USERS (5) */ + _whTest_Auth_DeleteUserByName(client, "noauthuser"); + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_UserAddOp(client, "alloweduser", perms, WH_AUTH_METHOD_PIN, + "pass", 4, &server_rc, &allowed_user_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + logged_in_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, + "alloweduser", "pass", 4, + &server_rc, &logged_in_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + server_rc = 0; + temp_id3 = WH_USER_ID_INVALID; + _whTest_Auth_UserAddOp(client, "testuser8", perms, WH_AUTH_METHOD_PIN, + "test", 4, &server_rc, &temp_id3); + /* alloweduser holds the USER_ADD action, so the add is authorized */ + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(temp_id3 != WH_USER_ID_INVALID); + + /* Test 5: Logged in as different user and not allowed */ + WH_TEST_PRINT(" Test: Logged in as different user and not allowed\n"); + _whTest_Auth_LogoutOp(client, logged_in_id, &server_rc); + + memset(&perms, 0, sizeof(perms)); + server_rc = 0; + logged_in_id = WH_USER_ID_INVALID; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, + "limiteduser", "pass", 4, + &server_rc, &logged_in_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + server_rc = 0; + temp_id3 = WH_USER_ID_INVALID; + _whTest_Auth_UserAddOp(client, "testuser9", perms, WH_AUTH_METHOD_PIN, + "test", 4, &server_rc, &temp_id3); + WH_TEST_ASSERT_RETURN(server_rc != WH_ERROR_OK); + + /* Cleanup */ + _whTest_Auth_LogoutOp(client, logged_in_id, &server_rc); + server_rc = 0; + WH_TEST_RETURN_ON_FAIL( + _whTest_Auth_LoginOp(client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, + TEST_ADMIN_PIN, 4, &server_rc, &admin_id)); + _whTest_Auth_DeleteUserByName(client, "limiteduser"); + _whTest_Auth_DeleteUserByName(client, "alloweduser"); + _whTest_Auth_DeleteUserByName(client, "testuser5"); + _whTest_Auth_DeleteUserByName(client, "testuser6"); + _whTest_Auth_DeleteUserByName(client, "testuser7"); + _whTest_Auth_DeleteUserByName(client, "testuser8"); + _whTest_Auth_DeleteUserByName(client, "testuser9"); + _whTest_Auth_LogoutOp(client, admin_id, &server_rc); + + return WH_TEST_SUCCESS; +} + + +/* ============================================================================ + * Bad-args tests (no live session needed; do not disturb the baseline) + * ============================================================================ + */ + +static int _whTest_Auth_BadArgs(void) +{ + int rc = 0; + int loggedIn = 1; + whAuthContext ctx; + whAuthConfig config; + whAuthPermissions perms; + whUserId user_id = WH_USER_ID_INVALID; + int32_t server_rc = 0; + + memset(&ctx, 0, sizeof(ctx)); + memset(&config, 0, sizeof(config)); + memset(&perms, 0, sizeof(perms)); + + WH_TEST_PRINT(" Test: Auth core bad args\n"); + rc = wh_Auth_Init(NULL, &config); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Auth_Init(&ctx, NULL); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + rc = wh_Auth_Cleanup(NULL); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Auth_Cleanup(&ctx); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + rc = + wh_Auth_Login(NULL, 0, WH_AUTH_METHOD_PIN, "user", "pin", 3, &loggedIn); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Auth_Login(&ctx, 0, WH_AUTH_METHOD_PIN, "user", "pin", 3, NULL); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Client_AuthLoginRequest(NULL, WH_AUTH_METHOD_PIN, "user", "pin", 3); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Client_AuthLoginResponse(NULL, &server_rc, &user_id); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + rc = wh_Auth_Logout(NULL, 1); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Auth_Logout(&ctx, 1); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + rc = wh_Auth_UserAdd(&ctx, "user", &user_id, perms, WH_AUTH_METHOD_PIN, + "pin", 3); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Auth_UserDelete(&ctx, 1); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Auth_UserSetPermissions(&ctx, 1, perms); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Auth_UserGet(&ctx, "user", &user_id, &perms); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Auth_UserSetCredentials(&ctx, 1, WH_AUTH_METHOD_PIN, "pin", 3, + "new", 3); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Auth_Logout(NULL, 999); /* port may not support 999 users */ + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + WH_TEST_PRINT(" Test: Auth client bad args\n"); + rc = wh_Client_AuthLoginRequest(NULL, WH_AUTH_METHOD_PIN, + TEST_ADMIN_USERNAME, TEST_ADMIN_PIN, + (uint16_t)strlen(TEST_ADMIN_PIN)); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Client_AuthLogoutRequest(NULL, WH_USER_ID_INVALID); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + rc = wh_Auth_CheckRequestAuthorization(NULL, WH_MESSAGE_GROUP_AUTH, + WH_MESSAGE_AUTH_ACTION_LOGIN); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + rc = wh_Client_AuthUserAddRequest(NULL, "baduser", perms, + WH_AUTH_METHOD_NONE, "x", 1); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Client_AuthUserDeleteRequest(NULL, WH_USER_ID_INVALID); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Client_AuthUserSetPermissionsRequest(NULL, WH_USER_ID_INVALID, + perms); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Client_AuthUserSetCredentialsRequest( + NULL, WH_USER_ID_INVALID, WH_AUTH_METHOD_PIN, NULL, 0, "new", 3); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_Client_AuthUserGetRequest(NULL, "missing"); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + return WH_TEST_SUCCESS; +} + +static int _whTest_Auth_MessageBadArgs(void) +{ + int rc = 0; + whMessageAuth_SimpleResponse simple = {0}; + whMessageAuth_LoginRequest login_hdr = {0}; + whMessageAuth_LoginRequest login_out = {0}; + whMessageAuth_UserAddRequest add_hdr = {0}; + whMessageAuth_UserAddRequest add_out = {0}; + whMessageAuth_UserSetCredentialsRequest set_hdr = {0}; + + WH_TEST_PRINT(" Test: Auth message bad args\n"); + rc = wh_MessageAuth_TranslateSimpleResponse(0, NULL, &simple); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_MessageAuth_TranslateSimpleResponse(0, &simple, NULL); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + rc = wh_MessageAuth_TranslateLoginRequest(0, NULL, 0, &login_out); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + rc = wh_MessageAuth_TranslateLoginRequest(0, &login_hdr, 0, &login_out); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BUFFER_SIZE); + + memset(&login_hdr, 0, sizeof(login_hdr)); + login_hdr.auth_data_len = + (uint16_t)(WH_MESSAGE_AUTH_LOGIN_MAX_AUTH_DATA_LEN + 1); + rc = wh_MessageAuth_TranslateLoginRequest(0, &login_hdr, sizeof(login_hdr), + &login_out); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BUFFER_SIZE); + + rc = wh_MessageAuth_TranslateUserAddRequest(0, NULL, 0, &add_out); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + memset(&add_hdr, 0, sizeof(add_hdr)); + add_hdr.credentials_len = + (uint16_t)(WH_MESSAGE_AUTH_USERADD_MAX_CREDENTIALS_LEN + 1); + rc = wh_MessageAuth_TranslateUserAddRequest(0, &add_hdr, sizeof(add_hdr), + &add_out); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BUFFER_SIZE); + + rc = + wh_MessageAuth_TranslateUserSetCredentialsRequest(0, NULL, 0, &set_hdr); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BADARGS); + + memset(&set_hdr, 0, sizeof(set_hdr)); + set_hdr.current_credentials_len = 4; + set_hdr.new_credentials_len = 4; + rc = wh_MessageAuth_TranslateUserSetCredentialsRequest( + 0, &set_hdr, sizeof(set_hdr), &set_hdr); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_BUFFER_SIZE); + + return WH_TEST_SUCCESS; +} + + +/* ============================================================================ + * Registered entry points + * ============================================================================ + */ + +int whTest_AuthBadArgs(whClientContext* client) +{ + (void)client; + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_BadArgs()); + WH_TEST_RETURN_ON_FAIL(_whTest_Auth_MessageBadArgs()); + return WH_TEST_SUCCESS; +} + +int whTest_AuthLogin(whClientContext* client) +{ + return _whTest_Auth_RunBracketed(client, _whTest_AuthLogin_impl); +} + +int whTest_AuthLogout(whClientContext* client) +{ + return _whTest_Auth_RunBracketed(client, _whTest_AuthLogout_impl); +} + +int whTest_AuthAddUser(whClientContext* client) +{ + return _whTest_Auth_RunBracketed(client, _whTest_AuthAddUser_impl); +} + +int whTest_AuthDeleteUser(whClientContext* client) +{ + return _whTest_Auth_RunBracketed(client, _whTest_AuthDeleteUser_impl); +} + +int whTest_AuthSetPermissions(whClientContext* client) +{ + return _whTest_Auth_RunBracketed(client, _whTest_AuthSetPermissions_impl); +} + +int whTest_AuthSetCredentials(whClientContext* client) +{ + return _whTest_Auth_RunBracketed(client, _whTest_AuthSetCredentials_impl); +} + +int whTest_AuthRequestAuthorization(whClientContext* client) +{ + return _whTest_Auth_RunBracketed(client, + _whTest_AuthRequestAuthorization_impl); +} + +#endif /* WOLFHSM_CFG_ENABLE_AUTHENTICATION && WOLFHSM_CFG_ENABLE_CLIENT && \ + WOLFHSM_CFG_ENABLE_SERVER */ diff --git a/test-refactor/client-server/wh_test_counter.c b/test-refactor/client-server/wh_test_counter.c new file mode 100644 index 000000000..960f051f9 --- /dev/null +++ b/test-refactor/client-server/wh_test_counter.c @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/client-server/wh_test_counter.c + * + * Exercise the persistent NVM counter API: init/reset, + * sequential increment past WOLFHSM_CFG_NVM_OBJECT_COUNT (catches + * slot leaks), saturate-on-overflow at UINT32_MAX, and + * reset+destroy across many slots. + */ + +#include +#include + +#include "wolfhsm/wh_settings.h" +#include "wolfhsm/wh_common.h" +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_client.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + + +/* + * Verify counter can update more than WOLFHSM_CFG_NVM_OBJECT_COUNT times. + * Each increment should reuse the same slot. + */ +static int _whTest_CounterSequentialIncrement(whClientContext* ctx) +{ + const whNvmId counterId = 1; + const size_t NUM_INCREMENTS = 2u * WOLFHSM_CFG_NVM_OBJECT_COUNT; + size_t i; + uint32_t counter = 0; + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterReset(ctx, counterId, &counter)); + WH_TEST_ASSERT_RETURN(counter == 0); + + for (i = 0; i < NUM_INCREMENTS; i++) { + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterIncrement(ctx, counterId, &counter)); + WH_TEST_ASSERT_RETURN(counter == (uint32_t)(i + 1)); + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterRead(ctx, counterId, &counter)); + WH_TEST_ASSERT_RETURN(counter == (uint32_t)(i + 1)); + } + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterReset(ctx, counterId, &counter)); + WH_TEST_ASSERT_RETURN(counter == 0); + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterDestroy(ctx, counterId)); + + return WH_ERROR_OK; +} + + +/* + * Verify the counter saturates at UINT32_MAX and does not wrap. + */ +static int _whTest_CounterSaturate(whClientContext* ctx) +{ + const whNvmId counterId = 1; + const uint32_t MAX_COUNTER = 0xFFFFFFFFu; + uint32_t counter = MAX_COUNTER; + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterInit(ctx, counterId, &counter)); + WH_TEST_ASSERT_RETURN(counter == MAX_COUNTER); + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterIncrement(ctx, counterId, &counter)); + WH_TEST_ASSERT_RETURN(counter == MAX_COUNTER); + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterIncrement(ctx, counterId, &counter)); + WH_TEST_ASSERT_RETURN(counter == MAX_COUNTER); + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterRead(ctx, counterId, &counter)); + WH_TEST_ASSERT_RETURN(counter == MAX_COUNTER); + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterReset(ctx, counterId, &counter)); + WH_TEST_ASSERT_RETURN(counter == 0); + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterDestroy(ctx, counterId)); + + return WH_ERROR_OK; +} + + +/* + * Reset+destroy across many slots: catches leaks in the destroy + * path and confirms that a destroyed counter reads back as + * NOTFOUND. + */ +static int _whTest_CounterDestroyMany(whClientContext* ctx) +{ + const size_t NUM_SLOTS = 2u * WOLFHSM_CFG_NVM_OBJECT_COUNT; + size_t i; + uint32_t counter; + + for (i = 1; i < NUM_SLOTS; i++) { + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterReset(ctx, (whNvmId)i, &counter)); + WH_TEST_ASSERT_RETURN(counter == 0); + + WH_TEST_RETURN_ON_FAIL( + wh_Client_CounterDestroy(ctx, (whNvmId)i)); + + WH_TEST_ASSERT_RETURN(WH_ERROR_NOTFOUND == + wh_Client_CounterRead(ctx, (whNvmId)i, &counter)); + } + + return WH_ERROR_OK; +} + + +int whTest_Counter(whClientContext* ctx) +{ + int32_t server_rc = 0; + uint32_t client_id = 0; + uint32_t server_id = 0; + uint32_t avail_size = 0; + uint32_t reclaim_size = 0; + whNvmId avail_objects = 0; + whNvmId reclaim_objects = 0; + whNvmId baseline = 0; + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmInit( + ctx, &server_rc, &client_id, &server_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetAvailable( + ctx, &server_rc, &avail_size, &avail_objects, + &reclaim_size, &reclaim_objects)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + baseline = avail_objects; + + WH_TEST_RETURN_ON_FAIL(_whTest_CounterSequentialIncrement(ctx)); + WH_TEST_RETURN_ON_FAIL(_whTest_CounterSaturate(ctx)); + WH_TEST_RETURN_ON_FAIL(_whTest_CounterDestroyMany(ctx)); + + /* No object slots leaked: available count is back where we + * started before the test ran. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetAvailable( + ctx, &server_rc, &avail_size, &avail_objects, + &reclaim_size, &reclaim_objects)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(avail_objects == baseline); + + return WH_ERROR_OK; +} diff --git a/test-refactor/client-server/wh_test_crypto_lms.c b/test-refactor/client-server/wh_test_crypto_lms.c new file mode 100644 index 000000000..a1bde8c07 --- /dev/null +++ b/test-refactor/client-server/wh_test_crypto_lms.c @@ -0,0 +1,460 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/client-server/wh_test_crypto_lms.c + * + * LMS (stateful hash-based) tests routed through the server over DMA: + * _whTest_CryptoLmsCryptoCb - generate / durability / sign / verify, public + * key export and import, and enforcement of the + * no-private-export / no-private-import policy + */ + +#include "wolfhsm/wh_settings.h" + +#if !defined(WOLFHSM_CFG_NO_CRYPTO) + +#include +#include + +#include "wolfssl/wolfcrypt/settings.h" +#include "wolfssl/wolfcrypt/types.h" +#include "wolfssl/wolfcrypt/random.h" +#include "wolfssl/wolfcrypt/error-crypt.h" +#if defined(WOLFSSL_HAVE_LMS) +#include "wolfssl/wolfcrypt/wc_lms.h" +#endif + +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_common.h" +#include "wolfhsm/wh_client.h" +#include "wolfhsm/wh_client_crypto.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + +#if defined(WOLFHSM_CFG_DMA) && \ + defined(WOLFSSL_HAVE_LMS) && !defined(WOLFSSL_LMS_VERIFY_ONLY) + +/* L=1, H=5, W=8 keeps the signature ~1.3 KB and gives 2^5 = 32 signatures. */ +#define WH_TEST_LMS_LEVELS (1) +#define WH_TEST_LMS_HEIGHT (5) +#define WH_TEST_LMS_WINTERNITZ (8) +/* Generous buffer that fits L1_H5_W8 (~1328) and any W<8 variant of the same + * height (W=1 ~8688). Keeps off the stack so ASAN builds stay happy. */ +static byte whTest_LmsSigBuf[8800]; + +static int _whTest_CryptoLmsCryptoCb(whClientContext* ctx, int devId, + WC_RNG* rng) +{ + int ret = 0; + LmsKey key[1]; + int keyInited = 0; + word32 sigLen = 0; + word32 sigCap = 0; + const byte msg[] = "wolfHSM LMS cryptocb test"; + word32 msgSz = (word32)sizeof(msg) - 1; + + (void)rng; + + memset(whTest_LmsSigBuf, 0, sizeof(whTest_LmsSigBuf)); + + ret = wc_LmsKey_Init(key, NULL, devId); + if (ret != 0) { + WH_ERROR_PRINT("Failed wc_LmsKey_Init devId=0x%X ret=%d\n", devId, ret); + return ret; + } + keyInited = 1; + + if (ret == 0) { + ret = wc_LmsKey_SetParameters(key, WH_TEST_LMS_LEVELS, + WH_TEST_LMS_HEIGHT, + WH_TEST_LMS_WINTERNITZ); + if (ret != 0) { + WH_ERROR_PRINT("Failed LMS SetParameters ret=%d\n", ret); + } + } + + if (ret == 0) { + ret = wc_LmsKey_GetSigLen(key, &sigCap); + if (ret != 0) { + WH_ERROR_PRINT("Failed LMS GetSigLen ret=%d\n", ret); + } + else if (sigCap > sizeof(whTest_LmsSigBuf)) { + WH_ERROR_PRINT("LMS sig buffer too small: need=%u have=%u\n", + (unsigned)sigCap, + (unsigned)sizeof(whTest_LmsSigBuf)); + ret = BUFFER_E; + } + } + + /* MakeKey via cryptocb: the server commits the key to NVM before + * returning the public key over DMA. */ + if (ret == 0) { + ret = wc_LmsKey_MakeKey(key, rng); + if (ret != 0) { + WH_ERROR_PRINT("Failed LMS MakeKey ret=%d\n", ret); + } + } + + /* wc_LmsKey_SigsLeft returns a boolean: nonzero = signatures available, + * 0 = exhausted. Fresh key should report nonzero. */ + if (ret == 0) { + if (wc_LmsKey_SigsLeft(key) == 0) { + WH_ERROR_PRINT("LMS reported exhausted on fresh key\n"); + ret = -1; + } + } + + /* Durability: keygen must commit the key to NVM before returning the pub, + * not defer it. Evict the volatile cache copy (as a power loss before the + * first sign would) and confirm the key is still resident in NVM. */ + if (ret == 0) { + whKeyId durId = WH_KEYID_ERASED; + if ((wh_Client_LmsGetKeyId(key, &durId) == 0) && + !WH_KEYID_ISERASED(durId)) { + ret = wh_Client_KeyEvict(ctx, durId); + if (ret != 0) { + WH_ERROR_PRINT("LMS durability evict failed: ret=%d\n", ret); + } + else { + /* SigsLeft reloads the key from NVM; a negative return means + * keygen failed to commit it. A fresh key reports 1. */ + ret = wh_Client_LmsSigsLeftDma(ctx, key); + if (ret < 0) { + WH_ERROR_PRINT("LMS key not durable after keygen: ret=%d\n", + ret); + } + else { + ret = 0; + } + } + } + } + + /* EPHEMERAL is invalid for a stateful private keygen and must be rejected + * locally with WH_ERROR_BADARGS (no server round-trip). */ + if (ret == 0) { + int badRet = wh_Client_LmsMakeKeyDma(ctx, key, NULL, + WH_NVM_FLAGS_EPHEMERAL, 0, NULL); + if (badRet != WH_ERROR_BADARGS) { + WH_ERROR_PRINT("LMS ephemeral keygen not rejected: ret=%d " + "(expected WH_ERROR_BADARGS)\n", badRet); + ret = WH_ERROR_ABORTED; + } + } + + /* Direct DMA keygen with a label and a caller-visible keyId. The cryptocb + * keygen above passes neither, so this drives the label-copy and keyId + * write-back paths on both client and server. */ + if (ret == 0) { + LmsKey lblKey[1]; + int lblInited = 0; + whKeyId lblId = WH_KEYID_ERASED; + byte label[] = "wolfHSM LMS key"; + + ret = wc_LmsKey_Init(lblKey, NULL, devId); + if (ret == 0) { + lblInited = 1; + ret = wc_LmsKey_SetParameters(lblKey, WH_TEST_LMS_LEVELS, + WH_TEST_LMS_HEIGHT, + WH_TEST_LMS_WINTERNITZ); + } + if (ret == 0) { + ret = wh_Client_LmsMakeKeyDma(ctx, lblKey, &lblId, + WH_NVM_FLAGS_NONE, + (uint16_t)(sizeof(label) - 1), label); + if (ret != 0) { + WH_ERROR_PRINT("LMS labeled keygen failed: ret=%d\n", ret); + } + } + if ((ret == 0) && WH_KEYID_ISERASED(lblId)) { + WH_ERROR_PRINT("LMS labeled keygen returned no keyId\n"); + ret = WH_ERROR_ABORTED; + } + /* Keygen is write-through, so erase (cache + NVM) to avoid leaking a + * committed key. */ + if (!WH_KEYID_ISERASED(lblId)) { + (void)wh_Client_KeyErase(ctx, lblId); + } + if (lblInited) { + wc_LmsKey_Free(lblKey); + } + } + + /* Sign via cryptocb. */ + if (ret == 0) { + sigLen = sigCap; + ret = wc_LmsKey_Sign(key, whTest_LmsSigBuf, &sigLen, msg, (int)msgSz); + if (ret != 0) { + WH_ERROR_PRINT("Failed LMS Sign ret=%d\n", ret); + } + else if (sigLen != sigCap) { + WH_ERROR_PRINT("LMS Sign produced unexpected length=%u expected=%u\n", + (unsigned)sigLen, (unsigned)sigCap); + ret = -1; + } + } + + /* Verify the signature via cryptocb. */ + if (ret == 0) { + ret = wc_LmsKey_Verify(key, whTest_LmsSigBuf, sigLen, msg, (int)msgSz); + if (ret != 0) { + WH_ERROR_PRINT("Failed LMS Verify ret=%d\n", ret); + } + } + + /* Tampered signature must fail to verify. */ + if (ret == 0) { + whTest_LmsSigBuf[0] ^= 0xFF; + ret = wc_LmsKey_Verify(key, whTest_LmsSigBuf, sigLen, msg, (int)msgSz); + whTest_LmsSigBuf[0] ^= 0xFF; + if (ret == 0) { + WH_ERROR_PRINT("LMS Verify unexpectedly accepted tampered sig\n"); + ret = -1; + } + else { + ret = 0; + } + } + + /* Wrong message must also fail to verify. */ + if (ret == 0) { + const byte wrongMsg[] = "wolfHSM LMS cryptocb wrong"; + ret = wc_LmsKey_Verify(key, whTest_LmsSigBuf, sigLen, wrongMsg, + (int)(sizeof(wrongMsg) - 1)); + if (ret == 0) { + WH_ERROR_PRINT("LMS Verify unexpectedly accepted wrong message\n"); + ret = -1; + } + else { + ret = 0; + } + } + + /* H=5 means 32 sigs total; after one sign, the key is still not + * exhausted. */ + if (ret == 0) { + if (wc_LmsKey_SigsLeft(key) == 0) { + WH_ERROR_PRINT("LMS reported exhausted after one sign\n"); + ret = -1; + } + } + + /* Verify the public key matches when read back */ + if (ret == 0) { + whKeyId pubId = WH_KEYID_ERASED; + word32 pubLen = 0; + uint8_t pubBuf[128]; + uint16_t pubBufLen = (uint16_t)sizeof(pubBuf); + if ((wh_Client_LmsGetKeyId(key, &pubId) == 0) && + !WH_KEYID_ISERASED(pubId) && + (wc_LmsKey_GetPubLen(key, &pubLen) == 0) && + (pubLen <= sizeof(pubBuf))) { + int pubRet = wh_Client_KeyExportPublic(ctx, pubId, WH_KEY_ALGO_LMS, + NULL, 0, pubBuf, &pubBufLen); + if (pubRet != WH_ERROR_OK) { + WH_ERROR_PRINT("LMS export pub failed: ret=%d\n", pubRet); + ret = pubRet; + } + else if (((word32)pubBufLen != pubLen) || + (memcmp(pubBuf, key->pub, pubLen) != 0)) { + WH_ERROR_PRINT("LMS export pub mismatch len=%u expected=%u\n", + (unsigned)pubBufLen, (unsigned)pubLen); + ret = WH_ERROR_ABORTED; + } + } + } + + /* Public-key import: provision a verify-only copy of this key's public + * half under a new keyId, verify the signature made above against it, and + * confirm signing with it is refused (no private state). */ + if (ret == 0) { + LmsKey pubKey[1]; + int pubInited = 0; + word32 pubLen = 0; + uint8_t pubRaw[128]; + whKeyId pubKeyId = WH_KEYID_ERASED; + int vres = 0; + + ret = wc_LmsKey_GetPubLen(key, &pubLen); + if ((ret == 0) && (pubLen > sizeof(pubRaw))) { + ret = BUFFER_E; + } + if (ret == 0) { + ret = wc_LmsKey_ExportPubRaw(key, pubRaw, &pubLen); + } + if (ret == 0) { + ret = wc_LmsKey_Init(pubKey, NULL, devId); + } + if (ret == 0) { + pubInited = 1; + ret = wc_LmsKey_SetParameters(pubKey, WH_TEST_LMS_LEVELS, + WH_TEST_LMS_HEIGHT, + WH_TEST_LMS_WINTERNITZ); + } + if (ret == 0) { + ret = wc_LmsKey_ImportPubRaw(pubKey, pubRaw, pubLen); + } + /* EPHEMERAL keeps it cache-only for an easy cleanup; production would + * pin with WH_NVM_FLAGS_NONMODIFIABLE and commit. */ + if (ret == 0) { + ret = wh_Client_LmsImportPubKey(ctx, pubKey, &pubKeyId, + WH_NVM_FLAGS_EPHEMERAL, 0, NULL); + if (ret != 0) { + WH_ERROR_PRINT("LMS import pub failed: ret=%d\n", ret); + } + } + if (ret == 0) { + ret = wh_Client_LmsVerifyDma(ctx, whTest_LmsSigBuf, sigLen, msg, + msgSz, &vres, pubKey); + if ((ret == 0) && (vres != 1)) { + WH_ERROR_PRINT("LMS verify with imported pub failed\n"); + ret = WH_ERROR_ABORTED; + } + } + if (ret == 0) { + word32 tmpSigLen = (word32)sizeof(whTest_LmsSigBuf); + int signRet = + wh_Client_LmsSignDma(ctx, msg, msgSz, whTest_LmsSigBuf, + &tmpSigLen, pubKey); + if (signRet == 0) { + WH_ERROR_PRINT("LMS sign with verify-only key unexpectedly " + "succeeded\n"); + ret = WH_ERROR_ABORTED; + } + } + if (!WH_KEYID_ISERASED(pubKeyId)) { + (void)wh_Client_KeyEvict(ctx, pubKeyId); + } + if (pubInited) { + wc_LmsKey_Free(pubKey); + } + } + + /* The generic export API must refuse to return the private key state. + * Keygen forces WH_NVM_FLAGS_NONEXPORTABLE, so export of the resident + * key is denied with WH_ERROR_ACCESS. */ + if (ret == 0) { + whKeyId exportId = WH_KEYID_ERASED; + uint8_t expBuf[256]; + uint16_t expLen = (uint16_t)sizeof(expBuf); + if ((wh_Client_LmsGetKeyId(key, &exportId) == 0) && + !WH_KEYID_ISERASED(exportId)) { + int expRet = + wh_Client_KeyExport(ctx, exportId, NULL, 0, expBuf, &expLen); + if (expRet != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("LMS export not blocked: ret=%d " + "(expected WH_ERROR_ACCESS)\n", expRet); + ret = (expRet == 0) ? WH_ERROR_ABORTED : expRet; + } + } + } + + /* Attempt to import an LMS key which must be rejected */ + if (ret == 0) { + uint8_t fakeBlob[64]; + uint32_t lmsMagic = 0x4C4D5301u; /* 'LMS\1', see wh_crypto.c */ + whKeyId impId = WH_KEYID_ERASED; + int impRet; + memset(fakeBlob, 0, sizeof(fakeBlob)); + memcpy(fakeBlob, &lmsMagic, sizeof(lmsMagic)); + fakeBlob[6] = 1; /* privLen field nonzero: a private-bearing blob */ + impRet = wh_Client_KeyCache(ctx, 0, NULL, 0, fakeBlob, + (uint16_t)sizeof(fakeBlob), &impId); + if (impRet != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("LMS blob import not blocked: ret=%d " + "(expected WH_ERROR_ACCESS)\n", impRet); + if ((impRet == 0) && !WH_KEYID_ISERASED(impId)) { + (void)wh_Client_KeyEvict(ctx, impId); + } + ret = (impRet == 0) ? WH_ERROR_ABORTED : impRet; + } + } + + /* Also ensure direct NVM import is blocked */ + if (ret == 0) { + uint8_t fakeBlob[64]; + uint32_t lmsMagic = 0x4C4D5301u; /* 'LMS\1', see wh_crypto.c */ + int32_t addRc = 0; + int addRet; + whNvmId addId = 0x1042; /* An arbitrary ID in the NVM range */ + memset(fakeBlob, 0, sizeof(fakeBlob)); + memcpy(fakeBlob, &lmsMagic, sizeof(lmsMagic)); + fakeBlob[6] = 1; /* privLen field nonzero: a private-bearing blob */ + addRet = wh_Client_NvmAddObject(ctx, addId, WH_NVM_ACCESS_ANY, + WH_NVM_FLAGS_NONE, 0, NULL, + (whNvmSize)sizeof(fakeBlob), fakeBlob, + &addRc); + if ((addRet != WH_ERROR_OK) || (addRc != WH_ERROR_ACCESS)) { + WH_ERROR_PRINT("LMS blob NVM import not blocked: ret=%d rc=%d " + "(expected rc WH_ERROR_ACCESS)\n", addRet, + (int)addRc); + ret = (addRc != 0) ? addRc : WH_ERROR_ABORTED; + } + } + + if (keyInited) { + whKeyId evictId = WH_KEYID_ERASED; + if ((wh_Client_LmsGetKeyId(key, &evictId) == 0) && + !WH_KEYID_ISERASED(evictId)) { + int evictRet = wh_Client_KeyEvict(ctx, evictId); + if ((evictRet != 0) && (ret == 0)) { + WH_ERROR_PRINT("Failed LMS evict keyId=0x%X ret=%d\n", + (unsigned)evictId, evictRet); + ret = evictRet; + } + } + wc_LmsKey_Free(key); + } + + if (ret == 0) { + WH_TEST_PRINT("LMS CryptoCb DEVID=0x%X SUCCESS\n", devId); + } + + return ret; +} + +int whTest_Crypto_Lms(whClientContext* ctx) +{ + int ret = 0; + int rngInited = 0; + WC_RNG rng[1]; + + ret = wc_InitRng_ex(rng, NULL, WH_CLIENT_DEVID(ctx)); + if (ret != 0) { + WH_ERROR_PRINT("Failed to wc_InitRng_ex %d\n", ret); + return ret; + } + rngInited = 1; + + /* LMS dispatches through the DMA-only cryptocb. */ + (void)wh_Client_SetDmaMode(ctx, 1); + ret = _whTest_CryptoLmsCryptoCb(ctx, WH_DEV_ID_DMA, rng); + (void)wh_Client_SetDmaMode(ctx, 0); + + if (rngInited) { + (void)wc_FreeRng(rng); + } + + return ret; +} + +#endif /* WOLFHSM_CFG_DMA && WOLFSSL_HAVE_LMS && !WOLFSSL_LMS_VERIFY_ONLY */ + +#endif /* !WOLFHSM_CFG_NO_CRYPTO */ diff --git a/test-refactor/client-server/wh_test_crypto_xmss.c b/test-refactor/client-server/wh_test_crypto_xmss.c new file mode 100644 index 000000000..d6ae392e8 --- /dev/null +++ b/test-refactor/client-server/wh_test_crypto_xmss.c @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/client-server/wh_test_crypto_xmss.c + * + * XMSS (stateful hash-based) tests routed through the server over DMA: + * _whTest_CryptoXmssCryptoCb - generate / durability / sign / verify, public + * key export and import, and enforcement of the + * no-private-export / no-private-import policy + */ + +#include "wolfhsm/wh_settings.h" + +#if !defined(WOLFHSM_CFG_NO_CRYPTO) + +#include +#include + +#include "wolfssl/wolfcrypt/settings.h" +#include "wolfssl/wolfcrypt/types.h" +#include "wolfssl/wolfcrypt/random.h" +#include "wolfssl/wolfcrypt/error-crypt.h" +#if defined(WOLFSSL_HAVE_XMSS) +#include "wolfssl/wolfcrypt/wc_xmss.h" +#endif + +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_common.h" +#include "wolfhsm/wh_client.h" +#include "wolfhsm/wh_client_crypto.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + +#if defined(WOLFHSM_CFG_DMA) && \ + defined(WOLFSSL_HAVE_XMSS) && !defined(WOLFSSL_XMSS_VERIFY_ONLY) + +/* "XMSS-SHA2_10_256" is the smallest standardized XMSS parameter set + * (height 10, 1024 signatures). pubLen=68, sigLen=2500. */ +#define WH_TEST_XMSS_PARAM_STR "XMSS-SHA2_10_256" +static byte whTest_XmssSigBuf[2500]; + +static int _whTest_CryptoXmssCryptoCb(whClientContext* ctx, int devId, + WC_RNG* rng) +{ + int ret = 0; + XmssKey key[1]; + int keyInited = 0; + word32 sigLen = 0; + word32 sigCap = 0; + const byte msg[] = "wolfHSM XMSS cryptocb test"; + word32 msgSz = (word32)sizeof(msg) - 1; + + (void)rng; + + memset(whTest_XmssSigBuf, 0, sizeof(whTest_XmssSigBuf)); + + ret = wc_XmssKey_Init(key, NULL, devId); + if (ret != 0) { + WH_ERROR_PRINT("Failed wc_XmssKey_Init devId=0x%X ret=%d\n", devId, ret); + return ret; + } + keyInited = 1; + + if (ret == 0) { + ret = wc_XmssKey_SetParamStr(key, WH_TEST_XMSS_PARAM_STR); + if (ret != 0) { + WH_ERROR_PRINT("Failed XMSS SetParamStr=\"%s\" ret=%d\n", + WH_TEST_XMSS_PARAM_STR, ret); + } + } + + if (ret == 0) { + ret = wc_XmssKey_GetSigLen(key, &sigCap); + if (ret != 0) { + WH_ERROR_PRINT("Failed XMSS GetSigLen ret=%d\n", ret); + } + else if (sigCap > sizeof(whTest_XmssSigBuf)) { + WH_ERROR_PRINT("XMSS sig buffer too small: need=%u have=%u\n", + (unsigned)sigCap, + (unsigned)sizeof(whTest_XmssSigBuf)); + ret = BUFFER_E; + } + } + + /* MakeKey via cryptocb: the server commits the key to NVM before + * returning the public key over DMA. */ + if (ret == 0) { + ret = wc_XmssKey_MakeKey(key, rng); + if (ret != 0) { + WH_ERROR_PRINT("Failed XMSS MakeKey ret=%d\n", ret); + } + } + + /* wc_XmssKey_SigsLeft returns a boolean: nonzero = signatures available, + * 0 = exhausted. */ + if (ret == 0) { + if (wc_XmssKey_SigsLeft(key) == 0) { + WH_ERROR_PRINT("XMSS reported exhausted on fresh key\n"); + ret = -1; + } + } + + /* Durability: keygen must commit the key to NVM before returning the pub. + * Evict the volatile cache copy (as a power loss before the first sign + * would) and confirm the key is still resident in NVM. */ + if (ret == 0) { + whKeyId durId = WH_KEYID_ERASED; + if ((wh_Client_XmssGetKeyId(key, &durId) == 0) && + !WH_KEYID_ISERASED(durId)) { + ret = wh_Client_KeyEvict(ctx, durId); + if (ret != 0) { + WH_ERROR_PRINT("XMSS durability evict failed: ret=%d\n", ret); + } + else { + /* SigsLeft reloads the key from NVM; a negative return means + * keygen failed to commit it. A fresh key reports 1. */ + ret = wh_Client_XmssSigsLeftDma(ctx, key); + if (ret < 0) { + WH_ERROR_PRINT("XMSS key not durable after keygen: " + "ret=%d\n", ret); + } + else { + ret = 0; + } + } + } + } + + /* EPHEMERAL is invalid for a stateful private keygen and must be rejected + * locally with WH_ERROR_BADARGS. */ + if (ret == 0) { + int badRet = wh_Client_XmssMakeKeyDma(ctx, key, NULL, + WH_NVM_FLAGS_EPHEMERAL, 0, NULL); + if (badRet != WH_ERROR_BADARGS) { + WH_ERROR_PRINT("XMSS ephemeral keygen not rejected: ret=%d " + "(expected WH_ERROR_BADARGS)\n", badRet); + ret = WH_ERROR_ABORTED; + } + } + + /* Direct DMA keygen with a label and a caller-visible keyId. The cryptocb + * keygen above passes neither, so this drives the label-copy and keyId + * write-back paths on both client and server. */ + if (ret == 0) { + XmssKey lblKey[1]; + int lblInited = 0; + whKeyId lblId = WH_KEYID_ERASED; + byte label[] = "wolfHSM XMSS key"; + + ret = wc_XmssKey_Init(lblKey, NULL, devId); + if (ret == 0) { + lblInited = 1; + ret = wc_XmssKey_SetParamStr(lblKey, WH_TEST_XMSS_PARAM_STR); + } + if (ret == 0) { + ret = wh_Client_XmssMakeKeyDma(ctx, lblKey, &lblId, + WH_NVM_FLAGS_NONE, + (uint16_t)(sizeof(label) - 1), label); + if (ret != 0) { + WH_ERROR_PRINT("XMSS labeled keygen failed: ret=%d\n", ret); + } + } + if ((ret == 0) && WH_KEYID_ISERASED(lblId)) { + WH_ERROR_PRINT("XMSS labeled keygen returned no keyId\n"); + ret = WH_ERROR_ABORTED; + } + /* Keygen is write-through, so erase (cache + NVM) to avoid leaking a + * committed key. */ + if (!WH_KEYID_ISERASED(lblId)) { + (void)wh_Client_KeyErase(ctx, lblId); + } + if (lblInited) { + wc_XmssKey_Free(lblKey); + } + } + + if (ret == 0) { + sigLen = sigCap; + ret = wc_XmssKey_Sign(key, whTest_XmssSigBuf, &sigLen, msg, (int)msgSz); + if (ret != 0) { + WH_ERROR_PRINT("Failed XMSS Sign ret=%d\n", ret); + } + else if (sigLen != sigCap) { + WH_ERROR_PRINT("XMSS Sign produced unexpected length=%u expected=%u\n", + (unsigned)sigLen, (unsigned)sigCap); + ret = -1; + } + } + + if (ret == 0) { + ret = wc_XmssKey_Verify(key, whTest_XmssSigBuf, sigLen, msg, (int)msgSz); + if (ret != 0) { + WH_ERROR_PRINT("Failed XMSS Verify ret=%d\n", ret); + } + } + + if (ret == 0) { + whTest_XmssSigBuf[0] ^= 0xFF; + ret = wc_XmssKey_Verify(key, whTest_XmssSigBuf, sigLen, msg, (int)msgSz); + whTest_XmssSigBuf[0] ^= 0xFF; + if (ret == 0) { + WH_ERROR_PRINT("XMSS Verify unexpectedly accepted tampered sig\n"); + ret = -1; + } + else { + ret = 0; + } + } + + if (ret == 0) { + const byte wrongMsg[] = "wolfHSM XMSS cryptocb wrong"; + ret = wc_XmssKey_Verify(key, whTest_XmssSigBuf, sigLen, wrongMsg, + (int)(sizeof(wrongMsg) - 1)); + if (ret == 0) { + WH_ERROR_PRINT("XMSS Verify unexpectedly accepted wrong message\n"); + ret = -1; + } + else { + ret = 0; + } + } + + /* H=10 means 1024 sigs total; after one sign, the key is still not + * exhausted. */ + if (ret == 0) { + if (wc_XmssKey_SigsLeft(key) == 0) { + WH_ERROR_PRINT("XMSS reported exhausted after one sign\n"); + ret = -1; + } + } + + /* Verify the public key matches when read back */ + if (ret == 0) { + whKeyId pubId = WH_KEYID_ERASED; + word32 pubLen = 0; + uint8_t pubBuf[128]; + uint16_t pubBufLen = (uint16_t)sizeof(pubBuf); + if ((wh_Client_XmssGetKeyId(key, &pubId) == 0) && + !WH_KEYID_ISERASED(pubId) && + (wc_XmssKey_GetPubLen(key, &pubLen) == 0) && + (pubLen <= sizeof(pubBuf))) { + int pubRet = wh_Client_KeyExportPublic(ctx, pubId, WH_KEY_ALGO_XMSS, + NULL, 0, pubBuf, &pubBufLen); + if (pubRet != WH_ERROR_OK) { + WH_ERROR_PRINT("XMSS export pub failed: ret=%d\n", pubRet); + ret = pubRet; + } + else if (((word32)pubBufLen != pubLen) || + (memcmp(pubBuf, key->pk, pubLen) != 0)) { + WH_ERROR_PRINT("XMSS export pub mismatch len=%u expected=%u\n", + (unsigned)pubBufLen, (unsigned)pubLen); + ret = WH_ERROR_ABORTED; + } + } + } + + /* Public-key import: provision a verify-only copy of this key's public + * half under a new keyId, verify the signature made above against it, and + * confirm signing with it is refused (no private state). */ + if (ret == 0) { + XmssKey pubKey[1]; + int pubInited = 0; + word32 pubLen = 0; + uint8_t pubRaw[128]; + whKeyId pubKeyId = WH_KEYID_ERASED; + int vres = 0; + + ret = wc_XmssKey_GetPubLen(key, &pubLen); + if ((ret == 0) && (pubLen > sizeof(pubRaw))) { + ret = BUFFER_E; + } + if (ret == 0) { + ret = wc_XmssKey_ExportPubRaw(key, pubRaw, &pubLen); + } + if (ret == 0) { + ret = wc_XmssKey_Init(pubKey, NULL, devId); + } + if (ret == 0) { + pubInited = 1; + ret = wc_XmssKey_SetParamStr(pubKey, WH_TEST_XMSS_PARAM_STR); + } + if (ret == 0) { + ret = wc_XmssKey_ImportPubRaw(pubKey, pubRaw, pubLen); + } + /* EPHEMERAL keeps it cache-only for an easy cleanup; production would + * pin with WH_NVM_FLAGS_NONMODIFIABLE and commit. */ + if (ret == 0) { + ret = wh_Client_XmssImportPubKey(ctx, pubKey, &pubKeyId, + WH_NVM_FLAGS_EPHEMERAL, 0, NULL); + if (ret != 0) { + WH_ERROR_PRINT("XMSS import pub failed: ret=%d\n", ret); + } + } + if (ret == 0) { + ret = wh_Client_XmssVerifyDma(ctx, whTest_XmssSigBuf, sigLen, msg, + msgSz, &vres, pubKey); + if ((ret == 0) && (vres != 1)) { + WH_ERROR_PRINT("XMSS verify with imported pub failed\n"); + ret = WH_ERROR_ABORTED; + } + } + if (ret == 0) { + word32 tmpSigLen = (word32)sizeof(whTest_XmssSigBuf); + int signRet = + wh_Client_XmssSignDma(ctx, msg, msgSz, whTest_XmssSigBuf, + &tmpSigLen, pubKey); + if (signRet == 0) { + WH_ERROR_PRINT("XMSS sign with verify-only key unexpectedly " + "succeeded\n"); + ret = WH_ERROR_ABORTED; + } + } + if (!WH_KEYID_ISERASED(pubKeyId)) { + (void)wh_Client_KeyEvict(ctx, pubKeyId); + } + if (pubInited) { + wc_XmssKey_Free(pubKey); + } + } + + /* The generic export API must refuse to return the private key state. + * Keygen forces WH_NVM_FLAGS_NONEXPORTABLE, so export of the resident + * key is denied with WH_ERROR_ACCESS. */ + if (ret == 0) { + whKeyId exportId = WH_KEYID_ERASED; + uint8_t expBuf[256]; + uint16_t expLen = (uint16_t)sizeof(expBuf); + if ((wh_Client_XmssGetKeyId(key, &exportId) == 0) && + !WH_KEYID_ISERASED(exportId)) { + int expRet = + wh_Client_KeyExport(ctx, exportId, NULL, 0, expBuf, &expLen); + if (expRet != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("XMSS export not blocked: ret=%d " + "(expected WH_ERROR_ACCESS)\n", expRet); + ret = (expRet == 0) ? WH_ERROR_ABORTED : expRet; + } + } + } + + /* Attempt to import an XMSS key which must be rejected */ + if (ret == 0) { + uint8_t fakeBlob[64]; + uint32_t xmssMagic = 0x584D5301u; /* 'XMS\1', see wh_crypto.c */ + whKeyId impId = WH_KEYID_ERASED; + int impRet; + memset(fakeBlob, 0, sizeof(fakeBlob)); + memcpy(fakeBlob, &xmssMagic, sizeof(xmssMagic)); + fakeBlob[6] = 1; /* privLen field nonzero: a private-bearing blob */ + impRet = wh_Client_KeyCache(ctx, 0, NULL, 0, fakeBlob, + (uint16_t)sizeof(fakeBlob), &impId); + if (impRet != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("XMSS blob import not blocked: ret=%d " + "(expected WH_ERROR_ACCESS)\n", impRet); + if ((impRet == 0) && !WH_KEYID_ISERASED(impId)) { + (void)wh_Client_KeyEvict(ctx, impId); + } + ret = (impRet == 0) ? WH_ERROR_ABORTED : impRet; + } + } + + /* Also ensure direct NVM import is blocked */ + if (ret == 0) { + uint8_t fakeBlob[64]; + uint32_t xmssMagic = 0x584D5301u; /* 'XMS\1', see wh_crypto.c */ + int32_t addRc = 0; + int addRet; + whNvmId addId = 0x1042; /* An arbitrary ID in the NVM range */ + memset(fakeBlob, 0, sizeof(fakeBlob)); + memcpy(fakeBlob, &xmssMagic, sizeof(xmssMagic)); + fakeBlob[6] = 1; /* privLen field nonzero: a private-bearing blob */ + addRet = wh_Client_NvmAddObject(ctx, addId, WH_NVM_ACCESS_ANY, + WH_NVM_FLAGS_NONE, 0, NULL, + (whNvmSize)sizeof(fakeBlob), fakeBlob, + &addRc); + if ((addRet != WH_ERROR_OK) || (addRc != WH_ERROR_ACCESS)) { + WH_ERROR_PRINT("XMSS blob NVM import not blocked: ret=%d rc=%d " + "(expected rc WH_ERROR_ACCESS)\n", addRet, + (int)addRc); + ret = (addRc != 0) ? addRc : WH_ERROR_ABORTED; + } + } + + if (keyInited) { + whKeyId evictId = WH_KEYID_ERASED; + if ((wh_Client_XmssGetKeyId(key, &evictId) == 0) && + !WH_KEYID_ISERASED(evictId)) { + int evictRet = wh_Client_KeyEvict(ctx, evictId); + if ((evictRet != 0) && (ret == 0)) { + WH_ERROR_PRINT("Failed XMSS evict keyId=0x%X ret=%d\n", + (unsigned)evictId, evictRet); + ret = evictRet; + } + } + wc_XmssKey_Free(key); + } + + if (ret == 0) { + WH_TEST_PRINT("XMSS CryptoCb DEVID=0x%X SUCCESS\n", devId); + } + + return ret; +} + +int whTest_Crypto_Xmss(whClientContext* ctx) +{ + int ret = 0; + int rngInited = 0; + WC_RNG rng[1]; + + ret = wc_InitRng_ex(rng, NULL, WH_CLIENT_DEVID(ctx)); + if (ret != 0) { + WH_ERROR_PRINT("Failed to wc_InitRng_ex %d\n", ret); + return ret; + } + rngInited = 1; + + /* XMSS dispatches through the DMA-only cryptocb. */ + (void)wh_Client_SetDmaMode(ctx, 1); + ret = _whTest_CryptoXmssCryptoCb(ctx, WH_DEV_ID_DMA, rng); + (void)wh_Client_SetDmaMode(ctx, 0); + + if (rngInited) { + (void)wc_FreeRng(rng); + } + + return ret; +} + +#endif /* WOLFHSM_CFG_DMA && WOLFSSL_HAVE_XMSS && !WOLFSSL_XMSS_VERIFY_ONLY */ + +#endif /* !WOLFHSM_CFG_NO_CRYPTO */ diff --git a/test-refactor/client-server/wh_test_nvm_ops.c b/test-refactor/client-server/wh_test_nvm_ops.c new file mode 100644 index 000000000..2460877d1 --- /dev/null +++ b/test-refactor/client-server/wh_test_nvm_ops.c @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/client-server/wh_test_nvm_ops.c + * + * NVM object lifecycle tests over both the blocking and DMA + * client APIs. The Add/Update/List/Destroy body is shared via + * a small dispatch struct (WhNvmTestObjectOps) so the same + * coverage applies to both transports. The DMA entry point + * only compiles when WOLFHSM_CFG_DMA is defined; otherwise the + * weak stub in wh_test_list reports it as skipped. + */ + +#include +#include +#include + +#include "wolfhsm/wh_settings.h" +#include "wolfhsm/wh_common.h" +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_client.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + +#define NVM_TEST_OBJECT_COUNT 5 +#define NVM_TEST_OBJECT_ID_BASE 20 +#define NVM_TEST_OOB_ID 30 +#define NVM_TEST_DMA_ID_BASE 40 + + +/* + * Helpers to unify the DMA and non-DMA test functions. + */ +typedef int (*WhNvmTestObjectAddFn)(whClientContext* ctx, whNvmId id, + whNvmAccess access, whNvmFlags flags, + const uint8_t* label, whNvmSize label_len, + const uint8_t* data, whNvmSize data_len, + int32_t* server_rc); + +typedef int (*WhNvmTestObjectReadFn)(whClientContext* ctx, whNvmId id, + whNvmSize offset, whNvmSize len, + uint8_t* buf, whNvmSize* out_len, + int32_t* server_rc); + +typedef struct { + WhNvmTestObjectAddFn add; + WhNvmTestObjectReadFn read; +} WhNvmTestObjectOps; + + +static int _nvmIdInRange(whNvmId id, whNvmId base, whNvmId count) +{ + return (id >= base) && (id < (whNvmId)(base + count)); +} + + +static int _destroyNvmId(whClientContext* ctx, whNvmId id) +{ + int32_t server_rc = 0; + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmDestroyObjects( + ctx, 1, &id, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadata( + ctx, id, &server_rc, NULL, NULL, NULL, NULL, 0, NULL)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_NOTFOUND); + + return WH_ERROR_OK; +} + + +/* + * Add (or re-Add) one object via ops->add and verify: + * - avail_objects drops by 1 (a new slot is always consumed, + * even on re-Add, since the log is append-only) + * - reclaim_objects rises by reclaim_grow (0 for a fresh Add, + * 1 for a re-Add that supersedes a prior version) + * - GetMetadata reports the just-written id/access/flags/len/label + * - ops->read returns the just-written payload + */ +static int _addAndVerifyOne(whClientContext* ctx, + const WhNvmTestObjectOps* ops, whNvmId id, + whNvmAccess access, whNvmFlags flags, + const uint8_t* label, whNvmSize label_len, + const uint8_t* data, whNvmSize data_len, + int reclaim_grow) +{ + int32_t server_rc = 0; + uint32_t avail_size = 0; + uint32_t reclaim_size = 0; + whNvmId prev_avail = 0; + whNvmId prev_reclaim = 0; + whNvmId avail_objects = 0; + whNvmId reclaim_objects = 0; + + whNvmId gid = 0; + whNvmAccess gaccess = 0; + whNvmFlags gflags = 0; + whNvmSize glen = 0; + char glabel[WH_NVM_LABEL_LEN] = {0}; + uint8_t buf[WOLFHSM_CFG_COMM_DATA_LEN]; + whNvmSize rlen = 0; + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetAvailable( + ctx, &server_rc, &avail_size, &prev_avail, + &reclaim_size, &prev_reclaim)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + WH_TEST_RETURN_ON_FAIL(ops->add( + ctx, id, access, flags, + label, label_len, data, data_len, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetAvailable( + ctx, &server_rc, &avail_size, &avail_objects, + &reclaim_size, &reclaim_objects)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(prev_avail - 1 == avail_objects); + WH_TEST_ASSERT_RETURN( + (whNvmId)(prev_reclaim + reclaim_grow) == reclaim_objects); + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadata( + ctx, id, &server_rc, + &gid, &gaccess, &gflags, &glen, + sizeof(glabel), (uint8_t*)glabel)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(gid == id); + WH_TEST_ASSERT_RETURN(gaccess == access); + WH_TEST_ASSERT_RETURN(gflags == flags); + WH_TEST_ASSERT_RETURN(glen == data_len); + WH_TEST_ASSERT_RETURN(memcmp(glabel, label, label_len) == 0); + + memset(buf, 0, sizeof(buf)); + WH_TEST_RETURN_ON_FAIL(ops->read( + ctx, id, 0, glen, + buf, &rlen, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(rlen == data_len); + WH_TEST_ASSERT_RETURN(memcmp(buf, data, data_len) == 0); + + return WH_ERROR_OK; +} + + +/* + * Create, Read, Update and Destroy test. + */ +static int _runNvmObjectTest(whClientContext* ctx, + const WhNvmTestObjectOps* ops, whNvmId id_base) +{ + int32_t server_rc = 0; + uint32_t client_id = 0; + uint32_t server_id = 0; + uint32_t avail_size = 0; + uint32_t reclaim_size = 0; + whNvmId avail_objects = 0; + whNvmId reclaim_objects = 0; + whNvmId baseline = 0; + char label[WH_NVM_LABEL_LEN]; + char data[WOLFHSM_CFG_COMM_DATA_LEN]; + whNvmSize label_len; + whNvmSize data_len; + int i; + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmInit( + ctx, &server_rc, &client_id, &server_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Capture the starting available count so we don't assume + * the suite is running on a virgin NVM. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetAvailable( + ctx, &server_rc, &avail_size, &avail_objects, + &reclaim_size, &reclaim_objects)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + /* Starting point for the test is available + reclaimable. + * NVM activity may recycle stale entries from prior tests, so + * `avail_objects` alone is not sufficient. */ + baseline = (whNvmId)(avail_objects + reclaim_objects); + + /* Add phase: fresh objects, no reclaim activity expected. */ + for (i = 0; i < NVM_TEST_OBJECT_COUNT; i++) { + whNvmId id = (whNvmId)(id_base + i); + memset(label, 0, sizeof(label)); + label_len = (whNvmSize)snprintf(label, sizeof(label), + "Label:%d", id); + data_len = (whNvmSize)snprintf(data, sizeof(data), + "Data:%d Counter:%d", id, i); + WH_TEST_RETURN_ON_FAIL(_addAndVerifyOne( + ctx, ops, id, + WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, + (const uint8_t*)label, label_len, + (const uint8_t*)data, data_len, + 0)); + } + + /* Update phase: re-Add each id with new label and payload. */ + for (i = 0; i < NVM_TEST_OBJECT_COUNT; i++) { + whNvmId id = (whNvmId)(id_base + i); + memset(label, 0, sizeof(label)); + label_len = (whNvmSize)snprintf(label, sizeof(label), + "Upd:%d", id); + data_len = (whNvmSize)snprintf(data, sizeof(data), + "Updated:%d Iter:%d", id, i); + WH_TEST_RETURN_ON_FAIL(_addAndVerifyOne( + ctx, ops, id, + WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, + (const uint8_t*)label, label_len, + (const uint8_t*)data, data_len, + 1)); + } + + /* Verify List enumerates the ids we own without assuming + * the test owns unrelated objects that may already exist. */ + { + whNvmAccess list_access = WH_NVM_ACCESS_ANY; + whNvmFlags list_flags = WH_NVM_FLAGS_NONE; + whNvmId list_id = 0; + whNvmId list_count = 0; + whNvmId found = 0; + + do { + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmList( + ctx, list_access, list_flags, list_id, + &server_rc, &list_count, &list_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + if ((list_count > 0) && _nvmIdInRange( + list_id, id_base, NVM_TEST_OBJECT_COUNT)) { + found++; + } + } while (list_count > 0); + + WH_TEST_ASSERT_RETURN(found == NVM_TEST_OBJECT_COUNT); + } + + for (i = 0; i < NVM_TEST_OBJECT_COUNT; i++) { + WH_TEST_RETURN_ON_FAIL(_destroyNvmId( + ctx, (whNvmId)(id_base + i))); + } + + /* Cleanup -> Init round-trip leaves NVM in a usable state + * with no leftovers. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmCleanup(ctx, &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmInit( + ctx, &server_rc, &client_id, &server_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetAvailable( + ctx, &server_rc, &avail_size, &avail_objects, + &reclaim_size, &reclaim_objects)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + /* Live-object count must match the baseline; see note above. */ + WH_TEST_ASSERT_RETURN( + (whNvmId)(avail_objects + reclaim_objects) == baseline); + + return WH_ERROR_OK; +} + + +/* Blocking-API adapters. */ + +static int _nvmTestObjectAddBlocking(whClientContext* ctx, whNvmId id, + whNvmAccess access, whNvmFlags flags, + const uint8_t* label, whNvmSize label_len, + const uint8_t* data, whNvmSize data_len, + int32_t* server_rc) +{ + return wh_Client_NvmAddObject(ctx, id, access, flags, + label_len, (uint8_t*)label, + data_len, data, server_rc); +} + + +static int _nvmTestObjectReadBlocking(whClientContext* ctx, whNvmId id, + whNvmSize offset, whNvmSize len, + uint8_t* buf, whNvmSize* out_len, + int32_t* server_rc) +{ + return wh_Client_NvmRead(ctx, id, offset, len, + server_rc, out_len, buf); +} + + +static const WhNvmTestObjectOps g_blockingTestOps = { + _nvmTestObjectAddBlocking, + _nvmTestObjectReadBlocking, +}; + + +/* + * Exercises NvmRead's offset/length clamping and overflow + * safety. Adds a single object, runs the boundary cases, then + * destroys it. Blocking-only -- the DMA read API has no + * equivalent out_len reporting. + */ +static int _whTest_NvmOpsReadOob(whClientContext* ctx) +{ + const whNvmId id = NVM_TEST_OOB_ID; + int32_t server_rc = 0; + uint32_t client_id = 0; + uint32_t server_id = 0; + uint8_t buf[WOLFHSM_CFG_COMM_DATA_LEN]; + char label[WH_NVM_LABEL_LEN] = {0}; + char data[] = "OOB read clamping payload"; + whNvmSize label_len; + whNvmSize data_len = (whNvmSize)sizeof(data); + whNvmSize out_len; + whNvmId gid; + whNvmAccess gaccess; + whNvmFlags gflags; + whNvmSize meta_len; + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmInit( + ctx, &server_rc, &client_id, &server_id)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + label_len = (whNvmSize)snprintf(label, sizeof(label), "OOB:%u", id); + + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmAddObject( + ctx, id, WH_NVM_ACCESS_ANY, WH_NVM_FLAGS_NONE, + label_len, (uint8_t*)label, + data_len, (const uint8_t*)data, + &server_rc)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + + /* Confirm metadata length so we can phrase the rest of the + * checks against meta_len rather than the raw write size. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmGetMetadata( + ctx, id, &server_rc, + &gid, &gaccess, &gflags, &meta_len, + 0, NULL)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(meta_len == data_len); + + /* len = meta_len + 1 -> clamp to meta_len */ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmRead( + ctx, id, 0, (whNvmSize)(meta_len + 1), + &server_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == meta_len); + + /* off=1, len=meta_len -> clamp to meta_len - 1 */ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmRead( + ctx, id, 1, meta_len, + &server_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == (whNvmSize)(meta_len - 1)); + + /* off=meta_len-1, len=meta_len -> clamp to 1 */ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmRead( + ctx, id, (whNvmSize)(meta_len - 1), meta_len, + &server_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN(out_len == 1); + + /* off == meta_len, len = 0 -> BADARGS (no readable bytes) */ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmRead( + ctx, id, meta_len, 0, + &server_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_BADARGS); + + /* off == UINT16_MAX -> BADARGS. Regression for integer + * overflow in the offset+len bound check. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmRead( + ctx, id, (whNvmSize)UINT16_MAX, 1, + &server_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_BADARGS); + + /* off=meta_len/2, len=meta_len -> clamp to meta_len - off. + * Verifies the overflow-safe form of the bounds check. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_NvmRead( + ctx, id, (whNvmSize)(meta_len / 2), meta_len, + &server_rc, &out_len, buf)); + WH_TEST_ASSERT_RETURN(server_rc == WH_ERROR_OK); + WH_TEST_ASSERT_RETURN( + out_len == (whNvmSize)(meta_len - (meta_len / 2))); + + return _destroyNvmId(ctx, id); +} + + +int whTest_NvmOps(whClientContext* ctx) +{ + WH_TEST_RETURN_ON_FAIL(_runNvmObjectTest( + ctx, &g_blockingTestOps, NVM_TEST_OBJECT_ID_BASE)); + WH_TEST_RETURN_ON_FAIL(_whTest_NvmOpsReadOob(ctx)); + + return WH_ERROR_OK; +} + + +#ifdef WOLFHSM_CFG_DMA + +/* DMA-API adapters. */ + +static int _nvmTestObjectAddDma(whClientContext* ctx, whNvmId id, + whNvmAccess access, whNvmFlags flags, + const uint8_t* label, whNvmSize label_len, + const uint8_t* data, whNvmSize data_len, + int32_t* server_rc) +{ + whNvmMetadata meta = { + .id = id, + .access = access, + .flags = flags, + .len = 0, + .label = {0}, + }; + if (label_len > sizeof(meta.label)) { + label_len = sizeof(meta.label); + } + memcpy(meta.label, label, label_len); + return wh_Client_NvmAddObjectDma( + ctx, &meta, data_len, data, server_rc); +} + + +static int _nvmTestObjectReadDma(whClientContext* ctx, whNvmId id, + whNvmSize offset, whNvmSize len, + uint8_t* buf, whNvmSize* out_len, + int32_t* server_rc) +{ + int ret = wh_Client_NvmReadDma( + ctx, id, offset, len, buf, server_rc); + /* DMA read returns exactly len bytes on success; mirror that + * into out_len so the shared body's rlen check matches. */ + if (ret == 0 && *server_rc == WH_ERROR_OK) { + *out_len = len; + } + return ret; +} + + +static const WhNvmTestObjectOps g_dmaTestOps = { + _nvmTestObjectAddDma, + _nvmTestObjectReadDma, +}; + + +int whTest_NvmDma(whClientContext* ctx) +{ + return _runNvmObjectTest(ctx, &g_dmaTestOps, NVM_TEST_DMA_ID_BASE); +} + +#endif /* WOLFHSM_CFG_DMA */ diff --git a/test-refactor/client-server/wh_test_she.c b/test-refactor/client-server/wh_test_she.c new file mode 100644 index 000000000..f5883e403 --- /dev/null +++ b/test-refactor/client-server/wh_test_she.c @@ -0,0 +1,552 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/client-server/wh_test_she.c + * + * Client-side SHE flow. SHE UID and secure-boot state is one-shot + * per server lifetime, so the whole sequence runs as a single test + * against one connected client: one SetUid and one secure boot + * (sized to the comm-buffer boundary), then the load-key vectors, + * UID handling, RND, ECB/CBC/MAC round-trips, and a write-protect + * rejection, which only require that UID is set and secure boot has + * completed. + */ + +#include "wolfhsm/wh_settings.h" + +#if defined(WOLFHSM_CFG_SHE_EXTENSION) && !defined(WOLFHSM_CFG_NO_CRYPTO) && \ + defined(WOLFHSM_CFG_ENABLE_CLIENT) + +#include +#include + +#include "wolfssl/wolfcrypt/settings.h" +#include "wolfssl/wolfcrypt/types.h" +#include "wolfssl/wolfcrypt/cmac.h" + +#include "wolfhsm/wh_common.h" +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_client.h" +#include "wolfhsm/wh_client_she.h" +#include "wolfhsm/wh_she_common.h" +#include "wolfhsm/wh_she_crypto.h" +#include "wolfhsm/wh_message_she.h" + +#include "wh_test_common.h" + +#ifndef TEST_ADMIN_USERNAME +#define TEST_ADMIN_USERNAME "admin" +#endif +#ifndef TEST_ADMIN_PIN +#define TEST_ADMIN_PIN "1234" +#endif + + +/* Destroy a SHE key so the unit tests don't leak NVM objects across + * invocations. Necessary, as SHE doesn't expose a destroy key API since + * SHE keys are supposed to be fixed hardware keys. */ +static int _destroySheKey(whClientContext* client, whNvmId clientSheKeyId) +{ + int rc = 0; + int32_t serverRc = 0; + whNvmId id = WH_MAKE_KEYID(WH_KEYTYPE_SHE, client->comm->client_id, + clientSheKeyId); + + rc = wh_Client_NvmDestroyObjects(client, 1, &id, &serverRc); + if (rc == WH_ERROR_OK) { + rc = serverRc; + } + + return rc; +} + + +/* + * Full SHE flow against a freshly connected client. SetUid and secure + * boot are one-shot per server lifetime, so they run once up front; the + * remaining checks (load-key vectors, UID handling, RND, ECB/CBC/MAC, + * write protect) only need UID set and secure boot complete. + */ +int whTest_She(whClientContext* client) +{ + int ret = 0; + WC_RNG rng[1]; + Cmac cmac[1]; + /* key doubles as the boot MAC key (secure boot) and later the RND + * output that gets loaded as the RAM plain key. */ + uint8_t key[16] = {0}; + uint32_t keySz = sizeof(key); + uint8_t iv[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; + uint8_t plainText[64]; + uint8_t cipherText[64]; + uint8_t finalText[64]; + /* secretKey and prngSeed are taken from SHE test vectors */ + uint8_t sheUid[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}; + uint8_t secretKey[] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, + 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c}; + uint8_t prngSeed[] = {0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, + 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a}; + uint8_t zeros[WH_SHE_BOOT_MAC_PREFIX_LEN] = {0}; + /* Bootloader sized to the comm-buffer boundary so the single secure + * boot exercises the secure-boot update path at the maximum chunk. */ + uint8_t bootloader[WOLFHSM_CFG_COMM_DATA_LEN - + sizeof(whMessageShe_SecureBootUpdateRequest)]; + uint8_t bootMacDigest[16] = {0}; + uint8_t vectorMasterEcuKey[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f}; + uint32_t digestSz = sizeof(bootMacDigest); + uint32_t bootloaderSz; + uint32_t serverCommDataLen = WOLFHSM_CFG_COMM_DATA_LEN; + uint32_t maxBoundaryUpdateChunk = + WOLFHSM_CFG_COMM_DATA_LEN - + sizeof(whMessageShe_SecureBootUpdateRequest); + uint8_t vectorMessageOne[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x41}; + uint8_t vectorMessageTwo[] = {0x2b, 0x11, 0x1e, 0x2d, 0x93, 0xf4, 0x86, + 0x56, 0x6b, 0xcb, 0xba, 0x1d, 0x7f, 0x7a, 0x97, 0x97, 0xc9, 0x46, 0x43, + 0xb0, 0x50, 0xfc, 0x5d, 0x4d, 0x7d, 0xe1, 0x4c, 0xff, 0x68, 0x22, 0x03, + 0xc3}; + uint8_t vectorMessageThree[] = {0xb9, 0xd7, 0x45, 0xe5, 0xac, 0xe7, 0xd4, + 0x18, 0x60, 0xbc, 0x63, 0xc2, 0xb9, 0xf5, 0xbb, 0x46}; + uint8_t vectorMessageFour[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x41, 0xb4, 0x72, 0xe8, + 0xd8, 0x72, 0x7d, 0x70, 0xd5, 0x72, 0x95, 0xe7, 0x48, 0x49, 0xa2, 0x79, + 0x17}; + uint8_t vectorMessageFive[] = {0x82, 0x0d, 0x8d, 0x95, 0xdc, 0x11, 0xb4, + 0x66, 0x88, 0x78, 0x16, 0x0c, 0xb2, 0xa4, 0xe2, 0x3e}; + uint8_t vectorRawKey[] = {0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, + 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00}; + uint8_t outMessageFour[sizeof(vectorMessageFour)]; + uint8_t outMessageFive[sizeof(vectorMessageFive)]; + uint8_t entropy[] = {0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, + 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51}; + uint8_t sreg; + uint8_t messageOne[WH_SHE_M1_SZ]; + uint8_t messageTwo[WH_SHE_M2_SZ]; + uint8_t messageThree[WH_SHE_M3_SZ]; + uint8_t messageFour[WH_SHE_M4_SZ]; + uint8_t messageFive[WH_SHE_M5_SZ]; + const uint32_t SHE_TEST_VECTOR_KEY_ID = 4; + const uint32_t SHE_WP_KEY_ID = 6; + + if (client == NULL) { + return WH_ERROR_BADARGS; + } + +#ifdef WOLFHSM_CFG_ENABLE_AUTHENTICATION + /* Log in as an admin user for the rest of the test */ + WH_TEST_RETURN_ON_FAIL(wh_Client_AuthLogin( + client, WH_AUTH_METHOD_PIN, TEST_ADMIN_USERNAME, TEST_ADMIN_PIN, + strlen(TEST_ADMIN_PIN), &ret, NULL)); +#endif /* WOLFHSM_CFG_ENABLE_AUTHENTICATION */ + + /* === SetUid + boundary-sized secure boot (one-shot per server) === */ + + /* Size the bootloader to the server's comm-buffer boundary so the + * single secure boot drives a maximum-sized secure-boot update. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CommInfo( + client, NULL, NULL, &serverCommDataLen, NULL, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, NULL, NULL, NULL)); + if (serverCommDataLen <= sizeof(whMessageShe_SecureBootUpdateRequest)) { + WH_ERROR_PRINT("Invalid server cfg_comm_data_len %u\n", + (unsigned int)serverCommDataLen); + return WH_ERROR_ABORTED; + } + if (serverCommDataLen < WOLFHSM_CFG_COMM_DATA_LEN) { + maxBoundaryUpdateChunk = + serverCommDataLen - sizeof(whMessageShe_SecureBootUpdateRequest); + } + bootloaderSz = maxBoundaryUpdateChunk; + + /* generate the boot MAC key and a fake bootloader */ + if ((ret = wc_InitRng_ex(rng, NULL, WH_DEV_ID)) != 0) { + WH_ERROR_PRINT("Failed to wc_InitRng_ex %d\n", ret); + goto exit; + } + if ((ret = wc_RNG_GenerateBlock(rng, key, sizeof(key))) != 0) { + WH_ERROR_PRINT("Failed to wc_RNG_GenerateBlock %d\n", ret); + wc_FreeRng(rng); + goto exit; + } + if ((ret = wc_RNG_GenerateBlock(rng, bootloader, + maxBoundaryUpdateChunk)) != 0) { + WH_ERROR_PRINT("Failed to wc_RNG_GenerateBlock %d\n", ret); + wc_FreeRng(rng); + goto exit; + } + /* Done generating test data, free RNG */ + wc_FreeRng(rng); + + /* boot MAC digest: CMAC(0..0 | size | bootloader) */ + if ((ret = wc_InitCmac(cmac, key, sizeof(key), WC_CMAC_AES, NULL)) != 0) { + WH_ERROR_PRINT("Failed to wc_InitCmac %d\n", ret); + goto exit; + } + if ((ret = wc_CmacUpdate(cmac, zeros, sizeof(zeros))) != 0) { + WH_ERROR_PRINT("Failed to wc_CmacUpdate %d\n", ret); + goto exit; + } + if ((ret = wc_CmacUpdate(cmac, (uint8_t*)&bootloaderSz, + sizeof(bootloaderSz))) != 0) { + WH_ERROR_PRINT("Failed to wc_CmacUpdate %d\n", ret); + goto exit; + } + if ((ret = wc_CmacUpdate(cmac, bootloader, bootloaderSz)) != 0) { + WH_ERROR_PRINT("Failed to wc_CmacUpdate %d\n", ret); + goto exit; + } + digestSz = AES_BLOCK_SIZE; + if ((ret = wc_CmacFinal(cmac, bootMacDigest, (word32*)&digestSz)) != 0) { + WH_ERROR_PRINT("Failed to wc_CmacFinal %d\n", ret); + goto exit; + } + /* store the boot MAC key and digest */ + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC_KEY_ID, 0, + key, sizeof(key))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); + goto exit; + } + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_BOOT_MAC, 0, + bootMacDigest, + sizeof(bootMacDigest))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); + goto exit; + } + /* set the she uid */ + if ((ret = wh_Client_SheSetUid(client, sheUid, sizeof(sheUid))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheSetUid %d\n", ret); + goto exit; + } + /* verify bootloader at the comm-buffer boundary */ + if ((ret = wh_Client_SheSecureBoot(client, bootloader, bootloaderSz)) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheSecureBoot %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheGetStatus(client, &sreg)) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheGetStatus %d\n", ret); + goto exit; + } + /* verify bootOk, bootFinished and secureBoot */ + if ((sreg & WH_SHE_SREG_BOOT_OK) == 0 || + (sreg & WH_SHE_SREG_BOOT_FINISHED) == 0 || + (sreg & WH_SHE_SREG_SECURE_BOOT) == 0) { + ret = WH_ERROR_ABORTED; + WH_ERROR_PRINT("Failed to secureBoot with SHE CMAC\n"); + goto exit; + } + WH_TEST_PRINT("SHE secure boot SUCCESS\n"); + + /* === Loadable keys and test vectors === */ + + /* load the secret key and prng seed using pre program */ + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_SECRET_KEY_ID, 0, + secretKey, sizeof(secretKey))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); + goto exit; + } + if ((ret = wh_Client_ShePreProgramKey(client, WH_SHE_PRNG_SEED_ID, 0, + prngSeed, sizeof(prngSeed))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_ShePreProgramKey %d\n", ret); + goto exit; + } + /* load the vector master ecu key */ + if ((ret = wh_She_GenerateLoadableKey(WH_SHE_MASTER_ECU_KEY_ID, + WH_SHE_SECRET_KEY_ID, 1, 0, sheUid, vectorMasterEcuKey, secretKey, + messageOne, messageTwo, messageThree, messageFour, + messageFive)) != 0) { + WH_ERROR_PRINT("Failed to wh_She_GenerateLoadableKey %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheLoadKey(client, messageOne, messageTwo, + messageThree, outMessageFour, outMessageFive)) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheLoadKey %d\n", ret); + goto exit; + } + /* verify that our helper function output matches the vector */ + if ((ret = wh_She_GenerateLoadableKey(SHE_TEST_VECTOR_KEY_ID, + WH_SHE_MASTER_ECU_KEY_ID, 1, 0, sheUid, vectorRawKey, + vectorMasterEcuKey, messageOne, messageTwo, messageThree, + messageFour, messageFive)) != 0) { + WH_ERROR_PRINT("Failed to wh_She_GenerateLoadableKey %d\n", ret); + goto exit; + } + if (memcmp(messageOne, vectorMessageOne, sizeof(vectorMessageOne)) != 0 || + memcmp(messageTwo, vectorMessageTwo, sizeof(vectorMessageTwo)) != 0 || + memcmp(messageThree, vectorMessageThree, + sizeof(vectorMessageThree)) != 0 || + memcmp(messageFour, vectorMessageFour, sizeof(vectorMessageFour)) != 0 || + memcmp(messageFive, vectorMessageFive, sizeof(vectorMessageFive)) != 0) { + ret = WH_ERROR_ABORTED; + WH_ERROR_PRINT("Failed to generate a loadable key to match the " + "vector\n"); + goto exit; + } + WH_TEST_PRINT("SHE wh_SheGenerateLoadableKey SUCCESS\n"); + /* test CMD_LOAD_KEY with test vector */ + if ((ret = wh_Client_SheLoadKey(client, vectorMessageOne, vectorMessageTwo, + vectorMessageThree, outMessageFour, outMessageFive)) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheLoadKey %d\n", ret); + goto exit; + } + if (memcmp(outMessageFour, vectorMessageFour, sizeof(vectorMessageFour)) + != 0 || + memcmp(outMessageFive, vectorMessageFive, + sizeof(vectorMessageFive)) != 0) { + ret = WH_ERROR_ABORTED; + WH_ERROR_PRINT("wh_Client_SheLoadKey FAILED TO MATCH\n"); + goto exit; + } + WH_TEST_PRINT("SHE LOAD KEY SUCCESS\n"); + + /* === LoadKey UID handling === */ + + /* A non-matching UID must be rejected, an all-zero UID must be + * rejected unless the stored target key has WH_SHE_FLAG_WILDCARD set. + * Use wh_She_GenerateLoadableKey with the authKey bytes so M3 is valid + * and the server reaches the UID check instead of failing earlier on + * CMAC verification. */ + { + uint8_t badUid[WH_SHE_UID_SZ]; + uint8_t zeroUid[WH_SHE_UID_SZ] = {0}; + const uint32_t SHE_WILDCARD_KEY_ID = 5; + + memset(badUid, 0xAA, sizeof(badUid)); + + /* Wrong UID targeting an existing key slot. Server must reject + * with WH_SHE_ERC_KEY_UPDATE_ERROR. */ + if ((ret = wh_She_GenerateLoadableKey(SHE_TEST_VECTOR_KEY_ID, + WH_SHE_MASTER_ECU_KEY_ID, 2, 0, badUid, vectorRawKey, + vectorMasterEcuKey, messageOne, messageTwo, messageThree, + messageFour, messageFive)) != 0) { + WH_ERROR_PRINT("Failed to generate bad-UID M1/M2/M3 %d\n", ret); + goto exit; + } + ret = wh_Client_SheLoadKey(client, messageOne, messageTwo, messageThree, + outMessageFour, outMessageFive); + if (ret != WH_SHE_ERC_KEY_UPDATE_ERROR) { + WH_ERROR_PRINT("SHE LOAD KEY bad UID: expected KEY_UPDATE_ERROR, " + "got %d\n", ret); + ret = WH_ERROR_ABORTED; + goto exit; + } + + /* Zero UID targeting an unused slot (stored flags == 0, so + * WH_SHE_FLAG_WILDCARD is clear). Server must reject. */ + if ((ret = wh_She_GenerateLoadableKey(SHE_WILDCARD_KEY_ID, + WH_SHE_MASTER_ECU_KEY_ID, 1, 0, zeroUid, vectorRawKey, + vectorMasterEcuKey, messageOne, messageTwo, messageThree, + messageFour, messageFive)) != 0) { + WH_ERROR_PRINT("Failed to generate zero-UID no-wildcard " + "M1/M2/M3 %d\n", ret); + goto exit; + } + ret = wh_Client_SheLoadKey(client, messageOne, messageTwo, messageThree, + outMessageFour, outMessageFive); + if (ret != WH_SHE_ERC_KEY_UPDATE_ERROR) { + WH_ERROR_PRINT("SHE LOAD KEY zero UID without wildcard: expected " + "KEY_UPDATE_ERROR, got %d\n", ret); + ret = WH_ERROR_ABORTED; + goto exit; + } + + /* Preload the target slot with WH_SHE_FLAG_WILDCARD and count + * 0 via ShePreProgramKey, which writes the meta label directly + * (wh_She_GenerateLoadableKey cannot encode flags > 4 bits due + * to the M2 layout overlap between flags and count). Then + * re-load the slot with an all-zero UID; the server must + * accept it because the stored flags contain WILDCARD. */ + if ((ret = wh_Client_ShePreProgramKey(client, SHE_WILDCARD_KEY_ID, + WH_SHE_FLAG_WILDCARD, vectorRawKey, sizeof(vectorRawKey))) + != 0) { + WH_ERROR_PRINT("Failed to preload wildcard key %d\n", ret); + goto exit; + } + if ((ret = wh_She_GenerateLoadableKey(SHE_WILDCARD_KEY_ID, + WH_SHE_MASTER_ECU_KEY_ID, 1, 0, zeroUid, vectorRawKey, + vectorMasterEcuKey, messageOne, messageTwo, messageThree, + messageFour, messageFive)) != 0) { + WH_ERROR_PRINT("Failed to generate zero-UID wildcard " + "M1/M2/M3 %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheLoadKey(client, messageOne, messageTwo, + messageThree, outMessageFour, outMessageFive)) != 0) { + WH_ERROR_PRINT("SHE LOAD KEY zero UID with wildcard: expected " + "success, got %d\n", ret); + goto exit; + } + + if ((ret = _destroySheKey(client, SHE_WILDCARD_KEY_ID)) != 0) { + WH_ERROR_PRINT("Failed to _destroySheKey wildcard slot, ret=%d\n", + ret); + goto exit; + } + WH_TEST_PRINT("SHE LOAD KEY UID checks SUCCESS\n"); + } + + /* === RND === */ + + if ((ret = wh_Client_SheInitRnd(client)) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheInitRnd %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheRnd(client, key, &keySz)) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheRnd %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheExtendSeed(client, entropy, sizeof(entropy))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheExtendSeed %d\n", ret); + goto exit; + } + WH_TEST_PRINT("SHE RND SUCCESS\n"); + + /* === RAM key ECB/CBC/MAC round-trips === */ + + if ((ret = wh_Client_SheLoadPlainKey(client, key, sizeof(key))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheLoadPlainKey %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheEncEcb(client, WH_SHE_RAM_KEY_ID, plainText, + cipherText, sizeof(plainText))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheEncEcb %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheExportRamKey(client, messageOne, messageTwo, + messageThree, messageFour, messageFive)) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheExportRamKey %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheLoadKey(client, messageOne, messageTwo, + messageThree, messageFour, messageFive)) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheLoadKey %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheDecEcb(client, WH_SHE_RAM_KEY_ID, cipherText, + finalText, sizeof(cipherText))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheDecEcb %d\n", ret); + goto exit; + } + if (memcmp(finalText, plainText, sizeof(plainText)) != 0) { + ret = WH_ERROR_ABORTED; + WH_ERROR_PRINT("SHE ECB FAILED TO MATCH\n"); + goto exit; + } + WH_TEST_PRINT("SHE ECB SUCCESS\n"); + if ((ret = wh_Client_SheEncCbc(client, WH_SHE_RAM_KEY_ID, iv, sizeof(iv), + plainText, cipherText, sizeof(plainText))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheEncCbc %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheDecCbc(client, WH_SHE_RAM_KEY_ID, iv, sizeof(iv), + cipherText, finalText, sizeof(cipherText))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheDecCbc %d\n", ret); + goto exit; + } + if (memcmp(finalText, plainText, sizeof(plainText)) != 0) { + ret = WH_ERROR_ABORTED; + WH_ERROR_PRINT("SHE CBC FAILED TO MATCH\n"); + goto exit; + } + WH_TEST_PRINT("SHE CBC SUCCESS\n"); + if ((ret = wh_Client_SheGenerateMac(client, WH_SHE_RAM_KEY_ID, plainText, + sizeof(plainText), cipherText, sizeof(cipherText))) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheGenerateMac %d\n", ret); + goto exit; + } + if ((ret = wh_Client_SheVerifyMac(client, WH_SHE_RAM_KEY_ID, plainText, + sizeof(plainText), cipherText, sizeof(cipherText), &sreg)) != 0) { + WH_ERROR_PRINT("Failed to wh_Client_SheVerifyMac %d\n", ret); + goto exit; + } + if (sreg != 0) { + ret = WH_ERROR_ABORTED; + WH_ERROR_PRINT("SHE CMAC FAILED TO VERIFY\n"); + goto exit; + } + WH_TEST_PRINT("SHE CMAC SUCCESS\n"); + + /* === Write protect === */ + + /* A key pre-programmed with WH_SHE_FLAG_WRITE_PROTECT cannot be + * overwritten via SHE LoadKey; the server must return + * WH_SHE_ERC_WRITE_PROTECTED. Reuses the secret key (auth) and the + * secure boot established above; uses a clean slot of its own. */ + if ((ret = wh_Client_ShePreProgramKey(client, SHE_WP_KEY_ID, + WH_SHE_FLAG_WRITE_PROTECT, + vectorRawKey, + sizeof(vectorRawKey))) != 0) { + WH_ERROR_PRINT("Failed to pre-program write-protected key %d\n", ret); + goto exit; + } + if ((ret = wh_She_GenerateLoadableKey(SHE_WP_KEY_ID, WH_SHE_SECRET_KEY_ID, + 1, 0, sheUid, vectorRawKey, secretKey, messageOne, messageTwo, + messageThree, messageFour, messageFive)) != 0) { + WH_ERROR_PRINT("Failed to generate loadable key %d\n", ret); + goto exit; + } + ret = wh_Client_SheLoadKey(client, messageOne, messageTwo, messageThree, + messageFour, messageFive); + if (ret != WH_SHE_ERC_WRITE_PROTECTED) { + WH_ERROR_PRINT("Expected WH_SHE_ERC_WRITE_PROTECTED, got %d\n", ret); + ret = WH_ERROR_ABORTED; + goto exit; + } + ret = 0; + WH_TEST_PRINT("SHE write protect SUCCESS\n"); + + /* === Cleanup: destroy provisioned keys so we don't leak NVM === */ + + if ((ret = _destroySheKey(client, WH_SHE_BOOT_MAC_KEY_ID)) != 0) { + WH_ERROR_PRINT("Failed to _destroySheKey, ret=%d\n", ret); + goto exit; + } + if ((ret = _destroySheKey(client, WH_SHE_BOOT_MAC)) != 0) { + WH_ERROR_PRINT("Failed to _destroySheKey, ret=%d\n", ret); + goto exit; + } + if ((ret = _destroySheKey(client, WH_SHE_SECRET_KEY_ID)) != 0) { + WH_ERROR_PRINT("Failed to _destroySheKey, ret=%d\n", ret); + goto exit; + } + if ((ret = _destroySheKey(client, WH_SHE_PRNG_SEED_ID)) != 0) { + WH_ERROR_PRINT("Failed to _destroySheKey, ret=%d\n", ret); + goto exit; + } + if ((ret = _destroySheKey(client, WH_SHE_MASTER_ECU_KEY_ID)) != 0) { + WH_ERROR_PRINT("Failed to _destroySheKey, ret=%d\n", ret); + goto exit; + } + if ((ret = _destroySheKey(client, SHE_TEST_VECTOR_KEY_ID)) != 0) { + WH_ERROR_PRINT("Failed to _destroySheKey, ret=%d\n", ret); + goto exit; + } + if ((ret = _destroySheKey(client, SHE_WP_KEY_ID)) != 0) { + WH_ERROR_PRINT("Failed to _destroySheKey, ret=%d\n", ret); + goto exit; + } + +exit: + return ret; +} + +#endif /* WOLFHSM_CFG_SHE_EXTENSION && !WOLFHSM_CFG_NO_CRYPTO && \ + WOLFHSM_CFG_ENABLE_CLIENT */ diff --git a/test-refactor/misc/wh_test_client_devid.c b/test-refactor/misc/wh_test_client_devid.c new file mode 100644 index 000000000..5c5568c2a --- /dev/null +++ b/test-refactor/misc/wh_test_client_devid.c @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2026 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 . + */ + +/* + * test-refactor/misc/wh_test_client_devid.c + * + * Client devId registration lifecycle. Equivalent coverage to + * test/wh_test_multiclient.c::whTest_MultiClientDevIdLifecycle. + * + * wh_Client_Init registers the client's devIds in wolfCrypt's process-global, + * fixed-size cryptoCb table and wh_Client_Cleanup must unregister them: the + * table is only reset when the last wolfCrypt user in the process cleans up, + * so a leaked entry both consumes a table slot and keeps dispatching into the + * dead client context. Every Init rebinds the global WH_DEV_ID (and + * WH_DEV_ID_DMA with DMA) to its own context and additionally registers the + * configured devId when it differs from WH_DEV_ID; any client's Cleanup + * unregisters the globals. These tests observe table occupancy through the + * only public accessors (Register/UnRegister) by counting how many throwaway + * registrations fit before the table is full. + */ + +#include "wolfhsm/wh_settings.h" + +#if defined(WOLFHSM_CFG_ENABLE_CLIENT) && !defined(WOLFHSM_CFG_NO_CRYPTO) && \ + defined(WOLF_CRYPTO_CB) + +#include +#include +#include + +#include "wolfssl/wolfcrypt/settings.h" +#include "wolfssl/wolfcrypt/types.h" +#include "wolfssl/wolfcrypt/cryptocb.h" + +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_comm.h" +#include "wolfhsm/wh_transport_mem.h" +#include "wolfhsm/wh_client.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + +#define BUFFER_SIZE 4096 + +/* Throwaway devId base for probing free cryptoCb table slots ("WHT\0"+i). + * Outside the global devIds (WH_DEV_ID / WH_DEV_ID_DMA), the custom test + * devIds, and the fill range below. */ +#define PROBE_DEV_ID_BASE 0x57485400 +/* Upper bound on probed slots. Must be >= wolfCrypt's + * MAX_CRYPTO_DEVID_CALLBACKS (internal to cryptocb.c; default 8). */ +#define PROBE_MAX_SLOTS 128 + +/* Separate devId base ("WHU\0"+i) for table-fill entries that stay + * registered while _countFreeCryptoCbSlots() runs: wolfCrypt re-registration + * of an existing devId reuses its entry, so fill ids must never collide with + * the probe ids or the count comes back wrong (and the counter's + * unregistration pass would tear the fill entries down). */ +#define FILL_DEV_ID_BASE 0x57485500 + +/* Global devIds rebound by every wh_Client_Init: WH_DEV_ID, plus + * WH_DEV_ID_DMA when DMA support is compiled in. Their table slots are + * shared by all clients in the process (each Init rebinds the same + * entries). */ +#ifdef WOLFHSM_CFG_DMA +#define GLOBAL_DEVID_COUNT 2 +#else +#define GLOBAL_DEVID_COUNT 1 +#endif + +/* Slots consumed by one wh_Client_Init with a custom (non-default) devId on + * an otherwise unoccupied table: the globals plus the configured devId */ +#define DEVIDS_PER_INIT (GLOBAL_DEVID_COUNT + 1) + +/* Custom per-client devIds for the two-client cases ("WH"+n). Distinct from + * WH_DEV_ID, WH_DEV_ID_DMA, and the probe range. */ +#define TEST_DEVID_1 0x57480001 +#define TEST_DEVID_2 0x57480002 + +static int _probeCryptoCb(int devId, wc_CryptoInfo* info, void* ctx) +{ + (void)devId; + (void)info; + (void)ctx; + return CRYPTOCB_UNAVAILABLE; +} + +/* Count free slots in the cryptoCb table by registering throwaway devIds + * until registration fails, then unregistering them all. */ +static int _countFreeCryptoCbSlots(void) +{ + int count = 0; + int i; + + for (i = 0; i < PROBE_MAX_SLOTS; i++) { + if (wc_CryptoCb_RegisterDevice(PROBE_DEV_ID_BASE + i, _probeCryptoCb, + NULL) != 0) { + break; + } + count++; + } + for (i = 0; i < count; i++) { + wc_CryptoCb_UnRegisterDevice(PROBE_DEV_ID_BASE + i); + } + return count; +} + +static int _whTest_ClientDevIdLifecycle(void) +{ + int slotsBase = 0; + int slots = 0; + int rc = 0; + int i = 0; + + /* Client transports: no servers needed, registration lifecycle only */ + static uint8_t req1[BUFFER_SIZE]; + static uint8_t resp1[BUFFER_SIZE]; + whTransportMemConfig tmcf1[1] = {{ + .req = (whTransportMemCsr*)req1, + .req_size = sizeof(req1), + .resp = (whTransportMemCsr*)resp1, + .resp_size = sizeof(resp1), + }}; + + whTransportClientCb tccb1[1] = {WH_TRANSPORT_MEM_CLIENT_CB}; + whTransportMemClientContext tmcc1[1] = {0}; + whCommClientConfig cc_conf1[1] = {{ + .transport_cb = tccb1, + .transport_context = (void*)tmcc1, + .transport_config = (void*)tmcf1, + .client_id = WH_TEST_DEFAULT_CLIENT_ID, + }}; + whClientContext client1[1] = {0}; + whClientConfig c_conf1[1] = {{ + .comm = cc_conf1, + }}; + + static uint8_t req2[BUFFER_SIZE]; + static uint8_t resp2[BUFFER_SIZE]; + whTransportMemConfig tmcf2[1] = {{ + .req = (whTransportMemCsr*)req2, + .req_size = sizeof(req2), + .resp = (whTransportMemCsr*)resp2, + .resp_size = sizeof(resp2), + }}; + + whTransportClientCb tccb2[1] = {WH_TRANSPORT_MEM_CLIENT_CB}; + whTransportMemClientContext tmcc2[1] = {0}; + whCommClientConfig cc_conf2[1] = {{ + .transport_cb = tccb2, + .transport_context = (void*)tmcc2, + .transport_config = (void*)tmcf2, + .client_id = WH_TEST_DEFAULT_CLIENT_ID + 1, + }}; + whClientContext client2[1] = {0}; + whClientConfig c_conf2[1] = {{ + .comm = cc_conf2, + }}; + + /* Client ids outside 1..WH_CLIENT_ID_MAX are rejected before any + * initialization */ + cc_conf1[0].client_id = 0; + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == wh_Client_Init(client1, c_conf1)); + cc_conf1[0].client_id = WH_CLIENT_ID_MAX + 1; + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == wh_Client_Init(client1, c_conf1)); + cc_conf1[0].client_id = WH_TEST_DEFAULT_CLIENT_ID; + + /* Negative devIds and (with DMA) the reserved WH_DEV_ID_DMA are + * rejected before any initialization */ + c_conf1[0].devId = -1; + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == wh_Client_Init(client1, c_conf1)); +#ifdef WOLFHSM_CFG_DMA + c_conf1[0].devId = WH_DEV_ID_DMA; + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == wh_Client_Init(client1, c_conf1)); +#endif /* WOLFHSM_CFG_DMA */ + c_conf1[0].devId = 0; + + /* Hold an app-level wolfCrypt reference for the whole test so the + * cryptoCb table is never reset by a final wolfCrypt_Cleanup: any entry + * a client leaks stays visible, as it would in a process with other + * active wolfCrypt users. */ + WH_TEST_RETURN_ON_FAIL(wolfCrypt_Init()); + + slotsBase = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slotsBase >= GLOBAL_DEVID_COUNT + 2); + + /* A config that leaves devId 0 binds the default WH_DEV_ID; only the + * global devIds occupy table slots */ + WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client1, c_conf1)); + WH_TEST_ASSERT_RETURN(WH_CLIENT_DEVID(client1) == WH_DEV_ID); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase - GLOBAL_DEVID_COUNT); + + /* Cleanup must release every slot Init consumed even though wolfCrypt + * stays initialized (the app still holds a reference) */ + WH_TEST_RETURN_ON_FAIL(wh_Client_Cleanup(client1)); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase); + + /* Re-init with the same config must succeed and register again */ + WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client1, c_conf1)); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase - GLOBAL_DEVID_COUNT); + WH_TEST_RETURN_ON_FAIL(wh_Client_Cleanup(client1)); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase); + + /* A custom configured devId is registered alongside the globals */ + c_conf1[0].devId = TEST_DEVID_1; + WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client1, c_conf1)); + WH_TEST_ASSERT_RETURN(WH_CLIENT_DEVID(client1) == TEST_DEVID_1); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase - DEVIDS_PER_INIT); + + /* Two simultaneously active clients with distinct devIds: the second + * Init rebinds the shared global entries (net zero new slots) and adds + * only its own devId */ + c_conf2[0].devId = TEST_DEVID_2; + WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client2, c_conf2)); + WH_TEST_ASSERT_RETURN(WH_CLIENT_DEVID(client2) == TEST_DEVID_2); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase - DEVIDS_PER_INIT - 1); + + /* Cleaning up one client releases its own devId and the shared global + * devIds -- the globals are yanked from the still-active sibling, which + * is the documented single-client contract for WH_DEV_ID/WH_DEV_ID_DMA. + * The sibling's own configured devId stays registered. */ + WH_TEST_RETURN_ON_FAIL(wh_Client_Cleanup(client1)); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase - 1); + + /* Re-init the first client while the second stays active: the globals + * are rebound and both custom devIds are live again */ + WH_TEST_RETURN_ON_FAIL(wh_Client_Init(client1, c_conf1)); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase - DEVIDS_PER_INIT - 1); + + WH_TEST_RETURN_ON_FAIL(wh_Client_Cleanup(client2)); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase - 1); + + WH_TEST_RETURN_ON_FAIL(wh_Client_Cleanup(client1)); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase); + c_conf1[0].devId = 0; + c_conf2[0].devId = 0; + + /* Init with a full cryptoCb table must fail cleanly (WH_ERROR_ABORTED) + * and the failure-path cleanup must not disturb existing entries */ + for (i = 0; i < slotsBase; i++) { + WH_TEST_RETURN_ON_FAIL(wc_CryptoCb_RegisterDevice( + FILL_DEV_ID_BASE + i, _probeCryptoCb, NULL)); + } + rc = wh_Client_Init(client1, c_conf1); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_ABORTED); + WH_TEST_ASSERT_RETURN(WH_CLIENT_DEVID(client1) == 0); + for (i = 0; i < slotsBase; i++) { + wc_CryptoCb_UnRegisterDevice(FILL_DEV_ID_BASE + i); + } + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase); + + /* Init that fails partway through its registrations (the custom devId + * fits, but a later global rebind hits the full table) must unwind + * exactly the entries it registered and leave the fill entries intact */ + for (i = 0; i < slotsBase - (DEVIDS_PER_INIT - 1); i++) { + WH_TEST_RETURN_ON_FAIL(wc_CryptoCb_RegisterDevice( + FILL_DEV_ID_BASE + i, _probeCryptoCb, NULL)); + } + c_conf1[0].devId = TEST_DEVID_1; + rc = wh_Client_Init(client1, c_conf1); + WH_TEST_ASSERT_RETURN(rc == WH_ERROR_ABORTED); + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == DEVIDS_PER_INIT - 1); + for (i = 0; i < slotsBase - (DEVIDS_PER_INIT - 1); i++) { + wc_CryptoCb_UnRegisterDevice(FILL_DEV_ID_BASE + i); + } + slots = _countFreeCryptoCbSlots(); + WH_TEST_ASSERT_RETURN(slots == slotsBase); + c_conf1[0].devId = 0; + + (void)wolfCrypt_Cleanup(); + + return 0; +} + +int whTest_ClientDevId(void* ctx) +{ + (void)ctx; + + WH_TEST_PRINT("Testing client devId registration lifecycle...\n"); + WH_TEST_RETURN_ON_FAIL(_whTest_ClientDevIdLifecycle()); + + return WH_ERROR_OK; +} + +#endif /* WOLFHSM_CFG_ENABLE_CLIENT && !WOLFHSM_CFG_NO_CRYPTO && \ + * WOLF_CRYPTO_CB */ diff --git a/test-refactor/misc/wh_test_hwkeystore.c b/test-refactor/misc/wh_test_hwkeystore.c new file mode 100644 index 000000000..fd59ff5e4 --- /dev/null +++ b/test-refactor/misc/wh_test_hwkeystore.c @@ -0,0 +1,664 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/misc/wh_test_hwkeystore.c + * + * End-to-end hardware-only key (WH_KEYTYPE_HW) coverage. Lives in the + * misc group because it requires a server with a known hardware keystore + * backend bound, which the port's shared server does not guarantee: this + * test spins up its own client/server pair over the mem transport, binds an + * emulated hardware keystore serving a single AES-256 KEK, and pumps the + * server inline between the split (non-blocking) client request/response + * calls so no threading is needed. + * + * _whTest_HwKeystoreKeyWrap - key wrap / unwrap-export / unwrap-and-cache + * with a hardware-only KEK; wrapped+hwonly + * flag precedence; unserved-id and wrong-KEK + * negative paths + * _whTest_HwKeystoreDataWrap - data wrap/unwrap roundtrip with a + * hardware-only KEK + * _whTest_HwKeystoreRejections - keystore operations on a hardware-only id + * must fail with WH_ERROR_ACCESS + * + * Crypto key use of a hardware-only id (rejected at the keystore freshen + * choke point) is covered server-side in server/wh_test_hwkeystore_server.c, + * since the blocking wolfCrypt callback cannot be pumped single-threaded. + */ + +#include "wolfhsm/wh_settings.h" + +#if defined(WOLFHSM_CFG_ENABLE_CLIENT) && \ + defined(WOLFHSM_CFG_ENABLE_SERVER) && !defined(WOLFHSM_CFG_NO_CRYPTO) && \ + defined(WOLFHSM_CFG_KEYWRAP) && defined(WOLFHSM_CFG_HWKEYSTORE) && \ + !defined(NO_AES) && defined(HAVE_AESGCM) + +#include +#include + +#include "wolfssl/wolfcrypt/settings.h" +#include "wolfssl/wolfcrypt/types.h" + +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_common.h" +#include "wolfhsm/wh_comm.h" +#include "wolfhsm/wh_keyid.h" +#include "wolfhsm/wh_hwkeystore.h" +#include "wolfhsm/wh_nvm.h" +#include "wolfhsm/wh_nvm_flash.h" +#include "wolfhsm/wh_flash_ramsim.h" +#include "wolfhsm/wh_transport_mem.h" +#include "wolfhsm/wh_client.h" +#include "wolfhsm/wh_server.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + +#define BUFFER_SIZE 4096 +#define FLASH_RAM_SIZE (1024 * 1024) +#define FLASH_SECTOR_SIZE (128 * 1024) +#define FLASH_PAGE_SIZE 8 + +/* Id and material of the only KEK served by the emulated hardware keystore */ +#define WH_TEST_HWKEK_ID 3 +#define WH_TEST_HWKEK_SIZE 32 + +#define WH_TEST_AES_KEYSIZE 32 +#define WH_TEST_WRAPPED_KEYID 20 +#define WH_TEST_WRAPPED_KEYSIZE \ + (WH_KEYWRAP_AES_GCM_HEADER_SIZE + WH_TEST_AES_KEYSIZE + \ + sizeof(whNvmMetadata)) + +static const uint8_t _hwKekMaterial[WH_TEST_HWKEK_SIZE] = { + 0x9a, 0x4e, 0x21, 0xc7, 0x5d, 0x10, 0xfb, 0x33, 0x6f, 0x82, 0xd4, + 0x59, 0xee, 0x07, 0xb1, 0x2c, 0x48, 0x95, 0x3a, 0xc6, 0x71, 0x0d, + 0xb8, 0xe5, 0x12, 0x6a, 0xf9, 0x84, 0x2f, 0xd0, 0x5b, 0xa7}; + +static int _HwKeystoreGetKey(void* context, whKeyId keyId, uint8_t* out, + uint16_t* inout_len) +{ + (void)context; + + /* Only hardware-only keyIds should ever reach a hardware keystore */ + if (WH_KEYID_TYPE(keyId) != WH_KEYTYPE_HW) { + return WH_ERROR_ACCESS; + } + + /* Serve only the known test KEK id, refuse everything else */ + if (WH_KEYID_ID(keyId) != WH_TEST_HWKEK_ID) { + return WH_ERROR_NOTFOUND; + } + + if ((out == NULL) || (inout_len == NULL) || + (*inout_len < sizeof(_hwKekMaterial))) { + return WH_ERROR_BUFFER_SIZE; + } + + memcpy(out, _hwKekMaterial, sizeof(_hwKekMaterial)); + *inout_len = sizeof(_hwKekMaterial); + return WH_ERROR_OK; +} + +/* clang-format off */ +static const whHwKeystoreCb _hwKeystoreCb = { + .Init = NULL, + .Cleanup = NULL, + .GetKey = _HwKeystoreGetKey, +}; +/* clang-format on */ + +/* Self-contained client/server pair over the mem transport. The server is + * pumped inline (wh_Server_HandleRequestMessage) between each split client + * request/response pair */ +typedef struct { + whServerContext server[1]; + whClientContext client[1]; + whNvmContext nvm[1]; + whHwKeystoreContext hwKeystore[1]; +#ifndef WOLFHSM_CFG_NO_CRYPTO + whServerCryptoContext crypto[1]; +#endif + /* Transport */ + uint8_t reqBuf[BUFFER_SIZE]; + uint8_t respBuf[BUFFER_SIZE]; + whTransportMemConfig tmcf[1]; + whTransportServerCb tscb[1]; + whTransportMemServerContext tmsc[1]; + whCommServerConfig cs_conf[1]; + whTransportClientCb tccb[1]; + whTransportMemClientContext tmcc[1]; + whCommClientConfig cc_conf[1]; + whClientConfig c_conf[1]; + /* Flash / NVM */ + whFlashRamsimCtx fc[1]; + whFlashRamsimCfg fc_conf[1]; + whFlashCb fcb[1]; + whNvmFlashConfig nf_conf[1]; + whNvmFlashContext nfc[1]; + whNvmCb nfcb[1]; + whNvmConfig n_conf[1]; + /* Hardware keystore */ + whHwKeystoreConfig hwks_conf[1]; + whServerConfig s_conf[1]; +} TestCtx; + +/* Static to keep the misc group's stack footprint small */ +static TestCtx _testCtx; +static uint8_t _flashMemory[FLASH_RAM_SIZE]; + +static int _SetupClientServer(TestCtx* t) +{ + uint32_t client_id = 0; + uint32_t server_id = 0; + + memset(t, 0, sizeof(*t)); + memset(_flashMemory, 0, sizeof(_flashMemory)); + + /* Transport */ + t->tmcf[0] = (whTransportMemConfig){ + .req = (whTransportMemCsr*)t->reqBuf, + .req_size = sizeof(t->reqBuf), + .resp = (whTransportMemCsr*)t->respBuf, + .resp_size = sizeof(t->respBuf), + }; + t->tscb[0] = (whTransportServerCb)WH_TRANSPORT_MEM_SERVER_CB; + t->cs_conf[0] = (whCommServerConfig){ + .transport_cb = t->tscb, + .transport_context = (void*)t->tmsc, + .transport_config = (void*)t->tmcf, + .server_id = 124, + }; + t->tccb[0] = (whTransportClientCb)WH_TRANSPORT_MEM_CLIENT_CB; + t->cc_conf[0] = (whCommClientConfig){ + .transport_cb = t->tccb, + .transport_context = (void*)t->tmcc, + .transport_config = (void*)t->tmcf, + .client_id = WH_TEST_DEFAULT_CLIENT_ID, + }; + t->c_conf[0] = (whClientConfig){ + .comm = t->cc_conf, + }; + + /* Flash / NVM */ + t->fc_conf[0] = (whFlashRamsimCfg){ + .size = FLASH_RAM_SIZE, + .sectorSize = FLASH_SECTOR_SIZE, + .pageSize = FLASH_PAGE_SIZE, + .erasedByte = ~(uint8_t)0, + .memory = _flashMemory, + }; + t->fcb[0] = (whFlashCb)WH_FLASH_RAMSIM_CB; + t->nf_conf[0] = (whNvmFlashConfig){ + .cb = t->fcb, + .context = t->fc, + .config = t->fc_conf, + }; + t->nfcb[0] = (whNvmCb)WH_NVM_FLASH_CB; + t->n_conf[0] = (whNvmConfig){ + .cb = t->nfcb, + .context = t->nfc, + .config = t->nf_conf, + }; + + /* Hardware keystore front-end backed by the test getKey callback */ + t->hwks_conf[0] = (whHwKeystoreConfig){ + .cb = &_hwKeystoreCb, + .context = NULL, + }; + + /* Server config */ + t->s_conf[0] = (whServerConfig){ + .comm_config = t->cs_conf, + .nvm = t->nvm, +#ifndef WOLFHSM_CFG_NO_CRYPTO + .crypto = t->crypto, + .devId = INVALID_DEVID, +#endif + .hwKeystore = t->hwKeystore, + }; + + WH_TEST_RETURN_ON_FAIL(wh_Nvm_Init(t->nvm, t->n_conf)); + WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Init(t->hwKeystore, t->hwks_conf)); + WH_TEST_RETURN_ON_FAIL(wolfCrypt_Init()); + WH_TEST_RETURN_ON_FAIL(wc_InitRng_ex(t->crypto->rng, NULL, INVALID_DEVID)); + WH_TEST_RETURN_ON_FAIL(wh_Server_Init(t->server, t->s_conf)); + WH_TEST_RETURN_ON_FAIL( + wh_Server_SetConnected(t->server, WH_COMM_CONNECTED)); + WH_TEST_RETURN_ON_FAIL(wh_Client_Init(t->client, t->c_conf)); + + /* Comm init so the server learns the client id */ + WH_TEST_RETURN_ON_FAIL(wh_Client_CommInitRequest(t->client)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_CommInitResponse(t->client, &client_id, &server_id)); + WH_TEST_ASSERT_RETURN(client_id == t->client->comm->client_id); + + return WH_ERROR_OK; +} + +static void _CleanupClientServer(TestCtx* t) +{ + (void)wh_Client_Cleanup(t->client); + (void)wh_Server_Cleanup(t->server); + (void)wh_Nvm_Cleanup(t->nvm); + (void)wh_HwKeystore_Cleanup(t->hwKeystore); + (void)wc_FreeRng(t->crypto->rng); + (void)wolfCrypt_Cleanup(); +} + +/* Sequential wrappers: send the request, pump the server once, then collect + * the response. The server handler reports operation errors in the response + * rc, which the client response call returns */ + +static int _KeyWrap(TestCtx* t, whKeyId kekId, uint8_t* keyIn, uint16_t keySz, + whNvmMetadata* meta, uint8_t* wrappedOut, + uint16_t* wrappedSz) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest( + t->client, WC_CIPHER_AES_GCM, kekId, keyIn, keySz, meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_KeyWrapResponse(t->client, WC_CIPHER_AES_GCM, wrappedOut, + wrappedSz); +} + +static int _KeyUnwrapAndExport(TestCtx* t, whKeyId kekId, uint8_t* wrappedIn, + uint16_t wrappedSz, whNvmMetadata* metaOut, + uint8_t* keyOut, uint16_t* keySz) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportRequest( + t->client, WC_CIPHER_AES_GCM, kekId, wrappedIn, wrappedSz)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_KeyUnwrapAndExportResponse(t->client, WC_CIPHER_AES_GCM, + metaOut, keyOut, keySz); +} + +static int _KeyUnwrapAndCache(TestCtx* t, whKeyId kekId, uint8_t* wrappedIn, + uint16_t wrappedSz, uint16_t* keyIdOut) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndCacheRequest( + t->client, WC_CIPHER_AES_GCM, kekId, wrappedIn, wrappedSz)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_KeyUnwrapAndCacheResponse(t->client, WC_CIPHER_AES_GCM, + keyIdOut); +} + +static int _DataWrap(TestCtx* t, whKeyId kekId, uint8_t* dataIn, + uint32_t dataSz, uint8_t* wrappedOut, uint32_t* wrappedSz) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_DataWrapRequest( + t->client, WC_CIPHER_AES_GCM, kekId, dataIn, dataSz)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_DataWrapResponse(t->client, WC_CIPHER_AES_GCM, wrappedOut, + wrappedSz); +} + +static int _DataUnwrap(TestCtx* t, whKeyId kekId, uint8_t* wrappedIn, + uint32_t wrappedSz, uint8_t* dataOut, uint32_t* dataSz) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_DataUnwrapRequest( + t->client, WC_CIPHER_AES_GCM, kekId, wrappedIn, wrappedSz)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_DataUnwrapResponse(t->client, WC_CIPHER_AES_GCM, dataOut, + dataSz); +} + +static int _KeyCache(TestCtx* t, uint32_t flags, uint8_t* label, + uint16_t labelSz, uint8_t* keyIn, uint16_t keySz, + uint16_t* keyIdInOut) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + t->client, flags, label, labelSz, keyIn, keySz, *keyIdInOut)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_KeyCacheResponse(t->client, keyIdInOut); +} + +static int _KeyEvict(TestCtx* t, uint16_t keyId) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(t->client, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_KeyEvictResponse(t->client); +} + +static int _KeyExport(TestCtx* t, uint16_t keyId, uint8_t* label, + uint16_t labelSz, uint8_t* keyOut, uint16_t* keySz) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(t->client, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_KeyExportResponse(t->client, label, labelSz, keyOut, + keySz); +} + +static int _KeyCommit(TestCtx* t, uint16_t keyId) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCommitRequest(t->client, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_KeyCommitResponse(t->client); +} + +static int _KeyErase(TestCtx* t, uint16_t keyId) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEraseRequest(t->client, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_KeyEraseResponse(t->client); +} + +static int _KeyRevoke(TestCtx* t, uint16_t keyId) +{ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyRevokeRequest(t->client, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + return wh_Client_KeyRevokeResponse(t->client); +} + +static int _whTest_HwKeystoreKeyWrap(TestCtx* t) +{ + int ret; + whKeyId hwKekId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID); + uint8_t plainKey[WH_TEST_AES_KEYSIZE]; + uint8_t tmpPlainKey[WH_TEST_AES_KEYSIZE]; + uint16_t tmpPlainKeySz = sizeof(tmpPlainKey); + uint8_t wrappedKey[WH_TEST_WRAPPED_KEYSIZE]; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint16_t unwrappedKeyId = WH_KEYID_ERASED; + whNvmMetadata metadata = {0}; + whNvmMetadata tmpMetadata = {0}; + size_t i; + + metadata.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(t->client->comm->client_id, + WH_TEST_WRAPPED_KEYID); + metadata.len = WH_TEST_AES_KEYSIZE; + metadata.flags = WH_NVM_FLAGS_USAGE_ANY; + memcpy(metadata.label, "HW KEK wrapped key", sizeof("HW KEK wrapped key")); + + /* Fixed pattern; distinct from the KEK material */ + for (i = 0; i < sizeof(plainKey); i++) { + plainKey[i] = (uint8_t)(0xA0 ^ i); + } + + /* Wrap a key using the hardware-only KEK */ + ret = _KeyWrap(t, hwKekId, plainKey, sizeof(plainKey), &metadata, + wrappedKey, &wrappedKeySz); + if (ret != WH_ERROR_OK) { + WH_ERROR_PRINT("Failed to KeyWrap with HW KEK %d\n", ret); + return ret; + } + + /* Unwrap and export with the hardware-only KEK, check the roundtrip */ + ret = _KeyUnwrapAndExport(t, hwKekId, wrappedKey, wrappedKeySz, + &tmpMetadata, tmpPlainKey, &tmpPlainKeySz); + if (ret != WH_ERROR_OK) { + WH_ERROR_PRINT("Failed to KeyUnwrapAndExport with HW KEK %d\n", ret); + return ret; + } + + if (memcmp(plainKey, tmpPlainKey, sizeof(plainKey)) != 0) { + WH_ERROR_PRINT("HW KEK wrap/unwrap key failed to match\n"); + return WH_ERROR_ABORTED; + } + + if (memcmp(&metadata, &tmpMetadata, sizeof(metadata)) != 0) { + WH_ERROR_PRINT("HW KEK wrap/unwrap metadata failed to match\n"); + return WH_ERROR_ABORTED; + } + + /* Unwrap-and-cache with the hardware-only KEK: the wrapped payload is an + * ordinary key and may enter the cache; only the KEK itself is + * hardware-resident */ + ret = _KeyUnwrapAndCache(t, hwKekId, wrappedKey, wrappedKeySz, + &unwrappedKeyId); + if (ret != WH_ERROR_OK) { + WH_ERROR_PRINT("Failed to KeyUnwrapAndCache with HW KEK %d\n", ret); + return ret; + } + WH_TEST_RETURN_ON_FAIL(_KeyEvict(t, unwrappedKeyId)); + + /* A wrapped+hardware-only KEK id must behave as hardware-only (the + * hardware-only flag takes precedence) */ + wrappedKeySz = sizeof(wrappedKey); + ret = _KeyWrap(t, hwKekId | WH_KEYID_CLIENT_WRAPPED_FLAG, plainKey, + sizeof(plainKey), &metadata, wrappedKey, &wrappedKeySz); + if (ret != WH_ERROR_OK) { + WH_ERROR_PRINT("Failed to KeyWrap with wrapped+HW KEK %d\n", ret); + return ret; + } + + /* Unwrapping a hardware-KEK-wrapped blob with a different, cached KEK + * must fail authentication, proving distinct key material was used */ + { + uint16_t cachedKekId = WH_KEYID_ERASED; + uint8_t cachedKek[WH_TEST_HWKEK_SIZE]; + uint8_t kekLabel[] = "cached-kek"; + + for (i = 0; i < sizeof(cachedKek); i++) { + cachedKek[i] = (uint8_t)(0x5C ^ i); + } + + WH_TEST_RETURN_ON_FAIL(_KeyCache( + t, WH_NVM_FLAGS_NONEXPORTABLE | WH_NVM_FLAGS_USAGE_WRAP, kekLabel, + sizeof(kekLabel), cachedKek, sizeof(cachedKek), &cachedKekId)); + + tmpPlainKeySz = sizeof(tmpPlainKey); + ret = _KeyUnwrapAndExport(t, cachedKekId, wrappedKey, wrappedKeySz, + &tmpMetadata, tmpPlainKey, &tmpPlainKeySz); + WH_TEST_RETURN_ON_FAIL(_KeyEvict(t, cachedKekId)); + if (ret == WH_ERROR_OK) { + WH_ERROR_PRINT("Unwrap with wrong KEK unexpectedly succeeded\n"); + return WH_ERROR_ABORTED; + } + } + + /* A hardware KEK id the backend does not serve must fail */ + wrappedKeySz = sizeof(wrappedKey); + ret = _KeyWrap(t, WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID + 1), plainKey, + sizeof(plainKey), &metadata, wrappedKey, &wrappedKeySz); + if (ret != WH_ERROR_NOTFOUND) { + WH_ERROR_PRINT("KeyWrap with unserved HW KEK expected NOTFOUND, " + "got %d\n", + ret); + return WH_ERROR_ABORTED; + } + + return WH_ERROR_OK; +} + +static int _whTest_HwKeystoreDataWrap(TestCtx* t) +{ + int ret; + whKeyId hwKekId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID); + uint8_t data[] = "Example data!"; + uint8_t unwrappedData[sizeof(data)] = {0}; + uint32_t unwrappedDataSz = sizeof(unwrappedData); + uint8_t wrappedData[sizeof(data) + WH_KEYWRAP_AES_GCM_HEADER_SIZE] = {0}; + uint32_t wrappedDataSz = sizeof(wrappedData); + + ret = + _DataWrap(t, hwKekId, data, sizeof(data), wrappedData, &wrappedDataSz); + if (ret != WH_ERROR_OK) { + WH_ERROR_PRINT("Failed to DataWrap with HW KEK %d\n", ret); + return ret; + } + + ret = _DataUnwrap(t, hwKekId, wrappedData, sizeof(wrappedData), + unwrappedData, &unwrappedDataSz); + if (ret != WH_ERROR_OK) { + WH_ERROR_PRINT("Failed to DataUnwrap with HW KEK %d\n", ret); + return ret; + } + + if (memcmp(data, unwrappedData, sizeof(data)) != 0) { + WH_ERROR_PRINT("HW KEK unwrapped data failed to match input data\n"); + return WH_ERROR_ABORTED; + } + + return WH_ERROR_OK; +} + +/* Hardware-only keys must be rejected by every keystore operation; only the + * keywrap KEK paths may resolve them. Crypto key use is rejected at the same + * keystore choke points, covered in server/wh_test_hwkeystore_server.c */ +static int _whTest_HwKeystoreRejections(TestCtx* t) +{ + int ret; + whKeyId hwKeyId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID); + uint8_t buf[WH_TEST_HWKEK_SIZE] = {0}; + uint16_t bufSz = sizeof(buf); + uint8_t label[WH_NVM_LABEL_LEN] = "hwonly reject"; + uint16_t cacheKeyId = hwKeyId; + + /* Caching key material under a hardware-only id must be rejected */ + ret = _KeyCache(t, WH_NVM_FLAGS_NONE, label, sizeof(label), buf, + sizeof(buf), &cacheKeyId); + if (ret != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("KeyCache of HW-only id expected ACCESS, got %d\n", ret); + return WH_ERROR_ABORTED; + } + +#ifdef WOLFHSM_CFG_DMA + /* The DMA cache path must reject hardware-only ids as well */ + cacheKeyId = hwKeyId; + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheDmaRequest( + t->client, WH_NVM_FLAGS_NONE, label, sizeof(label), buf, sizeof(buf), + cacheKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(t->server)); + ret = wh_Client_KeyCacheDmaResponse(t->client, &cacheKeyId); + if (ret != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("KeyCacheDma of HW-only id expected ACCESS, got %d\n", + ret); + return WH_ERROR_ABORTED; + } +#endif + + /* Exporting a hardware-only key must be rejected */ + ret = _KeyExport(t, hwKeyId, label, sizeof(label), buf, &bufSz); + if (ret != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("KeyExport of HW-only id expected ACCESS, got %d\n", + ret); + return WH_ERROR_ABORTED; + } + + /* Commit/evict/erase/revoke of a hardware-only key must be rejected */ + ret = _KeyCommit(t, hwKeyId); + if (ret != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("KeyCommit of HW-only id expected ACCESS, got %d\n", + ret); + return WH_ERROR_ABORTED; + } + + ret = _KeyEvict(t, hwKeyId); + if (ret != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("KeyEvict of HW-only id expected ACCESS, got %d\n", ret); + return WH_ERROR_ABORTED; + } + + ret = _KeyErase(t, hwKeyId); + if (ret != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("KeyErase of HW-only id expected ACCESS, got %d\n", ret); + return WH_ERROR_ABORTED; + } + + ret = _KeyRevoke(t, hwKeyId); + if (ret != WH_ERROR_ACCESS) { + WH_ERROR_PRINT("KeyRevoke of HW-only id expected ACCESS, got %d\n", + ret); + return WH_ERROR_ABORTED; + } + + return WH_ERROR_OK; +} + +/* A hardware-only KEK requested against a server that has no hardware keystore + * bound (HWKEYSTORE compiled in, whServerConfig.hwKeystore left NULL) must fail + * cleanly with NOTFOUND from the keywrap KEK resolver. Simulate the unbound + * configuration by detaching the server's keystore for the duration of the + * checks, then restore it for teardown */ +static int _whTest_HwKeystoreUnbound(TestCtx* t) +{ + int ret; + whKeyId hwKekId = WH_CLIENT_KEYID_MAKE_HW(WH_TEST_HWKEK_ID); + uint8_t plainKey[WH_TEST_AES_KEYSIZE] = {0}; + uint8_t wrappedKey[WH_TEST_WRAPPED_KEYSIZE]; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t data[] = "Example data!"; + uint8_t wrappedData[sizeof(data) + WH_KEYWRAP_AES_GCM_HEADER_SIZE]; + uint32_t wrappedDataSz = sizeof(wrappedData); + whNvmMetadata metadata = {0}; + whHwKeystoreContext* saved = t->server->hwKeystore; + + metadata.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(t->client->comm->client_id, + WH_TEST_WRAPPED_KEYID); + metadata.len = WH_TEST_AES_KEYSIZE; + metadata.flags = WH_NVM_FLAGS_USAGE_ANY; + + /* Detach the hardware keystore to mimic a server without one bound */ + t->server->hwKeystore = NULL; + + /* KeyWrap (usage-enforcing KEK path) must report NOTFOUND */ + ret = _KeyWrap(t, hwKekId, plainKey, sizeof(plainKey), &metadata, + wrappedKey, &wrappedKeySz); + if (ret != WH_ERROR_NOTFOUND) { + t->server->hwKeystore = saved; + WH_ERROR_PRINT("KeyWrap with HW KEK and no bound keystore expected " + "NOTFOUND, got %d\n", + ret); + return WH_ERROR_ABORTED; + } + + /* DataWrap (non-enforcing KEK path) must report NOTFOUND as well */ + ret = + _DataWrap(t, hwKekId, data, sizeof(data), wrappedData, &wrappedDataSz); + if (ret != WH_ERROR_NOTFOUND) { + t->server->hwKeystore = saved; + WH_ERROR_PRINT("DataWrap with HW KEK and no bound keystore expected " + "NOTFOUND, got %d\n", + ret); + return WH_ERROR_ABORTED; + } + + /* Restore the keystore for teardown */ + t->server->hwKeystore = saved; + + return WH_ERROR_OK; +} + +int whTest_HwKeystore(void* ctx) +{ + int ret; + TestCtx* t = &_testCtx; + + (void)ctx; + + WH_TEST_RETURN_ON_FAIL(_SetupClientServer(t)); + + ret = _whTest_HwKeystoreKeyWrap(t); + if (ret == WH_ERROR_OK) { + ret = _whTest_HwKeystoreDataWrap(t); + } + if (ret == WH_ERROR_OK) { + ret = _whTest_HwKeystoreRejections(t); + } + if (ret == WH_ERROR_OK) { + ret = _whTest_HwKeystoreUnbound(t); + } + + _CleanupClientServer(t); + + return ret; +} + +#endif /* WOLFHSM_CFG_ENABLE_CLIENT && WOLFHSM_CFG_ENABLE_SERVER && \ + !WOLFHSM_CFG_NO_CRYPTO && WOLFHSM_CFG_KEYWRAP && \ + WOLFHSM_CFG_HWKEYSTORE && !NO_AES && HAVE_AESGCM */ diff --git a/test-refactor/misc/wh_test_multiclient.c b/test-refactor/misc/wh_test_multiclient.c new file mode 100644 index 000000000..df9e74f45 --- /dev/null +++ b/test-refactor/misc/wh_test_multiclient.c @@ -0,0 +1,1658 @@ +/* + * 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 . + */ + +/* + * test-refactor/misc/wh_test_multiclient.c + * + * Multi-client test framework and test suites + * + * Provides reusable setup/teardown infrastructure for testing features that + * require multiple clients. Each client connects to its own server instance, + * but both servers share a common NVM context to enable testing of shared + * resources (global keys, shared counters, etc.). + */ + +#include "wolfhsm/wh_settings.h" + +/* Legacy test/wh_test.c gates the whTest_MultiClient() call site on + * !WOLFHSM_CFG_NO_CRYPTO. In test-refactor that gate has to live at the + * file level so the test reports SKIPPED rather than running an empty + * fixture under NOCRYPTO. */ +#if defined(WOLFHSM_CFG_ENABLE_CLIENT) && defined(WOLFHSM_CFG_ENABLE_SERVER) \ + && !defined(WOLFHSM_CFG_NO_CRYPTO) + +#include +#include +#include + +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_comm.h" +#include "wolfhsm/wh_message.h" +#include "wolfhsm/wh_transport_mem.h" +#include "wolfhsm/wh_client.h" +#include "wolfhsm/wh_server.h" +#include "wolfhsm/wh_server_keystore.h" +#include "wolfhsm/wh_nvm.h" +#include "wolfhsm/wh_nvm_flash.h" +#include "wolfhsm/wh_flash_ramsim.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + +/* Test configuration */ +#define FLASH_RAM_SIZE (1024 * 1024) /* 1MB */ +#define FLASH_SECTOR_SIZE (128 * 1024) /* 128KB */ +#define FLASH_PAGE_SIZE (8) /* 8B */ +#define BUFFER_SIZE 4096 + +#ifdef WOLFHSM_CFG_GLOBAL_KEYS +/* Test key data */ +static const uint8_t TEST_KEY_DATA_1[] = "TestGlobalKey1Data"; +static const uint8_t TEST_KEY_DATA_2[] = "TestLocalKey2Data"; +static const uint8_t TEST_KEY_DATA_3[] = "TestGlobalKey3DataLonger"; +#endif + +/* ============================================================================ + * DUMMY KEY ID DEFINITIONS + * + * Generic key ID values for use in tests. Actual keyIds are defined locally + * within each test function and macros (WH_CLIENT_KEYID_MAKE_GLOBAL, etc.) are + * applied at assignment time. + * ========================================================================== */ + +#define DUMMY_KEYID_1 1 +#define DUMMY_KEYID_2 2 + +/* ============================================================================ + * MULTI-CLIENT TEST FRAMEWORK INFRASTRUCTURE + * ========================================================================== */ + +/* Server contexts for connect callbacks */ +static whServerContext* testServer1 = NULL; +static whServerContext* testServer2 = NULL; + +/* Connect callback for client 1 */ +static int _connectCb1(void* context, whCommConnected connected) +{ + (void)context; + if (testServer1 == NULL) { + return WH_ERROR_BADARGS; + } + return wh_Server_SetConnected(testServer1, connected); +} + +/* Connect callback for client 2 */ +static int _connectCb2(void* context, whCommConnected connected) +{ + (void)context; + if (testServer2 == NULL) { + return WH_ERROR_BADARGS; + } + return wh_Server_SetConnected(testServer2, connected); +} + +/* ============================================================================ + * GLOBAL KEYS TEST SUITE + * ========================================================================== */ + +#ifdef WOLFHSM_CFG_GLOBAL_KEYS + +/* + * Test 1: Basic global key operations + * - Client 1 caches a global key + * - Client 2 reads the same global key and verifies the data matches + */ +static int _testGlobalKeyBasic(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int ret; + whKeyId keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + uint8_t label[WH_NVM_LABEL_LEN]; + uint16_t labelSz = sizeof(label); + uint8_t outBuf[sizeof(TEST_KEY_DATA_1)] = {0}; + uint16_t outSz = sizeof(outBuf); + + WH_TEST_PRINT("Test: Global key basic operations\n"); + + /* Client 1 caches a global key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, 0, (uint8_t*)"GlobalKey5", sizeof("GlobalKey5"), + (uint8_t*)TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1), keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &keyId)); + + /* Client 2 reads the same global key */ + keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client2, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportResponse(client2, label, labelSz, outBuf, &outSz)); + + /* Verify the key data matches */ + WH_TEST_ASSERT_RETURN(outSz == sizeof(TEST_KEY_DATA_1)); + WH_TEST_ASSERT_RETURN(0 == memcmp(outBuf, TEST_KEY_DATA_1, outSz)); + + WH_TEST_PRINT(" PASS: Basic global key operations\n"); + + (void)ret; + return 0; +} + +/* + * Test 2: Local key isolation + * - Both clients cache local keys with the same ID but different data + * - Each client verifies they can only read their own local key data, not the + * other's + */ +static int _testLocalKeyIsolation(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int ret; + whKeyId keyId1 = DUMMY_KEYID_1; /* Local key for client 1 */ + whKeyId keyId2 = + DUMMY_KEYID_1; /* Same ID for client 2 - should be different key */ + uint8_t label[WH_NVM_LABEL_LEN]; + uint16_t labelSz = sizeof(label); + uint8_t outBuf[32] = {0}; + uint16_t outSz; + + WH_TEST_PRINT("Test: Local key isolation\n"); + + /* Client 1 caches a local key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, 0, (uint8_t*)"LocalKey10_C1", sizeof("LocalKey10_C1"), + (uint8_t*)TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1), keyId1)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &keyId1)); + + /* Client 2 caches a different local key with same ID */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client2, 0, (uint8_t*)"LocalKey10_C2", sizeof("LocalKey10_C2"), + (uint8_t*)TEST_KEY_DATA_2, sizeof(TEST_KEY_DATA_2), keyId2)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client2, &keyId2)); + + /* Client 1 reads its own key */ + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client1, keyId1)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportResponse(client1, label, labelSz, outBuf, &outSz)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(outBuf, TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1))); + + /* Client 2 reads its own key (different data) */ + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client2, keyId2)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportResponse(client2, label, labelSz, outBuf, &outSz)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(outBuf, TEST_KEY_DATA_2, sizeof(TEST_KEY_DATA_2))); + + WH_TEST_PRINT(" PASS: Local key isolation\n"); + + (void)ret; + return 0; +} + +/* + * Test 3: Mixed global and local keys with no cross-cache interference + * - Client 1 caches both a global key and a local key with the same ID number + * - Client 2 caches a local key with the same ID (different data) + * - Client 1 can read both its global and local keys correctly + * - Client 2 can access the global key + * - Client 2 can read its own local key (different data than client 1's) + * - Client 2 correctly fails to access Client 1's local key + */ +static int _testMixedGlobalLocal(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int ret; + whKeyId globalKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + whKeyId localKeyId = DUMMY_KEYID_1; /* Same ID number but local */ + uint8_t label[WH_NVM_LABEL_LEN]; + uint16_t labelSz = sizeof(label); + uint8_t outBuf[32] = {0}; + uint16_t outSz; + + WH_TEST_DEBUG_PRINT( + "Test: Mixed global and local keys with no cross-cache interference\n"); + + /* Client 1 caches global key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, 0, (uint8_t*)"Global15", sizeof("Global15"), + (uint8_t*)TEST_KEY_DATA_3, sizeof(TEST_KEY_DATA_3), globalKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &globalKeyId)); + + /* Client 1 caches local key with same ID number */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, 0, (uint8_t*)"Local15_C1", sizeof("Local15_C1"), + (uint8_t*)TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1), localKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &localKeyId)); + + /* Client 2 caches local key with same ID number (different data) */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client2, 0, (uint8_t*)"Local15_C2", sizeof("Local15_C2"), + (uint8_t*)TEST_KEY_DATA_2, sizeof(TEST_KEY_DATA_2), localKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client2, &localKeyId)); + + /* Client 1 reads its global key */ + globalKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client1, globalKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportResponse(client1, label, labelSz, outBuf, &outSz)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(outBuf, TEST_KEY_DATA_3, sizeof(TEST_KEY_DATA_3))); + + /* Client 1 reads its local key */ + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client1, localKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportResponse(client1, label, labelSz, outBuf, &outSz)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(outBuf, TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1))); + + /* Client 2 accesses global key (should work) */ + globalKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client2, globalKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportResponse(client2, label, labelSz, outBuf, &outSz)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(outBuf, TEST_KEY_DATA_3, sizeof(TEST_KEY_DATA_3))); + + /* Client 2 reads its own local key 15 (different data) */ + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client2, localKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportResponse(client2, label, labelSz, outBuf, &outSz)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(outBuf, TEST_KEY_DATA_2, sizeof(TEST_KEY_DATA_2))); + + /* Client 1 tries to access Client 2's local key 15 - should fail */ + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client1, localKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + ret = wh_Client_KeyExportResponse(client1, label, labelSz, outBuf, &outSz); + /* Should get client 1's own local key, not client 2's */ + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(outBuf, TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1))); + + WH_TEST_PRINT(" PASS: Mixed global and local keys with no cross-cache " + "interference\n"); + + (void)ret; + return 0; +} + +/* + * Test 4: NVM persistence of global keys + * - Client 1 caches a global key and commits it to NVM, then evicts it from + * cache + * - Client 2 successfully reloads the global key from NVM + */ +static int _testGlobalKeyNvmPersistence(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int ret; + whKeyId keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + uint8_t label[WH_NVM_LABEL_LEN]; + uint16_t labelSz = sizeof(label); + uint8_t outBuf[sizeof(TEST_KEY_DATA_1)] = {0}; + uint16_t outSz; + + WH_TEST_PRINT("Test: NVM persistence of global keys\n"); + + /* Client 1 caches and commits a global key to NVM */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, 0, (uint8_t*)"GlobalNVM20", sizeof("GlobalNVM20"), + (uint8_t*)TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1), keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &keyId)); + + /* Commit to NVM */ + keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCommitRequest(client1, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCommitResponse(client1)); + + /* Evict from cache on server1 */ + keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + /* Client 2 reads from NVM (will reload to cache) */ + keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client2, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportResponse(client2, label, labelSz, outBuf, &outSz)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(outBuf, TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1))); + + /* Clean up - erase the key */ + keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEraseRequest(client1, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEraseResponse(client1)); + + WH_TEST_PRINT(" PASS: NVM persistence of global keys\n"); + + (void)ret; + return 0; +} + +/* + * Test 5: Export protection on global keys + * - Client 1 caches a non-exportable global key + * - Client 2 correctly fails when attempting to export the protected key + */ +static int _testGlobalKeyExportProtection(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int ret; + whKeyId keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + uint8_t label[WH_NVM_LABEL_LEN]; + uint16_t labelSz = sizeof(label); + uint8_t outBuf[sizeof(TEST_KEY_DATA_1)] = {0}; + uint16_t outSz; + + WH_TEST_PRINT("Test: Export protection on global keys\n"); + + /* Client 1 caches a non-exportable global key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_NONEXPORTABLE, (uint8_t*)"NoExport25", + sizeof("NoExport25"), (uint8_t*)TEST_KEY_DATA_1, + sizeof(TEST_KEY_DATA_1), keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &keyId)); + + /* Client 2 tries to export it - should fail */ + keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client2, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + ret = wh_Client_KeyExportResponse(client2, label, labelSz, outBuf, &outSz); + /* Should fail due to non-exportable flag */ + WH_TEST_ASSERT_RETURN(ret != 0); + + /* Clean up */ + keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Export protection on global keys\n"); + + return 0; +} + +#ifdef WOLFHSM_CFG_DMA +/* + * Test 6: DMA operations with global keys + * - Client 1 caches a global key using DMA transfer + * - Client 2 reads the DMA-cached global key via regular export + * - Client 1 caches another global key via regular method + * - Client 2 exports that global key via DMA transfer + */ +static int _testGlobalKeyDma(whClientContext* client1, whServerContext* server1, + whClientContext* client2, whServerContext* server2) +{ + int ret; + whKeyId keyId1 = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + whKeyId keyId2 = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_2); + uint8_t keyData1[32] = "GlobalDmaCacheTestKey123456!"; + uint8_t keyData2[32] = "GlobalDmaExportTestKey12345!"; + uint8_t outBuf[32] = {0}; + uint8_t label[WH_NVM_LABEL_LEN]; + uint16_t labelSz = sizeof(label); + uint16_t outSz; + + WH_TEST_PRINT("Test: DMA operations with global keys\n"); + + /* Part 1: Cache via DMA, export via regular */ + /* Client 1 caches a global key using DMA */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheDmaRequest( + client1, 0, (uint8_t*)"DmaGlobal35", sizeof("DmaGlobal35"), keyData1, + sizeof(keyData1), keyId1)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheDmaResponse(client1, &keyId1)); + + /* Client 2 reads the global key via regular export */ + keyId1 = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest(client2, keyId1)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportResponse(client2, label, labelSz, outBuf, &outSz)); + + /* Verify the key data matches */ + WH_TEST_ASSERT_RETURN(outSz == sizeof(keyData1)); + WH_TEST_ASSERT_RETURN(0 == memcmp(outBuf, keyData1, outSz)); + + /* Part 2: Cache via regular, export via DMA */ + /* Client 1 caches a global key using regular method */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, 0, (uint8_t*)"DmaExport40", sizeof("DmaExport40"), keyData2, + sizeof(keyData2), keyId2)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &keyId2)); + + /* Client 2 exports the global key via DMA */ + keyId2 = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_2); + outSz = sizeof(outBuf); + memset(outBuf, 0, sizeof(outBuf)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportDmaRequest(client2, keyId2, outBuf, sizeof(outBuf))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportDmaResponse(client2, label, labelSz, &outSz)); + + /* Verify the key data matches */ + WH_TEST_ASSERT_RETURN(outSz == sizeof(keyData2)); + WH_TEST_ASSERT_RETURN(0 == memcmp(outBuf, keyData2, outSz)); + + /* Clean up */ + keyId1 = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, keyId1)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + keyId2 = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_2); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, keyId2)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: DMA operations with global keys\n"); + + (void)ret; + return 0; +} +#endif /* WOLFHSM_CFG_DMA */ + +#ifdef WOLFHSM_CFG_KEYWRAP +/* + * Test 7: Key wrap with global server key + * - Client 1 caches a global wrapping key + * - Client 2 wraps a key using that global server key + * - Client 1 unwraps the key using the same global server key + */ +static int _testGlobalKeyWrapExport(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int ret; + whKeyId serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + uint8_t wrapKey[AES_256_KEY_SIZE] = "GlobalWrapKey123456789012345!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "PlainKeyToWrap1234567890123!"; + /* Wrapped key size = IV(12) + TAG(16) + KEYSIZE(32) + metadata */ +#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t unwrappedKey[AES_256_KEY_SIZE] = {0}; + uint16_t unwrappedKeySz = sizeof(unwrappedKey); + whNvmMetadata meta = {0}; + + WH_TEST_PRINT("Test: Key wrap with global server key\n"); + + /* Client 1 caches a global wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey45", + sizeof("WrapKey45"), wrapKey, sizeof(wrapKey), serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); + + /* Client 2 wraps a global key using the global server key */ + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + meta.id = + WH_CLIENT_KEYID_MAKE_WRAPPED_META(WH_KEYUSER_GLOBAL, DUMMY_KEYID_2); + meta.len = sizeof(plainKey); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client2, WC_CIPHER_AES_GCM, + serverKeyId, plainKey, + sizeof(plainKey), &meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( + client2, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); + + /* Client 1 unwraps the key using the same global server key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportRequest( + client1, WC_CIPHER_AES_GCM, serverKeyId, wrappedKey, + sizeof(wrappedKey))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportResponse( + client1, WC_CIPHER_AES_GCM, &meta, unwrappedKey, &unwrappedKeySz)); + + /* Verify the unwrapped key matches the original */ + WH_TEST_ASSERT_RETURN(0 == + memcmp(unwrappedKey, plainKey, sizeof(plainKey))); + + /* Clean up */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Key wrap with global server key\n"); + + (void)ret; + return 0; +#undef WRAPPED_KEY_SIZE +} + +/* + * Test 8: Key unwrap and cache with global server key + * - Client 1 caches a global wrapping key and wraps a key (also global) + * - Client 2 unwraps and caches the key using the global server key + * - Client 2 exports and verifies the cached key matches the original + */ +static int _testGlobalKeyUnwrapCache(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + int ret; + whKeyId serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + whKeyId cachedKeyId = 0; + uint8_t wrapKey[AES_256_KEY_SIZE] = "GlobalUnwrapKey123456789012!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "KeyToCacheViaUnwrap123456!!"; +#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t verifyBuf[AES_256_KEY_SIZE] = {0}; + uint8_t label[WH_NVM_LABEL_LEN]; + uint16_t labelSz = sizeof(label); + uint16_t verifySz = sizeof(verifyBuf); + whNvmMetadata meta = {0}; + + WH_TEST_PRINT("Test: Key unwrap and cache with global server key\n"); + + /* Client 1 caches a global wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"UnwrapKey50", + sizeof("UnwrapKey50"), wrapKey, sizeof(wrapKey), serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); + + /* Client 1 wraps a global key */ + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + meta.id = + WH_CLIENT_KEYID_MAKE_WRAPPED_META(WH_KEYUSER_GLOBAL, DUMMY_KEYID_2); + meta.len = sizeof(plainKey); + + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client1, WC_CIPHER_AES_GCM, + serverKeyId, plainKey, + sizeof(plainKey), &meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( + client1, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); + + /* Client 2 unwraps and caches the key using the global server key */ + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + ret = wh_Client_KeyUnwrapAndCacheRequest(client2, WC_CIPHER_AES_GCM, + serverKeyId, wrappedKey, + sizeof(wrappedKey)); + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_OK); + + ret = wh_Server_HandleRequestMessage(server2); + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_OK); + + ret = wh_Client_KeyUnwrapAndCacheResponse(client2, WC_CIPHER_AES_GCM, + &cachedKeyId); + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_OK); + + /* Verify the cached key by exporting it */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest( + client2, WH_CLIENT_KEYID_MAKE_WRAPPED_GLOBAL(cachedKeyId))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportResponse(client2, label, labelSz, + verifyBuf, &verifySz)); + + /* Verify the exported key matches the original */ + WH_TEST_ASSERT_RETURN(0 == memcmp(verifyBuf, plainKey, sizeof(plainKey))); + + /* Clean up */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest( + client2, WH_CLIENT_KEYID_MAKE_WRAPPED_GLOBAL(cachedKeyId))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client2)); + + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Key unwrap and cache with global server key\n"); + + (void)ret; + return 0; +#undef WRAPPED_KEY_SIZE +} + +/* + * Test 7a: Global wrapping key + Global wrapped key (Positive) + * - Client 1 caches a global wrapping key + * - Client 2 wraps a global key using it + * - Client 1 unwraps and exports successfully + */ +static int _testWrappedKey_GlobalWrap_GlobalKey_Positive( + whClientContext* client1, whServerContext* server1, + whClientContext* client2, whServerContext* server2) +{ + int ret; + whKeyId serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + uint8_t wrapKey[AES_256_KEY_SIZE] = "GlobalWrapKey2Test7aXXXXXXXXX!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "GlobalPlainKey2Test7aXXXXXXXX!"; +#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t unwrappedKey[AES_256_KEY_SIZE] = {0}; + uint16_t unwrappedKeySz = sizeof(unwrappedKey); + whNvmMetadata meta = {0}; + + WH_TEST_DEBUG_PRINT("Test 7a: Global wrap key + Global wrapped key (Positive)\n"); + + /* Client 1 caches a global wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey_7a", + sizeof("WrapKey_7a"), wrapKey, sizeof(wrapKey), serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); + + /* Client 2 wraps a GLOBAL key using the global server key */ + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + meta.id = + WH_CLIENT_KEYID_MAKE_WRAPPED_META(WH_KEYUSER_GLOBAL, DUMMY_KEYID_2); + meta.len = sizeof(plainKey); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client2, WC_CIPHER_AES_GCM, + serverKeyId, plainKey, + sizeof(plainKey), &meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( + client2, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); + + /* Client 1 unwraps and exports the global key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportRequest( + client1, WC_CIPHER_AES_GCM, serverKeyId, wrappedKey, + sizeof(wrappedKey))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportResponse( + client1, WC_CIPHER_AES_GCM, &meta, unwrappedKey, &unwrappedKeySz)); + + /* Verify the unwrapped key matches the original */ + WH_TEST_ASSERT_RETURN(0 == + memcmp(unwrappedKey, plainKey, sizeof(plainKey))); + + /* Clean up */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Global wrap key + Global wrapped key (Positive)\n"); + + (void)ret; + return 0; +#undef WRAPPED_KEY_SIZE +} + +/* + * Test 7b: Global wrapping key + Global wrapped key (Negative - NONEXPORTABLE) + * - Client 1 caches a global wrapping key + * - Client 2 wraps a global key with NONEXPORTABLE flag + * - Client 1 unwrap-and-export fails with WH_ERROR_ACCESS + */ +static int _testWrappedKey_GlobalWrap_GlobalKey_NonExportable( + whClientContext* client1, whServerContext* server1, + whClientContext* client2, whServerContext* server2) +{ + int ret; + whKeyId serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + uint8_t wrapKey[AES_256_KEY_SIZE] = "GlobalWrapKey2Test7bXXXXXXXXX!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "GlobalPlainKey2Test7bXXXXXXXX!"; +#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t unwrappedKey[AES_256_KEY_SIZE] = {0}; + uint16_t unwrappedKeySz = sizeof(unwrappedKey); + whNvmMetadata meta = {0}; + + WH_TEST_DEBUG_PRINT("Test 7b: Global wrap key + Global wrapped key (Non-exportable)\n"); + + /* Client 1 caches a global wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey_7b", + sizeof("WrapKey_7b"), wrapKey, sizeof(wrapKey), serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); + + /* Client 2 wraps a GLOBAL key with NONEXPORTABLE flag */ + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + meta.id = + WH_CLIENT_KEYID_MAKE_WRAPPED_META(WH_KEYUSER_GLOBAL, DUMMY_KEYID_2); + meta.len = sizeof(plainKey); + meta.flags = WH_NVM_FLAGS_NONEXPORTABLE; + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client2, WC_CIPHER_AES_GCM, + serverKeyId, plainKey, + sizeof(plainKey), &meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( + client2, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); + + /* Client 1 tries to unwrap and export - should fail */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportRequest( + client1, WC_CIPHER_AES_GCM, serverKeyId, wrappedKey, + sizeof(wrappedKey))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + ret = wh_Client_KeyUnwrapAndExportResponse( + client1, WC_CIPHER_AES_GCM, &meta, unwrappedKey, &unwrappedKeySz); + + /* Should fail due to non-exportable flag */ + WH_TEST_ASSERT_RETURN(ret != 0); + + /* Clean up */ + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Global wrap key + Global wrapped key (Non-exportable)\n"); + + return 0; +#undef WRAPPED_KEY_SIZE +} + +/* + * Test 8a: Global wrapping key + Local wrapped key (Positive - Owner) + * - Client 1 caches a global wrapping key + * - Client 2 wraps a LOCAL key (USER=client2_id) using global wrapping key + * - Client 2 unwraps and exports successfully (owner) + */ +static int _testWrappedKey_GlobalWrap_LocalKey_OwnerExport( + whClientContext* client1, whServerContext* server1, + whClientContext* client2, whServerContext* server2) +{ + int ret; + whKeyId serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + uint16_t client2Id = WH_TEST_DEFAULT_CLIENT_ID + 1; + uint8_t wrapKey[AES_256_KEY_SIZE] = "GlobalWrapKey2Test8aXXXXXXXXX!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "LocalPlainKey2Test8aXXXXXXXXX!"; +#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t unwrappedKey[AES_256_KEY_SIZE] = {0}; + uint16_t unwrappedKeySz = sizeof(unwrappedKey); + whNvmMetadata meta = {0}; + + WH_TEST_DEBUG_PRINT("Test 8a: Global wrap key + Local wrapped key (Owner export)\n"); + + /* Client 1 caches a global wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey_8a", + sizeof("WrapKey_8a"), wrapKey, sizeof(wrapKey), serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); + + /* Client 2 wraps a LOCAL key (USER=client2_id) */ + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + meta.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(client2Id, DUMMY_KEYID_2); + meta.len = sizeof(plainKey); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client2, WC_CIPHER_AES_GCM, + serverKeyId, plainKey, + sizeof(plainKey), &meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( + client2, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); + + /* Client 2 (owner) unwraps and exports the local key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportRequest( + client2, WC_CIPHER_AES_GCM, serverKeyId, wrappedKey, + sizeof(wrappedKey))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportResponse( + client2, WC_CIPHER_AES_GCM, &meta, unwrappedKey, &unwrappedKeySz)); + + /* Verify the unwrapped key matches the original */ + WH_TEST_ASSERT_RETURN(0 == + memcmp(unwrappedKey, plainKey, sizeof(plainKey))); + + /* Clean up */ + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Global wrap key + Local wrapped key (Owner export)\n"); + + (void)ret; + return 0; +#undef WRAPPED_KEY_SIZE +} + +/* + * Test 8b: Global wrapping key + Local wrapped key (Negative - Non-owner) + * - Client 1 caches a global wrapping key + * - Client 2 wraps a LOCAL key (USER=client2_id) + * - Client 1 unwrap-and-export fails with WH_ERROR_ACCESS (not owner) + * - Client 1 unwrap-and-cache also fails with WH_ERROR_ACCESS + */ +static int _testWrappedKey_GlobalWrap_LocalKey_NonOwnerFails( + whClientContext* client1, whServerContext* server1, + whClientContext* client2, whServerContext* server2) +{ + int ret; + whKeyId serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + uint16_t client2Id = WH_TEST_DEFAULT_CLIENT_ID + 1; + uint8_t wrapKey[AES_256_KEY_SIZE] = "GlobalWrapKey2Test8bXXXXXXXXX!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "LocalPlainKey2Test8bXXXXXXXXX!"; +#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t unwrappedKey[AES_256_KEY_SIZE] = {0}; + uint16_t unwrappedKeySz = sizeof(unwrappedKey); + whNvmMetadata meta = {0}; + whKeyId cachedKeyId = 0; + + WH_TEST_DEBUG_PRINT("Test 8b: Global wrap key + Local wrapped key (Non-owner fails)\n"); + + /* Client 1 caches a global wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey_8b", + sizeof("WrapKey_8b"), wrapKey, sizeof(wrapKey), serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); + + /* Client 2 wraps a LOCAL key (USER=client2_id) */ + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + meta.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(client2Id, DUMMY_KEYID_2); + meta.len = sizeof(plainKey); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client2, WC_CIPHER_AES_GCM, + serverKeyId, plainKey, + sizeof(plainKey), &meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( + client2, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); + + /* Client 1 (non-owner) tries to unwrap and export - should fail */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportRequest( + client1, WC_CIPHER_AES_GCM, serverKeyId, wrappedKey, + sizeof(wrappedKey))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + ret = wh_Client_KeyUnwrapAndExportResponse( + client1, WC_CIPHER_AES_GCM, &meta, unwrappedKey, &unwrappedKeySz); + + /* Should fail - Client 1 is not the owner */ + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_ACCESS); + + /* Client 1 (non-owner) tries to unwrap and cache - should also fail */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndCacheRequest( + client1, WC_CIPHER_AES_GCM, serverKeyId, wrappedKey, + sizeof(wrappedKey))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + ret = wh_Client_KeyUnwrapAndCacheResponse(client1, WC_CIPHER_AES_GCM, + &cachedKeyId); + + /* Should also fail - Client 1 is not the owner */ + WH_TEST_ASSERT_RETURN(ret == WH_ERROR_ACCESS); + + /* Clean up */ + serverKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Global wrap key + Local wrapped key (Non-owner fails)\n"); + + return WH_ERROR_OK; +#undef WRAPPED_KEY_SIZE +} + +/* + * Test 9a: Local wrapping key + Local wrapped key (Positive - Same owner) + * - Client 1 caches a local wrapping key + * - Client 1 wraps a LOCAL key (USER=client1_id) + * - Client 1 unwraps and exports successfully + */ +static int _testWrappedKey_LocalWrap_LocalKey_SameOwner( + whClientContext* client1, whServerContext* server1, + whClientContext* client2, whServerContext* server2) +{ + int ret; + whKeyId serverKeyId = DUMMY_KEYID_1; /* Local wrapping key */ + uint16_t client1Id = WH_TEST_DEFAULT_CLIENT_ID; + uint8_t wrapKey[AES_256_KEY_SIZE] = "LocalWrapKey2Test9aXXXXXXXXXX!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "LocalPlainKey2Test9aXXXXXXXXX!"; +#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t unwrappedKey[AES_256_KEY_SIZE] = {0}; + uint16_t unwrappedKeySz = sizeof(unwrappedKey); + whNvmMetadata meta = {0}; + + WH_TEST_DEBUG_PRINT("Test 9a: Local wrap key + Local wrapped key (Same owner)\n"); + + /* Client 1 caches a LOCAL wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey_9a", + sizeof("WrapKey_9a"), wrapKey, sizeof(wrapKey), serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); + + /* Client 1 wraps a LOCAL key (USER=client1_id) */ + serverKeyId = DUMMY_KEYID_1; /* Use local wrapping key */ + meta.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(client1Id, DUMMY_KEYID_2); + meta.len = sizeof(plainKey); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client1, WC_CIPHER_AES_GCM, + serverKeyId, plainKey, + sizeof(plainKey), &meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( + client1, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); + + /* Client 1 (owner) unwraps and exports the local key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportRequest( + client1, WC_CIPHER_AES_GCM, serverKeyId, wrappedKey, + sizeof(wrappedKey))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndExportResponse( + client1, WC_CIPHER_AES_GCM, &meta, unwrappedKey, &unwrappedKeySz)); + + /* Verify the unwrapped key matches the original */ + WH_TEST_ASSERT_RETURN(0 == + memcmp(unwrappedKey, plainKey, sizeof(plainKey))); + + /* Clean up */ + serverKeyId = DUMMY_KEYID_1; + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Local wrap key + Local wrapped key (Same owner)\n"); + + (void)ret; + (void)client2; + (void)server2; + return 0; +#undef WRAPPED_KEY_SIZE +} + +/* + * Test 9b: Local wrapping key + Local wrapped key (Negative - No access without + * wrap key) + * - Client 1 caches a local wrapping key + * - Client 1 wraps a local key + * - Client 2 cannot unwrap (doesn't have wrapping key) + */ +static int _testWrappedKey_LocalWrap_LocalKey_NoAccessWithoutWrapKey( + whClientContext* client1, whServerContext* server1, + whClientContext* client2, whServerContext* server2) +{ + int ret; + whKeyId serverKeyId = DUMMY_KEYID_1; /* Local wrapping key */ + uint16_t client1Id = WH_TEST_DEFAULT_CLIENT_ID; + uint8_t wrapKey[AES_256_KEY_SIZE] = "LocalWrapKey2Test9bXXXXXXXXXX!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "LocalPlainKey2Test9bXXXXXXXXX!"; +#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t unwrappedKey[AES_256_KEY_SIZE] = {0}; + uint16_t unwrappedKeySz = sizeof(unwrappedKey); + whNvmMetadata meta = {0}; + + WH_TEST_DEBUG_PRINT( + "Test 9b: Local wrap key + Local wrapped key (No wrap key access)\n"); + + /* Client 1 caches a LOCAL wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey_9b", + sizeof("WrapKey_9b"), wrapKey, sizeof(wrapKey), serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); + + /* Client 1 wraps a LOCAL key */ + serverKeyId = DUMMY_KEYID_1; /* Use local wrapping key */ + meta.id = WH_CLIENT_KEYID_MAKE_WRAPPED_META(client1Id, DUMMY_KEYID_2); + meta.len = sizeof(plainKey); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client1, WC_CIPHER_AES_GCM, + serverKeyId, plainKey, + sizeof(plainKey), &meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( + client1, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); + + /* Client 2 tries to unwrap - should fail (no wrapping key) */ + ret = wh_Client_KeyUnwrapAndExportRequest(client2, WC_CIPHER_AES_GCM, + serverKeyId, wrappedKey, + sizeof(wrappedKey)); + if (ret == 0) { + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + ret = wh_Client_KeyUnwrapAndExportResponse( + client2, WC_CIPHER_AES_GCM, &meta, unwrappedKey, &unwrappedKeySz); + } + + /* Should fail - Client 2 doesn't have the wrapping key */ + WH_TEST_ASSERT_RETURN(ret != 0); + + /* Clean up */ + serverKeyId = DUMMY_KEYID_1; + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Local wrap key + Local wrapped key (No wrap key access)\n"); + + return 0; +#undef WRAPPED_KEY_SIZE +} + +/* + * Test 10a: Local wrapping key + Global wrapped key (Positive - Any cache + * global) + * - Client 1 caches a local wrapping key + * - Client 1 wraps a GLOBAL key (USER=0) + * - Client 1 unwraps and caches to global cache + * - Client 2 can read from global cache via KeyExport + */ +static int _testWrappedKey_LocalWrap_GlobalKey_AnyCacheGlobal( + whClientContext* client1, whServerContext* server1, + whClientContext* client2, whServerContext* server2) +{ + int ret; + whKeyId serverKeyId = DUMMY_KEYID_1; /* Local wrapping key */ + uint8_t wrapKey[AES_256_KEY_SIZE] = "LocalWrapKey2Test10aXXXXXXXXX!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "GlobalPlainKey2Test10aXXXXXXX!"; +#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t exportedKey[AES_256_KEY_SIZE] = {0}; + uint8_t label[WH_NVM_LABEL_LEN]; + uint16_t labelSz = sizeof(label); + uint16_t exportedSz = sizeof(exportedKey); + whNvmMetadata meta = {0}; + whKeyId cachedKeyId = 0; + + WH_TEST_DEBUG_PRINT("Test 10a: Local wrap key + Global wrapped key (Cache global)\n"); + + /* Client 1 caches a LOCAL wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey_10a", + sizeof("WrapKey_10a"), wrapKey, sizeof(wrapKey), serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); + + /* Client 1 wraps a GLOBAL key (USER=0) */ + serverKeyId = DUMMY_KEYID_1; /* Use local wrapping key */ + meta.id = + WH_CLIENT_KEYID_MAKE_WRAPPED_META(WH_KEYUSER_GLOBAL, DUMMY_KEYID_2); + meta.len = sizeof(plainKey); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client1, WC_CIPHER_AES_GCM, + serverKeyId, plainKey, + sizeof(plainKey), &meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( + client1, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); + + /* Client 1 unwraps and caches to global cache */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndCacheRequest( + client1, WC_CIPHER_AES_GCM, serverKeyId, wrappedKey, + sizeof(wrappedKey))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyUnwrapAndCacheResponse( + client1, WC_CIPHER_AES_GCM, &cachedKeyId)); + + /* Client 2 reads from global cache via KeyExport */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportRequest( + client2, WH_CLIENT_KEYID_MAKE_WRAPPED_GLOBAL(cachedKeyId))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportResponse( + client2, label, labelSz, exportedKey, &exportedSz)); + + /* Verify the exported key matches the original */ + WH_TEST_ASSERT_RETURN(0 == memcmp(exportedKey, plainKey, sizeof(plainKey))); + + /* Clean up */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest( + client2, WH_CLIENT_KEYID_MAKE_WRAPPED_GLOBAL(cachedKeyId))); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client2)); + + serverKeyId = DUMMY_KEYID_1; + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Local wrap key + Global wrapped key (Cache global)\n"); + + (void)ret; + return 0; +#undef WRAPPED_KEY_SIZE +} + +/* + * Test 10b: Local wrapping key + Global wrapped key (Negative - No wrap key) + * - Client 1 caches a local wrapping key + * - Client 1 wraps a global key + * - Client 2 cannot unwrap (doesn't have wrapping key) + */ +static int _testWrappedKey_LocalWrap_GlobalKey_NonOwnerNoWrapKey( + whClientContext* client1, whServerContext* server1, + whClientContext* client2, whServerContext* server2) +{ + int ret; + whKeyId serverKeyId = DUMMY_KEYID_1; /* Local wrapping key */ + uint8_t wrapKey[AES_256_KEY_SIZE] = "LocalWrapKey2Test10bXXXXXXXXX!"; + uint8_t plainKey[AES_256_KEY_SIZE] = "GlobalPlainKey2Test10bXXXXXXX!"; +#define WRAPPED_KEY_SIZE (12 + 16 + AES_256_KEY_SIZE + sizeof(whNvmMetadata)) + uint8_t wrappedKey[WRAPPED_KEY_SIZE] = {0}; + uint16_t wrappedKeySz = sizeof(wrappedKey); + uint8_t unwrappedKey[AES_256_KEY_SIZE] = {0}; + uint16_t unwrappedKeySz = sizeof(unwrappedKey); + whNvmMetadata meta = {0}; + + WH_TEST_DEBUG_PRINT("Test 10b: Local wrap key + Global wrapped key (No wrap key)\n"); + + /* Client 1 caches a LOCAL wrapping key */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, WH_NVM_FLAGS_USAGE_WRAP, (uint8_t*)"WrapKey_10b", + sizeof("WrapKey_10b"), wrapKey, sizeof(wrapKey), serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheResponse(client1, &serverKeyId)); + + /* Client 1 wraps a GLOBAL key */ + serverKeyId = DUMMY_KEYID_1; /* Use local wrapping key */ + meta.id = + WH_CLIENT_KEYID_MAKE_WRAPPED_META(WH_KEYUSER_GLOBAL, DUMMY_KEYID_2); + meta.len = sizeof(plainKey); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapRequest(client1, WC_CIPHER_AES_GCM, + serverKeyId, plainKey, + sizeof(plainKey), &meta)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyWrapResponse( + client1, WC_CIPHER_AES_GCM, wrappedKey, &wrappedKeySz)); + + /* Client 2 tries to unwrap - should fail (no wrapping key) */ + ret = wh_Client_KeyUnwrapAndExportRequest(client2, WC_CIPHER_AES_GCM, + serverKeyId, wrappedKey, + sizeof(wrappedKey)); + if (ret == 0) { + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server2)); + ret = wh_Client_KeyUnwrapAndExportResponse( + client2, WC_CIPHER_AES_GCM, &meta, unwrappedKey, &unwrappedKeySz); + } + + /* Should fail - Client 2 doesn't have the wrapping key */ + WH_TEST_ASSERT_RETURN(ret != 0); + + /* Clean up */ + serverKeyId = DUMMY_KEYID_1; + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictRequest(client1, serverKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Local wrap key + Global wrapped key (No wrap key)\n"); + + return 0; +#undef WRAPPED_KEY_SIZE +} +#endif /* WOLFHSM_CFG_KEYWRAP */ + +/* + * Test: KeyId flag preservation + * - Tests that global and wrapped flags are preserved in server responses + * - Verifies keyCache operations return correct flags + */ +static int _testKeyIdFlagPreservation(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + (void)client2; + (void)server2; + + WH_TEST_PRINT("Test: KeyId flag preservation\n"); + + /* Test 1: Global key cache preserves global flag */ + { + whKeyId keyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + whKeyId returnedKeyId = 0; + + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, 0, (uint8_t*)"GlobalKeyFlags", sizeof("GlobalKeyFlags"), + (uint8_t*)TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1), keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyCacheResponse(client1, &returnedKeyId)); + + /* Verify global flag is preserved */ + WH_TEST_ASSERT_RETURN((returnedKeyId & WH_KEYID_CLIENT_GLOBAL_FLAG) != + 0); + WH_TEST_ASSERT_RETURN((returnedKeyId & WH_KEYID_MASK) == DUMMY_KEYID_1); + + /* Clean up */ + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyEvictRequest(client1, returnedKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Global key cache preserves global flag\n"); + } + + /* Test 2: Local key cache does not have global flag */ + { + whKeyId keyId = DUMMY_KEYID_2; /* Local key - no flags */ + whKeyId returnedKeyId = 0; + + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, 0, (uint8_t*)"LocalKeyFlags", sizeof("LocalKeyFlags"), + (uint8_t*)TEST_KEY_DATA_2, sizeof(TEST_KEY_DATA_2), keyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyCacheResponse(client1, &returnedKeyId)); + + /* Verify no global flag */ + WH_TEST_ASSERT_RETURN((returnedKeyId & WH_KEYID_CLIENT_GLOBAL_FLAG) == + 0); + WH_TEST_ASSERT_RETURN((returnedKeyId & WH_KEYID_MASK) == DUMMY_KEYID_2); + + /* Clean up */ + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyEvictRequest(client1, returnedKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Local key cache has no global flag\n"); + } + + /* Test 3: Reusing returned keyId works correctly */ + { + whKeyId requestKeyId = WH_CLIENT_KEYID_MAKE_GLOBAL(DUMMY_KEYID_1); + whKeyId returnedKeyId = 0; + uint8_t outBuf[sizeof(TEST_KEY_DATA_1)] = {0}; + uint16_t outSz = sizeof(outBuf); + uint8_t label[WH_NVM_LABEL_LEN]; + uint16_t labelSz = sizeof(label); + + /* Cache a global key and get keyId back */ + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyCacheRequest_ex( + client1, 0, (uint8_t*)"ReuseTest", sizeof("ReuseTest"), + (uint8_t*)TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1), requestKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyCacheResponse(client1, &returnedKeyId)); + + /* Use the returned keyId to export the key (common pattern) */ + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyExportRequest(client1, returnedKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyExportResponse( + client1, label, labelSz, outBuf, &outSz)); + + /* Verify data matches */ + WH_TEST_ASSERT_RETURN(outSz == sizeof(TEST_KEY_DATA_1)); + WH_TEST_ASSERT_RETURN( + 0 == memcmp(outBuf, TEST_KEY_DATA_1, sizeof(TEST_KEY_DATA_1))); + + /* Clean up using returned keyId */ + WH_TEST_RETURN_ON_FAIL( + wh_Client_KeyEvictRequest(client1, returnedKeyId)); + WH_TEST_RETURN_ON_FAIL(wh_Server_HandleRequestMessage(server1)); + WH_TEST_RETURN_ON_FAIL(wh_Client_KeyEvictResponse(client1)); + + WH_TEST_PRINT(" PASS: Reusing returned keyId works correctly\n"); + } + + return 0; +} + +/* Helper function to run all global keys tests */ +static int _runGlobalKeysTests(whClientContext* client1, + whServerContext* server1, + whClientContext* client2, + whServerContext* server2) +{ + WH_TEST_RETURN_ON_FAIL( + _testKeyIdFlagPreservation(client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL( + _testGlobalKeyBasic(client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL( + _testLocalKeyIsolation(client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL( + _testMixedGlobalLocal(client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL( + _testGlobalKeyNvmPersistence(client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL( + _testGlobalKeyExportProtection(client1, server1, client2, server2)); + +#ifdef WOLFHSM_CFG_DMA + WH_TEST_RETURN_ON_FAIL( + _testGlobalKeyDma(client1, server1, client2, server2)); +#endif + +#ifdef WOLFHSM_CFG_KEYWRAP + WH_TEST_RETURN_ON_FAIL( + _testGlobalKeyWrapExport(client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL( + _testGlobalKeyUnwrapCache(client1, server1, client2, server2)); + + /* Comprehensive wrapped key access control tests */ + WH_TEST_RETURN_ON_FAIL(_testWrappedKey_GlobalWrap_GlobalKey_Positive( + client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL(_testWrappedKey_GlobalWrap_GlobalKey_NonExportable( + client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL(_testWrappedKey_GlobalWrap_LocalKey_OwnerExport( + client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL(_testWrappedKey_GlobalWrap_LocalKey_NonOwnerFails( + client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL(_testWrappedKey_LocalWrap_LocalKey_SameOwner( + client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL( + _testWrappedKey_LocalWrap_LocalKey_NoAccessWithoutWrapKey( + client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL(_testWrappedKey_LocalWrap_GlobalKey_AnyCacheGlobal( + client1, server1, client2, server2)); + + WH_TEST_RETURN_ON_FAIL( + _testWrappedKey_LocalWrap_GlobalKey_NonOwnerNoWrapKey( + client1, server1, client2, server2)); +#endif + + WH_TEST_PRINT("All Global Keys Tests PASSED ===\n"); + return 0; +} + +#endif /* WOLFHSM_CFG_GLOBAL_KEYS */ + +/* ============================================================================ + * MULTI-CLIENT SEQUENTIAL TEST FRAMEWORK + * ========================================================================== */ + +/* Generic setup/teardown for multi-client sequential tests using shared memory + */ +static int _whTest_MultiClient(void) +{ + int ret = 0; + + /* Transport memory configurations for both clients */ + static uint8_t req1[BUFFER_SIZE]; + static uint8_t resp1[BUFFER_SIZE]; + whTransportMemConfig tmcf1[1] = {{ + .req = (whTransportMemCsr*)req1, + .req_size = sizeof(req1), + .resp = (whTransportMemCsr*)resp1, + .resp_size = sizeof(resp1), + }}; + + static uint8_t req2[BUFFER_SIZE]; + static uint8_t resp2[BUFFER_SIZE]; + whTransportMemConfig tmcf2[1] = {{ + .req = (whTransportMemCsr*)req2, + .req_size = sizeof(req2), + .resp = (whTransportMemCsr*)resp2, + .resp_size = sizeof(resp2), + }}; + + /* Client 1 configuration */ + whTransportClientCb tccb1[1] = {WH_TRANSPORT_MEM_CLIENT_CB}; + whTransportMemClientContext tmcc1[1] = {0}; + whCommClientConfig cc_conf1[1] = {{ + .transport_cb = tccb1, + .transport_context = (void*)tmcc1, + .transport_config = (void*)tmcf1, + .client_id = WH_TEST_DEFAULT_CLIENT_ID, + .connect_cb = _connectCb1, + }}; + whClientContext client1[1] = {0}; + whClientConfig c_conf1[1] = {{ + .comm = cc_conf1, + }}; + + /* Client 2 configuration */ + whTransportClientCb tccb2[1] = {WH_TRANSPORT_MEM_CLIENT_CB}; + whTransportMemClientContext tmcc2[1] = {0}; + whCommClientConfig cc_conf2[1] = {{ + .transport_cb = tccb2, + .transport_context = (void*)tmcc2, + .transport_config = (void*)tmcf2, + .client_id = WH_TEST_DEFAULT_CLIENT_ID + 1, + .connect_cb = _connectCb2, + }}; + whClientContext client2[1] = {0}; + whClientConfig c_conf2[1] = {{ + .comm = cc_conf2, + }}; + + /* Shared NVM configuration using RamSim Flash */ + static uint8_t memory[FLASH_RAM_SIZE] = {0}; + whFlashRamsimCtx fc[1] = {0}; + whFlashRamsimCfg fc_conf[1] = {{ + .size = FLASH_RAM_SIZE, + .sectorSize = FLASH_SECTOR_SIZE, + .pageSize = FLASH_PAGE_SIZE, + .erasedByte = ~(uint8_t)0, + .memory = memory, + }}; + const whFlashCb fcb[1] = {WH_FLASH_RAMSIM_CB}; + + whNvmFlashConfig nf_conf[1] = {{ + .cb = fcb, + .context = fc, + .config = fc_conf, + }}; + whNvmFlashContext nfc[1] = {0}; + whNvmCb nfcb[1] = {WH_NVM_FLASH_CB}; + + whNvmConfig n_conf[1] = {{ + .cb = nfcb, + .context = nfc, + .config = nf_conf, + }}; + whNvmContext nvm[1] = {0}; /* Shared NVM */ + +#if !defined(WOLFHSM_CFG_NO_CRYPTO) + /* Crypto contexts for both servers */ + whServerCryptoContext crypto1[1] = {0}; + whServerCryptoContext crypto2[1] = {0}; +#endif + + /* Server 1 configuration */ + whTransportServerCb tscb1[1] = {WH_TRANSPORT_MEM_SERVER_CB}; + whTransportMemServerContext tmsc1[1] = {0}; + whCommServerConfig cs_conf1[1] = {{ + .transport_cb = tscb1, + .transport_context = (void*)tmsc1, + .transport_config = (void*)tmcf1, + .server_id = 101, + }}; + whServerConfig s_conf1[1] = {{ + .comm_config = cs_conf1, + .nvm = nvm, /* Shared NVM */ +#if !defined(WOLFHSM_CFG_NO_CRYPTO) + .crypto = crypto1, +#endif + }}; + whServerContext server1[1] = {0}; + + /* Server 2 configuration */ + whTransportServerCb tscb2[1] = {WH_TRANSPORT_MEM_SERVER_CB}; + whTransportMemServerContext tmsc2[1] = {0}; + whCommServerConfig cs_conf2[1] = {{ + .transport_cb = tscb2, + .transport_context = (void*)tmsc2, + .transport_config = (void*)tmcf2, + .server_id = 102, + }}; + whServerConfig s_conf2[1] = {{ + .comm_config = cs_conf2, + .nvm = nvm, /* Shared NVM */ + +#if !defined(WOLFHSM_CFG_NO_CRYPTO) + .crypto = crypto2, +#endif + }}; + whServerContext server2[1] = {0}; + + /* Expose server contexts to connect callbacks */ + testServer1 = server1; + testServer2 = server2; + +#if !defined(WOLFHSM_CFG_NO_CRYPTO) + /* Initialize wolfCrypt */ + ret = wolfCrypt_Init(); + if (ret != 0) + return ret; +#endif + + /* Initialize NVM (shared) */ + ret = wh_Nvm_Init(nvm, n_conf); + if (ret != 0) + return ret; + +#if !defined(WOLFHSM_CFG_NO_CRYPTO) + /* Initialize RNGs */ + ret = wc_InitRng_ex(crypto1->rng, NULL, INVALID_DEVID); + if (ret != 0) + return ret; + + ret = wc_InitRng_ex(crypto2->rng, NULL, INVALID_DEVID); + if (ret != 0) + return ret; +#endif + + /* Initialize servers */ + ret = wh_Server_Init(server1, s_conf1); + if (ret != 0) + return ret; + + ret = wh_Server_Init(server2, s_conf2); + if (ret != 0) + return ret; + + /* Initialize clients */ + ret = wh_Client_Init(client1, c_conf1); + if (ret != 0) + return ret; + + ret = wh_Client_Init(client2, c_conf2); + if (ret != 0) + return ret; + + /* Initialize communication for both clients */ + uint32_t client_id = 0; + uint32_t server_id = 0; + + ret = wh_Client_CommInitRequest(client1); + if (ret != 0) + return ret; + ret = wh_Server_HandleRequestMessage(server1); + if (ret != 0) + return ret; + ret = wh_Client_CommInitResponse(client1, &client_id, &server_id); + if (ret != 0) + return ret; + + ret = wh_Client_CommInitRequest(client2); + if (ret != 0) + return ret; + ret = wh_Server_HandleRequestMessage(server2); + if (ret != 0) + return ret; + ret = wh_Client_CommInitResponse(client2, &client_id, &server_id); + if (ret != 0) + return ret; + + WH_TEST_PRINT("=== Multi-Client Sequential Tests Begin ===\n"); + /* Run test suites that require multiple clients */ +#ifdef WOLFHSM_CFG_GLOBAL_KEYS + WH_TEST_RETURN_ON_FAIL( + _runGlobalKeysTests(client1, server1, client2, server2)); +#endif + + /* Future test suites here */ + + /* Cleanup */ + wh_Client_Cleanup(client1); + wh_Client_Cleanup(client2); + wh_Server_Cleanup(server1); + wh_Server_Cleanup(server2); +#if !defined(WOLFHSM_CFG_NO_CRYPTO) + wc_FreeRng(crypto1->rng); + wc_FreeRng(crypto2->rng); + wolfCrypt_Cleanup(); +#endif + wh_Nvm_Cleanup(nvm); + + WH_TEST_PRINT("=== Multi-Client Sequential Tests Complete ===\n"); + + return 0; +} + +/* ============================================================================ + * PUBLIC API + * ========================================================================== */ + +/* Main entry point for multi-client tests */ +int whTest_MultiClient(void* ctx) +{ + (void)ctx; + return _whTest_MultiClient(); +} + +#endif /* WOLFHSM_CFG_ENABLE_CLIENT && WOLFHSM_CFG_ENABLE_SERVER */ diff --git a/test-refactor/posix/wh_test_flash_fault_inject.c b/test-refactor/posix/wh_test_flash_fault_inject.c new file mode 100644 index 000000000..47312aa8c --- /dev/null +++ b/test-refactor/posix/wh_test_flash_fault_inject.c @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/wh_test_flash_fault_inject.c + * + * Flash fault-injection wrapper. Forwards every flash op to a real + * callback but forces an abort on the Nth Program call. Used by the + * NVM recovery test (wh_test_nvm_flash.c) to fail a write mid-object. + */ + +/* Pick up compile-time configuration */ +#include "wolfhsm/wh_settings.h" + +#include +#include /* For NULL */ + +#include +#include + +#include "wolfhsm/wh_error.h" +#include "wh_test_flash_fault_inject.h" + +int whFlashFaultInject_Init(void* context, const void* config) +{ + whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context; + const whFlashFaultInjectCfg* cfg = (const whFlashFaultInjectCfg*)config; + + if (ctx == NULL || cfg == NULL || cfg->realCb == NULL) { + return WH_ERROR_BADARGS; + } + + memset(ctx, 0, sizeof(*ctx)); + ctx->realCb = cfg->realCb; + ctx->realCtx = cfg->realCtx; + + if (cfg->realCb->Init != NULL) + return cfg->realCb->Init(cfg->realCtx, cfg->realCfg); + + return WH_ERROR_OK; +} + +int whFlashFaultInject_Cleanup(void* context) +{ + whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context; + + if (ctx == NULL || ctx->realCb == NULL) { + return WH_ERROR_BADARGS; + } + if (ctx->realCb->Cleanup != NULL) + return ctx->realCb->Cleanup(ctx->realCtx); + + return WH_ERROR_OK; +} + +int whFlashFaultInject_Program(void* context, uint32_t offset, uint32_t size, + const uint8_t* data) +{ + whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context; + + if ((ctx == NULL) || (ctx->realCb == NULL)) + return WH_ERROR_BADARGS; + /* Check if we need to simulate a failure */ + if (ctx->failAfterPrograms > 0) { + ctx->failAfterPrograms--; + if (ctx->failAfterPrograms == 0) + return WH_ERROR_ABORTED; + } + + if (ctx->realCb->Program != NULL) + return ctx->realCb->Program(ctx->realCtx, offset, size, data); + + return WH_ERROR_OK; +} + +int whFlashFaultInject_Read(void* context, uint32_t offset, uint32_t size, + uint8_t* data) +{ + whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context; + + if ((ctx == NULL) || (ctx->realCb == NULL)) + return WH_ERROR_BADARGS; + + if (ctx->realCb->Read != NULL) + return ctx->realCb->Read(ctx->realCtx, offset, size, data); + + return WH_ERROR_OK; +} + +int whFlashFaultInject_Erase(void* context, uint32_t offset, uint32_t size) +{ + whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context; + + if ((ctx == NULL) || (ctx->realCb == NULL)) + return WH_ERROR_BADARGS; + + if (ctx->realCb->Erase != NULL) + return ctx->realCb->Erase(ctx->realCtx, offset, size); + + return WH_ERROR_OK; +} + +int whFlashFaultInject_Verify(void* context, uint32_t offset, uint32_t size, + const uint8_t* data) +{ + whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context; + + if ((ctx == NULL) || (ctx->realCb == NULL)) + return WH_ERROR_BADARGS; + + if (ctx->realCb->Verify != NULL) + return ctx->realCb->Verify(ctx->realCtx, offset, size, data); + + return WH_ERROR_OK; +} + +int whFlashFaultInject_BlankCheck(void* context, uint32_t offset, uint32_t size) +{ + whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context; + + if ((ctx == NULL) || (ctx->realCb == NULL)) + return WH_ERROR_BADARGS; + + if (ctx->realCb->BlankCheck != NULL) + return ctx->realCb->BlankCheck(ctx->realCtx, offset, size); + + return WH_ERROR_OK; +} + +uint32_t whFlashFaultInject_PartitionSize(void* context) +{ + whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context; + + if ((ctx == NULL) || (ctx->realCb == NULL)) + return WH_ERROR_BADARGS; + + if (ctx->realCb->PartitionSize != NULL) + return ctx->realCb->PartitionSize(ctx->realCtx); + + return WH_ERROR_OK; +} + +int whFlashFaultInject_WriteLock(void* context, uint32_t offset, uint32_t size) +{ + whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context; + + if ((ctx == NULL) || (ctx->realCb == NULL)) + return WH_ERROR_BADARGS; + + if (ctx->realCb->WriteLock != NULL) + return ctx->realCb->WriteLock(ctx->realCtx, offset, size); + + return WH_ERROR_OK; +} + +int whFlashFaultInject_WriteUnlock(void* context, uint32_t offset, + uint32_t size) +{ + whFlashFaultInjectCtx* ctx = (whFlashFaultInjectCtx*)context; + + if ((ctx == NULL) || (ctx->realCb == NULL)) + return WH_ERROR_BADARGS; + + if (ctx->realCb->WriteUnlock != NULL) + return ctx->realCb->WriteUnlock(ctx->realCtx, offset, size); + + return WH_ERROR_OK; +} diff --git a/test-refactor/posix/wh_test_flash_fault_inject.h b/test-refactor/posix/wh_test_flash_fault_inject.h new file mode 100644 index 000000000..afbdf22b5 --- /dev/null +++ b/test-refactor/posix/wh_test_flash_fault_inject.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/wh_test_flash_fault_inject.h + * + * Flash fault-injection wrapper. Wraps a real flash callback and + * forces a failure on the Nth Program call, used to drive the NVM + * recovery test on a host-sim flash backend. + */ +#ifndef WH_FLASH_FAULTINJECT_H_ +#define WH_FLASH_FAULTINJECT_H_ + +/* Pick up compile-time configuration */ +#include "wolfhsm/wh_settings.h" + +#include + +#include "wolfhsm/wh_flash.h" + +typedef struct { + const whFlashCb* realCb; + void* realCtx; + int failAfterPrograms; +} whFlashFaultInjectCtx; + +typedef struct { + const whFlashCb* realCb; + void* realCtx; + void* realCfg; +} whFlashFaultInjectCfg; + +int whFlashFaultInject_Init(void* context, const void* config); +int whFlashFaultInject_Cleanup(void* context); +int whFlashFaultInject_Program(void* context, uint32_t offset, uint32_t size, + const uint8_t* data); +int whFlashFaultInject_Read(void* context, uint32_t offset, uint32_t size, + uint8_t* data); +int whFlashFaultInject_Erase(void* context, uint32_t offset, uint32_t size); +int whFlashFaultInject_Verify(void* context, uint32_t offset, uint32_t size, + const uint8_t* data); +int whFlashFaultInject_BlankCheck(void* context, uint32_t offset, + uint32_t size); +uint32_t whFlashFaultInject_PartitionSize(void* context); +int whFlashFaultInject_WriteLock(void* context, uint32_t offset, uint32_t size); +int whFlashFaultInject_WriteUnlock(void* context, uint32_t offset, + uint32_t size); + +/* clang-format off */ +#define WH_FLASH_FAULTINJECT_CB \ + { \ + .Init = whFlashFaultInject_Init, \ + .Cleanup = whFlashFaultInject_Cleanup, \ + .PartitionSize = whFlashFaultInject_PartitionSize, \ + .WriteLock = whFlashFaultInject_WriteLock, \ + .WriteUnlock = whFlashFaultInject_WriteUnlock, \ + .Read = whFlashFaultInject_Read, \ + .Program = whFlashFaultInject_Program, \ + .Erase = whFlashFaultInject_Erase, \ + .Verify = whFlashFaultInject_Verify, \ + .BlankCheck = whFlashFaultInject_BlankCheck, \ + } +/* clang-format on */ + +#endif /* !WH_FLASH_FAULTINJECT_H_ */ diff --git a/test-refactor/server/wh_test_hwkeystore_server.c b/test-refactor/server/wh_test_hwkeystore_server.c new file mode 100644 index 000000000..5a642fd03 --- /dev/null +++ b/test-refactor/server/wh_test_hwkeystore_server.c @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/server/wh_test_hwkeystore_server.c + * + * Server-side hardware keystore coverage that needs no client: + * + * _whTest_HwKeystoreModule - wh_HwKeystore_Init/GetKey/Cleanup argument + * validation and GetKey dispatch, using a + * local context (the shared server context + * is not touched) + * _whTest_HwKeystoreLifecycle - optional backend Init/Cleanup callback + * dispatch and the Init-failure return path, + * using a backend that records its calls + * _whTest_KeystoreHwOnlyReject - every public keystore entry point must + * reject a hardware-only keyId with + * WH_ERROR_ACCESS before reaching the cache, + * NVM, or any hardware backend (none is + * bound to the shared server). FreshenKey is + * also the choke point through which all + * crypto handlers resolve keys, so this + * covers crypto key use of hardware-only ids + * + * End-to-end keywrap use of hardware-only KEKs is covered by the misc group + * test misc/wh_test_hwkeystore.c, which binds a hardware keystore to its own + * private client/server pair. + */ + +#include "wolfhsm/wh_settings.h" + +#if defined(WOLFHSM_CFG_ENABLE_SERVER) && defined(WOLFHSM_CFG_HWKEYSTORE) + +#include +#include + +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_common.h" +#include "wolfhsm/wh_keyid.h" +#include "wolfhsm/wh_hwkeystore.h" +#include "wolfhsm/wh_nvm.h" +#include "wolfhsm/wh_server.h" +#include "wolfhsm/wh_server_keystore.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + +/* Id and material of the only key served by the local test backend */ +#define WH_TEST_HWKEK_ID 3 +#define WH_TEST_HWKEK_SIZE 32 + +/* USER field value for constructing server-internal hardware-only keyIds */ +#define WH_TEST_HW_USER 1 + +static const uint8_t _hwKekMaterial[WH_TEST_HWKEK_SIZE] = { + 0x9a, 0x4e, 0x21, 0xc7, 0x5d, 0x10, 0xfb, 0x33, 0x6f, 0x82, 0xd4, + 0x59, 0xee, 0x07, 0xb1, 0x2c, 0x48, 0x95, 0x3a, 0xc6, 0x71, 0x0d, + 0xb8, 0xe5, 0x12, 0x6a, 0xf9, 0x84, 0x2f, 0xd0, 0x5b, 0xa7}; + +static int _HwKeystoreGetKey(void* context, whKeyId keyId, uint8_t* out, + uint16_t* inout_len) +{ + (void)context; + + /* Only hardware-only keyIds should ever reach a hardware keystore */ + if (WH_KEYID_TYPE(keyId) != WH_KEYTYPE_HW) { + return WH_ERROR_ACCESS; + } + + /* Serve only the known test KEK id, refuse everything else */ + if (WH_KEYID_ID(keyId) != WH_TEST_HWKEK_ID) { + return WH_ERROR_NOTFOUND; + } + + if ((out == NULL) || (inout_len == NULL) || + (*inout_len < sizeof(_hwKekMaterial))) { + return WH_ERROR_BUFFER_SIZE; + } + + memcpy(out, _hwKekMaterial, sizeof(_hwKekMaterial)); + *inout_len = sizeof(_hwKekMaterial); + return WH_ERROR_OK; +} + +/* clang-format off */ +static const whHwKeystoreCb _hwKeystoreCb = { + .Init = NULL, + .Cleanup = NULL, + .GetKey = _HwKeystoreGetKey, +}; +/* clang-format on */ + +/* Backend context recording optional Init/Cleanup callback dispatch. initRet + * and cleanupRet let a test force the Init/Cleanup failure paths; initConfig + * captures the config pointer forwarded to Init to prove it is plumbed + * through */ +typedef struct { + int initCalls; + int cleanupCalls; + int initRet; + int cleanupRet; + const void* initConfig; +} HwKeystoreLifecycle; + +static int _HwKeystoreInit(void* context, const void* config) +{ + HwKeystoreLifecycle* lc = (HwKeystoreLifecycle*)context; + if (lc != NULL) { + lc->initCalls++; + lc->initConfig = config; + return lc->initRet; + } + return WH_ERROR_OK; +} + +static int _HwKeystoreCleanup(void* context) +{ + HwKeystoreLifecycle* lc = (HwKeystoreLifecycle*)context; + if (lc != NULL) { + lc->cleanupCalls++; + return lc->cleanupRet; + } + return WH_ERROR_OK; +} + +/* clang-format off */ +static const whHwKeystoreCb _hwKeystoreLifecycleCb = { + .Init = _HwKeystoreInit, + .Cleanup = _HwKeystoreCleanup, + .GetKey = _HwKeystoreGetKey, +}; +/* clang-format on */ + +/* wh_HwKeystore module front-end in isolation: argument validation, callback + * dispatch, and lifecycle */ +static int _whTest_HwKeystoreModule(void) +{ + int ret; + whHwKeystoreContext hwks[1] = {{0}}; + whHwKeystoreConfig conf[1] = {{0}}; + whHwKeystoreConfig badConf[1] = {{0}}; + whKeyId servedId = + WH_MAKE_KEYID(WH_KEYTYPE_HW, WH_TEST_HW_USER, WH_TEST_HWKEK_ID); + uint8_t out[WH_TEST_HWKEK_SIZE] = {0}; + uint16_t outLen = sizeof(out); + + conf->cb = &_hwKeystoreCb; + + /* Init argument validation: NULL context/config/getKey are rejected */ + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == wh_HwKeystore_Init(NULL, conf)); + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == wh_HwKeystore_Init(hwks, NULL)); + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + wh_HwKeystore_Init(hwks, badConf)); + + /* GetKey must reject NULL and uninitialized contexts */ + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + wh_HwKeystore_GetKey(NULL, servedId, out, &outLen)); + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + wh_HwKeystore_GetKey(hwks, servedId, out, &outLen)); + + WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Init(hwks, conf)); + + /* GetKey argument validation on an initialized context */ + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + wh_HwKeystore_GetKey(hwks, servedId, NULL, &outLen)); + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + wh_HwKeystore_GetKey(hwks, servedId, out, NULL)); + + /* Served id: material and reported length must match the backend's */ + ret = wh_HwKeystore_GetKey(hwks, servedId, out, &outLen); + WH_TEST_ASSERT_RETURN(WH_ERROR_OK == ret); + WH_TEST_ASSERT_RETURN(outLen == sizeof(_hwKekMaterial)); + WH_TEST_ASSERT_RETURN(0 == + memcmp(out, _hwKekMaterial, sizeof(_hwKekMaterial))); + + /* Backend policy: unserved id, undersized buffer, non-HW type */ + outLen = sizeof(out); + WH_TEST_ASSERT_RETURN( + WH_ERROR_NOTFOUND == + wh_HwKeystore_GetKey( + hwks, + WH_MAKE_KEYID(WH_KEYTYPE_HW, WH_TEST_HW_USER, WH_TEST_HWKEK_ID + 1), + out, &outLen)); + outLen = WH_TEST_HWKEK_SIZE - 1; + WH_TEST_ASSERT_RETURN(WH_ERROR_BUFFER_SIZE == + wh_HwKeystore_GetKey(hwks, servedId, out, &outLen)); + outLen = sizeof(out); + WH_TEST_ASSERT_RETURN( + WH_ERROR_ACCESS == + wh_HwKeystore_GetKey( + hwks, + WH_MAKE_KEYID(WH_KEYTYPE_CRYPTO, WH_TEST_HW_USER, WH_TEST_HWKEK_ID), + out, &outLen)); + + /* Cleanup zeroizes the context, after which GetKey must reject it */ + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == wh_HwKeystore_Cleanup(NULL)); + WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Cleanup(hwks)); + outLen = sizeof(out); + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + wh_HwKeystore_GetKey(hwks, servedId, out, &outLen)); + + return WH_ERROR_OK; +} + +/* Optional backend Init/Cleanup dispatch and the Init-failure return path, + * using a backend that records its callback invocations */ +static int _whTest_HwKeystoreLifecycle(void) +{ + HwKeystoreLifecycle lc = {0}; + whHwKeystoreContext hwks[1] = {{0}}; + whHwKeystoreConfig conf[1] = {{0}}; + const int backendConfig = 0; /* sentinel forwarded to Init */ + whKeyId servedId = + WH_MAKE_KEYID(WH_KEYTYPE_HW, WH_TEST_HW_USER, WH_TEST_HWKEK_ID); + uint8_t out[WH_TEST_HWKEK_SIZE] = {0}; + uint16_t outLen = sizeof(out); + + conf->cb = &_hwKeystoreLifecycleCb; + conf->context = &lc; + conf->config = &backendConfig; + + /* Init-failure path: the backend Init rc is propagated, the config pointer + * was forwarded, and the context is left uninitialized (GetKey rejects it). + * Cleanup must NOT have run for a backend whose Init never succeeded */ + lc.initRet = WH_ERROR_ABORTED; + WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED == wh_HwKeystore_Init(hwks, conf)); + WH_TEST_ASSERT_RETURN(lc.initCalls == 1); + WH_TEST_ASSERT_RETURN(lc.initConfig == &backendConfig); + WH_TEST_ASSERT_RETURN(lc.cleanupCalls == 0); + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + wh_HwKeystore_GetKey(hwks, servedId, out, &outLen)); + + /* Init-success path: the backend Init is dispatched exactly once and the + * context becomes usable */ + lc.initRet = WH_ERROR_OK; + lc.initCalls = 0; + WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Init(hwks, conf)); + WH_TEST_ASSERT_RETURN(lc.initCalls == 1); + + outLen = sizeof(out); + WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_GetKey(hwks, servedId, out, &outLen)); + WH_TEST_ASSERT_RETURN(outLen == sizeof(_hwKekMaterial)); + + /* Cleanup dispatches the backend Cleanup exactly once */ + WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Cleanup(hwks)); + WH_TEST_ASSERT_RETURN(lc.cleanupCalls == 1); + + /* Cleanup-failure path: a backend Cleanup error is surfaced to the caller, + * yet the context is still torn down (a subsequent GetKey rejects it) */ + lc.initRet = WH_ERROR_OK; + lc.initCalls = 0; + lc.cleanupCalls = 0; + lc.cleanupRet = WH_ERROR_ABORTED; + WH_TEST_RETURN_ON_FAIL(wh_HwKeystore_Init(hwks, conf)); + WH_TEST_ASSERT_RETURN(WH_ERROR_ABORTED == wh_HwKeystore_Cleanup(hwks)); + WH_TEST_ASSERT_RETURN(lc.cleanupCalls == 1); + outLen = sizeof(out); + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + wh_HwKeystore_GetKey(hwks, servedId, out, &outLen)); + + return WH_ERROR_OK; +} + +/* Hardware-only keyIds must be rejected by every public keystore entry point + * with WH_ERROR_ACCESS (or WH_ERROR_BADARGS for id generation). These checks + * are pure keyId-type checks: they fire whether or not a hardware keystore + * is bound to the server, and before the cache or NVM is consulted */ +static int _whTest_KeystoreHwOnlyReject(whServerContext* server) +{ + whKeyId hwId = + WH_MAKE_KEYID(WH_KEYTYPE_HW, WH_TEST_HW_USER, WH_TEST_HWKEK_ID); + uint8_t keyBuf[WH_TEST_HWKEK_SIZE] = {0}; + uint8_t* outBuf = NULL; + whNvmMetadata* outMeta = NULL; + whNvmMetadata meta = {0}; + uint32_t outSz = sizeof(keyBuf); + whNvmId uniqueId; + + /* Id generation: hardware-only ids are assigned by the backend, never by + * the keystore */ + uniqueId = WH_MAKE_KEYID(WH_KEYTYPE_HW, WH_TEST_HW_USER, 0); + WH_TEST_ASSERT_RETURN(WH_ERROR_BADARGS == + wh_Server_KeystoreGetUniqueId(server, &uniqueId)); + + /* Cache slot allocation (also the sole guard on the DMA cache path) */ + WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == + wh_Server_KeystoreGetCacheSlot( + server, hwId, sizeof(keyBuf), &outBuf, &outMeta)); + + /* Caching key material under a hardware-only id */ + meta.id = hwId; + meta.len = sizeof(keyBuf); + WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == + wh_Server_KeystoreCacheKey(server, &meta, keyBuf)); + + /* Freshen: the resolution path used by all crypto handlers, so this also + * proves crypto operations cannot use hardware-only keys */ + WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == wh_Server_KeystoreFreshenKey( + server, hwId, NULL, NULL)); + + /* Reading hardware-only key material out of the keystore */ + WH_TEST_ASSERT_RETURN( + WH_ERROR_ACCESS == + wh_Server_KeystoreReadKey(server, hwId, NULL, keyBuf, &outSz)); + + /* Lifecycle operations */ + WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == + wh_Server_KeystoreEvictKey(server, hwId)); + WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == + wh_Server_KeystoreCommitKey(server, hwId)); + WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == + wh_Server_KeystoreEraseKey(server, hwId)); + WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == + wh_Server_KeystoreEraseKeyChecked(server, hwId)); + WH_TEST_ASSERT_RETURN(WH_ERROR_ACCESS == + wh_Server_KeystoreRevokeKey(server, hwId)); + + return WH_ERROR_OK; +} + +int whTest_HwKeystoreServer(whServerContext* ctx) +{ + WH_TEST_RETURN_ON_FAIL(_whTest_HwKeystoreModule()); + WH_TEST_RETURN_ON_FAIL(_whTest_HwKeystoreLifecycle()); + WH_TEST_RETURN_ON_FAIL(_whTest_KeystoreHwOnlyReject(ctx)); + + return WH_ERROR_OK; +} + +#endif /* WOLFHSM_CFG_ENABLE_SERVER && WOLFHSM_CFG_HWKEYSTORE */ diff --git a/test-refactor/server/wh_test_she_server.c b/test-refactor/server/wh_test_she_server.c new file mode 100644 index 000000000..c772f34fb --- /dev/null +++ b/test-refactor/server/wh_test_she_server.c @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2026 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 . + */ +/* + * test-refactor/server/wh_test_she_server.c + * + * Server-side SHE test suite. Exercises internal server SHE + * behavior through direct server API calls against the shared + * server context: the master ECU key metadata fallback and the + * per-action request-size validation in the SHE handlers. + */ + +#include "wolfhsm/wh_settings.h" + +#if defined(WOLFHSM_CFG_SHE_EXTENSION) && !defined(WOLFHSM_CFG_NO_CRYPTO) + +#include +#include + +#include "wolfhsm/wh_common.h" +#include "wolfhsm/wh_error.h" +#include "wolfhsm/wh_nvm.h" +#include "wolfhsm/wh_server.h" +#include "wolfhsm/wh_server_keystore.h" +#include "wolfhsm/wh_server_she.h" +#include "wolfhsm/wh_she_common.h" +#include "wolfhsm/wh_message.h" +#include "wolfhsm/wh_message_she.h" +#include "wolfhsm/wh_comm.h" + +#include "wh_test_common.h" +#include "wh_test_list.h" + +/* Value of WH_SHE_SB_SUCCESS from the wh_server_she.c internal enum. + * Mirrored here since the enum is private to that translation unit. */ +#define TEST_SHE_SB_STATE_SUCCESS 3 + + +/* + * Reading the master ECU key when it has never been provisioned must + * succeed and return an all-zero key with correctly populated metadata. + */ +int whTest_SheMasterEcuKeyFallback(whServerContext* server) +{ + int ret; + whNvmMetadata outMeta[1] = {0}; + uint8_t keyBuf[WH_SHE_KEY_SZ] = {0}; + uint32_t keySz = sizeof(keyBuf); + uint8_t zeros[WH_SHE_KEY_SZ] = {0}; + whKeyId masterEcuKeyId; + + if (server == NULL) { + return WH_ERROR_BADARGS; + } + + masterEcuKeyId = WH_MAKE_KEYID(WH_KEYTYPE_SHE, server->comm->client_id, + WH_SHE_MASTER_ECU_KEY_ID); + + /* Fill keyBuf with non-zero to ensure it gets overwritten */ + memset(keyBuf, 0xFF, sizeof(keyBuf)); + + ret = wh_Server_KeystoreReadKey(server, masterEcuKeyId, outMeta, keyBuf, + &keySz); + + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(keySz == WH_SHE_KEY_SZ); + WH_TEST_ASSERT_RETURN(memcmp(keyBuf, zeros, WH_SHE_KEY_SZ) == 0); + WH_TEST_ASSERT_RETURN(outMeta->len == WH_SHE_KEY_SZ); + WH_TEST_ASSERT_RETURN(outMeta->id == masterEcuKeyId); + + WH_TEST_PRINT("SHE master ECU key fallback metadata test SUCCESS\n"); + + return 0; +} + + +/* + * Test that SHE server handlers reject requests with an invalid + * req_size while still producing an action-specific response packet. + * Each handler is called directly via wh_Server_HandleSheRequest() + * with a realistic but incorrectly sized request packet. + */ +int whTest_SheReqSizeChecking(whServerContext* server) +{ + int ret = 0; + uint16_t req_size = 0; + uint16_t resp_size = 0; + + /* Buffers for request and response packets */ + uint8_t req_packet[WOLFHSM_CFG_COMM_DATA_LEN]; + uint8_t resp_packet[WOLFHSM_CFG_COMM_DATA_LEN]; + + if (server == NULL) { + return WH_ERROR_BADARGS; + } + + /* + * Set SHE state so _ReportInvalidSheState allows requests through. + * WH_SHE_SET_UID always passes the state gate, but most other handlers + * require uidSet=1 and sbState=WH_SHE_SB_SUCCESS. + */ + server->she->uidSet = 1; + server->she->sbState = TEST_SHE_SB_STATE_SUCCESS; + + /* + * Test 1: WH_SHE_SET_UID with truncated request. + * Populate a valid UID in the packet, but pass req_size one byte short. + */ + { + whMessageShe_SetUidRequest* req = + (whMessageShe_SetUidRequest*)req_packet; + whMessageShe_SetUidResponse* setUidResp = + (whMessageShe_SetUidResponse*)resp_packet; + memset(setUidResp, 0, sizeof(*setUidResp)); + memset(req->uid, 0xAA, WH_SHE_UID_SZ); + req_size = sizeof(whMessageShe_SetUidRequest) - 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_SET_UID, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*setUidResp)); + WH_TEST_ASSERT_RETURN(setUidResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 2: WH_SHE_SECURE_BOOT_INIT with truncated request. + * Set a valid bootloader size, but pass req_size one byte short. + */ + { + whMessageShe_SecureBootInitRequest* req = + (whMessageShe_SecureBootInitRequest*)req_packet; + whMessageShe_SecureBootInitResponse* secureBootInitResp = + (whMessageShe_SecureBootInitResponse*)resp_packet; + /* The state gate allows SECURE_BOOT_INIT through regardless of + * sbState; we are testing the size check which happens first. */ + memset(secureBootInitResp, 0, sizeof(*secureBootInitResp)); + req->sz = 256; + req_size = sizeof(whMessageShe_SecureBootInitRequest) - 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_SECURE_BOOT_INIT, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*secureBootInitResp)); + WH_TEST_ASSERT_RETURN(secureBootInitResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* Secure boot failure resets sbState to SB_INIT, restore it */ + server->she->sbState = TEST_SHE_SB_STATE_SUCCESS; + + /* + * Test 3: WH_SHE_SECURE_BOOT_UPDATE with truncated fixed header. + * Set a valid chunk size but pass req_size smaller than the header. + */ + { + whMessageShe_SecureBootUpdateRequest* req = + (whMessageShe_SecureBootUpdateRequest*)req_packet; + whMessageShe_SecureBootUpdateResponse* secureBootUpdateResp = + (whMessageShe_SecureBootUpdateResponse*)resp_packet; + memset(secureBootUpdateResp, 0, sizeof(*secureBootUpdateResp)); + req->sz = 64; + req_size = sizeof(whMessageShe_SecureBootUpdateRequest) - 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_SECURE_BOOT_UPDATE, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*secureBootUpdateResp)); + WH_TEST_ASSERT_RETURN(secureBootUpdateResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* Secure boot failure resets sbState to SB_INIT, restore it */ + server->she->sbState = TEST_SHE_SB_STATE_SUCCESS; + + /* + * Test 4: WH_SHE_SECURE_BOOT_FINISH expects no request body. + * Send a nonzero req_size to trigger the check. + */ + { + whMessageShe_SecureBootFinishResponse* secureBootFinishResp = + (whMessageShe_SecureBootFinishResponse*)resp_packet; + memset(secureBootFinishResp, 0, sizeof(*secureBootFinishResp)); + req_size = 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_SECURE_BOOT_FINISH, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*secureBootFinishResp)); + WH_TEST_ASSERT_RETURN(secureBootFinishResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* Secure boot failure resets sbState to SB_INIT, restore it */ + server->she->sbState = TEST_SHE_SB_STATE_SUCCESS; + + /* + * Test 5: WH_SHE_LOAD_KEY with truncated request. + * Fill M1/M2/M3 with nonzero data, pass req_size one byte short. + * _LoadKey maps the malformed request to an action-specific response; + * verify the request still completes with a response packet instead + * of failing the transport path. + */ + { + whMessageShe_LoadKeyRequest* req = + (whMessageShe_LoadKeyRequest*)req_packet; + whMessageShe_LoadKeyResponse* loadKeyResp = + (whMessageShe_LoadKeyResponse*)resp_packet; + memset(loadKeyResp, 0, sizeof(*loadKeyResp)); + memset(req->messageOne, 0x11, WH_SHE_M1_SZ); + memset(req->messageTwo, 0x22, WH_SHE_M2_SZ); + memset(req->messageThree, 0x33, WH_SHE_M3_SZ); + req_size = sizeof(whMessageShe_LoadKeyRequest) - 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_LOAD_KEY, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*loadKeyResp)); + WH_TEST_ASSERT_RETURN(loadKeyResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 6: WH_SHE_LOAD_PLAIN_KEY with truncated request. + * Fill a valid key, pass req_size one byte short. + */ + { + whMessageShe_LoadPlainKeyRequest* req = + (whMessageShe_LoadPlainKeyRequest*)req_packet; + whMessageShe_LoadPlainKeyResponse* loadPlainKeyResp = + (whMessageShe_LoadPlainKeyResponse*)resp_packet; + memset(loadPlainKeyResp, 0, sizeof(*loadPlainKeyResp)); + memset(req->key, 0xBB, WH_SHE_KEY_SZ); + req_size = sizeof(whMessageShe_LoadPlainKeyRequest) - 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_LOAD_PLAIN_KEY, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*loadPlainKeyResp)); + WH_TEST_ASSERT_RETURN(loadPlainKeyResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 7: WH_SHE_EXPORT_RAM_KEY expects no request body. + * Send a nonzero req_size to trigger the check. + */ + { + whMessageShe_ExportRamKeyResponse* exportRamKeyResp = + (whMessageShe_ExportRamKeyResponse*)resp_packet; + memset(exportRamKeyResp, 0, sizeof(*exportRamKeyResp)); + req_size = 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_EXPORT_RAM_KEY, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*exportRamKeyResp)); + WH_TEST_ASSERT_RETURN(exportRamKeyResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 8: WH_SHE_INIT_RND expects no request body. + * Send a nonzero req_size to trigger the check. + */ + { + whMessageShe_InitRngResponse* initRngResp = + (whMessageShe_InitRngResponse*)resp_packet; + memset(initRngResp, 0, sizeof(*initRngResp)); + req_size = 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_INIT_RND, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*initRngResp)); + WH_TEST_ASSERT_RETURN(initRngResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 9: WH_SHE_RND expects no request body. + * Send a nonzero req_size to trigger the check. + */ + { + whMessageShe_RndResponse* rndResp = + (whMessageShe_RndResponse*)resp_packet; + memset(rndResp, 0, sizeof(*rndResp)); + req_size = 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_RND, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*rndResp)); + WH_TEST_ASSERT_RETURN(rndResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 10: WH_SHE_EXTEND_SEED with truncated request. + * Fill valid entropy data, pass req_size one byte short. + */ + { + whMessageShe_ExtendSeedRequest* req = + (whMessageShe_ExtendSeedRequest*)req_packet; + whMessageShe_ExtendSeedResponse* extendSeedResp = + (whMessageShe_ExtendSeedResponse*)resp_packet; + memset(extendSeedResp, 0, sizeof(*extendSeedResp)); + memset(req->entropy, 0xCC, WH_SHE_KEY_SZ); + req_size = sizeof(whMessageShe_ExtendSeedRequest) - 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_EXTEND_SEED, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*extendSeedResp)); + WH_TEST_ASSERT_RETURN(extendSeedResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 11: WH_SHE_ENC_ECB with valid header but truncated payload. + * Set sz to 16 (one AES block) but only include the header, no data. + */ + { + whMessageShe_EncEcbRequest* req = + (whMessageShe_EncEcbRequest*)req_packet; + whMessageShe_EncEcbResponse* encEcbResp = + (whMessageShe_EncEcbResponse*)resp_packet; + memset(encEcbResp, 0, sizeof(*encEcbResp)); + req->sz = 16; + req->keyId = WH_SHE_RAM_KEY_ID; + req_size = sizeof(whMessageShe_EncEcbRequest); + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_ENC_ECB, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*encEcbResp)); + WH_TEST_ASSERT_RETURN(encEcbResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 12: WH_SHE_ENC_ECB with truncated header. + * Pass req_size one byte short of the header struct. + */ + { + whMessageShe_EncEcbRequest* req = + (whMessageShe_EncEcbRequest*)req_packet; + whMessageShe_EncEcbResponse* encEcbResp = + (whMessageShe_EncEcbResponse*)resp_packet; + memset(encEcbResp, 0, sizeof(*encEcbResp)); + req->sz = 16; + req->keyId = WH_SHE_RAM_KEY_ID; + req_size = sizeof(whMessageShe_EncEcbRequest) - 1; + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_ENC_ECB, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*encEcbResp)); + WH_TEST_ASSERT_RETURN(encEcbResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 13: WH_SHE_ENC_CBC with valid header but truncated payload. + * Set sz to 16 (one AES block), fill a valid IV, but only include the + * header with no cipher data following it. + */ + { + whMessageShe_EncCbcRequest* req = + (whMessageShe_EncCbcRequest*)req_packet; + whMessageShe_EncCbcResponse* encCbcResp = + (whMessageShe_EncCbcResponse*)resp_packet; + memset(encCbcResp, 0, sizeof(*encCbcResp)); + req->sz = 16; + req->keyId = WH_SHE_RAM_KEY_ID; + memset(req->iv, 0xDD, WH_SHE_KEY_SZ); + req_size = sizeof(whMessageShe_EncCbcRequest); + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_ENC_CBC, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*encCbcResp)); + WH_TEST_ASSERT_RETURN(encCbcResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 14: WH_SHE_DEC_ECB with valid header but truncated payload. + */ + { + whMessageShe_DecEcbRequest* req = + (whMessageShe_DecEcbRequest*)req_packet; + whMessageShe_DecEcbResponse* decEcbResp = + (whMessageShe_DecEcbResponse*)resp_packet; + memset(decEcbResp, 0, sizeof(*decEcbResp)); + req->sz = 16; + req->keyId = WH_SHE_RAM_KEY_ID; + req_size = sizeof(whMessageShe_DecEcbRequest); + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_DEC_ECB, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*decEcbResp)); + WH_TEST_ASSERT_RETURN(decEcbResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 15: WH_SHE_DEC_CBC with valid header but truncated payload. + */ + { + whMessageShe_DecCbcRequest* req = + (whMessageShe_DecCbcRequest*)req_packet; + whMessageShe_DecCbcResponse* decCbcResp = + (whMessageShe_DecCbcResponse*)resp_packet; + memset(decCbcResp, 0, sizeof(*decCbcResp)); + req->sz = 16; + req->keyId = WH_SHE_RAM_KEY_ID; + memset(req->iv, 0xEE, WH_SHE_KEY_SZ); + req_size = sizeof(whMessageShe_DecCbcRequest); + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_DEC_CBC, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*decCbcResp)); + WH_TEST_ASSERT_RETURN(decCbcResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 16: WH_SHE_GEN_MAC with valid header but truncated payload. + * Set sz to 16 bytes of message data, but only pass the header. + */ + { + whMessageShe_GenMacRequest* req = + (whMessageShe_GenMacRequest*)req_packet; + whMessageShe_GenMacResponse* genMacResp = + (whMessageShe_GenMacResponse*)resp_packet; + memset(genMacResp, 0, sizeof(*genMacResp)); + req->keyId = WH_SHE_RAM_KEY_ID; + req->sz = 16; + req_size = sizeof(whMessageShe_GenMacRequest); + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_GEN_MAC, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*genMacResp)); + WH_TEST_ASSERT_RETURN(genMacResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* + * Test 17: WH_SHE_VERIFY_MAC with valid header but truncated payload. + * Set messageLen=16 and macLen=16 but only pass the header. + */ + { + whMessageShe_VerifyMacRequest* req = + (whMessageShe_VerifyMacRequest*)req_packet; + whMessageShe_VerifyMacResponse* verifyMacResp = + (whMessageShe_VerifyMacResponse*)resp_packet; + memset(verifyMacResp, 0, sizeof(*verifyMacResp)); + req->keyId = WH_SHE_RAM_KEY_ID; + req->messageLen = 16; + req->macLen = 16; + req_size = sizeof(whMessageShe_VerifyMacRequest); + ret = wh_Server_HandleSheRequest(server, WH_COMM_MAGIC_NATIVE, + WH_SHE_VERIFY_MAC, req_size, + req_packet, &resp_size, resp_packet); + WH_TEST_ASSERT_RETURN(ret == 0); + WH_TEST_ASSERT_RETURN(resp_size == sizeof(*verifyMacResp)); + WH_TEST_ASSERT_RETURN(verifyMacResp->rc != WH_SHE_ERC_NO_ERROR); + } + + /* Restore a clean SHE context so the poked uidSet/sbState don't + * leak into the live request loop the server enters next. */ + memset(server->she, 0, sizeof(*server->she)); + + WH_TEST_PRINT("SHE req_size checking test SUCCESS\n"); + + return 0; +} + +#endif /* WOLFHSM_CFG_SHE_EXTENSION && !WOLFHSM_CFG_NO_CRYPTO */ diff --git a/wolfhsm/wh_hwkeystore.h b/wolfhsm/wh_hwkeystore.h new file mode 100644 index 000000000..b298c875d --- /dev/null +++ b/wolfhsm/wh_hwkeystore.h @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2026 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_hwkeystore.h + * + * Hardware keystore front-end module. Provides a platform-agnostic, fully + * configurable abstraction over hardware-backed key storage (OTP, fuses, + * secure enclaves, SoC key managers, etc.) through a backend callback table + * (whHwKeystoreCb), mirroring the configurable-backend paradigm used by the + * wh_lock and wh_log modules. + * + * The server uses this module to fetch the material of "hardware-only" keys + * (WH_KEYTYPE_HW, requested by clients via WH_CLIENT_KEYID_MAKE_HW) + * on demand. Hardware-only key material never enters the server key cache or + * NVM and is never returned to a client; the server fetches it into a local + * (stack) buffer, uses it, and zeroizes it. Hardware-only keys are only + * usable as KEKs in the keywrap API. + * + * A backend provides a whHwKeystoreCb table (GetKey required; Init and Cleanup + * optional) plus optional opaque context/config pointers for backend-specific + * state. A whHwKeystoreContext is owned by the application, initialized once + * with wh_HwKeystore_Init(), and bound to one or more server contexts through + * the optional hwKeystore member of whServerConfig. The embedded lock + * serializes callback invocations when the backing hardware is shared across + * server threads (WOLFHSM_CFG_THREADSAFE). + */ + +#ifndef WOLFHSM_WH_HWKEYSTORE_H_ +#define WOLFHSM_WH_HWKEYSTORE_H_ + +/* Pick up compile-time configuration */ +#include "wolfhsm/wh_settings.h" + +#ifdef WOLFHSM_CFG_HWKEYSTORE + +#include + +#include "wolfhsm/wh_keyid.h" + +#ifdef WOLFHSM_CFG_THREADSAFE +#include "wolfhsm/wh_lock.h" +#endif + +/** + * Hardware keystore backend callback signatures. + * + * All callbacks receive the backend's opaque context pointer (from + * whHwKeystoreConfig). GetKey is required; Init and Cleanup are optional and + * may be NULL in the callback table. + * + * Return: WH_ERROR_OK on success, negative error code on failure. + */ + +/** Initialize the backend - called once from wh_HwKeystore_Init(). Optional. */ +typedef int (*whHwKeystoreInitCb)(void* context, const void* config); + +/** Cleanup the backend - called once from wh_HwKeystore_Cleanup(). Optional. */ +typedef int (*whHwKeystoreCleanupCb)(void* context); + +/** + * Hardware keystore getKey callback. Required. + * + * Copies the material of the requested key into the caller-provided buffer. + * The callback receives the full server-internal keyId (TYPE/USER/ID fields, + * see wolfhsm/wh_keyid.h); backends typically dispatch on WH_KEYID_ID() and + * may use WH_KEYID_USER() to partition keys between clients. + * + * The backend is the policy authority for hardware-only keys: it must return + * an error (e.g. WH_ERROR_ACCESS or WH_ERROR_NOTFOUND) for any keyId it does + * not serve. + * + * Output-buffer contract: on entry *inout_len is the capacity of out in bytes. + * The backend MUST NOT write more than that many bytes to out and, on success, + * MUST set *inout_len to the number of bytes actually written (which therefore + * must not exceed the input capacity). If the key does not fit, the backend + * must return WH_ERROR_BUFFER_SIZE and write nothing. + * + * @param[in] context Backend context pointer from whHwKeystoreConfig + * @param[in] keyId Server-internal keyId of the requested key + * @param[out] out Buffer to receive the key material + * @param[in,out] inout_len In: size of out in bytes. Out: actual key size + * @return WH_ERROR_OK on success, negative error code on failure + */ +typedef int (*whHwKeystoreGetKeyCb)(void* context, whKeyId keyId, uint8_t* out, + uint16_t* inout_len); + +/** + * Hardware keystore backend callback table. + * + * A backend provides implementations of these callbacks. GetKey is required. + * Init and Cleanup may be NULL if the backend needs no setup/teardown. + */ +typedef struct whHwKeystoreCb_t { + whHwKeystoreInitCb Init; /* Initialize backend (optional) */ + whHwKeystoreCleanupCb Cleanup; /* Cleanup backend (optional) */ + whHwKeystoreGetKeyCb GetKey; /* Fetch key material (required) */ +} whHwKeystoreCb; + +/* Context structure associated with a hardware keystore instance */ +typedef struct whHwKeystoreContext_t { + const whHwKeystoreCb* cb; /* Backend callback table */ + void* context; /* Opaque backend context */ +#ifdef WOLFHSM_CFG_THREADSAFE + whLock lock; /* Lock serializing callback invocations */ +#endif + int initialized; /* 1 if initialized, 0 otherwise */ + uint8_t WH_PAD[4]; +} whHwKeystoreContext; + +/* Configuration structure associated with a hardware keystore instance */ +typedef struct whHwKeystoreConfig_t { + const whHwKeystoreCb* cb; /* Backend callback table (GetKey required) */ + void* context; /* Opaque backend context (passed to cbs) */ + const void* config; /* Backend-specific config (passed to Init) */ +#ifdef WOLFHSM_CFG_THREADSAFE + whLockConfig* lockConfig; /* Lock configuration (NULL for no-op locking) */ +#endif +} whHwKeystoreConfig; + +/** + * @brief Initialize a hardware keystore instance. + * + * Binds the backend callback table, initializes the embedded lock, and invokes + * the backend Init callback (if present). Must be called in a single-threaded + * context before the context is bound to any server. + * + * @param[in] context Pointer to the hardware keystore context + * @param[in] config Pointer to the configuration. config->cb and + * config->cb->GetKey are required + * @return int WH_ERROR_OK on success, WH_ERROR_BADARGS if context, config, + * config->cb, or config->cb->GetKey is NULL, or a negative error code + * from the backend Init callback or lock initialization + */ +int wh_HwKeystore_Init(whHwKeystoreContext* context, + const whHwKeystoreConfig* config); + +/** + * @brief Cleanup a hardware keystore instance. + * + * Cleans up the embedded lock and zeros the context. Must only be called + * when no servers are using the context. + * + * @param[in] context Pointer to the hardware keystore context + * @return int WH_ERROR_OK on success, WH_ERROR_BADARGS if context is NULL + */ +int wh_HwKeystore_Cleanup(whHwKeystoreContext* context); + +/** + * @brief Fetch key material from the hardware keystore backend. + * + * Acquires the lock, invokes the backend GetKey callback, and releases the + * lock. + * + * @param[in] context Pointer to an initialized hardware keystore context + * @param[in] keyId Server-internal keyId of the requested key + * @param[out] out Buffer to receive the key material + * @param[in,out] inout_len In: size of out in bytes. Out: actual key size + * @return int WH_ERROR_OK on success, WH_ERROR_BADARGS on invalid arguments + * or uninitialized context, or the error returned by the lock or the + * backend callback + */ +int wh_HwKeystore_GetKey(whHwKeystoreContext* context, whKeyId keyId, + uint8_t* out, uint16_t* inout_len); + +#endif /* WOLFHSM_CFG_HWKEYSTORE */ + +#endif /* !WOLFHSM_WH_HWKEYSTORE_H_ */ From 4cef60ffb0f6d2e28702768f8ee56d2a30cdea18 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Fri, 10 Jul 2026 14:08:47 -0700 Subject: [PATCH 5/5] feat(sbom): capture build config via WOLFHSM_CFG_DIR --- Makefile | 34 ++++++++++++++++++++++++++++++++-- README.md | 20 +++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 0d558be4a..894b62e3c 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,7 @@ clean: # ---- SBOM generation ---- CC ?= cc WOLFSSL_DIR ?= ../wolfssl +WOLFHSM_CFG_DIR ?= test/config VERSION = $(shell sed -n 's/^. wolfHSM Release v//p' ChangeLog.md | head -1 | cut -d' ' -f1) SRCS := $(sort $(wildcard src/*.c)) SBOM_CDX = wolfhsm-$(VERSION).cdx.json @@ -84,12 +85,41 @@ sbom: echo " Set WOLFSSL_DIR to such a tree." >&2; \ exit 1; \ fi + @if [ ! -f "$(WOLFHSM_CFG_DIR)/wolfhsm_cfg.h" ]; then \ + echo "ERROR: $(WOLFHSM_CFG_DIR)/wolfhsm_cfg.h not found." >&2; \ + echo " Set WOLFHSM_CFG_DIR to the directory holding the" >&2; \ + echo " wolfhsm_cfg.h (and user_settings.h) your build uses." >&2; \ + exit 1; \ + fi @echo "wolfHSM version: $(VERSION)" @echo "Sources: $(words $(SRCS)) .c files in src/" + @echo "Config: $(WOLFHSM_CFG_DIR)/wolfhsm_cfg.h" +# Effective build config for the SBOM: preprocess wolfhsm/wh_settings.h with +# the same defines and include path the test build compiles under, so the -dM +# dump holds every WOLFHSM_CFG_* option (explicit and defaulted) plus the +# wolfSSL options from user_settings.h — the configuration the library is +# actually built with. Point WOLFHSM_CFG_DIR at the directory holding your +# build's wolfhsm_cfg.h/user_settings.h for an integrator-accurate SBOM. +# +# ponytail: wh_settings.h pulls libc headers (stdint/stdio/strings/stdatomic), +# so ~330 toolchain constants (INT16_MAX, ACCESSPERMS, ...) ride along into +# the SBOM next to the ~175 real config macros. The dump is deliberately NOT +# filtered here: a prefix allowlist would silently drop real options that +# carry no standard prefix (GCM_TABLE_4BIT, FP_MAX_BITS, SINGLE_THREADED). +# The durable fix belongs in gen-sbom's noise filter (wolfSSL PR #10343, +# scripts/gen-sbom _NOISE_MACRO_RE), either of: +# a) provenance filtering: accept a -dD dump and use its #line markers to +# drop macros defined in system headers, or +# b) an --options-baseline flag: subtract a second -dM dump made with the +# same flags minus the -include, plus the libc headers it pulls. +# Once gen-sbom grows that, this recipe needs no change — it already hands +# over the full dump. @_defines=$$(mktemp "$${TMPDIR:-/tmp}/wolfhsm-defines.XXXXXX") && \ trap 'rm -f "$$_defines"' 0 && \ - if ! $(CC) -dM -E -I. -I$(WOLFSSL_DIR) -x c /dev/null >"$$_defines" 2>/dev/null; then \ - echo "ERROR: $(CC) -dM -E failed." >&2; exit 1; \ + if ! $(CC) -dM -E -DWOLFHSM_CFG -DWOLFSSL_USER_SETTINGS \ + -I. -I$(WOLFHSM_CFG_DIR) -I$(WOLFSSL_DIR) \ + -include wolfhsm/wh_settings.h -x c /dev/null >"$$_defines"; then \ + echo "ERROR: $(CC) -dM -E on wolfhsm/wh_settings.h failed." >&2; exit 1; \ fi && \ if ! command -v python3 >/dev/null 2>&1; then \ echo "ERROR: python3 not found." >&2; exit 1; \ diff --git a/README.md b/README.md index 5dcda024d..4ea736643 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,21 @@ make sbom WOLFSSL_DIR=../wolfssl This parses the version from `ChangeLog.md`, collects `src/*.c`, and writes `wolfhsm-.cdx.json` and `wolfhsm-.spdx.json`. +The SBOM records the build configuration by preprocessing +`wolfhsm/wh_settings.h` against a config directory. `WOLFHSM_CFG_DIR` +(default: `test/config`) selects the directory holding the `wolfhsm_cfg.h` +and `user_settings.h` your build uses — point it at your port's config so the +recorded `WOLFHSM_CFG_*` and wolfSSL options match the library you ship: + +```sh +make sbom WOLFSSL_DIR=../wolfssl WOLFHSM_CFG_DIR=path/to/your/config +``` + +Note: alongside the real config macros, the dump currently includes libc +constants pulled in by `wh_settings.h`'s system includes; see the comment on +the `sbom` target in `Makefile` for why they are not filtered here and what +the planned gen-sbom fix is. + `WOLFSSL_DIR` must point to a wolfssl source tree containing `scripts/gen-sbom`, which ships in wolfSSL PR #10343 (pending a future wolfSSL release). If the script is absent the target fails with a message telling you what is missing. @@ -50,12 +65,15 @@ To invoke `gen-sbom` directly instead of through the target, run the same command it runs: ```sh +cc -dM -E -DWOLFHSM_CFG -DWOLFSSL_USER_SETTINGS \ + -I. -Itest/config -I$WOLFSSL_DIR \ + -include wolfhsm/wh_settings.h -x c /dev/null > wolfhsm-defines.h python3 $WOLFSSL_DIR/scripts/gen-sbom \ --name wolfhsm \ --version $(sed -n 's/^# wolfHSM Release v\([0-9][0-9.]*\).*/\1/p' ChangeLog.md | head -1) \ --supplier "wolfSSL Inc." \ --license-file LICENSING \ - --options-h $WOLFSSL_DIR/include/wolfssl/options.h \ + --options-h wolfhsm-defines.h \ --srcs src/*.c \ --cdx-out wolfhsm.cdx.json \ --spdx-out wolfhsm.spdx.json