From 7de3a9d39bbd15458275f883e08d16efe3f240fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 23 Jul 2026 17:05:57 +0200 Subject: [PATCH] Zephyr: add wolfPSA as a Zephyr PSA Crypto provider Add a Zephyr module that plugs wolfPSA into the in-tree PSA_CRYPTO_PROVIDER choice (CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM, Zephyr >= 4.3), supplying the PSA Crypto API in place of Mbed TLS while reusing the wolfCrypt core compiled by the sibling wolfSSL Zephyr module. - Provider selection depends on WOLFSSL (never selects it) to avoid the socket Kconfig's !WOLFSSL dependency loop; consumers set CONFIG_WOLFSSL=y. - The PSA surface is generated at CMake-configure time from the compiled wolfCrypt config intersected with what wolfPSA implements, so it tracks the build. wolfPSA owns no wolfCrypt config of its own and follows the wolfSSL module's, coexisting with the wolfSSL TLS stack in one image. - Persistent keys are backed by Zephyr secure_storage via a wolfCrypt AES-256-GCM custom ITS transform (encryption at rest, no Mbed TLS symbol in the image); ZMS store backend on STM32H7. - Optional thread-safe key store built on wolfCrypt's portable wc_*Mutex API. - DRBG seeding is owned by the wolfSSL module; wolfPSA requires HAVE_HASHDRBG. - Coverage favors Zephyr's own provider-agnostic PSA apps run against wolfPSA, plus bespoke tests for the key-store lock, entropy path, and ITS transform. Version numbering tracks wolfSSL. --- .github/scripts/run-tests.sh | 97 ++++ .github/scripts/zephyr-4.x/zephyr-test.sh | 148 +++++++ .github/workflows/zephyr-4.x.yml | 71 +++ .gitignore | 7 + CHANGELOG.md | 23 + src/psa_api_stub.c | 5 - src/psa_asymmetric_api.c | 9 + src/psa_crypto.c | 44 +- src/psa_ecc.c | 7 + src/psa_key_storage.c | 236 +++++++--- src/psa_lock.h | 100 +++++ src/psa_rsa.c | 5 + src/psa_store_posix.c | 3 +- src/psa_store_zephyr.c | 319 ++++++++++++++ test/psa_server/psa_14_misc_test.c | 41 ++ user_settings.h | 14 + wolfpsa/psa/crypto_config.h | 11 + wolfpsa/psa_store.h | 7 + zephyr/CMakeLists.txt | 127 ++++++ zephyr/Kconfig | 85 ++++ zephyr/README.md | 215 +++++++++ zephyr/gen/gen-crypto-config.py | 92 ++++ zephyr/gen/psa_want_probe.c | 205 +++++++++ zephyr/module.yml | 6 + zephyr/samples/psa_smoke/CMakeLists.txt | 9 + zephyr/samples/psa_smoke/prj.conf | 21 + zephyr/samples/psa_smoke/src/main.c | 417 ++++++++++++++++++ zephyr/src/psa_its_transform_wolfcrypt.c | 309 +++++++++++++ zephyr/tests/psa_concurrency/CMakeLists.txt | 9 + zephyr/tests/psa_concurrency/prj.conf | 23 + zephyr/tests/psa_concurrency/src/main.c | 199 +++++++++ zephyr/tests/psa_concurrency/testcase.yaml | 11 + zephyr/tests/psa_consumer/CMakeLists.txt | 15 + zephyr/tests/psa_consumer/prj.conf | 31 ++ zephyr/tests/psa_consumer/testcase.yaml | 22 + zephyr/tests/psa_entropy/CMakeLists.txt | 9 + zephyr/tests/psa_entropy/prj.conf | 21 + zephyr/tests/psa_entropy/src/main.c | 51 +++ zephyr/tests/psa_entropy/testcase.yaml | 11 + zephyr/tests/psa_its/CMakeLists.txt | 9 + zephyr/tests/psa_its/prj.conf | 22 + zephyr/tests/psa_its/testcase.yaml | 17 + .../tests/psa_persistent_key/CMakeLists.txt | 9 + zephyr/tests/psa_persistent_key/prj.conf | 24 + zephyr/tests/psa_persistent_key/testcase.yaml | 19 + zephyr/tests/psa_purge/CMakeLists.txt | 9 + zephyr/tests/psa_purge/prj.conf | 17 + zephyr/tests/psa_purge/src/main.c | 85 ++++ zephyr/tests/psa_purge/testcase.yaml | 16 + .../tests/psa_secure_storage/CMakeLists.txt | 10 + zephyr/tests/psa_secure_storage/prj.conf | 23 + zephyr/tests/psa_secure_storage/testcase.yaml | 12 + .../psa_store_unavailable/CMakeLists.txt | 9 + zephyr/tests/psa_store_unavailable/prj.conf | 21 + zephyr/tests/psa_store_unavailable/src/main.c | 91 ++++ .../tests/psa_store_unavailable/testcase.yaml | 18 + zephyr/tests/psa_tls_coexist/CMakeLists.txt | 7 + zephyr/tests/psa_tls_coexist/prj.conf | 29 ++ zephyr/tests/psa_tls_coexist/src/main.c | 183 ++++++++ zephyr/tests/psa_tls_coexist/testcase.yaml | 12 + zephyr/tests/psa_transform/CMakeLists.txt | 9 + zephyr/tests/psa_transform/prj.conf | 36 ++ zephyr/tests/psa_transform/src/main.c | 149 +++++++ zephyr/tests/psa_transform/testcase.yaml | 18 + zephyr/user_settings_example.h | 176 ++++++++ zephyr/zephyr_init.c | 80 ++++ 66 files changed, 4065 insertions(+), 80 deletions(-) create mode 100755 .github/scripts/run-tests.sh create mode 100644 .github/scripts/zephyr-4.x/zephyr-test.sh create mode 100644 .github/workflows/zephyr-4.x.yml create mode 100644 src/psa_lock.h create mode 100644 src/psa_store_zephyr.c create mode 100644 zephyr/CMakeLists.txt create mode 100644 zephyr/Kconfig create mode 100644 zephyr/README.md create mode 100644 zephyr/gen/gen-crypto-config.py create mode 100644 zephyr/gen/psa_want_probe.c create mode 100644 zephyr/module.yml create mode 100644 zephyr/samples/psa_smoke/CMakeLists.txt create mode 100644 zephyr/samples/psa_smoke/prj.conf create mode 100644 zephyr/samples/psa_smoke/src/main.c create mode 100644 zephyr/src/psa_its_transform_wolfcrypt.c create mode 100644 zephyr/tests/psa_concurrency/CMakeLists.txt create mode 100644 zephyr/tests/psa_concurrency/prj.conf create mode 100644 zephyr/tests/psa_concurrency/src/main.c create mode 100644 zephyr/tests/psa_concurrency/testcase.yaml create mode 100644 zephyr/tests/psa_consumer/CMakeLists.txt create mode 100644 zephyr/tests/psa_consumer/prj.conf create mode 100644 zephyr/tests/psa_consumer/testcase.yaml create mode 100644 zephyr/tests/psa_entropy/CMakeLists.txt create mode 100644 zephyr/tests/psa_entropy/prj.conf create mode 100644 zephyr/tests/psa_entropy/src/main.c create mode 100644 zephyr/tests/psa_entropy/testcase.yaml create mode 100644 zephyr/tests/psa_its/CMakeLists.txt create mode 100644 zephyr/tests/psa_its/prj.conf create mode 100644 zephyr/tests/psa_its/testcase.yaml create mode 100644 zephyr/tests/psa_persistent_key/CMakeLists.txt create mode 100644 zephyr/tests/psa_persistent_key/prj.conf create mode 100644 zephyr/tests/psa_persistent_key/testcase.yaml create mode 100644 zephyr/tests/psa_purge/CMakeLists.txt create mode 100644 zephyr/tests/psa_purge/prj.conf create mode 100644 zephyr/tests/psa_purge/src/main.c create mode 100644 zephyr/tests/psa_purge/testcase.yaml create mode 100644 zephyr/tests/psa_secure_storage/CMakeLists.txt create mode 100644 zephyr/tests/psa_secure_storage/prj.conf create mode 100644 zephyr/tests/psa_secure_storage/testcase.yaml create mode 100644 zephyr/tests/psa_store_unavailable/CMakeLists.txt create mode 100644 zephyr/tests/psa_store_unavailable/prj.conf create mode 100644 zephyr/tests/psa_store_unavailable/src/main.c create mode 100644 zephyr/tests/psa_store_unavailable/testcase.yaml create mode 100644 zephyr/tests/psa_tls_coexist/CMakeLists.txt create mode 100644 zephyr/tests/psa_tls_coexist/prj.conf create mode 100644 zephyr/tests/psa_tls_coexist/src/main.c create mode 100644 zephyr/tests/psa_tls_coexist/testcase.yaml create mode 100644 zephyr/tests/psa_transform/CMakeLists.txt create mode 100644 zephyr/tests/psa_transform/prj.conf create mode 100644 zephyr/tests/psa_transform/src/main.c create mode 100644 zephyr/tests/psa_transform/testcase.yaml create mode 100644 zephyr/user_settings_example.h create mode 100644 zephyr/zephyr_init.c diff --git a/.github/scripts/run-tests.sh b/.github/scripts/run-tests.sh new file mode 100755 index 0000000..adea124 --- /dev/null +++ b/.github/scripts/run-tests.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Build and run every wolfPSA Zephyr sample and test on native_sim, reporting a +# pass/fail summary and exiting non-zero on any failure. Used by CI +# (.github/workflows/zephyr-4.x.yml, via .github/scripts/zephyr-4.x/zephyr-test.sh) +# and handy locally. +# +# Must be invoked from inside a west workspace that has the Zephyr RTOS, the +# wolfSSL module, and the mbedtls module (the latter defines the +# PSA_CRYPTO_PROVIDER choice wolfPSA plugs into). The wolfPSA module itself is +# added via EXTRA_ZEPHYR_MODULES, so no manifest entry is required. +# +# Env: +# BOARD board/qualifier (default native_sim/native/64) +# BUILD_ROOT where build dirs go (default: $PWD/build-wolfpsa-ci) +# EXTRA_MODULE_ARG CMake arg to locate the wolfPSA module. Defaults to +# -DEXTRA_ZEPHYR_MODULES= (for workspaces where +# wolfPSA is NOT in the west manifest). Set it to "" when +# wolfPSA is already a manifest module (e.g. CI, where +# zephyr-test.sh injects it into the manifest), to avoid +# registering it twice. + +set -u + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +# repo root is two levels up: .github/scripts/ -> repo root +WOLFPSA_ROOT=$(CDPATH= cd -- "${script_dir}/../.." && pwd) +BOARD="${BOARD:-native_sim/native/64}" +BUILD_ROOT="${BUILD_ROOT:-${PWD}/build-wolfpsa-ci}" +EXTRA_MODULE_ARG="${EXTRA_MODULE_ARG--DEXTRA_ZEPHYR_MODULES=${WOLFPSA_ROOT}}" + +# name | app path | mode(ztest|sample) | extra west-build args (optional) +# +# psa_consumer_st reruns the consumer with CONFIG_WOLFPSA_THREAD_SAFE=n so the +# single-threaded baseline path (SINGLE_THREADED, no-op lock macros) is built +# and run in CI, not just the default thread-safe (=y) path. +TARGETS=" +psa_smoke|${WOLFPSA_ROOT}/zephyr/samples/psa_smoke|sample +psa_consumer|${WOLFPSA_ROOT}/zephyr/tests/psa_consumer|ztest +psa_consumer_st|${WOLFPSA_ROOT}/zephyr/tests/psa_consumer|ztest|-DCONFIG_WOLFPSA_THREAD_SAFE=n +psa_concurrency|${WOLFPSA_ROOT}/zephyr/tests/psa_concurrency|ztest +psa_entropy|${WOLFPSA_ROOT}/zephyr/tests/psa_entropy|ztest +psa_transform|${WOLFPSA_ROOT}/zephyr/tests/psa_transform|ztest +psa_purge|${WOLFPSA_ROOT}/zephyr/tests/psa_purge|ztest +psa_secure_storage|${WOLFPSA_ROOT}/zephyr/tests/psa_secure_storage|ztest +psa_store_unavailable|${WOLFPSA_ROOT}/zephyr/tests/psa_store_unavailable|ztest +psa_tls_coexist|${WOLFPSA_ROOT}/zephyr/tests/psa_tls_coexist|ztest +psa_its|${WOLFPSA_ROOT}/zephyr/tests/psa_its|sample +psa_persistent_key|${WOLFPSA_ROOT}/zephyr/tests/psa_persistent_key|sample +" + +mkdir -p "${BUILD_ROOT}" + +rc=0 +printf '%-16s %-8s %s\n' "TARGET" "BUILD" "RESULT" +printf '%-16s %-8s %s\n' "------" "-----" "------" + +while IFS='|' read -r name app mode extra; do + [ -z "${name}" ] && continue + bdir="${BUILD_ROOT}/${name}" + if ! west build -p always -b "${BOARD}" -d "${bdir}" "${app}" \ + -- ${EXTRA_MODULE_ARG} ${extra} \ + > "${bdir}.build.log" 2>&1; then + printf '%-16s %-8s %s\n' "${name}" "FAIL" "(build failed; see ${bdir}.build.log)" + rc=1 + continue + fi + # native_sim block-buffers piped stdout; stdbuf forces line buffering so the + # (kernel-idle-looping) sample still flushes before the timeout kill. + timeout 120 stdbuf -oL -eL "${bdir}/zephyr/zephyr.exe" > "${bdir}.run.log" 2>&1 + if [ "${mode}" = ztest ]; then + if grep -q "PROJECT EXECUTION SUCCESSFUL" "${bdir}.run.log" && \ + ! grep -q "PROJECT EXECUTION FAILED" "${bdir}.run.log"; then + printf '%-16s %-8s %s\n' "${name}" "ok" "PASS" + else + printf '%-16s %-8s %s\n' "${name}" "ok" "FAIL (see ${bdir}.run.log)" + rc=1 + fi + else + if grep -q "ALL PSA SMOKE TESTS PASSED" "${bdir}.run.log" || \ + grep -q "Sample finished successfully." "${bdir}.run.log"; then + printf '%-16s %-8s %s\n' "${name}" "ok" "PASS" + else + printf '%-16s %-8s %s\n' "${name}" "ok" "FAIL (see ${bdir}.run.log)" + rc=1 + fi + fi +done < wolfPSA git repo URL (the code under test) +# -b, --branch wolfPSA branch/revision (the code under test) +# -z, --zephyr Zephyr version tag (>= v4.3.0) +# --wolfssl-repo wolfSSL repo URL (default: wolfSSL/wolfssl) +# --wolfssl-ref wolfSSL revision (default: master; must carry the +# native Zephyr threading -- override until it lands) +# -h, --help Show this help +# +# Examples: +# ./zephyr-test.sh -z v4.3.0 +# ./zephyr-test.sh -r https://github.com/me/wolfPSA -b my-fix -z v4.4.0 + +set -euo pipefail + +# Defaults point at the upstream wolfSSL org so this script is ready for the +# eventual upstream PR. The workflow overrides --wolfssl-repo/--wolfssl-ref (and +# -r/-b) as needed; in particular the native Zephyr threading a thread-safe +# wolfPSA build needs is not yet in upstream wolfSSL master, so CI overrides the +# wolfSSL ref until it lands. +WOLFPSA_REPO="https://github.com/wolfSSL/wolfPSA" +WOLFPSA_BRANCH="master" +ZEPHYR_VERSION="v4.4.0" +WOLFSSL_REPO="https://github.com/wolfSSL/wolfssl" +WOLFSSL_REF="master" + +GHCR="ghcr.io/zephyrproject-rtos/zephyr-build" + +usage() { sed -n '3,/^$/s/^# \?//p' "$0"; exit 0; } + +# wolfPSA needs Zephyr >= 4.3 (the PSA_CRYPTO_PROVIDER_CUSTOM hook). Pick the SDK +# image the way the wolfSSL 4.x driver does. +select_docker_image() { + local ver="${1#v}" + local major="${ver%%.*}" + local minor="${ver#*.}" + minor="${minor%%.*}" + if [[ "$major" -ge 4 && "$minor" -ge 4 ]]; then + echo "${GHCR}:v0.29.2" # Zephyr 4.4+ : SDK 1.x + elif [[ "$major" -ge 4 && "$minor" -ge 3 ]]; then + echo "${GHCR}:v0.28.8" # Zephyr 4.3 : SDK 0.17.x + else + echo "ERROR: wolfPSA requires Zephyr >= 4.3 (got v${ver})" >&2 + exit 1 + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -r|--repo) WOLFPSA_REPO="$2"; shift 2 ;; + -b|--branch) WOLFPSA_BRANCH="$2"; shift 2 ;; + -z|--zephyr) ZEPHYR_VERSION="$2"; shift 2 ;; + --wolfssl-repo) WOLFSSL_REPO="$2"; shift 2 ;; + --wolfssl-ref) WOLFSSL_REF="$2"; shift 2 ;; + -h|--help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +DOCKER_IMAGE=$(select_docker_image "$ZEPHYR_VERSION") +CONTAINER_NAME="wolfpsa-zephyr-${ZEPHYR_VERSION#v}-$$" + +echo "==> wolfPSA repo/ref: ${WOLFPSA_REPO}@${WOLFPSA_BRANCH}" +echo "==> wolfSSL repo/ref: ${WOLFSSL_REPO}@${WOLFSSL_REF}" +echo "==> Zephyr version: ${ZEPHYR_VERSION}" +echo "==> Docker image: ${DOCKER_IMAGE}" + +echo "==> Pulling Docker image..." +docker pull "${DOCKER_IMAGE}" + +BUILD_SCRIPT=$(cat <<'INNER_SCRIPT' +#!/bin/bash +set -euo pipefail + +ZEPHYR_VERSION="__ZEPHYR_VERSION__" +WOLFPSA_REPO="__WOLFPSA_REPO__" +WOLFPSA_BRANCH="__WOLFPSA_BRANCH__" +WOLFSSL_REPO="__WOLFSSL_REPO__" +WOLFSSL_REF="__WOLFSSL_REF__" + +cd /workdir + +echo "==> [container] west init (${ZEPHYR_VERSION})..." +west init --mr "${ZEPHYR_VERSION}" zephyrproject +cd zephyrproject/zephyr + +# Inject the wolfSSL module (wolfCrypt core) and wolfPSA (self) into west.yml, +# the same sed approach the wolfSSL CI uses. +WSSL_BASE=$(echo "${WOLFSSL_REPO}" | sed 's|/[^/]*$||') +WPSA_BASE=$(echo "${WOLFPSA_REPO}" | sed 's|/[^/]*$||') +WSSL_REF=$(echo "${WOLFSSL_REF}" | sed 's/\//\\\//g') +WPSA_REF=$(echo "${WOLFPSA_BRANCH}" | sed 's/\//\\\//g') + +sed -i "s|remotes:|remotes:\n - name: wolfssl\n url-base: ${WSSL_BASE}\n - name: wolfpsa\n url-base: ${WPSA_BASE}|" west.yml +sed -i "s|projects:|projects:\n - name: wolfssl\n path: modules/crypto/wolfssl\n remote: wolfssl\n revision: ${WSSL_REF}\n - name: wolfPSA\n path: modules/crypto/wolfPSA\n remote: wolfpsa\n revision: ${WPSA_REF}|" west.yml +echo "==> [container] west.yml module entries:"; grep -A2 -E "wolfssl|wolfpsa|wolfPSA" west.yml +cd .. + +echo "==> [container] west update..." +export GIT_TERMINAL_PROMPT=0 +west update -n -o=--depth=1 +west zephyr-export + +echo "==> [container] Python deps..." +sudo apt-get update -qq && sudo apt-get install -y -qq python3-venv >/dev/null 2>&1 || true +python3 -m venv .venv && source .venv/bin/activate +pip3 install west >/dev/null +pip3 install -r zephyr/scripts/requirements.txt >/dev/null + +export ZEPHYR_BASE="/workdir/zephyrproject/zephyr" +if [[ -z "${ZEPHYR_SDK_INSTALL_DIR:-}" ]]; then + SDK_DIR=$(find /opt -maxdepth 2 -name "zephyr-sdk-*" -type d 2>/dev/null | head -1) + [[ -n "$SDK_DIR" ]] && export ZEPHYR_SDK_INSTALL_DIR="$SDK_DIR" +fi + +# Both modules are in the manifest now, so the runner must NOT also add wolfPSA +# via EXTRA_ZEPHYR_MODULES. +echo "==> [container] Running the wolfPSA test suite..." +EXTRA_MODULE_ARG="" \ + bash modules/crypto/wolfPSA/.github/scripts/run-tests.sh +INNER_SCRIPT +) + +BUILD_SCRIPT="${BUILD_SCRIPT//__ZEPHYR_VERSION__/$ZEPHYR_VERSION}" +BUILD_SCRIPT="${BUILD_SCRIPT//__WOLFPSA_REPO__/$WOLFPSA_REPO}" +BUILD_SCRIPT="${BUILD_SCRIPT//__WOLFPSA_BRANCH__/$WOLFPSA_BRANCH}" +BUILD_SCRIPT="${BUILD_SCRIPT//__WOLFSSL_REPO__/$WOLFSSL_REPO}" +BUILD_SCRIPT="${BUILD_SCRIPT//__WOLFSSL_REF__/$WOLFSSL_REF}" + +cleanup() { docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true; } +trap cleanup EXIT +docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true + +echo "==> Starting container..." +docker run --name "${CONTAINER_NAME}" --rm "${DOCKER_IMAGE}" bash -c "${BUILD_SCRIPT}" diff --git a/.github/workflows/zephyr-4.x.yml b/.github/workflows/zephyr-4.x.yml new file mode 100644 index 0000000..ecb50b1 --- /dev/null +++ b/.github/workflows/zephyr-4.x.yml @@ -0,0 +1,71 @@ +name: Zephyr 4.x tests + +# Builds and runs the wolfPSA Zephyr samples/tests across the supported Zephyr +# range. Mirrors the wolfSSL module's zephyr-4.x workflow: a matrix over Zephyr +# versions driving a Docker-based test script (.github/scripts/zephyr-4.x/ +# zephyr-test.sh), which sets up the wolfSSL + wolfPSA modules and runs the +# shared test runner (.github/scripts/run-tests.sh). wolfPSA requires Zephyr +# >= 4.3 (the PSA_CRYPTO_PROVIDER_CUSTOM hook), so the floor of the matrix is 4.3. + +on: + push: + branches: [ 'release/**' ] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: [ '*' ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: ${{ matrix.zephyr-ref }} + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + runs-on: ubuntu-22.04 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + # 4.3 = the wolfPSA floor (upstream), 4.4 = current target. + zephyr-ref: [ v4.3.0, v4.4.0 ] + steps: + - name: Checkout wolfPSA CI driver + uses: actions/checkout@v5 + with: + sparse-checkout: .github/scripts + fetch-depth: 1 + + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ + /opt/hostedtoolcache/CodeQL "$AGENT_TOOLSDIRECTORY" || true + docker system prune -af || true + df -h / + + - name: Resolve wolfPSA repo and ref + id: src + run: | + # Test the PR merged into its base (matches the wolfSSL workflow), so + # fixes already on the target branch are included. + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "repo=https://github.com/${{ github.repository }}" >> "$GITHUB_OUTPUT" + echo "ref=refs/pull/${{ github.event.pull_request.number }}/merge" >> "$GITHUB_OUTPUT" + else + echo "repo=https://github.com/${{ github.repository }}" >> "$GITHUB_OUTPUT" + echo "ref=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + fi + + - name: Build and run wolfPSA samples/tests + working-directory: .github/scripts/zephyr-4.x + run: | + # The wolfCrypt native Zephyr threading a thread-safe wolfPSA build + # needs is not yet in upstream wolfSSL master (the script's default), + # so point the wolfSSL dependency at the PR owner's fork/branch that + # carries it. Drop this override once the change lands upstream. + bash ./zephyr-test.sh \ + -r "${{ steps.src.outputs.repo }}" \ + -b "${{ steps.src.outputs.ref }}" \ + -z "${{ matrix.zephyr-ref }}" \ + --wolfssl-repo "https://github.com/${{ github.repository_owner }}/wolfssl" \ + --wolfssl-ref zephyr_fixes diff --git a/.gitignore b/.gitignore index f6b5499..6ba85ef 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,10 @@ test/psa_lms_xmss_verify_test test/psa_ascon_xchacha_test test/psa_sp800_108_test test/psa_14_misc_test + +# Local-only editor/agent context (not part of the module) +CLAUDE.md + +# Real-hardware board configs are kept local, not in-tree (CI is native_sim only) +zephyr/**/boards/nucleo_*.conf +zephyr/**/boards/nucleo_*.overlay diff --git a/CHANGELOG.md b/CHANGELOG.md index 9539a0f..5d5884a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,29 @@ wolfSSL master. SLH-DSA key types are recognized but report NOT_SUPPORTED. - New coverage tests: ML-DSA, ML-KEM/KEM API, XOF, AES-KW, signature contexts, LMS/XMSS verify, Ascon/XChaCha, SP800-108 and 1.4 misc. +- `psa_purge_key()`: confirms a key exists and reports its status (wolfPSA + keeps no in-RAM cache of persistent key material, so there is nothing to + evict). +- Optional thread-safe key store: with `WOLFPSA_THREAD_SAFE` a single mutex + built on wolfCrypt's portable `wc_*Mutex` API (created in `psa_crypto_init()`) + guards the volatile-key list and id counter for concurrent PSA callers; a + no-op in single-threaded builds. + +### Zephyr module + +- Added wolfPSA as a Zephyr **PSA Crypto provider**: selected via + `CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM` (Zephyr >= 4.3), it supplies the PSA Crypto + API in place of Mbed TLS while reusing the wolfCrypt core built by the wolfSSL + Zephyr module. Volatile and persistent keys, RNG, and the standard PSA surface + work with no Mbed TLS symbol in the image. +- wolfCrypt AES-256-GCM custom ITS transform + (`CONFIG_SECURE_STORAGE_ITS_TRANSFORM_IMPLEMENTATION_CUSTOM`) provides + encryption-at-rest for persistent keys, replacing Zephyr's Mbed-TLS-coupled + AEAD transform. `psa_get_key_attributes()` restores the key id; persistent + keys use the crypto-provider ITS namespace (isolated from application + `psa_its_*`/`psa_ps_*`). +- wolfPSA follows the user's wolfCrypt configuration and exposes exactly the + enabled, wolfPSA-implemented algorithms as the PSA API. ## v5.9.1 diff --git a/src/psa_api_stub.c b/src/psa_api_stub.c index 2b0398f..46e39fa 100644 --- a/src/psa_api_stub.c +++ b/src/psa_api_stub.c @@ -126,11 +126,6 @@ psa_status_t psa_pake_setup(psa_pake_operation_t *operation, psa_key_id_t passwo return wolfPSA_StubNotSupported(); } -psa_status_t psa_purge_key(psa_key_id_t key) { - (void)key; - return wolfPSA_StubNotSupported(); -} - /* --- Key attachment (hardware-bound keys) --- */ psa_status_t psa_attach_key(const psa_key_attributes_t *attributes, diff --git a/src/psa_asymmetric_api.c b/src/psa_asymmetric_api.c index e926dc7..d83c479 100644 --- a/src/psa_asymmetric_api.c +++ b/src/psa_asymmetric_api.c @@ -1269,11 +1269,13 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, uint8_t *key_data = NULL; size_t key_data_length = 0; psa_status_t status; +#ifdef HAVE_ECC int ret; ecc_key priv; ecc_key pub; int curve_id; word32 out_len; +#endif if (PSA_ALG_KEY_AGREEMENT_GET_BASE(alg) != PSA_ALG_ECDH) { return PSA_ERROR_NOT_SUPPORTED; @@ -1325,6 +1327,7 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, return status; } +#ifdef HAVE_ECC curve_id = wc_psa_get_ecc_curve_id(attributes.type, attributes.bits); if (curve_id == ECC_CURVE_INVALID) { wolfpsa_forcezero_free_key_data(key_data, key_data_length); @@ -1399,6 +1402,12 @@ psa_status_t wolfpsa_key_agreement_secret(psa_algorithm_t alg, *output_length = (size_t)out_len; return PSA_SUCCESS; +#else + /* Generic (Weierstrass) ECDH needs wolfCrypt ECC (HAVE_ECC), which this + * build does not enable. Montgomery X25519/X448 is handled above. */ + wolfpsa_forcezero_free_key_data(key_data, key_data_length); + return PSA_ERROR_NOT_SUPPORTED; +#endif /* HAVE_ECC */ } psa_status_t psa_raw_key_agreement(psa_algorithm_t alg, diff --git a/src/psa_crypto.c b/src/psa_crypto.c index 7391ebf..b727048 100644 --- a/src/psa_crypto.c +++ b/src/psa_crypto.c @@ -29,33 +29,59 @@ #include #include "psa_trace.h" +#include "psa_lock.h" #include #include +/* Init latch. The check-and-set in psa_crypto_init() is serialized so + * wolfCrypt_Init() runs exactly once even if several threads race at boot, and + * the read below takes the same lock so it is not a data race with that write. */ static int g_psa_crypto_initialized = 0; int wolfPSA_CryptoIsInitialized(void) { - return g_psa_crypto_initialized; + int initialized; + + if (WOLFPSA_LOCK_INIT() != 0) { + return 0; + } + + WOLFPSA_LOCK(); + initialized = g_psa_crypto_initialized; + WOLFPSA_UNLOCK(); + + return initialized; } psa_status_t psa_crypto_init(void) { - int ret; + int ret = 0; + psa_status_t status = PSA_SUCCESS; wolfpsa_trace("psa_crypto_init()"); - if (g_psa_crypto_initialized) { - return PSA_SUCCESS; + /* Create the key-store mutex before any WOLFPSA_LOCK() runs. This is the + * platform-neutral init point: the PSA contract requires psa_crypto_init() + * before any other PSA call, so a bare (non-Zephyr) build lands here just as + * Zephyr's boot glue does. The init is idempotent, so repeated calls (or an + * additional platform bootstrap) are harmless. */ + if (WOLFPSA_LOCK_INIT() != 0) { + return PSA_ERROR_GENERIC_ERROR; } - ret = wolfCrypt_Init(); - if (ret != 0) { - return wc_error_to_psa_status(ret); + WOLFPSA_LOCK(); + if (!g_psa_crypto_initialized) { + ret = wolfCrypt_Init(); + if (ret != 0) { + status = wc_error_to_psa_status(ret); + } + else { + g_psa_crypto_initialized = 1; + } } + WOLFPSA_UNLOCK(); - g_psa_crypto_initialized = 1; - return PSA_SUCCESS; + return status; } #endif /* WOLFSSL_PSA_ENGINE */ diff --git a/src/psa_ecc.c b/src/psa_ecc.c index 18887b3..9531bf0 100644 --- a/src/psa_ecc.c +++ b/src/psa_ecc.c @@ -120,6 +120,7 @@ psa_status_t psa_asymmetric_sign_ecc(psa_key_type_t key_type, } if (PSA_ALG_IS_DETERMINISTIC_ECDSA(alg)) { +#ifdef WOLFSSL_ECDSA_DETERMINISTIC_K int hash_type = wc_psa_get_hash_type(alg); if (hash_type == WC_HASH_TYPE_NONE) { wc_FreeRng(&rng); @@ -132,6 +133,12 @@ psa_status_t psa_asymmetric_sign_ecc(psa_key_type_t key_type, wc_ecc_free(&ecc); return wc_error_to_psa_status(ret); } +#else + /* Deterministic ECDSA needs WOLFSSL_ECDSA_DETERMINISTIC_K. */ + wc_FreeRng(&rng); + wc_ecc_free(&ecc); + return PSA_ERROR_NOT_SUPPORTED; +#endif } sig_len = wc_ecc_sig_size(&ecc); diff --git a/src/psa_key_storage.c b/src/psa_key_storage.c index 2edb0c0..3c647a3 100644 --- a/src/psa_key_storage.c +++ b/src/psa_key_storage.c @@ -32,6 +32,7 @@ #include #include #include "psa_trace.h" +#include "psa_lock.h" #include "psa_size.h" #include #include @@ -65,6 +66,31 @@ typedef struct wolfpsa_volatile_key_node { static wolfpsa_volatile_key_node* g_volatile_keys = NULL; +/* Global lock guarding all shared key-store state above (g_volatile_keys, + * g_next_key_id, g_key_storage_initialized). No-op unless WOLFPSA_THREAD_SAFE. + * See psa_lock.h. */ +WOLFPSA_DEFINE_LOCK(); + +#if defined(WOLFPSA_THREAD_SAFE) +/* One-time creation of the global key-store mutex (WOLFPSA_LOCK_INIT). The plain + * flag guard is safe under the PSA contract that psa_crypto_init() runs before + * any concurrent PSA use, so no two threads reach wc_InitMutex() at once. */ +static int g_wolfpsa_lock_ready = 0; + +int wolfpsa_lock_ensure_init(void) +{ + int ret = 0; + + if (!g_wolfpsa_lock_ready) { + ret = wc_InitMutex(&wolfpsa_global_mutex); + if (ret == 0) { + g_wolfpsa_lock_ready = 1; + } + } + return ret; +} +#endif /* WOLFPSA_THREAD_SAFE */ + /* Internal init state from psa_crypto_init() */ extern int wolfPSA_CryptoIsInitialized(void); extern int wc_psa_get_ecc_curve_id(psa_key_type_t type, size_t bits); @@ -288,44 +314,55 @@ static psa_status_t wolfpsa_volatile_store(psa_key_id_t key_id, size_t data_length) { wolfpsa_volatile_key_node* node; + psa_status_t st = PSA_SUCCESS; - if (attributes == NULL || data == NULL || data_length == 0) { - return PSA_ERROR_INVALID_ARGUMENT; - } + WOLFPSA_LOCK(); - if (wolfpsa_volatile_find(key_id) != NULL) { - return PSA_ERROR_ALREADY_EXISTS; + if (attributes == NULL || data == NULL || data_length == 0) { + st = PSA_ERROR_INVALID_ARGUMENT; } - - node = (wolfpsa_volatile_key_node*)XMALLOC(sizeof(*node), NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (node == NULL) { - return PSA_ERROR_INSUFFICIENT_MEMORY; + else if (wolfpsa_volatile_find(key_id) != NULL) { + st = PSA_ERROR_ALREADY_EXISTS; } - XMEMSET(node, 0, sizeof(*node)); + else { + node = (wolfpsa_volatile_key_node*)XMALLOC(sizeof(*node), NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (node == NULL) { + st = PSA_ERROR_INSUFFICIENT_MEMORY; + } + else { + XMEMSET(node, 0, sizeof(*node)); + node->data = (uint8_t*)XMALLOC(data_length, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (node->data == NULL) { + XFREE(node, NULL, DYNAMIC_TYPE_TMP_BUFFER); + st = PSA_ERROR_INSUFFICIENT_MEMORY; + } + else { + XMEMCPY(node->data, data, data_length); + node->data_length = data_length; + node->attributes = *attributes; + node->id = key_id; - node->data = (uint8_t*)XMALLOC(data_length, NULL, DYNAMIC_TYPE_TMP_BUFFER); - if (node->data == NULL) { - XFREE(node, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return PSA_ERROR_INSUFFICIENT_MEMORY; + node->next = g_volatile_keys; + g_volatile_keys = node; + } + } } - XMEMCPY(node->data, data, data_length); - node->data_length = data_length; - node->attributes = *attributes; - node->id = key_id; - - node->next = g_volatile_keys; - g_volatile_keys = node; - - return PSA_SUCCESS; + WOLFPSA_UNLOCK(); + return st; } static psa_status_t wolfpsa_volatile_remove(psa_key_id_t key_id) { - wolfpsa_volatile_key_node* cur = g_volatile_keys; + wolfpsa_volatile_key_node* cur; wolfpsa_volatile_key_node* prev = NULL; + psa_status_t st = PSA_ERROR_INVALID_HANDLE; + + WOLFPSA_LOCK(); + cur = g_volatile_keys; while (cur != NULL) { if (cur->id == key_id) { if (prev != NULL) { @@ -340,21 +377,24 @@ static psa_status_t wolfpsa_volatile_remove(psa_key_id_t key_id) } XMEMSET(cur, 0, sizeof(*cur)); XFREE(cur, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return PSA_SUCCESS; + st = PSA_SUCCESS; + break; } prev = cur; cur = cur->next; } - return PSA_ERROR_INVALID_HANDLE; + WOLFPSA_UNLOCK(); + return st; } static psa_status_t wolfpsa_volatile_get(psa_key_id_t key_id, - psa_key_attributes_t* attributes, - uint8_t** key_data, - size_t* key_data_length) + psa_key_attributes_t* attributes, + uint8_t** key_data, + size_t* key_data_length) { wolfpsa_volatile_key_node* node; + psa_status_t st = PSA_SUCCESS; if (key_data == NULL || key_data_length == NULL) { return PSA_ERROR_INVALID_ARGUMENT; @@ -363,47 +403,59 @@ static psa_status_t wolfpsa_volatile_get(psa_key_id_t key_id, *key_data = NULL; *key_data_length = 0; + WOLFPSA_LOCK(); + node = wolfpsa_volatile_find(key_id); if (node == NULL) { - return PSA_ERROR_INVALID_HANDLE; - } - - if (attributes != NULL) { - *attributes = node->attributes; - } - - if (node->data_length == 0 || node->data == NULL) { - return PSA_ERROR_DATA_INVALID; + st = PSA_ERROR_INVALID_HANDLE; } + else { + if (attributes != NULL) { + *attributes = node->attributes; + } - *key_data = (uint8_t*)XMALLOC(node->data_length, NULL, - DYNAMIC_TYPE_TMP_BUFFER); - if (*key_data == NULL) { - return PSA_ERROR_INSUFFICIENT_MEMORY; + if (node->data_length == 0 || node->data == NULL) { + st = PSA_ERROR_DATA_INVALID; + } + else { + *key_data = (uint8_t*)XMALLOC(node->data_length, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (*key_data == NULL) { + st = PSA_ERROR_INSUFFICIENT_MEMORY; + } + else { + XMEMCPY(*key_data, node->data, node->data_length); + *key_data_length = node->data_length; + } + } } - XMEMCPY(*key_data, node->data, node->data_length); - *key_data_length = node->data_length; - - return PSA_SUCCESS; + WOLFPSA_UNLOCK(); + return st; } static psa_status_t wolfpsa_volatile_get_attributes(psa_key_id_t key_id, psa_key_attributes_t* attributes) { wolfpsa_volatile_key_node* node; + psa_status_t st = PSA_SUCCESS; if (attributes == NULL) { return PSA_ERROR_INVALID_ARGUMENT; } + WOLFPSA_LOCK(); + node = wolfpsa_volatile_find(key_id); if (node == NULL) { - return PSA_ERROR_INVALID_HANDLE; + st = PSA_ERROR_INVALID_HANDLE; + } + else { + *attributes = node->attributes; } - *attributes = node->attributes; - return PSA_SUCCESS; + WOLFPSA_UNLOCK(); + return st; } static psa_status_t wolfpsa_infer_key_bits(psa_key_attributes_t* attr, @@ -669,14 +721,27 @@ static size_t psa_der_write_int(uint8_t* out, const uint8_t* val, size_t len) psa_status_t psa_key_storage_init(const wc_KeyVault_Callbacks* callbacks) { (void)callbacks; + /* Publicly reachable before psa_crypto_init(); create the mutex first so + * WOLFPSA_LOCK() never runs on an uninitialized lock. */ + if (WOLFPSA_LOCK_INIT() != 0) { + return PSA_ERROR_GENERIC_ERROR; + } + WOLFPSA_LOCK(); g_key_storage_initialized = 1; + WOLFPSA_UNLOCK(); return PSA_SUCCESS; } /* Cleanup the PSA key storage subsystem */ void psa_key_storage_cleanup(void) { - wolfpsa_volatile_key_node* cur = g_volatile_keys; + wolfpsa_volatile_key_node* cur; + + if (WOLFPSA_LOCK_INIT() != 0) { + return; + } + WOLFPSA_LOCK(); + cur = g_volatile_keys; while (cur != NULL) { wolfpsa_volatile_key_node* next = cur->next; if (cur->data != NULL) { @@ -689,16 +754,29 @@ void psa_key_storage_cleanup(void) } g_volatile_keys = NULL; g_key_storage_initialized = 0; + WOLFPSA_UNLOCK(); } psa_key_id_t wolfpsa_test_get_next_key_id(void) { - return g_next_key_id; + psa_key_id_t id; + if (WOLFPSA_LOCK_INIT() != 0) { + return 0; + } + WOLFPSA_LOCK(); + id = g_next_key_id; + WOLFPSA_UNLOCK(); + return id; } void wolfpsa_test_set_next_key_id(psa_key_id_t key_id) { + if (WOLFPSA_LOCK_INIT() != 0) { + return; + } + WOLFPSA_LOCK(); g_next_key_id = key_id; + WOLFPSA_UNLOCK(); } /* Check if the key storage is initialized */ @@ -708,11 +786,12 @@ static psa_status_t psa_key_storage_check_init(void) return PSA_ERROR_BAD_STATE; } - if (!g_key_storage_initialized) { - (void)psa_key_storage_init(NULL); - } - - return PSA_SUCCESS; + /* psa_key_storage_init() is idempotent and takes the lock itself; call it + * directly rather than nesting a second acquisition inside our own lock. + * Avoiding the nested lock keeps the global mutex non-recursive, so + * WOLFPSA_THREAD_SAFE works with a plain mutex off Zephyr too (see + * psa_lock.h). */ + return psa_key_storage_init(NULL); } #ifdef WOLFPSA_DEBUG_IMPORT @@ -861,7 +940,7 @@ psa_status_t wolfpsa_get_key_data(psa_key_id_t key_id, sizeof(psa_key_lifetime_t); ret = wolfPSA_Store_Open(WOLFPSA_STORE_KEY, (unsigned long)key_id, 0, 1, &store); - if (ret == -4) { + if (ret == WOLFPSA_STORE_NOT_AVAILABLE) { return PSA_ERROR_INVALID_HANDLE; } if (ret != 0) { @@ -1164,12 +1243,16 @@ psa_status_t psa_import_key( * they cannot collide with caller-specified persistent ids, which * the PSA API reserves to the user range. A collision would let an * auto-assigned volatile key shadow a persistent record on read and - * survive psa_destroy_key() on disk. */ + * survive psa_destroy_key() on disk. Guard the counter so concurrent + * callers each get a distinct id. */ + WOLFPSA_LOCK(); if (g_next_key_id < PSA_KEY_ID_VENDOR_MIN || g_next_key_id > PSA_KEY_ID_VENDOR_MAX) { + WOLFPSA_UNLOCK(); return PSA_ERROR_INSUFFICIENT_STORAGE; } *key_id = g_next_key_id++; + WOLFPSA_UNLOCK(); } } @@ -1529,7 +1612,7 @@ psa_status_t psa_destroy_key(psa_key_id_t key_id) /* Remove key from persistent storage */ ret = wolfPSA_Store_Remove(WOLFPSA_STORE_KEY, (unsigned long)key_id, 0); - if (ret == -4) { + if (ret == WOLFPSA_STORE_NOT_AVAILABLE) { return PSA_ERROR_INVALID_HANDLE; } if (ret != 0) { @@ -1608,7 +1691,7 @@ psa_status_t psa_export_key( sizeof(psa_key_lifetime_t); ret = wolfPSA_Store_Open(WOLFPSA_STORE_KEY, (unsigned long)key_id, 0, 1, &store); - if (ret == -4) { + if (ret == WOLFPSA_STORE_NOT_AVAILABLE) { return PSA_ERROR_INVALID_HANDLE; } if (ret != 0) { @@ -1719,7 +1802,7 @@ psa_status_t psa_export_public_key( ret = wolfPSA_Store_Open(WOLFPSA_STORE_KEY, (unsigned long)key_id, 0, 1, &store); - if (ret == -4) { + if (ret == WOLFPSA_STORE_NOT_AVAILABLE) { return PSA_ERROR_INVALID_HANDLE; } if (ret != 0) { @@ -2022,7 +2105,7 @@ psa_status_t psa_get_key_attributes( sizeof(psa_key_lifetime_t); ret = wolfPSA_Store_Open(WOLFPSA_STORE_KEY, (unsigned long)key_id, 0, 1, &store); - if (ret == -4) { + if (ret == WOLFPSA_STORE_NOT_AVAILABLE) { return PSA_ERROR_INVALID_HANDLE; } if (ret != 0) { @@ -2038,6 +2121,31 @@ psa_status_t psa_get_key_attributes( /* Deserialize key attributes */ status = psa_key_attributes_deserialize(buffer, attr_length, attributes); + if (status == PSA_SUCCESS) { + /* The key id is the storage locator, not part of the serialized attr + * blob, so it must be restored in the returned attributes. Preserve the + * deserialized lifetime, which psa_set_key_id() would otherwise reset to + * the default persistent lifetime. */ + psa_key_lifetime_t lifetime = psa_get_key_lifetime(attributes); + psa_set_key_id(attributes, key_id); + psa_set_key_lifetime(attributes, lifetime); + } + return status; +} + +psa_status_t psa_purge_key(psa_key_id_t key) +{ + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_status_t status; + + /* wolfPSA keeps no in-RAM cache of persistent key material: persistent keys + * are reloaded from the store on every use (see wolfpsa_get_key_data()), + * and volatile keys must stay resident. There is therefore nothing to + * purge. Confirm the key exists and report success, which matches the PSA + * semantics for a key that has no purgeable cached copies. */ + status = psa_get_key_attributes(key, &attributes); + psa_reset_key_attributes(&attributes); + return status; } @@ -2124,7 +2232,7 @@ psa_status_t psa_copy_key( sizeof(psa_key_lifetime_t); ret = wolfPSA_Store_Open(WOLFPSA_STORE_KEY, (unsigned long)source_key, 0, 1, &store); - if (ret == -4) { + if (ret == WOLFPSA_STORE_NOT_AVAILABLE) { return PSA_ERROR_INVALID_HANDLE; } if (ret != 0) { diff --git a/src/psa_lock.h b/src/psa_lock.h new file mode 100644 index 0000000..794c190 --- /dev/null +++ b/src/psa_lock.h @@ -0,0 +1,100 @@ +/* psa_lock.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Optional serialization of wolfPSA's shared mutable state. + * + * wolfPSA keeps its volatile-key list and the auto-id counter in file-scope + * globals in psa_key_storage.c. When independent threads may call the PSA API + * concurrently, those shared-state accesses must be serialized. + * + * WOLFPSA_LOCK()/WOLFPSA_UNLOCK() wrap every access to that shared state. They + * map to a single global mutex when WOLFPSA_THREAD_SAFE is defined, and to + * no-ops otherwise (a single-threaded build). The mutex uses wolfCrypt's + * portable mutex API (wc_InitMutex / wc_LockMutex / wc_UnLockMutex), so it works + * on any platform wolfCrypt runs on -- there is nothing platform-specific here. + * WOLFPSA_THREAD_SAFE must be paired with a multi-threaded wolfCrypt build. + * + * The mutex is created once via WOLFPSA_LOCK_INIT() (idempotent) and is never + * taken recursively: no guarded entry point calls another while holding it, so + * a plain non-recursive mutex is sufficient. + * + * Scope: this lock covers only wolfPSA's in-memory key-store structures. + * Per-operation crypto uses stack-local wolfCrypt contexts and per-call local + * WC_RNGs, and wolfpsa_get_key_data() returns an owned COPY of the key material, + * so no live list node escapes the locked region. The persistent-store backends + * are deliberately NOT covered by this lock and do not need it: they are + * concurrency-safe on their own (the file backend commits with an atomic + * temp-file rename and reads through a held file handle; a platform store such + * as Zephyr secure_storage owns its own consistency). + */ + +#ifndef WOLFPSA_PSA_LOCK_H +#define WOLFPSA_PSA_LOCK_H + +/* WOLFPSA_THREAD_SAFE is the portable switch. A Kconfig-based build (e.g. + * Zephyr's CONFIG_WOLFPSA_THREAD_SAFE) maps its option onto it here; any other + * build defines WOLFPSA_THREAD_SAFE directly. */ +#if defined(CONFIG_WOLFPSA_THREAD_SAFE) && !defined(WOLFPSA_THREAD_SAFE) + #define WOLFPSA_THREAD_SAFE +#endif + +#if defined(WOLFPSA_THREAD_SAFE) + +#include +#include + +/* The single global mutex is defined once (via WOLFPSA_DEFINE_LOCK) in + * psa_key_storage.c, alongside wolfpsa_lock_ensure_init(). */ +extern wolfSSL_Mutex wolfpsa_global_mutex; + +#define WOLFPSA_DEFINE_LOCK() wolfSSL_Mutex wolfpsa_global_mutex + +/* One-time, idempotent mutex creation (defined in psa_key_storage.c). It must + * run before any WOLFPSA_LOCK() can; psa_crypto_init() calls it, and the PSA + * contract requires psa_crypto_init() before any other PSA call -- so this is a + * platform-neutral bootstrap (no Zephyr init hook needed). Returns the + * wc_InitMutex return code so a caller can surface a mutex-init failure instead + * of reporting a dead lock as success. */ +int wolfpsa_lock_ensure_init(void); +#define WOLFPSA_LOCK_INIT() wolfpsa_lock_ensure_init() + +/* LOCK/UNLOCK discard their return codes, matching wolfCrypt's own + * (void)wc_LockMutex convention: a correctly-initialized, non-recursive mutex + * only fails on a programming error (a bad handle or a self-deadlock), which + * this code designs out -- not a runtime condition worth branching on. The one + * failure that CAN happen at runtime, creating the mutex, is surfaced by + * WOLFPSA_LOCK_INIT() above. */ +#define WOLFPSA_LOCK() (void)wc_LockMutex(&wolfpsa_global_mutex) +#define WOLFPSA_UNLOCK() (void)wc_UnLockMutex(&wolfpsa_global_mutex) + +#else /* single-threaded / standalone build: no-ops */ + +/* Expands to a forward struct declaration so the `;` at the single use site is + * consumed -- no stray file-scope semicolon under -Wpedantic. */ +#define WOLFPSA_DEFINE_LOCK() struct wolfpsa_lock_placeholder +#define WOLFPSA_LOCK_INIT() 0 +#define WOLFPSA_LOCK() ((void)0) +#define WOLFPSA_UNLOCK() ((void)0) + +#endif + +#endif /* WOLFPSA_PSA_LOCK_H */ diff --git a/src/psa_rsa.c b/src/psa_rsa.c index 15e401b..fedc051 100644 --- a/src/psa_rsa.c +++ b/src/psa_rsa.c @@ -47,6 +47,10 @@ int wc_psa_get_rsa_padding(psa_algorithm_t alg); int wc_psa_get_hash_type(psa_algorithm_t alg); +/* Only the PSS and OAEP paths use MGF1; guard the helper with the same + * condition as its callers so a config with neither (e.g. RSA sign/verify with + * PKCS#1 v1.5 only) does not trip -Werror=unused-function. */ +#if defined(WC_RSA_PSS) || defined(WOLFSSL_RSA_OAEP) static int wc_psa_get_mgf(int hash_type) { switch (hash_type) { @@ -68,6 +72,7 @@ static int wc_psa_get_mgf(int hash_type) return WC_MGF1NONE; } } +#endif /* WC_RSA_PSS || WOLFSSL_RSA_OAEP */ #ifdef WC_RSA_PSS static int wc_psa_rsa_pss_check_padding(const byte* hash, word32 hash_length, diff --git a/src/psa_store_posix.c b/src/psa_store_posix.c index 7951bea..0c3b48c 100644 --- a/src/psa_store_posix.c +++ b/src/psa_store_posix.c @@ -42,8 +42,7 @@ #include #endif -#define WOLFPSA_STORE_NOT_AVAILABLE (-4) -#define WOLFPSA_STORE_IO_ERROR (-5) +/* WOLFPSA_STORE_NOT_AVAILABLE / WOLFPSA_STORE_IO_ERROR come from psa_store.h. */ #define WOLFPSA_STORE_MAX_PATH 256 #if defined(_WIN32) || defined(_MSC_VER) diff --git a/src/psa_store_zephyr.c b/src/psa_store_zephyr.c new file mode 100644 index 0000000..f44e39a --- /dev/null +++ b/src/psa_store_zephyr.c @@ -0,0 +1,319 @@ +/* psa_store_zephyr.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Zephyr PSA store backend. + * + * This is the wolfPSA persistent-store backend for Zephyr, selected by defining + * WOLFPSA_CUSTOM_STORE (which compiles out the default POSIX backend in + * psa_store_posix.c). It implements the six wolfPSA_Store_* entry points from + * . + * + * When CONFIG_SECURE_STORAGE is enabled the six entry points map onto the + * Zephyr PSA Internal Trusted Storage (ITS) API (psa_its_set/get/get_info/ + * remove). ITS is a whole-object store, so: + * - writes accumulate into a heap buffer and are committed with psa_its_set() + * inside wolfPSA_Store_Write() (Close is void and cannot report failure, so + * the commit happens where its status can propagate through the return + * value that psa_key_storage.c checks); psa_its_set() is atomic, so a failed + * commit leaves any prior value intact, mirroring the POSIX backend's + * atomic-rename-on-close semantics; + * - reads use psa_its_get() with an advancing offset cursor, satisfying the + * sequential partial reads (header then body) that psa_key_storage.c does. + * The UID is the key id directly (mirrors tf-psa-crypto's non-owner + * psa_its_identifier_of_slot(); the PSA user key-id range is 30-bit, matching + * Zephyr's default ITS UID width). + * + * When CONFIG_SECURE_STORAGE is not enabled the entry points fall back to a + * "not available" stub: only volatile keys work (they never touch the store), + * and persistent-key APIs degrade cleanly to a storage error. + */ + +#ifdef HAVE_CONFIG_H + #include +#endif + +#include + +#if defined(WOLFPSA_CUSTOM_STORE) + +#include + +/* WOLFPSA_STORE_NOT_AVAILABLE / WOLFPSA_STORE_IO_ERROR come from psa_store.h. */ + +#if defined(CONFIG_SECURE_STORAGE) + +#include +#include + +/* wolfPSA is the PSA Crypto provider, so its persistent-key records must live in + * the crypto-provider ITS caller namespace -- isolated from application + * psa_its_*()/psa_ps_*() data at the same numeric id. secure_storage picks the + * namespace from ITS_CALLER_ID in , which is + * SECURE_STORAGE_ITS_CALLER_MBEDTLS when BUILDING_MBEDTLS_CRYPTO is defined + * (that is where the Mbed TLS provider stores keys) and the application-facing + * SECURE_STORAGE_ITS_CALLER_PSA_ITS otherwise. Select the provider namespace so + * wolfPSA occupies the same isolated slot Mbed TLS would. */ +#ifndef BUILDING_MBEDTLS_CRYPTO +#define BUILDING_MBEDTLS_CRYPTO +#endif +#include + +/* Per-open context. For writes, buf accumulates the object until it is + * committed; for reads, off is the sequential cursor and len the total size. */ +typedef struct WolfpsaZephyrStore { + psa_storage_uid_t uid; + unsigned char* buf; + size_t len; + size_t off; + int write; +} WolfpsaZephyrStore; + +/* Map a wolfPSA store record identity to an ITS UID. Only WOLFPSA_STORE_KEY + * records exist today and id2 is always 0; the UID is the key id itself. */ +static psa_storage_uid_t wolfpsa_store_uid(int type, unsigned long id1, + unsigned long id2) +{ + (void)type; + (void)id2; + return (psa_storage_uid_t)id1; +} + +int wolfPSA_Store_OpenSz(int type, unsigned long id1, unsigned long id2, int read, + int variableSz, void** store) +{ + int ret = WOLFPSA_STORE_OK; + psa_storage_uid_t uid; + WolfpsaZephyrStore* ctx = NULL; + struct psa_storage_info_t info; + psa_status_t st; + + /* variableSz is only a hint and understates the total written, so the + * write buffer grows on demand in wolfPSA_Store_Write() instead. */ + (void)variableSz; + + if (store == NULL) { + return WOLFPSA_STORE_IO_ERROR; + } + *store = NULL; + + uid = wolfpsa_store_uid(type, id1, id2); + if (uid == 0) { + /* ITS requires a nonzero UID; PSA_KEY_ID_NULL is never persisted. */ + return WOLFPSA_STORE_IO_ERROR; + } + + if (read) { + st = psa_its_get_info(uid, &info); + if (st == PSA_ERROR_DOES_NOT_EXIST) { + return WOLFPSA_STORE_NOT_AVAILABLE; + } + if (st != PSA_SUCCESS) { + return WOLFPSA_STORE_IO_ERROR; + } + } + + ctx = (WolfpsaZephyrStore*)XMALLOC(sizeof(*ctx), NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (ctx == NULL) { + return WOLFPSA_STORE_IO_ERROR; + } + XMEMSET(ctx, 0, sizeof(*ctx)); + ctx->uid = uid; + ctx->write = (read == 0); + if (read) { + ctx->len = (size_t)info.size; + } + + *store = ctx; + return ret; +} + +int wolfPSA_Store_Open(int type, unsigned long id1, unsigned long id2, int read, + void** store) +{ + return wolfPSA_Store_OpenSz(type, id1, id2, read, 0, store); +} + +int wolfPSA_Store_Remove(int type, unsigned long id1, unsigned long id2) +{ + psa_storage_uid_t uid; + psa_status_t st; + + uid = wolfpsa_store_uid(type, id1, id2); + if (uid == 0) { + return WOLFPSA_STORE_IO_ERROR; + } + + st = psa_its_remove(uid); + if (st == PSA_ERROR_DOES_NOT_EXIST) { + return WOLFPSA_STORE_NOT_AVAILABLE; + } + if (st != PSA_SUCCESS) { + return WOLFPSA_STORE_IO_ERROR; + } + return WOLFPSA_STORE_OK; +} + +void wolfPSA_Store_Close(void* store) +{ + WolfpsaZephyrStore* ctx = (WolfpsaZephyrStore*)store; + + if (ctx != NULL) { + if (ctx->buf != NULL) { + /* The write buffer holds serialized key material. */ + wc_ForceZero(ctx->buf, ctx->len); + XFREE(ctx->buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + XMEMSET(ctx, 0, sizeof(*ctx)); + XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } +} + +int wolfPSA_Store_Read(void* store, unsigned char* buffer, int len) +{ + WolfpsaZephyrStore* ctx = (WolfpsaZephyrStore*)store; + psa_status_t st; + size_t got = 0; + + if (ctx == NULL || ctx->write || buffer == NULL || len < 0) { + return WOLFPSA_STORE_IO_ERROR; + } + if (len == 0) { + return 0; + } + + st = psa_its_get(ctx->uid, ctx->off, (size_t)len, buffer, &got); + if (st != PSA_SUCCESS) { + return WOLFPSA_STORE_IO_ERROR; + } + ctx->off += got; + return (int)got; +} + +int wolfPSA_Store_Write(void* store, unsigned char* buffer, int len) +{ + WolfpsaZephyrStore* ctx = (WolfpsaZephyrStore*)store; + unsigned char* grown; + psa_status_t st; + + if (ctx == NULL || ctx->write == 0 || buffer == NULL || len < 0) { + return WOLFPSA_STORE_IO_ERROR; + } + + if (len > 0) { + /* Grow the accumulation buffer manually (not XREALLOC) so the old + * buffer, which holds key material, is zeroed before being freed. */ + grown = (unsigned char*)XMALLOC(ctx->len + (size_t)len, NULL, + DYNAMIC_TYPE_TMP_BUFFER); + if (grown == NULL) { + return WOLFPSA_STORE_IO_ERROR; + } + if (ctx->buf != NULL) { + XMEMCPY(grown, ctx->buf, ctx->len); + wc_ForceZero(ctx->buf, ctx->len); + XFREE(ctx->buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } + XMEMCPY(grown + ctx->len, buffer, (size_t)len); + ctx->buf = grown; + ctx->len += (size_t)len; + } + + if (ctx->len == 0) { + return len; + } + + /* Commit the whole accumulated object now (atomic) so a storage failure is + * reported through this return value rather than swallowed by the void + * Close. Re-committing on each Write is harmless for the single-Write usage + * in psa_key_storage.c and leaves the final object correct if streamed. */ + st = psa_its_set(ctx->uid, ctx->len, ctx->buf, 0); + if (st != PSA_SUCCESS) { + return WOLFPSA_STORE_IO_ERROR; + } + return len; +} + +#else /* !CONFIG_SECURE_STORAGE */ + +/* + * No persistent backend available: report "not available" so that reads of a + * persistent key miss cleanly and writes are treated as unsupported. Volatile + * keys never reach the store, so all volatile-key PSA flows still work. + */ + +int wolfPSA_Store_OpenSz(int type, unsigned long id1, unsigned long id2, int read, + int variableSz, void** store) +{ + (void)type; + (void)id1; + (void)id2; + (void)read; + (void)variableSz; + + if (store != NULL) { + *store = NULL; + } + + return WOLFPSA_STORE_NOT_AVAILABLE; +} + +int wolfPSA_Store_Open(int type, unsigned long id1, unsigned long id2, int read, + void** store) +{ + return wolfPSA_Store_OpenSz(type, id1, id2, read, 0, store); +} + +int wolfPSA_Store_Remove(int type, unsigned long id1, unsigned long id2) +{ + (void)type; + (void)id1; + (void)id2; + + return WOLFPSA_STORE_NOT_AVAILABLE; +} + +void wolfPSA_Store_Close(void* store) +{ + (void)store; +} + +int wolfPSA_Store_Read(void* store, unsigned char* buffer, int len) +{ + (void)store; + (void)buffer; + (void)len; + + return WOLFPSA_STORE_IO_ERROR; +} + +int wolfPSA_Store_Write(void* store, unsigned char* buffer, int len) +{ + (void)store; + (void)buffer; + (void)len; + + return WOLFPSA_STORE_IO_ERROR; +} + +#endif /* CONFIG_SECURE_STORAGE */ + +#endif /* WOLFPSA_CUSTOM_STORE */ diff --git a/test/psa_server/psa_14_misc_test.c b/test/psa_server/psa_14_misc_test.c index 2a69c62..48d5920 100644 --- a/test/psa_server/psa_14_misc_test.c +++ b/test/psa_server/psa_14_misc_test.c @@ -828,6 +828,43 @@ static int test_gcm_nonce_lengths(void) (void)psa_destroy_key(key); printf("PASS: GCM nonce length handling\n"); + + return 0; +} + +/* psa_purge_key(): a live key purges to PSA_SUCCESS, an absent one to + * PSA_ERROR_INVALID_HANDLE (wolfPSA keeps no purgeable persistent-key cache). */ +static int test_purge_key(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + const uint8_t key[16] = { 0 }; + psa_status_t st; + + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&a, PSA_ALG_GCM); + psa_set_key_type(&a, PSA_KEY_TYPE_AES); + psa_set_key_bits(&a, 128); + + if (psa_import_key(&a, key, sizeof(key), &k) != PSA_SUCCESS) { + printf("FAIL psa_purge_key import\n"); + return 1; + } + st = psa_purge_key(k); + if (st != PSA_SUCCESS) { + printf("FAIL psa_purge_key(live) status=%d\n", (int)st); + return 1; + } + if (psa_destroy_key(k) != PSA_SUCCESS) { + printf("FAIL psa_purge_key destroy\n"); + return 1; + } + st = psa_purge_key(k); + if (st != PSA_ERROR_INVALID_HANDLE) { + printf("FAIL psa_purge_key(absent) status=%d expected=%d\n", + (int)st, (int)PSA_ERROR_INVALID_HANDLE); + return 1; + } return 0; } @@ -893,6 +930,10 @@ int main(void) if (test_gcm_nonce_lengths() != 0) return 1; + /* Case 14: psa_purge_key live + absent */ + if (test_purge_key() != 0) + return 1; + printf("PSA 1.4 misc test: OK\n"); return 0; } diff --git a/user_settings.h b/user_settings.h index 3090fb7..b5f8ef3 100644 --- a/user_settings.h +++ b/user_settings.h @@ -22,6 +22,18 @@ #ifndef WOLFSSL_USER_SETTINGS_H #define WOLFSSL_USER_SETTINGS_H +#if defined(__ZEPHYR__) +/* Zephyr build: the wolfSSL module's user_settings.h is authoritative -- it + * honors CONFIG_WOLFSSL_SETTINGS_FILE (the user's own config) and applies the + * wolfPSA structural baseline, and the module's wolfCrypt objects compile + * against it too, so wolfPSA's own sources must share it (identical struct ABI). + * Because the wolfpsa/ include dir sits ahead of the module's on the include + * path, wolfcrypt/settings.h's `#include "user_settings.h"` resolves HERE first; + * defer to the next user_settings.h on the path (the module's) rather than apply + * this standalone-Makefile configuration. */ +#include_next "user_settings.h" +#else + #define WOLFCRYPT_ONLY #define SINGLE_THREADED #define WOLFSSL_PSA_ENGINE @@ -93,4 +105,6 @@ #define HAVE_XCHACHA #define HAVE_CMAC_KDF +#endif /* __ZEPHYR__ */ + #endif /* WOLFSSL_USER_SETTINGS_H */ diff --git a/wolfpsa/psa/crypto_config.h b/wolfpsa/psa/crypto_config.h index ab2665e..c1ba0de 100644 --- a/wolfpsa/psa/crypto_config.h +++ b/wolfpsa/psa/crypto_config.h @@ -22,6 +22,15 @@ #ifndef WOLFPSA_CRYPTO_CONFIG_H #define WOLFPSA_CRYPTO_CONFIG_H +#if defined(CONFIG_WOLFPSA) + +/* Zephyr build: derive the PSA_WANT_* surface from the consumer's Kconfig + * (CONFIG_PSA_WANT_*) instead of the small hardcoded list below, so that the + * size macros in crypto_sizes.h and any PSA_WANT_*-gated consumer code match + * whatever capabilities the application enabled. */ +#include "crypto_config_zephyr.h" + +#else /* standalone (Makefile) build */ #define PSA_WANT_ALG_SHA_1 #define PSA_WANT_ALG_SHA_224 @@ -35,4 +44,6 @@ #define PSA_WANT_KEY_TYPE_ML_DSA #define PSA_WANT_KEY_TYPE_ML_KEM +#endif /* CONFIG_WOLFPSA */ + #endif diff --git a/wolfpsa/psa_store.h b/wolfpsa/psa_store.h index 6ecfb15..f5b6d8f 100644 --- a/wolfpsa/psa_store.h +++ b/wolfpsa/psa_store.h @@ -26,6 +26,13 @@ #define WOLFPSA_STORE_KEY 0x00 +/* Shared return codes for the wolfPSA_Store_* backend interface below. + * Backends (psa_store_posix.c, psa_store_zephyr.c, custom vaults) must use + * these so callers in psa_key_storage.c interpret results consistently. */ +#define WOLFPSA_STORE_OK 0 +#define WOLFPSA_STORE_NOT_AVAILABLE (-4) +#define WOLFPSA_STORE_IO_ERROR (-5) + /* * Opens access to location to read/write PSA data. * diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt new file mode 100644 index 0000000..71c2b97 --- /dev/null +++ b/zephyr/CMakeLists.txt @@ -0,0 +1,127 @@ +# wolfPSA Zephyr module - PSA Crypto provider on top of wolfCrypt +# +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# wolfPSA compiles ONLY its own PSA frontend (src/*.c). The wolfCrypt core is +# provided by the sibling wolfSSL module: its headers are on the global include +# path and its compiled objects link into the final image, so wolfPSA resolves +# every wc_* symbol at final link with zero recompilation of wolfCrypt. + +if(CONFIG_WOLFPSA) + set(WOLFPSA_DIR ${ZEPHYR_CURRENT_MODULE_DIR}) + + # Expose the PSA headers to the WHOLE build so that resolves + # for every Zephyr consumer (BLE, TLS credentials, samples, ...). This is how + # wolfPSA becomes THE system PSA provider, mirroring the mbedTLS interface + # library's include propagation. + # ${WOLFPSA_DIR} -> , + # ${WOLFPSA_DIR}/wolfpsa -> , , + # + # BEFORE (prepend): wolfPSA is THE PSA provider, so its psa/ headers must win + # the global include order over any subsystem that ships a partial psa/ set. + # In particular Zephyr's secure_storage subsystem ships a storage-only + # guarded by the same PSA_ERROR_H; searched first it would + # starve wolfPSA of the full PSA error set. wolfPSA's error codes are a + # value-compatible superset, so secure_storage's own files still build. + # (Storage-only headers wolfPSA does not provide -- , + # -- still resolve to secure_storage.) + target_include_directories(zephyr_interface BEFORE INTERFACE + ${WOLFPSA_DIR} + ${WOLFPSA_DIR}/wolfpsa + ) + + # --- Build-time PSA surface generation (config-driven, Model B) ------------ + # wolfPSA exposes as the PSA API exactly the crypto that the user's wolfCrypt + # configuration enabled (and that wolfPSA implements). We derive the + # consumer-visible PSA_WANT_ surface from the ACTUAL compiled config by + # preprocessing zephyr/gen/psa_want_probe.c and generating + # crypto_config_zephyr.h, which -> crypto_config.h includes. + # Generated at CONFIGURE time (autoconf.h already exists) so it is present + # before any TU that includes compiles. + if(DEFINED ZEPHYR_WOLFSSL_MODULE_DIR) + set(_wolfssl_mod_dir ${ZEPHYR_WOLFSSL_MODULE_DIR}) + else() + set(_wolfssl_mod_dir ${WOLFPSA_DIR}/../wolfssl) + endif() + if(DEFINED AUTOCONF_H) + set(_wolfpsa_autoconf ${AUTOCONF_H}) + else() + set(_wolfpsa_autoconf + ${CMAKE_BINARY_DIR}/zephyr/include/generated/zephyr/autoconf.h) + endif() + + set(WOLFPSA_GEN_DIR ${CMAKE_CURRENT_BINARY_DIR}/wolfpsa_gen) + set(WOLFPSA_GEN_HDR ${WOLFPSA_GEN_DIR}/crypto_config_zephyr.h) + set(WOLFPSA_PROBE ${WOLFPSA_DIR}/zephyr/gen/psa_want_probe.c) + set(WOLFPSA_GEN_PY ${WOLFPSA_DIR}/zephyr/gen/gen-crypto-config.py) + file(MAKE_DIRECTORY ${WOLFPSA_GEN_DIR}) + + set(_wolfpsa_probe_flags + -D__ZEPHYR__=1 -DWOLFSSL_USER_SETTINGS -DWOLFSSL_ZEPHYR + -I${WOLFPSA_DIR} -I${WOLFPSA_DIR}/wolfpsa + -I${_wolfssl_mod_dir}/zephyr + -imacros ${_wolfpsa_autoconf}) + # If the user supplied their own wolfCrypt config, the probe must see it too. + if(CONFIG_WOLFSSL_SETTINGS_FILE) + list(APPEND _wolfpsa_probe_flags + "-DWOLFSSL_SETTINGS_FILE=\"${CONFIG_WOLFSSL_SETTINGS_FILE}\"") + endif() + + execute_process( + COMMAND ${PYTHON_EXECUTABLE} ${WOLFPSA_GEN_PY} + ${WOLFPSA_GEN_HDR} ${CMAKE_C_COMPILER} ${WOLFPSA_PROBE} + ${_wolfpsa_probe_flags} + RESULT_VARIABLE _wolfpsa_gen_rc) + if(NOT _wolfpsa_gen_rc EQUAL 0) + message(FATAL_ERROR + "wolfPSA: failed to generate the PSA_WANT surface (rc=${_wolfpsa_gen_rc})") + endif() + + # The generated header wins because the checked-in one was removed; put its + # dir on the global include path so -> crypto_config.h finds it. + zephyr_include_directories(${WOLFPSA_GEN_DIR}) + + # Re-run configure (and thus regenerate) when the config inputs change. The + # effective wolfCrypt config now comes entirely from the wolfSSL module's + # user_settings.h (plus the user's settings file, if any) -- wolfPSA no longer + # provides a config of its own. + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + ${WOLFPSA_PROBE} ${WOLFPSA_GEN_PY} + ${_wolfssl_mod_dir}/zephyr/user_settings.h) + if(CONFIG_WOLFSSL_SETTINGS_FILE) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + ${CONFIG_WOLFSSL_SETTINGS_FILE}) + endif() + + # Collect all PSA frontend sources, minus the POSIX store backend (replaced by + # the Zephyr backend and disabled via WOLFPSA_CUSTOM_STORE). + file(GLOB WOLFPSA_SOURCES ${WOLFPSA_DIR}/src/*.c) + list(REMOVE_ITEM WOLFPSA_SOURCES ${WOLFPSA_DIR}/src/psa_store_posix.c) + + zephyr_library() + zephyr_library_sources(${WOLFPSA_SOURCES}) + zephyr_library_sources(${WOLFPSA_DIR}/zephyr/zephyr_init.c) + + # wolfCrypt-backed custom ITS transform for the secure_storage subsystem. + # Selecting the CUSTOM transform keeps Zephyr's Mbed-TLS-coupled aead.c out of + # the build, so persistent-key storage encryption runs through wolfCrypt only. + zephyr_library_sources_ifdef(CONFIG_SECURE_STORAGE_ITS_TRANSFORM_IMPLEMENTATION_CUSTOM + ${WOLFPSA_DIR}/zephyr/src/psa_its_transform_wolfcrypt.c) + + # Internal headers ("psa_trace.h", "psa_size.h", *_internal.h) live in src/. + zephyr_library_include_directories(${WOLFPSA_DIR}/src) + + # Select the pluggable store backend: our Zephyr implementation, not POSIX. + zephyr_library_compile_definitions(WOLFPSA_CUSTOM_STORE) + + # WOLFSSL_PSA_ENGINE is the master gate for wolfPSA's own sources. Nothing in + # the shared wolfCrypt library reads it, so it is defined ONLY for wolfPSA's + # translation units here -- it is not part of the wolfCrypt config the module + # builds against. + zephyr_library_compile_definitions(WOLFSSL_PSA_ENGINE) + + # Inherit WOLFSSL_USER_SETTINGS + WOLFSSL_ZEPHYR and the wolfcrypt include + # dirs from the wolfSSL module's interface library. + zephyr_library_link_libraries(wolfSSL) +endif() diff --git a/zephyr/Kconfig b/zephyr/Kconfig new file mode 100644 index 0000000..b48bdc7 --- /dev/null +++ b/zephyr/Kconfig @@ -0,0 +1,85 @@ +# wolfPSA - PSA Crypto API provider on top of wolfCrypt +# +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +config ZEPHYR_WOLFPSA_MODULE + bool + +# wolfPSA is the out-of-tree ("custom") PSA Crypto provider. It plugs into the +# PSA_CRYPTO_PROVIDER choice defined by the in-tree mbedtls module +# (zephyr/modules/mbedtls/Kconfig.psa.logic), which was ADDED IN ZEPHYR 4.3 -- +# so wolfPSA requires Zephyr >= 4.3 (below that PSA_CRYPTO_PROVIDER_CUSTOM does +# not exist and CONFIG_WOLFPSA can never be selected). Selecting the CUSTOM +# option keeps +# Mbed TLS / TF-M from being pulled in and leaves the PSA surface to us: +# - we supply (from wolfpsa/psa/, put on the global include +# path by our CMakeLists), +# - we select PSA_CRYPTO_CLIENT to unlock the whole CONFIG_PSA_WANT_* tree +# (that symbol gates it in zephyr/modules/mbedtls/Kconfig.psa.auto), and +# - we register our own SYS_INIT that calls psa_crypto_init(). +# +# The wolfCrypt core is reused from the sibling wolfSSL module rather than +# rebuilt, so we select WOLFSSL (WOLFSSL_BUILTIN is its choice default). +config WOLFPSA + bool "wolfPSA PSA Crypto provider" + depends on PSA_CRYPTO_PROVIDER_CUSTOM + depends on WOLFSSL + default y if PSA_CRYPTO_PROVIDER_CUSTOM + select PSA_CRYPTO_CLIENT + # wolfPSA does not provide its own wolfCrypt configuration; it derives the + # PSA surface from whatever config the wolfSSL module was built with. The + # structural profile its RNG needs -- a Hash-DRBG (seeded by the wolfSSL + # module's wc_GenerateSeed() from the HW entropy driver when present) and + # single-threaded wolfCrypt on a no-threads kernel -- comes from the wolfSSL + # module's own Kconfig/user_settings.h defaults, so wolfPSA selects nothing + # here. + # It deliberately does NOT touch WOLFCRYPT_ONLY: whether the wolfSSL TLS layer + # is in the build is a wolfCrypt-config decision (WOLFSSL_CRYPTO_ONLY / your + # settings file), and wolfPSA works either way -- crypto-only, or alongside + # TLS in one image (wolfCrypt is shared; TLS still uses wc_* directly, not + # the PSA interface). + help + Use wolfPSA (PSA Crypto API implemented on wolfCrypt) as the system's + PSA Crypto provider. Requires PSA_CRYPTO_PROVIDER_CUSTOM to be chosen so + that the Mbed TLS provider is not built, and CONFIG_WOLFSSL=y so the + wolfCrypt core it reuses is compiled. + + Note: wolfPSA deliberately does NOT "select WOLFSSL". Zephyr's socket + Kconfig makes PSA_CRYPTO's availability depend on "!WOLFSSL", so any + select of WOLFSSL from inside the PSA_CRYPTO_PROVIDER choice forms a + Kconfig dependency loop. Enable CONFIG_WOLFSSL=y explicitly instead. + + Whether the wolfSSL TLS layer is in the build is a wolfCrypt-config choice, + not a wolfPSA one: set CONFIG_WOLFSSL_CRYPTO_ONLY=y (or use a WOLFCRYPT_ONLY + settings file) for a crypto-only image, or leave TLS in to run the wolfSSL + TLS stack and the wolfPSA provider together in one image. wolfPSA works + either way, and TLS keeps using wolfCrypt directly (not the PSA interface). + +if WOLFPSA + +config WOLFPSA_THREAD_SAFE + bool "Serialize wolfPSA shared state with a global mutex" + default y + depends on MULTITHREADING + help + wolfPSA keeps its volatile key list and auto-id counter in process-global + state with no internal locking. Enable this to guard that shared state + with a single global, non-recursive mutex so multiple threads can call the + PSA API concurrently. Disable only if all PSA use is confined to a single thread + (saves a mutex and the lock/unlock overhead). + +# Ergonomics: wolfPSA's wolfCrypt AES-GCM custom ITS transform +# (zephyr/src/psa_its_transform_wolfcrypt.c) adds a 12-byte nonce plus a 16-byte +# GCM tag, so default the secure_storage transform overhead to 28 when the +# custom transform is selected. Consumers enabling persistent keys via +# secure_storage then need not set this by hand (a BUILD_ASSERT in the transform +# still guards against a mismatch). +config SECURE_STORAGE_ITS_TRANSFORM_OUTPUT_OVERHEAD + default 28 if SECURE_STORAGE_ITS_TRANSFORM_IMPLEMENTATION_CUSTOM + +module = WOLFPSA +module-str = wolfPSA +source "subsys/logging/Kconfig.template.log_config" + +endif # WOLFPSA diff --git a/zephyr/README.md b/zephyr/README.md new file mode 100644 index 0000000..ba164f4 --- /dev/null +++ b/zephyr/README.md @@ -0,0 +1,215 @@ +# wolfPSA as a Zephyr PSA Crypto provider + +This directory makes wolfPSA a Zephyr module that provides the PSA Crypto API +(replacing the Mbed TLS provider), reusing the wolfCrypt core built by the +sibling wolfSSL Zephyr module. See the "Second build path: Zephyr module" +section of `../CLAUDE.md` for the architecture. + +## Requirements + +- **Zephyr 4.3 or newer.** wolfPSA plugs into Zephyr's pluggable PSA Crypto + provider choice (`CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM`), which was introduced in + **Zephyr 4.3**. On older Zephyr that Kconfig symbol does not exist, so + `CONFIG_WOLFPSA` can never be enabled and the module is inert -- there is no + extension point to plug into. Persistent keys additionally use the + `secure_storage` subsystem (Zephyr 4.0+), which the 4.3 floor already covers. +- **Tested on Zephyr 4.3 and 4.4** (CI builds and runs the samples/tests on + native_sim for both). + +(This floor is specific to wolfPSA-as-a-PSA-provider. The sibling wolfSSL module +that supplies the wolfCrypt core is a plain library and supports a much wider +Zephyr range.) + +## Enabling wolfPSA in an application + +In `prj.conf`: + +``` +CONFIG_WOLFSSL=y # wolfCrypt core (not auto-selected; see Kconfig) +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y # selects wolfPSA (CONFIG_WOLFPSA defaults y) +CONFIG_ENTROPY_GENERATOR=y # DRBG seed source; auto-enables CSPRNG_ENABLED +``` + +You do not set `CONFIG_WOLFPSA` yourself: `CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y` +auto-selects it (it defaults `y`). It is the symbol that gates every wolfPSA +source, so it is the one to reference from your own Kconfig (`depends on WOLFPSA`) +if needed. + +`CONFIG_ENTROPY_GENERATOR=y` is enough to have wolfPSA seed the DRBG from the +platform CSPRNG (`sys_csrand_get`): it pulls in the entropy driver, and +`CONFIG_CSPRNG_ENABLED` is then `default y`. Avoid `CONFIG_CSPRNG_NEEDED` -- that +request symbol only exists in Zephyr 4.4+ and aborts the Kconfig parse on 4.3. + +RNG interplay with the wolfSSL module: wolfPSA reuses the single wolfCrypt core +built by the wolfSSL module, so there is ONE wolfCrypt HashDRBG shared by both -- +not a separate wolfPSA RNG. wolfPSA's boot init registers the Zephyr-CSPRNG seed +callback so that shared DRBG is seeded from Zephyr's entropy source (the +platform's hardware TRNG / entropy driver via `sys_csrand_get`). + +## Configuring which crypto wolfPSA exposes + +wolfPSA owns **no** wolfCrypt configuration of its own. It follows whatever the +wolfSSL module was built with and exposes exactly the enabled (and +wolfPSA-implemented) algorithms as the PSA API. You configure wolfCrypt the +normal wolfSSL way -- either the module's Kconfig options, or your own settings +file: + +``` +CONFIG_WOLFSSL_SETTINGS_FILE="/abs/path/to/my_user_settings.h" +``` + +The consumer-visible `PSA_WANT_*` surface is generated from the resulting +compiled config at build time; enabling fewer wolfCrypt features shrinks both the +PSA surface and the image, and an app that requests a family you did not enable +gets an honest compile-time miss. A ready-made broad example config is provided +at `zephyr/user_settings_example.h` (opt in with +`CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h"`); the samples/tests use +it. (Realistic reductions -- dropping asymmetric families, PQC, curves, or +RSA/ECC -- are supported; dropping the symmetric core is not yet guarded.) + +The only wolfCrypt profile wolfPSA imposes is structural, and it is applied +through generic wolfSSL Kconfig knobs the wolfPSA Kconfig `select`s +(`WOLFSSL_HASH_DRBG`, `WOLFSSL_RNG_SEED_CB`, and `WOLFSSL_SINGLE_THREADED` on a +no-threads kernel) -- the wolfSSL module stays entirely wolfPSA-unaware, and +`WOLFSSL_PSA_ENGINE` is defined only for wolfPSA's own sources. `WOLFSSL_CRYPTO_ONLY` +is NOT among them: crypto-only versus TLS coexistence is a user choice (see below), +not a wolfPSA requirement. + +### Coexisting with the wolfSSL TLS stack + +Whether the wolfSSL TLS layer is in the build is a wolfCrypt-config decision, not +a wolfPSA one -- wolfPSA works crypto-only or alongside TLS without any knob of +its own: + +- **Crypto-only** (no TLS): set `CONFIG_WOLFSSL_CRYPTO_ONLY=y` (or use a + `WOLFCRYPT_ONLY` settings file). This is what the samples/tests do. +- **TLS + wolfPSA in one image**: leave `WOLFCRYPT_ONLY` off and use a + TLS-capable wolfCrypt config (e.g. the module's default -- do NOT point + `CONFIG_WOLFSSL_SETTINGS_FILE` at the crypto-only example), then configure TLS + features the usual way (`WOLFSSL_TLS_*`, cipher suites, ...). + +wolfCrypt is shared by both layers, so there is no duplication, and the TLS layer +keeps calling wolfCrypt **directly** -- it does not route its crypto through the +PSA interface (`WOLFSSL_HAVE_PSA` stays off). `zephyr/tests/psa_tls_coexist` +builds exactly this: it runs a TLS handshake and PSA operations together in one +image. (The TLS layer's socket I/O pulls in the Zephyr networking stack.) + +## Persistent keys (secure_storage) + +Volatile keys work with the config above. For **persistent** keys, wolfPSA backs +the PSA store with the Zephyr `secure_storage` subsystem (the same PSA ITS path +Mbed TLS uses), with **no Mbed TLS in the image**: + +- `src/psa_store_zephyr.c` maps the `wolfPSA_Store_*` backend onto the PSA ITS API + (`psa_its_set`/`get`/`get_info`/`remove`) when `CONFIG_SECURE_STORAGE` is set + (otherwise it stays a volatile-only stub). +- `zephyr/src/psa_its_transform_wolfcrypt.c` is a wolfCrypt **AES-256-GCM** custom + ITS transform, selected with `CONFIG_SECURE_STORAGE_ITS_TRANSFORM_IMPLEMENTATION_CUSTOM`. + This replaces Zephyr's stock AEAD transform, which is hardwired to Mbed TLS core + internals (`psa_driver_wrapper_*`, `mbedtls_platform_zeroize`) and would not link + in a wolfPSA build. + +Add to `prj.conf` (on top of the base config), choosing a store backend +(`SETTINGS` shown; `ZMS` also works): + +``` +CONFIG_SECURE_STORAGE=y +CONFIG_SECURE_STORAGE_ITS_STORE_IMPLEMENTATION_SETTINGS=y +CONFIG_SECURE_STORAGE_ITS_TRANSFORM_IMPLEMENTATION_CUSTOM=y +CONFIG_SETTINGS=y +CONFIG_SETTINGS_NVS=y +CONFIG_NVS=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +# AES-GCM for the transform (wolfPSA defaults OUTPUT_OVERHEAD to 28 for you): +CONFIG_PSA_WANT_KEY_TYPE_AES=y +CONFIG_PSA_WANT_ALG_GCM=y +``` + +Validated by running Zephyr's own `samples/psa/persistent_key`, `samples/psa/its` +and `tests/subsys/secure_storage/psa/crypto` against wolfPSA (the `psa_its`, +`psa_persistent_key` and `psa_secure_storage` wrappers), plus the `psa_transform` +unit test. + +## Module discovery + +The recommended way is to add wolfPSA as a project in your workspace's +`manifest/west.yml`, exactly like the wolfSSL module, so `west update` pulls it +in and it is discovered automatically (no extra build flags needed): + +```yaml + - name: wolfpsa + path: modules/crypto/wolfpsa + revision: + # remote: +``` + +CI does not use a checked-in manifest: `.github/scripts/zephyr-4.x/zephyr-test.sh` +runs its own `west init` and injects the wolfSSL and wolfPSA module entries. + +Alternatively, for an out-of-manifest checkout, point the build at the module +explicitly with `EXTRA_ZEPHYR_MODULES` (absolute path to the wolfPSA repo root): + +```sh +# host is aarch64 here -> use the 64-bit native_sim variant +WOLFPSA=$(cd .. && pwd) # wolfPSA repo root (the dir containing zephyr/) +``` + +## Build & run the samples (native_sim) + +```sh +cd # e.g. /home/tobi/wolfssl/zephyr +west build -p always -b native_sim/native/64 -d build-x \ + "$WOLFPSA/zephyr/samples/psa_smoke" -- -DEXTRA_ZEPHYR_MODULES="$WOLFPSA" +# native_sim block-buffers piped stdout; force line-buffering to see logs: +stdbuf -oL -eL ./build-x/zephyr/zephyr.exe +``` + +Sample: `samples/psa_smoke` -- a broad capability smoke +(hashes/MAC/AES/RSA/ECDSA/ECDH/ML-KEM/ML-DSA). + +## Run the samples and tests + +The simplest way (and what CI uses) is the helper script, run from inside the +west workspace -- it builds and runs every sample and test on native_sim and +prints a pass/fail summary: + +```sh +cd # e.g. /home/tobi/wolfssl/zephyr +"$WOLFPSA/.github/scripts/run-tests.sh" # exits non-zero on any failure +``` + +CI (`.github/workflows/zephyr-4.x.yml`) runs the same script across a Zephyr 4.x +matrix via the Docker driver `.github/scripts/zephyr-4.x/zephyr-test.sh`, +mirroring the wolfSSL module's Zephyr CI. + +To run the ztests through twister instead, pass the module as a CMake extra +arg with twister's `-x` flag (there is no `--extra-module`/`--west-flags` +option): + +```sh +cd +west twister -p native_sim/native/64 \ + -T "$WOLFPSA/zephyr/tests" \ + -x=EXTRA_ZEPHYR_MODULES="$WOLFPSA" +``` + +Tests are (a) thin **wrappers that run Zephyr's OWN provider-agnostic apps +against wolfPSA** (the drop-in proof, reusing their source from `$ZEPHYR_BASE`) +and (b) checks of wolfPSA-internal behavior Zephyr cannot cover: +- `tests/psa_consumer` -- Zephyr's `tests/crypto/mbedtls_psa` source, unchanged. +- `tests/psa_its` -- Zephyr's `samples/psa/its`. +- `tests/psa_persistent_key` -- Zephyr's `samples/psa/persistent_key`. +- `tests/psa_secure_storage` -- Zephyr's `tests/subsys/secure_storage/psa/crypto` + (persistent key + ITS + PS caller isolation). +- `tests/psa_concurrency` -- wolfPSA's key-store lock (no Zephyr equivalent). +- `tests/psa_entropy` -- seed-failure -> `PSA_ERROR_INSUFFICIENT_ENTROPY`. +- `tests/psa_transform` -- wolfCrypt AES-GCM ITS transform confidentiality / + tamper / UID-binding (Zephyr's custom-transform test is a plaintext passthrough). +- `tests/psa_tls_coexist` -- a wolfSSL TLS handshake and PSA operations in one + image (the TLS layer and the provider coexist; TLS is not made crypto-only). + +For CI/production, add a `wolfpsa` project to `manifest/west.yml` (path +`modules/crypto/wolfpsa`) so the module is discovered without `EXTRA_ZEPHYR_MODULES`. + diff --git a/zephyr/gen/gen-crypto-config.py b/zephyr/gen/gen-crypto-config.py new file mode 100644 index 0000000..e64966d --- /dev/null +++ b/zephyr/gen/gen-crypto-config.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Generate wolfpsa/psa/crypto_config_zephyr.h from the ACTUAL compiled wolfCrypt +# configuration, by preprocessing psa_want_probe.c with the same flags as the +# rest of the build. The result is the consumer-visible PSA_WANT_ surface: +# exactly the algorithm families wolfCrypt has enabled AND wolfPSA implements. +# +# Python (not shell) to fit the Zephyr build ecosystem, invoked via CMake's +# PYTHON_EXECUTABLE from wolfPSA's zephyr/CMakeLists.txt. +# +# Usage: +# gen-crypto-config.py [preprocessor flags...] +# +# The preprocessor flags must reproduce the build's wolfCrypt config -- at +# minimum -DWOLFSSL_USER_SETTINGS -DWOLFSSL_ZEPHYR, the wolfssl/wolfpsa include +# dirs, any -DWOLFSSL_SETTINGS_FILE, and -imacros . + +import re +import subprocess +import sys + +HEADER = """\ +/* crypto_config_zephyr.h + * + * Copyright (C) 2026 wolfSSL Inc. + * SPDX-License-Identifier: GPL-3.0-or-later + * + * GENERATED at build time by zephyr/gen/gen-crypto-config.py from the compiled + * wolfCrypt configuration -- DO NOT EDIT and do not check in. It defines the + * consumer-visible PSA_WANT_ surface (what wolfPSA exposes as the PSA API), + * derived from the wolfCrypt features actually enabled in this build intersected + * with what wolfPSA implements. wolfpsa/psa/crypto.h -> crypto_config.h includes + * it under CONFIG_WOLFPSA, so PSA sizing macros (crypto_sizes.h) and consumer + * PSA_WANT_-gated code track the real build. + */ + +#ifndef WOLFPSA_CRYPTO_CONFIG_ZEPHYR_H +#define WOLFPSA_CRYPTO_CONFIG_ZEPHYR_H + +""" + +FOOTER = "\n#endif /* WOLFPSA_CRYPTO_CONFIG_ZEPHYR_H */\n" + +EMIT_TAG = "__WPSA_EMIT__" +WANT_RE = re.compile(r"PSA_WANT_[A-Z0-9_]+") + + +def main(argv): + if len(argv) < 4: + sys.stderr.write( + "usage: gen-crypto-config.py [flags...]\n") + return 2 + + out_path, cc, probe = argv[1], argv[2], argv[3] + flags = argv[4:] + + try: + preprocessed = subprocess.run( + [cc, "-E", "-P", *flags, probe], + check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True).stdout + except subprocess.CalledProcessError as exc: + sys.stderr.write(exc.stderr or "") + sys.stderr.write("gen-crypto-config.py: preprocessing the probe failed\n") + return 1 + + # Collect PSA_WANT_ symbols from the surviving __WPSA_EMIT__ lines only. + wants = sorted({ + match.group(0) + for line in preprocessed.splitlines() if EMIT_TAG in line + for match in WANT_RE.finditer(line) + }) + + if not wants: + sys.stderr.write( + "gen-crypto-config.py: probe produced no PSA_WANT symbols\n") + return 1 + + with open(out_path, "w") as out_file: + out_file.write(HEADER) + for want in wants: + out_file.write("#define {} 1\n".format(want)) + out_file.write(FOOTER) + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/zephyr/gen/psa_want_probe.c b/zephyr/gen/psa_want_probe.c new file mode 100644 index 0000000..ae4d242 --- /dev/null +++ b/zephyr/gen/psa_want_probe.c @@ -0,0 +1,205 @@ +/* psa_want_probe.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * NOT a compiled translation unit. This file is *preprocessed* at build time + * (gcc -E -P) with the same wolfCrypt configuration as the rest of the build, + * and gen-crypto-config.py extracts the surviving `__WPSA_EMIT__` + * lines to generate wolfpsa/psa/crypto_config_zephyr.h -- the consumer-visible + * PSA_WANT_ surface. Each block below maps an enabled wolfCrypt feature to the + * PSA_WANT_ symbol(s) wolfPSA exposes for it, so the advertised PSA surface is + * exactly "what wolfCrypt compiled" intersected with "what wolfPSA implements". + * + * Excluded on purpose (wolfPSA has no PSA binding): finite-field DH, PAKE, + * Camellia, ARIA, ECC Brainpool/Koblitz. Keep this in sync with src/ when a + * feature graduates. + */ + +/* Pull in the effective wolfCrypt feature configuration (the module's + * user_settings.h + wolfPSA baseline). We include user_settings.h directly + * rather than so this probe preprocesses without + * dragging in the Zephyr kernel headers settings.h pulls under WOLFSSL_ZEPHYR; + * every feature macro this probe tests is materialized by user_settings.h. */ +#include "user_settings.h" + +/* ----- Hashes ----- */ +#if !defined(NO_SHA) +__WPSA_EMIT__PSA_WANT_ALG_SHA_1 +#endif +#if defined(WOLFSSL_SHA224) +__WPSA_EMIT__PSA_WANT_ALG_SHA_224 +#endif +#if !defined(NO_SHA256) +__WPSA_EMIT__PSA_WANT_ALG_SHA_256 +#endif +#if defined(WOLFSSL_SHA384) +__WPSA_EMIT__PSA_WANT_ALG_SHA_384 +#endif +#if defined(WOLFSSL_SHA512) +__WPSA_EMIT__PSA_WANT_ALG_SHA_512 +#endif +#if defined(WOLFSSL_SHA3) +__WPSA_EMIT__PSA_WANT_ALG_SHA3_224 +__WPSA_EMIT__PSA_WANT_ALG_SHA3_256 +__WPSA_EMIT__PSA_WANT_ALG_SHA3_384 +__WPSA_EMIT__PSA_WANT_ALG_SHA3_512 +#endif +#if defined(WOLFSSL_SHAKE128) +__WPSA_EMIT__PSA_WANT_ALG_SHAKE128 +#endif +#if defined(WOLFSSL_SHAKE256) +__WPSA_EMIT__PSA_WANT_ALG_SHAKE256 +#endif +#if !defined(NO_MD5) +__WPSA_EMIT__PSA_WANT_ALG_MD5 +#endif +#if defined(WOLFSSL_RIPEMD) +__WPSA_EMIT__PSA_WANT_ALG_RIPEMD160 +#endif + +/* ----- MAC ----- */ +#if !defined(NO_HMAC) +__WPSA_EMIT__PSA_WANT_ALG_HMAC +__WPSA_EMIT__PSA_WANT_KEY_TYPE_HMAC +#endif +#if defined(WOLFSSL_CMAC) +__WPSA_EMIT__PSA_WANT_ALG_CMAC +#endif + +/* ----- AES + modes ----- */ +#if !defined(NO_AES) +__WPSA_EMIT__PSA_WANT_KEY_TYPE_AES +/* AES-CBC is present in wolfCrypt unless explicitly removed. */ +#if !defined(NO_AES_CBC) +__WPSA_EMIT__PSA_WANT_ALG_CBC_NO_PADDING +__WPSA_EMIT__PSA_WANT_ALG_CBC_PKCS7 +#endif +#if defined(HAVE_AES_ECB) +__WPSA_EMIT__PSA_WANT_ALG_ECB_NO_PADDING +#endif +#if defined(WOLFSSL_AES_COUNTER) +__WPSA_EMIT__PSA_WANT_ALG_CTR +#endif +#if defined(WOLFSSL_AES_CFB) +__WPSA_EMIT__PSA_WANT_ALG_CFB +#endif +#if defined(WOLFSSL_AES_OFB) +__WPSA_EMIT__PSA_WANT_ALG_OFB +#endif +#if defined(HAVE_AESGCM) +__WPSA_EMIT__PSA_WANT_ALG_GCM +#endif +#if defined(HAVE_AESCCM) +__WPSA_EMIT__PSA_WANT_ALG_CCM +__WPSA_EMIT__PSA_WANT_ALG_CCM_STAR_NO_TAG +#endif +#endif /* !NO_AES */ + +/* ----- ChaCha20 / Poly1305 ----- */ +#if defined(HAVE_CHACHA) && defined(HAVE_POLY1305) +__WPSA_EMIT__PSA_WANT_KEY_TYPE_CHACHA20 +__WPSA_EMIT__PSA_WANT_ALG_CHACHA20_POLY1305 +__WPSA_EMIT__PSA_WANT_ALG_STREAM_CIPHER +#endif + +/* ----- RSA ----- */ +#if !defined(NO_RSA) +__WPSA_EMIT__PSA_WANT_KEY_TYPE_RSA_PUBLIC_KEY +__WPSA_EMIT__PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_BASIC +__WPSA_EMIT__PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_IMPORT +__WPSA_EMIT__PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_EXPORT +__WPSA_EMIT__PSA_WANT_ALG_RSA_PKCS1V15_SIGN +__WPSA_EMIT__PSA_WANT_ALG_RSA_PKCS1V15_CRYPT +#if defined(WOLFSSL_KEY_GEN) +__WPSA_EMIT__PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_GENERATE +#endif +#if defined(WC_RSA_PSS) +__WPSA_EMIT__PSA_WANT_ALG_RSA_PSS +#endif +#if defined(WOLFSSL_RSA_OAEP) || defined(WC_RSA_OAEP) +__WPSA_EMIT__PSA_WANT_ALG_RSA_OAEP +#endif +#endif /* !NO_RSA */ + +/* ----- ECC (secp r1) ----- */ +#if defined(HAVE_ECC) +__WPSA_EMIT__PSA_WANT_KEY_TYPE_ECC_PUBLIC_KEY +__WPSA_EMIT__PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_BASIC +__WPSA_EMIT__PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT +__WPSA_EMIT__PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_EXPORT +__WPSA_EMIT__PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_GENERATE +__WPSA_EMIT__PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_DERIVE +__WPSA_EMIT__PSA_WANT_ALG_ECDSA +__WPSA_EMIT__PSA_WANT_ALG_ECDH +#if defined(WOLFSSL_ECDSA_DETERMINISTIC_K) +__WPSA_EMIT__PSA_WANT_ALG_DETERMINISTIC_ECDSA +#endif +#if !defined(NO_ECC256) +__WPSA_EMIT__PSA_WANT_ECC_SECP_R1_256 +#endif +#if defined(HAVE_ECC384) +__WPSA_EMIT__PSA_WANT_ECC_SECP_R1_384 +#endif +#if defined(HAVE_ECC521) +__WPSA_EMIT__PSA_WANT_ECC_SECP_R1_521 +#endif +#endif /* HAVE_ECC */ + +/* ----- Montgomery curves ----- */ +#if defined(HAVE_CURVE25519) +__WPSA_EMIT__PSA_WANT_ECC_MONTGOMERY_255 +#endif +#if defined(HAVE_CURVE448) +__WPSA_EMIT__PSA_WANT_ECC_MONTGOMERY_448 +#endif + +/* ----- KDFs ----- */ +#if defined(HAVE_HKDF) +__WPSA_EMIT__PSA_WANT_ALG_HKDF +__WPSA_EMIT__PSA_WANT_ALG_HKDF_EXTRACT +__WPSA_EMIT__PSA_WANT_ALG_HKDF_EXPAND +#endif +#if defined(HAVE_PBKDF2) +__WPSA_EMIT__PSA_WANT_ALG_PBKDF2_HMAC +#endif +#if defined(HAVE_PBKDF2) && defined(WOLFSSL_CMAC) +__WPSA_EMIT__PSA_WANT_ALG_PBKDF2_AES_CMAC_PRF_128 +#endif +#if defined(WOLFSSL_HAVE_PRF) +__WPSA_EMIT__PSA_WANT_ALG_TLS12_PRF +__WPSA_EMIT__PSA_WANT_ALG_TLS12_PSK_TO_MS +#endif + +/* ----- Key-derivation input key types ----- */ +__WPSA_EMIT__PSA_WANT_KEY_TYPE_RAW_DATA +__WPSA_EMIT__PSA_WANT_KEY_TYPE_DERIVE +#if defined(HAVE_PBKDF2) +__WPSA_EMIT__PSA_WANT_KEY_TYPE_PASSWORD +__WPSA_EMIT__PSA_WANT_KEY_TYPE_PASSWORD_HASH +#endif + +/* ----- PQC ----- */ +#if defined(WOLFSSL_HAVE_MLKEM) +__WPSA_EMIT__PSA_WANT_KEY_TYPE_ML_KEM +#endif +#if defined(WOLFSSL_HAVE_MLDSA) +__WPSA_EMIT__PSA_WANT_KEY_TYPE_ML_DSA +#endif diff --git a/zephyr/module.yml b/zephyr/module.yml new file mode 100644 index 0000000..63b192e --- /dev/null +++ b/zephyr/module.yml @@ -0,0 +1,6 @@ +name: wolfpsa +build: + cmake: zephyr + kconfig: zephyr/Kconfig + depends: + - wolfssl diff --git a/zephyr/samples/psa_smoke/CMakeLists.txt b/zephyr/samples/psa_smoke/CMakeLists.txt new file mode 100644 index 0000000..eb6e347 --- /dev/null +++ b/zephyr/samples/psa_smoke/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(wolfpsa_psa_smoke) + +target_sources(app PRIVATE src/main.c) diff --git a/zephyr/samples/psa_smoke/prj.conf b/zephyr/samples/psa_smoke/prj.conf new file mode 100644 index 0000000..f0688ab --- /dev/null +++ b/zephyr/samples/psa_smoke/prj.conf @@ -0,0 +1,21 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +CONFIG_LOG=y +CONFIG_LOG_DEFAULT_LEVEL=3 + +# wolfPSA as the custom PSA provider (reusing the wolfSSL module's wolfCrypt). +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y + +# Entropy for the wolfCrypt DRBG seed. +CONFIG_ENTROPY_GENERATOR=y + +# RSA-2048 / ML-DSA key generation and signing are stack-heavy. +CONFIG_MAIN_STACK_SIZE=65536 + +# Broad example wolfCrypt config so wolfPSA exposes a full PSA surface (the +# example is self-contained and crypto-only). A real application points this at +# its own config or uses the module Kconfig. +CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" diff --git a/zephyr/samples/psa_smoke/src/main.c b/zephyr/samples/psa_smoke/src/main.c new file mode 100644 index 0000000..8c63e76 --- /dev/null +++ b/zephyr/samples/psa_smoke/src/main.c @@ -0,0 +1,417 @@ +/* main.c - wolfPSA broad-surface PSA smoke test + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Runtime verification that the wolfPSA provider (on wolfCrypt from the + * wolfSSL module) works across the major PSA algorithm families on Zephyr: + * hashes, HMAC, AES-CTR, AES-CMAC, RSA-2048 PSS, ECDSA P-256, ECDH P-256, + * ML-KEM-768 and ML-DSA-65. Every check logs PASS/FAIL and the run ends with + * a summary. Uses volatile keys only (no persistent store dependency). + */ + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(psa_smoke, LOG_LEVEL_INF); + +static int g_pass; +static int g_fail; + +static void report(const char *name, bool ok) +{ + if (ok) { + g_pass++; + LOG_INF("PASS %s", name); + } else { + g_fail++; + LOG_ERR("FAIL %s", name); + } +} + +/* Compare helper. */ +static bool eq(const uint8_t *a, const uint8_t *b, size_t n) +{ + return memcmp(a, b, n) == 0; +} + +static const uint8_t msg[] = "The quick brown fox jumps over the lazy dog."; + +static void test_hash(void) +{ + uint8_t h[64]; + size_t hlen = 0; + psa_status_t st; + + st = psa_hash_compute(PSA_ALG_SHA_256, msg, sizeof(msg) - 1, + h, sizeof(h), &hlen); + report("SHA-256 compute", st == PSA_SUCCESS && hlen == 32); + + st = psa_hash_compute(PSA_ALG_SHA_384, msg, sizeof(msg) - 1, + h, sizeof(h), &hlen); + report("SHA-384 compute", st == PSA_SUCCESS && hlen == 48); +} + +static void test_hmac(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + uint8_t key[32]; + uint8_t mac[32]; + size_t maclen = 0; + psa_status_t st; + const psa_algorithm_t alg = PSA_ALG_HMAC(PSA_ALG_SHA_256); + + memset(key, 0x0b, sizeof(key)); + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE); + psa_set_key_algorithm(&a, alg); + psa_set_key_type(&a, PSA_KEY_TYPE_HMAC); + psa_set_key_bits(&a, sizeof(key) * 8); + + st = psa_import_key(&a, key, sizeof(key), &k); + if (st != PSA_SUCCESS) { + report("HMAC-SHA256 import", false); + return; + } + st = psa_mac_compute(k, alg, msg, sizeof(msg) - 1, mac, sizeof(mac), &maclen); + report("HMAC-SHA256 compute", st == PSA_SUCCESS && maclen == 32); + + st = psa_mac_verify(k, alg, msg, sizeof(msg) - 1, mac, maclen); + report("HMAC-SHA256 verify", st == PSA_SUCCESS); + + psa_destroy_key(k); +} + +static void test_aes_ctr(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + uint8_t key[16]; + uint8_t ct[64]; /* IV(16) + ciphertext */ + uint8_t pt[sizeof(msg) - 1]; + size_t ctlen = 0, ptlen = 0; + psa_status_t st; + + memset(key, 0x2a, sizeof(key)); + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&a, PSA_ALG_CTR); + psa_set_key_type(&a, PSA_KEY_TYPE_AES); + psa_set_key_bits(&a, sizeof(key) * 8); + + st = psa_import_key(&a, key, sizeof(key), &k); + if (st != PSA_SUCCESS) { + report("AES-CTR import", false); + return; + } + st = psa_cipher_encrypt(k, PSA_ALG_CTR, msg, sizeof(msg) - 1, + ct, sizeof(ct), &ctlen); + if (st != PSA_SUCCESS) { + report("AES-CTR encrypt", false); + psa_destroy_key(k); + return; + } + st = psa_cipher_decrypt(k, PSA_ALG_CTR, ct, ctlen, pt, sizeof(pt), &ptlen); + report("AES-CTR round-trip", + st == PSA_SUCCESS && ptlen == sizeof(msg) - 1 && + eq(pt, msg, sizeof(msg) - 1)); + + psa_destroy_key(k); +} + +static void test_cmac(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + uint8_t key[16]; + uint8_t mac[16]; + size_t maclen = 0; + psa_status_t st; + + memset(key, 0x3c, sizeof(key)); + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE); + psa_set_key_algorithm(&a, PSA_ALG_CMAC); + psa_set_key_type(&a, PSA_KEY_TYPE_AES); + psa_set_key_bits(&a, sizeof(key) * 8); + + st = psa_import_key(&a, key, sizeof(key), &k); + if (st != PSA_SUCCESS) { + report("AES-CMAC import", false); + return; + } + st = psa_mac_compute(k, PSA_ALG_CMAC, msg, sizeof(msg) - 1, + mac, sizeof(mac), &maclen); + if (st != PSA_SUCCESS || maclen != 16) { + report("AES-CMAC compute", false); + psa_destroy_key(k); + return; + } + st = psa_mac_verify(k, PSA_ALG_CMAC, msg, sizeof(msg) - 1, mac, maclen); + report("AES-CMAC compute+verify", st == PSA_SUCCESS); + + psa_destroy_key(k); +} + +static void test_rsa_pss(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + uint8_t hash[32]; + size_t hlen = 0; + uint8_t sig[256]; + size_t siglen = 0; + psa_status_t st; + const psa_algorithm_t alg = PSA_ALG_RSA_PSS(PSA_ALG_SHA_256); + + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_SIGN_HASH | PSA_KEY_USAGE_VERIFY_HASH); + psa_set_key_algorithm(&a, alg); + psa_set_key_type(&a, PSA_KEY_TYPE_RSA_KEY_PAIR); + psa_set_key_bits(&a, 2048); + + LOG_INF("...generating RSA-2048 key (may take a moment)"); + st = psa_generate_key(&a, &k); + if (st != PSA_SUCCESS) { + report("RSA-2048 generate", false); + return; + } + report("RSA-2048 generate", true); + + (void)psa_hash_compute(PSA_ALG_SHA_256, msg, sizeof(msg) - 1, + hash, sizeof(hash), &hlen); + st = psa_sign_hash(k, alg, hash, hlen, sig, sizeof(sig), &siglen); + if (st != PSA_SUCCESS) { + report("RSA-2048 PSS sign", false); + psa_destroy_key(k); + return; + } + st = psa_verify_hash(k, alg, hash, hlen, sig, siglen); + report("RSA-2048 PSS sign+verify", st == PSA_SUCCESS); + + psa_destroy_key(k); +} + +static void test_ecdsa(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + uint8_t hash[32]; + size_t hlen = 0; + uint8_t sig[80]; + size_t siglen = 0; + psa_status_t st; + const psa_algorithm_t alg = PSA_ALG_ECDSA(PSA_ALG_SHA_256); + + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_SIGN_HASH | PSA_KEY_USAGE_VERIFY_HASH); + psa_set_key_algorithm(&a, alg); + psa_set_key_type(&a, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); + psa_set_key_bits(&a, 256); + + st = psa_generate_key(&a, &k); + if (st != PSA_SUCCESS) { + report("ECDSA P-256 generate", false); + return; + } + report("ECDSA P-256 generate", true); + + (void)psa_hash_compute(PSA_ALG_SHA_256, msg, sizeof(msg) - 1, + hash, sizeof(hash), &hlen); + st = psa_sign_hash(k, alg, hash, hlen, sig, sizeof(sig), &siglen); + if (st != PSA_SUCCESS) { + report("ECDSA P-256 sign", false); + psa_destroy_key(k); + return; + } + st = psa_verify_hash(k, alg, hash, hlen, sig, siglen); + report("ECDSA P-256 sign+verify", st == PSA_SUCCESS); + + psa_destroy_key(k); +} + +static void test_ecdh(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k1 = PSA_KEY_ID_NULL, k2 = PSA_KEY_ID_NULL; + uint8_t pub1[133], pub2[133]; + size_t pub1len = 0, pub2len = 0; + uint8_t ss1[32], ss2[32]; + size_t ss1len = 0, ss2len = 0; + psa_status_t st; + + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_DERIVE); + psa_set_key_algorithm(&a, PSA_ALG_ECDH); + psa_set_key_type(&a, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); + psa_set_key_bits(&a, 256); + + if (psa_generate_key(&a, &k1) != PSA_SUCCESS || + psa_generate_key(&a, &k2) != PSA_SUCCESS) { + report("ECDH P-256 generate pair", false); + goto out; + } + if (psa_export_public_key(k1, pub1, sizeof(pub1), &pub1len) != PSA_SUCCESS || + psa_export_public_key(k2, pub2, sizeof(pub2), &pub2len) != PSA_SUCCESS) { + report("ECDH P-256 export pub", false); + goto out; + } + + st = psa_raw_key_agreement(PSA_ALG_ECDH, k1, pub2, pub2len, + ss1, sizeof(ss1), &ss1len); + if (st != PSA_SUCCESS) { + report("ECDH P-256 agreement A", false); + goto out; + } + st = psa_raw_key_agreement(PSA_ALG_ECDH, k2, pub1, pub1len, + ss2, sizeof(ss2), &ss2len); + if (st != PSA_SUCCESS) { + report("ECDH P-256 agreement B", false); + goto out; + } + report("ECDH P-256 shared-secret match", + ss1len == ss2len && ss1len == 32 && eq(ss1, ss2, ss1len)); +out: + psa_destroy_key(k1); + psa_destroy_key(k2); +} + +static psa_key_attributes_t make_ss_attrs(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + + psa_set_key_type(&a, PSA_KEY_TYPE_DERIVE); + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_EXPORT); + psa_set_key_algorithm(&a, PSA_ALG_HKDF(PSA_ALG_SHA_256)); + psa_set_key_bits(&a, 0); + return a; +} + +static void test_mlkem(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_attributes_t ss_attrs; + psa_key_id_t kp = PSA_KEY_ID_NULL; + psa_key_id_t ss_enc = PSA_KEY_ID_NULL, ss_dec = PSA_KEY_ID_NULL; + uint8_t ct[1600]; + size_t ctlen = 0; + uint8_t sse[32], ssd[32]; + size_t sselen = 0, ssdlen = 0; + psa_status_t st; + + psa_set_key_type(&a, PSA_KEY_TYPE_ML_KEM_KEY_PAIR); + psa_set_key_bits(&a, 768); + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&a, PSA_ALG_ML_KEM); + + st = psa_generate_key(&a, &kp); + if (st != PSA_SUCCESS) { + report("ML-KEM-768 generate", false); + return; + } + report("ML-KEM-768 generate", true); + + ss_attrs = make_ss_attrs(); + st = psa_encapsulate(kp, PSA_ALG_ML_KEM, &ss_attrs, &ss_enc, + ct, sizeof(ct), &ctlen); + if (st != PSA_SUCCESS) { + report("ML-KEM-768 encapsulate", false); + goto out; + } + ss_attrs = make_ss_attrs(); + st = psa_decapsulate(kp, PSA_ALG_ML_KEM, ct, ctlen, &ss_attrs, &ss_dec); + if (st != PSA_SUCCESS) { + report("ML-KEM-768 decapsulate", false); + goto out; + } + if (psa_export_key(ss_enc, sse, sizeof(sse), &sselen) != PSA_SUCCESS || + psa_export_key(ss_dec, ssd, sizeof(ssd), &ssdlen) != PSA_SUCCESS) { + report("ML-KEM-768 export secrets", false); + goto out; + } + report("ML-KEM-768 shared-secret match", + sselen == ssdlen && sselen > 0 && eq(sse, ssd, sselen)); +out: + psa_destroy_key(ss_enc); + psa_destroy_key(ss_dec); + psa_destroy_key(kp); +} + +static void test_mldsa(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + uint8_t sig[4096]; + size_t siglen = 0; + psa_status_t st; + + psa_set_key_type(&a, PSA_KEY_TYPE_ML_DSA_KEY_PAIR); + psa_set_key_bits(&a, 192); /* ML-DSA-65 */ + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE); + psa_set_key_algorithm(&a, PSA_ALG_ML_DSA); + + LOG_INF("...generating ML-DSA-65 key"); + st = psa_generate_key(&a, &k); + if (st != PSA_SUCCESS) { + report("ML-DSA-65 generate", false); + return; + } + report("ML-DSA-65 generate", true); + + st = psa_sign_message(k, PSA_ALG_ML_DSA, msg, sizeof(msg) - 1, + sig, sizeof(sig), &siglen); + if (st != PSA_SUCCESS) { + report("ML-DSA-65 sign", false); + psa_destroy_key(k); + return; + } + st = psa_verify_message(k, PSA_ALG_ML_DSA, msg, sizeof(msg) - 1, sig, siglen); + report("ML-DSA-65 sign+verify", st == PSA_SUCCESS); + + psa_destroy_key(k); +} + +int main(void) +{ + psa_status_t st; + + LOG_INF("=== wolfPSA broad PSA smoke test ==="); + + st = psa_crypto_init(); + if (st != PSA_SUCCESS) { + LOG_ERR("psa_crypto_init failed: %d", (int)st); + return 0; + } + + test_hash(); + test_hmac(); + test_aes_ctr(); + test_cmac(); + test_rsa_pss(); + test_ecdsa(); + test_ecdh(); + test_mlkem(); + test_mldsa(); + + LOG_INF("=== summary: %d passed, %d failed ===", g_pass, g_fail); + if (g_fail == 0) { + LOG_INF("ALL PSA SMOKE TESTS PASSED"); + } else { + LOG_ERR("SOME PSA SMOKE TESTS FAILED"); + } + return 0; +} diff --git a/zephyr/src/psa_its_transform_wolfcrypt.c b/zephyr/src/psa_its_transform_wolfcrypt.c new file mode 100644 index 0000000..47bd36e --- /dev/null +++ b/zephyr/src/psa_its_transform_wolfcrypt.c @@ -0,0 +1,309 @@ +/* psa_its_transform_wolfcrypt.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * wolfCrypt-backed implementation of the Zephyr secure_storage ITS "transform" + * module (CONFIG_SECURE_STORAGE_ITS_TRANSFORM_IMPLEMENTATION_CUSTOM). + * + * Zephyr's stock AEAD transform (subsys/secure_storage/src/its/transform/ + * aead.c) is hardwired to Mbed TLS core internals: it calls + * psa_driver_wrapper_aead_encrypt/decrypt and mbedtls_platform_zeroize, which + * are compiled only under CONFIG_MBEDTLS. Selecting the CUSTOM transform makes + * secure_storage use this file instead, so a wolfPSA build links no Mbed TLS + * symbol at all: all storage-path crypto runs through wolfCrypt. + * + * Provides confidentiality and integrity at rest with AES-256-GCM. The stored + * layout is: + * + * [ packed create_flags : 1 ][ nonce : 12 ][ ciphertext : data_len ][ tag : 16 ] + * + * so the transform adds NONCE + TAG = 28 bytes beyond the plaintext (the + * create_flags byte is accounted separately by the secure_storage framing); + * CONFIG_SECURE_STORAGE_ITS_TRANSFORM_OUTPUT_OVERHEAD must therefore be 28. The + * per-entry key is derived from a device identifier (HW info when available) + * salted with the entry UID, and the GCM AAD binds the create_flags and UID so + * a stored record cannot be replayed under a different identity. Unlike the + * stock aead.c, this uses raw wolfCrypt (stack-local Aes) and never re-enters + * the PSA keystore, so it is safe to run while a persistent key is mid-save. + */ + +#include +#include /* full PSA status/error set (secure_storage's psa/error.h is a subset) */ +#include + +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_HWINFO) +#include +#endif + +#define WOLFPSA_ITS_NONCE_SZ 12 +#define WOLFPSA_ITS_TAG_SZ 16 +#define WOLFPSA_ITS_KEY_SZ 32 +#define WOLFPSA_ITS_FLAGS_SZ ((int)sizeof(secure_storage_packed_create_flags_t)) + +/* The overhead the app declares to secure_storage must match this transform. + * The framing sizes the stored buffer as MAX_DATA + sizeof(create_flags) + + * OUTPUT_OVERHEAD (see secure_storage/its/common.h), i.e. the flags byte is + * counted separately, so OUTPUT_OVERHEAD covers only the nonce and tag. */ +BUILD_ASSERT(CONFIG_SECURE_STORAGE_ITS_TRANSFORM_OUTPUT_OVERHEAD + == WOLFPSA_ITS_NONCE_SZ + WOLFPSA_ITS_TAG_SZ, + "CONFIG_SECURE_STORAGE_ITS_TRANSFORM_OUTPUT_OVERHEAD must be 28 " + "(12-byte nonce + 16-byte AES-GCM tag) for the wolfCrypt ITS transform"); + +/* The stored layout puts create_flags in a single leading byte (stored_data[0], + * read back at from_store); assert the packed type is actually one byte so the + * framing and this transform stay in agreement if the Zephyr type ever changes. */ +BUILD_ASSERT(sizeof(secure_storage_packed_create_flags_t) == 1, + "the wolfCrypt ITS transform assumes a single-byte packed create_flags"); + +/* Serialized (padding-free, fixed little-endian) uid: 8-byte uid || 4-byte + * caller_id. Key derivation and the GCM AAD hash/copy these discrete fields + * rather than the raw secure_storage_its_uid_t object representation, so they + * never depend on struct padding -- a persistent key stays decryptable across + * compilers/ABIs (GCC happens to zero the padding, but the C standard does + * not require it). */ +#define WOLFPSA_ITS_UID_SZ 12 + +static void wolfpsa_its_serialize_uid(secure_storage_its_uid_t uid, + byte out[WOLFPSA_ITS_UID_SZ]) +{ + uint64_t id = (uint64_t)uid.uid; + uint32_t caller = (uint32_t)uid.caller_id; + int i; + + for (i = 0; i < 8; i++) { + out[i] = (byte)(id >> (8 * i)); + } + for (i = 0; i < 4; i++) { + out[8 + i] = (byte)(caller >> (8 * i)); + } +} + +/* Derive the per-entry AES-256 key: SHA-256(device-id || uid). The uid salt + * gives every entry a distinct key. On targets without a HW device id (e.g. + * native_sim) a fixed constant is hashed instead: this is functional but not + * secret, matching the caveat on Zephyr's own device-id/uid key providers. + * When CONFIG_HWINFO is configured but the driver returns no id, key derivation + * fails closed (PSA_ERROR_HARDWARE_FAILURE) rather than silently falling back to + * a device-independent key. */ +static psa_status_t wolfpsa_its_derive_key(secure_storage_its_uid_t uid, + byte key[WOLFPSA_ITS_KEY_SZ]) +{ + wc_Sha256 sha; + byte uidbuf[WOLFPSA_ITS_UID_SZ]; + int rc; +#if defined(CONFIG_HWINFO) + byte devid[16]; + ssize_t n; +#else + /* ASCII bytes of "wolfPSA-its-key!" -- a fixed 16-byte domain-separation + * seed used only when CONFIG_HWINFO is unavailable to supply a per-device + * id. With no HWINFO the derived ITS key is not device-unique (the same + * caveat Zephyr documents for its own fixed key provider). */ + static const byte fixed[16] = { + 0x77, 0x6f, 0x6c, 0x66, 0x50, 0x53, 0x41, 0x2d, /* "wolfPSA-" */ + 0x69, 0x74, 0x73, 0x2d, 0x6b, 0x65, 0x79, 0x21 /* "its-key!" */ + }; +#endif + + rc = wc_InitSha256(&sha); + if (rc != 0) { + return PSA_ERROR_GENERIC_ERROR; + } + +#if defined(CONFIG_HWINFO) + n = hwinfo_get_device_id(devid, sizeof(devid)); + if (n > 0) { + rc = wc_Sha256Update(&sha, devid, (word32)n); + } + else { + /* CONFIG_HWINFO asked for a device-bound key but no device id is + * available. Fail closed instead of silently deriving a + * device-independent key that would weaken confidentiality at rest. */ + wc_ForceZero(devid, sizeof(devid)); + wc_Sha256Free(&sha); + return PSA_ERROR_HARDWARE_FAILURE; + } + /* Scrub the device id from the stack once it has been hashed. */ + wc_ForceZero(devid, sizeof(devid)); +#else + rc = wc_Sha256Update(&sha, fixed, sizeof(fixed)); +#endif + + if (rc == 0) { + wolfpsa_its_serialize_uid(uid, uidbuf); + rc = wc_Sha256Update(&sha, uidbuf, sizeof(uidbuf)); + } + if (rc == 0) { + rc = wc_Sha256Final(&sha, key); + } + wc_Sha256Free(&sha); + + return (rc == 0) ? PSA_SUCCESS : PSA_ERROR_GENERIC_ERROR; +} + +/* Build the GCM additional-authenticated-data: create_flags || uid. */ +static void wolfpsa_its_build_aad(secure_storage_packed_create_flags_t flags, + secure_storage_its_uid_t uid, + byte aad[WOLFPSA_ITS_FLAGS_SZ + WOLFPSA_ITS_UID_SZ]) +{ + aad[0] = flags; + wolfpsa_its_serialize_uid(uid, aad + WOLFPSA_ITS_FLAGS_SZ); +} + +psa_status_t secure_storage_its_transform_to_store( + secure_storage_its_uid_t uid, size_t data_len, const void *data, + secure_storage_packed_create_flags_t create_flags, + uint8_t stored_data[static SECURE_STORAGE_ITS_TRANSFORM_MAX_STORED_DATA_SIZE], + size_t *stored_data_len) +{ + Aes aes; + WC_RNG rng; + byte key[WOLFPSA_ITS_KEY_SZ]; + byte aad[WOLFPSA_ITS_FLAGS_SZ + WOLFPSA_ITS_UID_SZ]; + byte *nonce = stored_data + WOLFPSA_ITS_FLAGS_SZ; + byte *ct = nonce + WOLFPSA_ITS_NONCE_SZ; + byte *tag = ct + data_len; + psa_status_t status; + int rc; + int rng_init = 0; + int aes_init = 0; + + status = wolfpsa_its_derive_key(uid, key); + if (status != PSA_SUCCESS) { + return status; + } + + stored_data[0] = create_flags; + wolfpsa_its_build_aad(create_flags, uid, aad); + + rc = wc_InitRng(&rng); + if (rc == 0) { + rng_init = 1; + rc = wc_RNG_GenerateBlock(&rng, nonce, WOLFPSA_ITS_NONCE_SZ); + } + if (rc != 0) { + status = PSA_ERROR_INSUFFICIENT_ENTROPY; + } + + if (status == PSA_SUCCESS) { + rc = wc_AesInit(&aes, NULL, INVALID_DEVID); + if (rc == 0) { + aes_init = 1; + rc = wc_AesGcmSetKey(&aes, key, WOLFPSA_ITS_KEY_SZ); + } + if (rc == 0) { + rc = wc_AesGcmEncrypt(&aes, ct, (const byte *)data, (word32)data_len, + nonce, WOLFPSA_ITS_NONCE_SZ, tag, WOLFPSA_ITS_TAG_SZ, + aad, (word32)sizeof(aad)); + } + if (rc != 0) { + status = PSA_ERROR_GENERIC_ERROR; + } + } + + if (aes_init) { + wc_AesFree(&aes); + } + if (rng_init) { + wc_FreeRng(&rng); + } + wc_ForceZero(key, sizeof(key)); + + if (status == PSA_SUCCESS) { + *stored_data_len = (size_t)WOLFPSA_ITS_FLAGS_SZ + WOLFPSA_ITS_NONCE_SZ + + data_len + WOLFPSA_ITS_TAG_SZ; + } + return status; +} + +psa_status_t secure_storage_its_transform_from_store( + secure_storage_its_uid_t uid, size_t stored_data_len, + const uint8_t stored_data[static SECURE_STORAGE_ITS_TRANSFORM_MAX_STORED_DATA_SIZE], + size_t data_size, void *data, size_t *data_len, + psa_storage_create_flags_t *create_flags) +{ + Aes aes; + byte key[WOLFPSA_ITS_KEY_SZ]; + byte aad[WOLFPSA_ITS_FLAGS_SZ + WOLFPSA_ITS_UID_SZ]; + secure_storage_packed_create_flags_t flags; + const byte *nonce; + const byte *ct; + const byte *tag; + size_t ct_len; + psa_status_t status; + int rc; + int aes_init = 0; + + if (stored_data_len < (size_t)WOLFPSA_ITS_FLAGS_SZ + WOLFPSA_ITS_NONCE_SZ + + WOLFPSA_ITS_TAG_SZ) { + return PSA_ERROR_DATA_CORRUPT; + } + ct_len = stored_data_len - WOLFPSA_ITS_FLAGS_SZ - WOLFPSA_ITS_NONCE_SZ + - WOLFPSA_ITS_TAG_SZ; + if (ct_len > data_size) { + return PSA_ERROR_BUFFER_TOO_SMALL; + } + + flags = stored_data[0]; + nonce = stored_data + WOLFPSA_ITS_FLAGS_SZ; + ct = nonce + WOLFPSA_ITS_NONCE_SZ; + tag = ct + ct_len; + + status = wolfpsa_its_derive_key(uid, key); + if (status != PSA_SUCCESS) { + return status; + } + wolfpsa_its_build_aad(flags, uid, aad); + + rc = wc_AesInit(&aes, NULL, INVALID_DEVID); + if (rc == 0) { + aes_init = 1; + rc = wc_AesGcmSetKey(&aes, key, WOLFPSA_ITS_KEY_SZ); + } + if (rc == 0) { + rc = wc_AesGcmDecrypt(&aes, (byte *)data, ct, (word32)ct_len, + nonce, WOLFPSA_ITS_NONCE_SZ, tag, WOLFPSA_ITS_TAG_SZ, + aad, (word32)sizeof(aad)); + } + + if (aes_init) { + wc_AesFree(&aes); + } + wc_ForceZero(key, sizeof(key)); + + if (rc == AES_GCM_AUTH_E) { + return PSA_ERROR_INVALID_SIGNATURE; + } + if (rc != 0) { + return PSA_ERROR_GENERIC_ERROR; + } + + *create_flags = flags; + *data_len = ct_len; + return PSA_SUCCESS; +} diff --git a/zephyr/tests/psa_concurrency/CMakeLists.txt b/zephyr/tests/psa_concurrency/CMakeLists.txt new file mode 100644 index 0000000..fd06e17 --- /dev/null +++ b/zephyr/tests/psa_concurrency/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(wolfpsa_psa_concurrency) + +target_sources(app PRIVATE src/main.c) diff --git a/zephyr/tests/psa_concurrency/prj.conf b/zephyr/tests/psa_concurrency/prj.conf new file mode 100644 index 0000000..53f567b --- /dev/null +++ b/zephyr/tests/psa_concurrency/prj.conf @@ -0,0 +1,23 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +CONFIG_ZTEST=y +CONFIG_ZTEST_STACK_SIZE=32768 +CONFIG_MAIN_STACK_SIZE=32768 + +# wolfPSA provider with concurrency guard on (the default). +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y +CONFIG_WOLFPSA_THREAD_SAFE=y + +CONFIG_ENTROPY_GENERATOR=y + +# Preemptive interleaving to actually exercise the lock. +CONFIG_TIMESLICING=y +CONFIG_TIMESLICE_SIZE=1 + +# Broad example wolfCrypt config so wolfPSA exposes a full PSA surface (the +# example is self-contained and crypto-only). A real application points this at +# its own config or uses the module Kconfig. +CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" diff --git a/zephyr/tests/psa_concurrency/src/main.c b/zephyr/tests/psa_concurrency/src/main.c new file mode 100644 index 0000000..083163c --- /dev/null +++ b/zephyr/tests/psa_concurrency/src/main.c @@ -0,0 +1,199 @@ +/* main.c - wolfPSA concurrent key-store stress test + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Hammers the wolfPSA volatile key store from several threads at once. Each + * worker repeatedly imports/generates its OWN keys, uses them, and destroys + * them, so the threads race on the shared volatile-key list and the auto-id + * counter. With CONFIG_WOLFPSA_THREAD_SAFE the global (non-recursive) lock must keep + * that shared state consistent; without it this reliably corrupts/crashes. + * + * Preemptive time-slicing (prj.conf) forces interleaving on the single native + * CPU, including preemption mid list-traversal. + */ + +#include +#include +#include +#include + +#define NTHREADS 4 +#define NITERS 60 + +K_THREAD_STACK_ARRAY_DEFINE(worker_stacks, NTHREADS, 24576); +static struct k_thread worker_threads[NTHREADS]; + +static atomic_t g_ops; +static atomic_t g_fail; + +/* One key imported once and used (read-only lookup) by every thread at the same + * time, so the workers race on a single shared list node while they also churn + * their own keys — exercising concurrent readers vs. list mutation. */ +static psa_key_id_t g_shared_key = PSA_KEY_ID_NULL; + +static const uint8_t pt[] = "wolfPSA concurrent key-store stress payload!"; + +/* One AES-GCM volatile-key round-trip on a fresh key. */ +static int do_aes_gcm(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + uint8_t key[16], nonce[12]; + uint8_t ct[sizeof(pt) + 16]; + uint8_t out[sizeof(pt)]; + size_t ctlen = 0, outlen = 0; + int rc = -1; + + if (psa_generate_random(key, sizeof(key)) != PSA_SUCCESS || + psa_generate_random(nonce, sizeof(nonce)) != PSA_SUCCESS) { + return -1; + } + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&a, PSA_ALG_GCM); + psa_set_key_type(&a, PSA_KEY_TYPE_AES); + psa_set_key_bits(&a, 128); + + if (psa_import_key(&a, key, sizeof(key), &k) != PSA_SUCCESS) { + return -1; + } + if (psa_aead_encrypt(k, PSA_ALG_GCM, nonce, sizeof(nonce), NULL, 0, + pt, sizeof(pt), ct, sizeof(ct), &ctlen) == PSA_SUCCESS && + psa_aead_decrypt(k, PSA_ALG_GCM, nonce, sizeof(nonce), NULL, 0, + ct, ctlen, out, sizeof(out), &outlen) == PSA_SUCCESS && + outlen == sizeof(pt) && memcmp(out, pt, sizeof(pt)) == 0) { + rc = 0; + } + (void)psa_destroy_key(k); + return rc; +} + +/* One ECDSA P-256 generate/sign/verify/destroy on a fresh key. */ +static int do_ecdsa(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + const psa_algorithm_t alg = PSA_ALG_ECDSA(PSA_ALG_SHA_256); + uint8_t hash[32], sig[80]; + size_t hlen = 0, siglen = 0; + int rc = -1; + + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_SIGN_HASH | PSA_KEY_USAGE_VERIFY_HASH); + psa_set_key_algorithm(&a, alg); + psa_set_key_type(&a, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1)); + psa_set_key_bits(&a, 256); + + if (psa_generate_key(&a, &k) != PSA_SUCCESS) { + return -1; + } + if (psa_hash_compute(PSA_ALG_SHA_256, pt, sizeof(pt), hash, sizeof(hash), + &hlen) == PSA_SUCCESS && + psa_sign_hash(k, alg, hash, hlen, sig, sizeof(sig), &siglen) == PSA_SUCCESS && + psa_verify_hash(k, alg, hash, hlen, sig, siglen) == PSA_SUCCESS) { + rc = 0; + } + (void)psa_destroy_key(k); + return rc; +} + +/* AES-GCM round-trip using an EXISTING key id (shared across threads). Does not + * import/destroy — it only looks the key up, so many threads read the same list + * node concurrently. */ +static int do_shared_gcm(psa_key_id_t k) +{ + uint8_t nonce[12]; + uint8_t ct[sizeof(pt) + 16]; + uint8_t out[sizeof(pt)]; + size_t ctlen = 0, outlen = 0; + + if (psa_generate_random(nonce, sizeof(nonce)) != PSA_SUCCESS) { + return -1; + } + if (psa_aead_encrypt(k, PSA_ALG_GCM, nonce, sizeof(nonce), NULL, 0, + pt, sizeof(pt), ct, sizeof(ct), &ctlen) != PSA_SUCCESS || + psa_aead_decrypt(k, PSA_ALG_GCM, nonce, sizeof(nonce), NULL, 0, + ct, ctlen, out, sizeof(out), &outlen) != PSA_SUCCESS || + outlen != sizeof(pt) || memcmp(out, pt, sizeof(pt)) != 0) { + return -1; + } + return 0; +} + +static void worker(void *p1, void *p2, void *p3) +{ + ARG_UNUSED(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + for (int i = 0; i < NITERS; i++) { + if (do_aes_gcm() != 0) { + atomic_inc(&g_fail); + } + atomic_inc(&g_ops); + + if (do_ecdsa() != 0) { + atomic_inc(&g_fail); + } + atomic_inc(&g_ops); + + if (do_shared_gcm(g_shared_key) != 0) { + atomic_inc(&g_fail); + } + atomic_inc(&g_ops); + } +} + +ZTEST(wolfpsa_concurrency, test_parallel_keystore) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + uint8_t key[16]; + + zassert_equal(psa_crypto_init(), PSA_SUCCESS); + + /* Import the shared key that every worker will read concurrently. */ + memset(key, 0x5a, sizeof(key)); + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&a, PSA_ALG_GCM); + psa_set_key_type(&a, PSA_KEY_TYPE_AES); + psa_set_key_bits(&a, 128); + zassert_equal(psa_import_key(&a, key, sizeof(key), &g_shared_key), PSA_SUCCESS); + + atomic_set(&g_ops, 0); + atomic_set(&g_fail, 0); + + for (int i = 0; i < NTHREADS; i++) { + k_thread_create(&worker_threads[i], worker_stacks[i], + K_THREAD_STACK_SIZEOF(worker_stacks[i]), + worker, NULL, NULL, NULL, + K_PRIO_PREEMPT(5), 0, K_NO_WAIT); + } + for (int i = 0; i < NTHREADS; i++) { + zassert_equal(k_thread_join(&worker_threads[i], K_FOREVER), 0); + } + + zassert_equal(atomic_get(&g_ops), NTHREADS * NITERS * 3, + "unexpected op count"); + zassert_equal(atomic_get(&g_fail), 0, + "%d concurrent PSA operations failed", (int)atomic_get(&g_fail)); + + (void)psa_destroy_key(g_shared_key); +} + +ZTEST_SUITE(wolfpsa_concurrency, NULL, NULL, NULL, NULL, NULL); diff --git a/zephyr/tests/psa_concurrency/testcase.yaml b/zephyr/tests/psa_concurrency/testcase.yaml new file mode 100644 index 0000000..2c05d41 --- /dev/null +++ b/zephyr/tests/psa_concurrency/testcase.yaml @@ -0,0 +1,11 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +common: + filter: not CONFIG_BUILD_WITH_TFM + tags: + - wolfpsa + - psa +tests: + crypto.wolfpsa.psa_concurrency: + integration_platforms: + - native_sim/native/64 diff --git a/zephyr/tests/psa_consumer/CMakeLists.txt b/zephyr/tests/psa_consumer/CMakeLists.txt new file mode 100644 index 0000000..3eb695b --- /dev/null +++ b/zephyr/tests/psa_consumer/CMakeLists.txt @@ -0,0 +1,15 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Drop-in consumer test: builds Zephyr's own provider-agnostic PSA crypto +# ztest (tests/crypto/mbedtls_psa) UNCHANGED, but against the wolfPSA custom +# provider instead of Mbed TLS. Proves a real Zephyr PSA consumer works on +# wolfPSA and that the CONFIG_PSA_WANT_* -> PSA_WANT_* mapping resolves. + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(wolfpsa_psa_consumer) + +# Reuse the in-tree test source verbatim (it only depends on ). +target_sources(app PRIVATE ${ZEPHYR_BASE}/tests/crypto/mbedtls_psa/src/main.c) diff --git a/zephyr/tests/psa_consumer/prj.conf b/zephyr/tests/psa_consumer/prj.conf new file mode 100644 index 0000000..cd80a4e --- /dev/null +++ b/zephyr/tests/psa_consumer/prj.conf @@ -0,0 +1,31 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +CONFIG_ZTEST=y +CONFIG_ZTEST_STACK_SIZE=16384 +CONFIG_MAIN_STACK_SIZE=16384 + +# wolfPSA as the custom PSA provider, reusing the wolfSSL module's wolfCrypt. +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y + +# Entropy for the wolfCrypt DRBG seed. +CONFIG_ENTROPY_GENERATOR=y + +# Capabilities the consumer requests (identical to the in-tree mbedtls_psa +# test). These drive wolfPSA's PSA_WANT_* surface via the Zephyr mapping. +CONFIG_PSA_WANT_ALG_SHA_1=y +CONFIG_PSA_WANT_ALG_SHA_224=y +CONFIG_PSA_WANT_ALG_SHA_256=y +CONFIG_PSA_WANT_ALG_SHA_384=y +CONFIG_PSA_WANT_ALG_SHA_512=y +CONFIG_PSA_WANT_ALG_HMAC=y +CONFIG_PSA_WANT_KEY_TYPE_HMAC=y +CONFIG_PSA_WANT_KEY_TYPE_AES=y +CONFIG_PSA_WANT_ALG_ECB_NO_PADDING=y + +# Broad example wolfCrypt config so wolfPSA exposes a full PSA surface (the +# example is self-contained and crypto-only). A real application points this at +# its own config or uses the module Kconfig. +CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" diff --git a/zephyr/tests/psa_consumer/testcase.yaml b/zephyr/tests/psa_consumer/testcase.yaml new file mode 100644 index 0000000..ae68f9b --- /dev/null +++ b/zephyr/tests/psa_consumer/testcase.yaml @@ -0,0 +1,22 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Runs Zephyr's provider-agnostic PSA crypto ztest against the wolfPSA custom +# provider (instead of Mbed TLS), proving wolfPSA is a drop-in PSA provider. +common: + filter: not CONFIG_BUILD_WITH_TFM + harness: ztest + tags: + - wolfpsa + - psa +tests: + crypto.wolfpsa.psa_consumer: + integration_platforms: + - native_sim/native/64 + # Single-threaded variant: exercises the CONFIG_WOLFPSA_THREAD_SAFE=n baseline + # path (SINGLE_THREADED, no-op lock macros) so it is not left untested. + crypto.wolfpsa.psa_consumer.single_threaded: + extra_configs: + - CONFIG_WOLFPSA_THREAD_SAFE=n + integration_platforms: + - native_sim/native/64 diff --git a/zephyr/tests/psa_entropy/CMakeLists.txt b/zephyr/tests/psa_entropy/CMakeLists.txt new file mode 100644 index 0000000..cbc5431 --- /dev/null +++ b/zephyr/tests/psa_entropy/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(wolfpsa_psa_entropy) + +target_sources(app PRIVATE src/main.c) diff --git a/zephyr/tests/psa_entropy/prj.conf b/zephyr/tests/psa_entropy/prj.conf new file mode 100644 index 0000000..7ad139d --- /dev/null +++ b/zephyr/tests/psa_entropy/prj.conf @@ -0,0 +1,21 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +CONFIG_ZTEST=y +CONFIG_ZTEST_STACK_SIZE=8192 +CONFIG_MAIN_STACK_SIZE=8192 + +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y + +# Pull in the hardware entropy driver so wolfCrypt's wc_GenerateSeed() seeds the +# DRBG from it (the module's random.c uses the entropy driver whenever +# ENTROPY_HAS_DRIVER is set), exercising the real seed path rather than +# sys_rand_get(). +CONFIG_ENTROPY_GENERATOR=y + +# Broad example wolfCrypt config so wolfPSA exposes a full PSA surface (the +# example is self-contained and crypto-only). A real application points this at +# its own config or uses the module Kconfig. +CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" diff --git a/zephyr/tests/psa_entropy/src/main.c b/zephyr/tests/psa_entropy/src/main.c new file mode 100644 index 0000000..49aeebc --- /dev/null +++ b/zephyr/tests/psa_entropy/src/main.c @@ -0,0 +1,51 @@ +/* main.c - wolfPSA entropy smoke test + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Verifies the PSA RNG works end-to-end: psa_generate_random() succeeds, which + * means wolfCrypt's Hash-DRBG got a valid seed. On Zephyr that seed comes from + * the hardware entropy driver via wc_GenerateSeed() (see the wolfSSL module's + * wolfcrypt/src/random.c); wolfPSA registers no seed callback of its own. + * + * The dead-entropy -> PSA_ERROR_INSUFFICIENT_ENTROPY mapping is a + * wolfCrypt-internal contract (wc_GenerateSeed() error -> wc_InitRng() fails -> + * wc_error_to_psa_status()) covered by wolfCrypt's own tests; it is no longer + * driven from here now that the injectable seed callback is gone. + */ + +#include +#include + +ZTEST(wolfpsa_entropy, test_psa_generate_random_succeeds) +{ + uint8_t buf[32]; + psa_status_t status; + + zassert_equal(psa_crypto_init(), PSA_SUCCESS); + + /* Each psa_generate_random() seeds a fresh WC_RNG via wc_GenerateSeed(), so + * PSA_SUCCESS proves the entropy-driver seed path works end-to-end. */ + status = psa_generate_random(buf, sizeof(buf)); + zassert_equal(status, PSA_SUCCESS, + "psa_generate_random failed to seed: %d", (int)status); +} + +ZTEST_SUITE(wolfpsa_entropy, NULL, NULL, NULL, NULL, NULL); diff --git a/zephyr/tests/psa_entropy/testcase.yaml b/zephyr/tests/psa_entropy/testcase.yaml new file mode 100644 index 0000000..32b247b --- /dev/null +++ b/zephyr/tests/psa_entropy/testcase.yaml @@ -0,0 +1,11 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +common: + filter: not CONFIG_BUILD_WITH_TFM + tags: + - wolfpsa + - psa +tests: + crypto.wolfpsa.psa_entropy: + integration_platforms: + - native_sim/native/64 diff --git a/zephyr/tests/psa_its/CMakeLists.txt b/zephyr/tests/psa_its/CMakeLists.txt new file mode 100644 index 0000000..c911011 --- /dev/null +++ b/zephyr/tests/psa_its/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Drop-in proof: Zephyr's own PSA ITS sample source, run against wolfPSA's PSA +# ITS store backend + wolfCrypt custom transform. +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(wolfpsa_psa_its) +target_sources(app PRIVATE ${ZEPHYR_BASE}/samples/psa/its/src/main.c) diff --git a/zephyr/tests/psa_its/prj.conf b/zephyr/tests/psa_its/prj.conf new file mode 100644 index 0000000..4867aef --- /dev/null +++ b/zephyr/tests/psa_its/prj.conf @@ -0,0 +1,22 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y +CONFIG_ENTROPY_GENERATOR=y +CONFIG_SECURE_STORAGE=y +CONFIG_SECURE_STORAGE_ITS_STORE_IMPLEMENTATION_SETTINGS=y +CONFIG_SECURE_STORAGE_ITS_TRANSFORM_IMPLEMENTATION_CUSTOM=y +CONFIG_SETTINGS=y +CONFIG_SETTINGS_NVS=y +CONFIG_NVS=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_LOG=y +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_MAIN_STACK_SIZE=4096 + +# Broad example wolfCrypt config so wolfPSA exposes a full PSA surface (the +# example is self-contained and crypto-only). A real application points this at +# its own config or uses the module Kconfig. +CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" diff --git a/zephyr/tests/psa_its/testcase.yaml b/zephyr/tests/psa_its/testcase.yaml new file mode 100644 index 0000000..0176c2d --- /dev/null +++ b/zephyr/tests/psa_its/testcase.yaml @@ -0,0 +1,17 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +common: + filter: not CONFIG_BUILD_WITH_TFM + tags: + - wolfpsa + - psa + - secure_storage + harness: console + harness_config: + type: one_line + regex: + - "Sample finished successfully." +tests: + crypto.wolfpsa.psa_its: + integration_platforms: + - native_sim/native/64 diff --git a/zephyr/tests/psa_persistent_key/CMakeLists.txt b/zephyr/tests/psa_persistent_key/CMakeLists.txt new file mode 100644 index 0000000..0f53251 --- /dev/null +++ b/zephyr/tests/psa_persistent_key/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Drop-in proof: builds Zephyr's own samples/psa/persistent_key source verbatim, +# but against the wolfPSA custom PSA provider + wolfPSA's wolfCrypt ITS transform. +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(wolfpsa_psa_persistent_key) +target_sources(app PRIVATE ${ZEPHYR_BASE}/samples/psa/persistent_key/src/main.c) diff --git a/zephyr/tests/psa_persistent_key/prj.conf b/zephyr/tests/psa_persistent_key/prj.conf new file mode 100644 index 0000000..90cc0e4 --- /dev/null +++ b/zephyr/tests/psa_persistent_key/prj.conf @@ -0,0 +1,24 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y +CONFIG_ENTROPY_GENERATOR=y +# Persistent keys via secure_storage + wolfPSA's wolfCrypt AES-GCM ITS transform. +CONFIG_SECURE_STORAGE=y +CONFIG_SECURE_STORAGE_ITS_STORE_IMPLEMENTATION_SETTINGS=y +CONFIG_SECURE_STORAGE_ITS_TRANSFORM_IMPLEMENTATION_CUSTOM=y +CONFIG_SETTINGS=y +CONFIG_SETTINGS_NVS=y +CONFIG_NVS=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_LOG=y +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_ASSERT=y +CONFIG_MAIN_STACK_SIZE=4096 + +# Broad example wolfCrypt config so wolfPSA exposes a full PSA surface (the +# example is self-contained and crypto-only). A real application points this at +# its own config or uses the module Kconfig. +CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" diff --git a/zephyr/tests/psa_persistent_key/testcase.yaml b/zephyr/tests/psa_persistent_key/testcase.yaml new file mode 100644 index 0000000..60e2c89 --- /dev/null +++ b/zephyr/tests/psa_persistent_key/testcase.yaml @@ -0,0 +1,19 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Runs Zephyr's own persistent-key PSA sample against wolfPSA (drop-in proof). +common: + filter: not CONFIG_BUILD_WITH_TFM + tags: + - wolfpsa + - psa + - secure_storage + harness: console + harness_config: + type: one_line + regex: + - "Sample finished successfully." +tests: + crypto.wolfpsa.psa_persistent_key: + integration_platforms: + - native_sim/native/64 diff --git a/zephyr/tests/psa_purge/CMakeLists.txt b/zephyr/tests/psa_purge/CMakeLists.txt new file mode 100644 index 0000000..0ea09dd --- /dev/null +++ b/zephyr/tests/psa_purge/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(wolfpsa_psa_purge) + +target_sources(app PRIVATE src/main.c) diff --git a/zephyr/tests/psa_purge/prj.conf b/zephyr/tests/psa_purge/prj.conf new file mode 100644 index 0000000..1c832c4 --- /dev/null +++ b/zephyr/tests/psa_purge/prj.conf @@ -0,0 +1,17 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +CONFIG_ZTEST=y +CONFIG_ZTEST_STACK_SIZE=16384 +CONFIG_MAIN_STACK_SIZE=16384 + +# wolfPSA as the custom PSA provider, reusing the wolfSSL module's wolfCrypt. +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y + +# Entropy for the wolfCrypt DRBG seed. +CONFIG_ENTROPY_GENERATOR=y + +# Broad example config (self-contained, crypto-only) so the AES key type is available. +CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" diff --git a/zephyr/tests/psa_purge/src/main.c b/zephyr/tests/psa_purge/src/main.c new file mode 100644 index 0000000..00a5927 --- /dev/null +++ b/zephyr/tests/psa_purge/src/main.c @@ -0,0 +1,85 @@ +/* main.c - wolfPSA psa_purge_key() unit test + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * psa_purge_key() confirms a key exists and reports its status; wolfPSA keeps no + * purgeable in-RAM cache of persistent key material, so there is nothing to + * evict. These tests pin both outcomes: success for a live key, and + * PSA_ERROR_INVALID_HANDLE for one that does not exist -- the latter path is + * otherwise undriven, since every other caller purges a key it just created. + * psa_purge_key() itself is platform-neutral (src/psa_key_storage.c); this is + * the Zephyr ztest, mirrored in the standalone tier by a case in + * test/psa_server/psa_14_misc_test.c. + */ + +#include +#include + +/* Import a volatile AES-128 key; returns its id or PSA_KEY_ID_NULL on failure. */ +static psa_key_id_t import_volatile_key(void) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + const uint8_t key[16] = { 0 }; + + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&a, PSA_ALG_GCM); + psa_set_key_type(&a, PSA_KEY_TYPE_AES); + psa_set_key_bits(&a, 128); + + if (psa_import_key(&a, key, sizeof(key), &k) != PSA_SUCCESS) { + return PSA_KEY_ID_NULL; + } + return k; +} + +/* A live key has no purgeable cached copy, so purge reports success. */ +ZTEST(wolfpsa_purge, test_purge_live_key) +{ + psa_key_id_t k; + + zassert_equal(psa_crypto_init(), PSA_SUCCESS, "crypto init"); + + k = import_volatile_key(); + zassert_not_equal(k, PSA_KEY_ID_NULL, "import volatile key"); + + zassert_equal(psa_purge_key(k), PSA_SUCCESS, "purge of a live key succeeds"); + + zassert_equal(psa_destroy_key(k), PSA_SUCCESS, "destroy"); +} + +/* Purging a key that does not exist must be rejected, not silently succeed. */ +ZTEST(wolfpsa_purge, test_purge_absent_key) +{ + psa_key_id_t k; + + zassert_equal(psa_crypto_init(), PSA_SUCCESS, "crypto init"); + + k = import_volatile_key(); + zassert_not_equal(k, PSA_KEY_ID_NULL, "import volatile key"); + zassert_equal(psa_destroy_key(k), PSA_SUCCESS, "destroy"); + + /* k now refers to no key. */ + zassert_equal(psa_purge_key(k), PSA_ERROR_INVALID_HANDLE, + "purge of an absent key is rejected"); +} + +ZTEST_SUITE(wolfpsa_purge, NULL, NULL, NULL, NULL, NULL); diff --git a/zephyr/tests/psa_purge/testcase.yaml b/zephyr/tests/psa_purge/testcase.yaml new file mode 100644 index 0000000..2e2c74b --- /dev/null +++ b/zephyr/tests/psa_purge/testcase.yaml @@ -0,0 +1,16 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Direct coverage for psa_purge_key(): a live key purges to PSA_SUCCESS, and an +# absent key is rejected with PSA_ERROR_INVALID_HANDLE -- the latter path is not +# driven by any other wolfPSA test (all existing callers purge a key that +# exists). +common: + filter: not CONFIG_BUILD_WITH_TFM + tags: + - wolfpsa + - psa +tests: + crypto.wolfpsa.psa_purge: + integration_platforms: + - native_sim/native/64 diff --git a/zephyr/tests/psa_secure_storage/CMakeLists.txt b/zephyr/tests/psa_secure_storage/CMakeLists.txt new file mode 100644 index 0000000..9dba7f5 --- /dev/null +++ b/zephyr/tests/psa_secure_storage/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Drop-in proof: Zephyr's own secure_storage PSA crypto ztest (persistent keys + +# ITS + PS coexistence), run against wolfPSA + its wolfCrypt custom transform. +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(wolfpsa_psa_secure_storage) +target_sources(app PRIVATE + ${ZEPHYR_BASE}/tests/subsys/secure_storage/psa/crypto/src/main.c) diff --git a/zephyr/tests/psa_secure_storage/prj.conf b/zephyr/tests/psa_secure_storage/prj.conf new file mode 100644 index 0000000..6ca31c4 --- /dev/null +++ b/zephyr/tests/psa_secure_storage/prj.conf @@ -0,0 +1,23 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +CONFIG_ZTEST=y +CONFIG_ZTEST_STACK_SIZE=4096 +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y +CONFIG_ENTROPY_GENERATOR=y +CONFIG_SECURE_STORAGE=y +CONFIG_SECURE_STORAGE_PS_IMPLEMENTATION_ITS=y +CONFIG_SECURE_STORAGE_ITS_STORE_IMPLEMENTATION_SETTINGS=y +CONFIG_SECURE_STORAGE_ITS_TRANSFORM_IMPLEMENTATION_CUSTOM=y +CONFIG_SETTINGS=y +CONFIG_SETTINGS_NVS=y +CONFIG_NVS=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y + +# Broad example wolfCrypt config so wolfPSA exposes a full PSA surface (the +# example is self-contained and crypto-only). A real application points this at +# its own config or uses the module Kconfig. +CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" diff --git a/zephyr/tests/psa_secure_storage/testcase.yaml b/zephyr/tests/psa_secure_storage/testcase.yaml new file mode 100644 index 0000000..d1934cf --- /dev/null +++ b/zephyr/tests/psa_secure_storage/testcase.yaml @@ -0,0 +1,12 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +common: + filter: not CONFIG_BUILD_WITH_TFM + tags: + - wolfpsa + - psa + - secure_storage +tests: + crypto.wolfpsa.psa_secure_storage: + integration_platforms: + - native_sim/native/64 diff --git a/zephyr/tests/psa_store_unavailable/CMakeLists.txt b/zephyr/tests/psa_store_unavailable/CMakeLists.txt new file mode 100644 index 0000000..5e61c07 --- /dev/null +++ b/zephyr/tests/psa_store_unavailable/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(wolfpsa_psa_store_unavailable) + +target_sources(app PRIVATE src/main.c) diff --git a/zephyr/tests/psa_store_unavailable/prj.conf b/zephyr/tests/psa_store_unavailable/prj.conf new file mode 100644 index 0000000..748c80b --- /dev/null +++ b/zephyr/tests/psa_store_unavailable/prj.conf @@ -0,0 +1,21 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +CONFIG_ZTEST=y +CONFIG_ZTEST_STACK_SIZE=16384 +CONFIG_MAIN_STACK_SIZE=16384 + +# wolfPSA as the custom PSA provider, reusing the wolfSSL module's wolfCrypt. +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y + +CONFIG_ENTROPY_GENERATOR=y + +# DELIBERATELY no secure_storage: this exercises the store backend's degradation +# path (psa_store_zephyr.c #else branch), where every wolfPSA_Store_* returns +# "not available". Volatile keys must keep working; persistent-key operations +# must fail cleanly rather than crash or silently succeed. +CONFIG_SECURE_STORAGE=n + +CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" diff --git a/zephyr/tests/psa_store_unavailable/src/main.c b/zephyr/tests/psa_store_unavailable/src/main.c new file mode 100644 index 0000000..2475def --- /dev/null +++ b/zephyr/tests/psa_store_unavailable/src/main.c @@ -0,0 +1,91 @@ +/* main.c - wolfPSA store-unavailable degradation test + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Built with CONFIG_SECURE_STORAGE deliberately unset, so wolfPSA's Zephyr store + * backend (src/psa_store_zephyr.c) compiles its degradation stubs: every + * wolfPSA_Store_* entry point returns "not available". This pins the documented + * contract of that branch -- volatile keys keep working, and a persistent-key + * operation fails cleanly (a storage error, no crash) rather than silently + * succeeding. + */ + +#include +#include +#include + +/* Volatile keys never touch the store, so they must work regardless. */ +ZTEST(wolfpsa_store_unavailable, test_volatile_key_still_works) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + const uint8_t key[16] = { 0 }; + const uint8_t pt[16] = "0123456789abcde"; + uint8_t nonce[12] = { 0 }; + uint8_t ct[sizeof(pt) + 16]; + uint8_t out[sizeof(pt)]; + size_t ctlen = 0, outlen = 0; + + zassert_equal(psa_crypto_init(), PSA_SUCCESS, "crypto init"); + + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&a, PSA_ALG_GCM); + psa_set_key_type(&a, PSA_KEY_TYPE_AES); + psa_set_key_bits(&a, 128); + + zassert_equal(psa_import_key(&a, key, sizeof(key), &k), PSA_SUCCESS, + "import volatile key"); + zassert_equal(psa_aead_encrypt(k, PSA_ALG_GCM, nonce, sizeof(nonce), + NULL, 0, pt, sizeof(pt), ct, sizeof(ct), &ctlen), + PSA_SUCCESS, "encrypt"); + zassert_equal(psa_aead_decrypt(k, PSA_ALG_GCM, nonce, sizeof(nonce), + NULL, 0, ct, ctlen, out, sizeof(out), &outlen), + PSA_SUCCESS, "decrypt"); + zassert_equal(outlen, sizeof(pt), "roundtrip length"); + zassert_mem_equal(out, pt, sizeof(pt), "roundtrip data"); + + zassert_equal(psa_destroy_key(k), PSA_SUCCESS, "destroy"); +} + +/* A persistent key needs the store; with none available the import must fail + * cleanly (some error status, no crash) rather than silently succeed. */ +ZTEST(wolfpsa_store_unavailable, test_persistent_key_degrades) +{ + psa_key_attributes_t a = PSA_KEY_ATTRIBUTES_INIT; + psa_key_id_t k = PSA_KEY_ID_NULL; + const uint8_t key[16] = { 0 }; + psa_status_t status; + + zassert_equal(psa_crypto_init(), PSA_SUCCESS, "crypto init"); + + psa_set_key_usage_flags(&a, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&a, PSA_ALG_GCM); + psa_set_key_type(&a, PSA_KEY_TYPE_AES); + psa_set_key_bits(&a, 128); + psa_set_key_lifetime(&a, PSA_KEY_LIFETIME_PERSISTENT); + psa_set_key_id(&a, 0x00000041); + + status = psa_import_key(&a, key, sizeof(key), &k); + zassert_not_equal(status, PSA_SUCCESS, + "persistent import must not succeed with no store"); +} + +ZTEST_SUITE(wolfpsa_store_unavailable, NULL, NULL, NULL, NULL, NULL); diff --git a/zephyr/tests/psa_store_unavailable/testcase.yaml b/zephyr/tests/psa_store_unavailable/testcase.yaml new file mode 100644 index 0000000..b1d0401 --- /dev/null +++ b/zephyr/tests/psa_store_unavailable/testcase.yaml @@ -0,0 +1,18 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Pins the documented degradation contract of wolfPSA's Zephyr store backend +# when CONFIG_SECURE_STORAGE is unset: volatile keys keep working, and a +# persistent-key operation fails cleanly (a storage error, no crash) instead of +# silently succeeding. Complements psa_its / psa_persistent_key / psa_secure_storage, +# which all build the CONFIG_SECURE_STORAGE branch. +common: + filter: not CONFIG_BUILD_WITH_TFM + tags: + - wolfpsa + - psa + - secure_storage +tests: + crypto.wolfpsa.psa_store_unavailable: + integration_platforms: + - native_sim/native/64 diff --git a/zephyr/tests/psa_tls_coexist/CMakeLists.txt b/zephyr/tests/psa_tls_coexist/CMakeLists.txt new file mode 100644 index 0000000..9086a7a --- /dev/null +++ b/zephyr/tests/psa_tls_coexist/CMakeLists.txt @@ -0,0 +1,7 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(psa_tls_coexist) + +target_sources(app PRIVATE src/main.c) diff --git a/zephyr/tests/psa_tls_coexist/prj.conf b/zephyr/tests/psa_tls_coexist/prj.conf new file mode 100644 index 0000000..b593347 --- /dev/null +++ b/zephyr/tests/psa_tls_coexist/prj.conf @@ -0,0 +1,29 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# TLS + wolfPSA coexistence: the wolfSSL TLS stack AND the wolfPSA PSA provider +# in one image, both on the same shared wolfCrypt core. + +CONFIG_ZTEST=y +CONFIG_ZTEST_STACK_SIZE=32768 +CONFIG_MAIN_STACK_SIZE=32768 +CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE=262144 + +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y + +# Keep the wolfSSL TLS layer in the build (do NOT force WOLFCRYPT_ONLY) so it +# coexists with the wolfPSA provider. No CONFIG_WOLFSSL_SETTINGS_FILE here: use +# the module's default config, which enables the TLS layer plus a broad crypto +# set (the wolfPSA knobs still add the Hash-DRBG for the PSA RNG). + +CONFIG_ENTROPY_GENERATOR=y + +# The wolfSSL TLS layer's socket I/O (wolfio.c) uses Zephyr's BSD sockets, so +# the networking stack must be present for it to link. (The sockets TLS backend +# is wolfSSL here, since CONFIG_WOLFSSL is set.) +CONFIG_NETWORKING=y +CONFIG_NET_SOCKETS=y +CONFIG_NET_TCP=y +CONFIG_NET_IPV4=y diff --git a/zephyr/tests/psa_tls_coexist/src/main.c b/zephyr/tests/psa_tls_coexist/src/main.c new file mode 100644 index 0000000..1109e37 --- /dev/null +++ b/zephyr/tests/psa_tls_coexist/src/main.c @@ -0,0 +1,183 @@ +/* main.c - wolfSSL-TLS + wolfPSA coexistence test + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Proves that the wolfSSL TLS layer and the wolfPSA PSA Crypto provider live in + * one image on the same wolfCrypt core: the PSA API works, AND the TLS layer is + * present and functional (methods, contexts, and an in-memory client<->server + * handshake over a memory transport, no network stack needed). + */ + +#include +#include + +#include + +#include +#include +#define USE_CERT_BUFFERS_2048 /* expose server_cert_der_2048 etc. */ +#include + +/* --- The wolfPSA PSA provider works ------------------------------------- */ +ZTEST(psa_tls_coexist, test_psa_provider_works) +{ + uint8_t buf[32]; + uint8_t hash[32]; + size_t hash_len = 0; + + zassert_equal(psa_crypto_init(), PSA_SUCCESS, "psa_crypto_init"); + zassert_equal(psa_generate_random(buf, sizeof(buf)), PSA_SUCCESS, + "psa_generate_random"); + zassert_equal(psa_hash_compute(PSA_ALG_SHA_256, (const uint8_t *)"wolf", 4, + hash, sizeof(hash), &hash_len), + PSA_SUCCESS, "psa_hash_compute"); + zassert_equal(hash_len, 32, "SHA-256 output length"); +} + +/* --- The wolfSSL TLS layer is present ----------------------------------- */ +ZTEST(psa_tls_coexist, test_wolfssl_tls_layer_present) +{ + WOLFSSL_CTX *ctx; + + zassert_equal(wolfSSL_Init(), WOLFSSL_SUCCESS, "wolfSSL_Init"); + + /* Creating a TLS context requires the full TLS layer to be linked -- it + * would be absent under WOLFCRYPT_ONLY. */ + ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()); + zassert_not_null(ctx, "TLS context creation failed (TLS layer missing?)"); + + wolfSSL_CTX_free(ctx); + wolfSSL_Cleanup(); +} + +/* --- A real TLS handshake runs in the same image as the PSA provider ----- */ + +/* In-memory transport: one FIFO per direction, driven by the I/O callbacks. */ +struct membuf { + unsigned char data[16384]; + int len; /* bytes written */ + int pos; /* bytes consumed */ +}; +static struct membuf g_c2s; /* client -> server */ +static struct membuf g_s2c; /* server -> client */ + +static int mem_recv(WOLFSSL *ssl, char *buf, int sz, void *ctx) +{ + struct membuf *m = (struct membuf *)ctx; + int avail = m->len - m->pos; + + (void)ssl; + if (avail <= 0) { + return WOLFSSL_CBIO_ERR_WANT_READ; + } + if (sz > avail) { + sz = avail; + } + memcpy(buf, m->data + m->pos, (size_t)sz); + m->pos += sz; + return sz; +} + +static int mem_send(WOLFSSL *ssl, char *buf, int sz, void *ctx) +{ + struct membuf *m = (struct membuf *)ctx; + + (void)ssl; + if (m->len + sz > (int)sizeof(m->data)) { + return WOLFSSL_CBIO_ERR_WANT_WRITE; + } + memcpy(m->data + m->len, buf, (size_t)sz); + m->len += sz; + return sz; +} + +ZTEST(psa_tls_coexist, test_tls_handshake_with_psa) +{ + WOLFSSL_CTX *sctx = NULL, *cctx = NULL; + WOLFSSL *ssl_s = NULL, *ssl_c = NULL; + uint8_t rnd[16]; + int cret = -1, sret = -1; + int i; + + memset(&g_c2s, 0, sizeof(g_c2s)); + memset(&g_s2c, 0, sizeof(g_s2c)); + + zassert_equal(wolfSSL_Init(), WOLFSSL_SUCCESS, "wolfSSL_Init"); + + /* Server: cert + key from wolfSSL's built-in test material. */ + sctx = wolfSSL_CTX_new(wolfTLSv1_2_server_method()); + zassert_not_null(sctx, "server CTX"); + zassert_equal(wolfSSL_CTX_use_certificate_buffer(sctx, server_cert_der_2048, + sizeof_server_cert_der_2048, WOLFSSL_FILETYPE_ASN1), + WOLFSSL_SUCCESS, "server cert"); + zassert_equal(wolfSSL_CTX_use_PrivateKey_buffer(sctx, server_key_der_2048, + sizeof_server_key_der_2048, WOLFSSL_FILETYPE_ASN1), + WOLFSSL_SUCCESS, "server key"); + + /* Client: skip peer verification to keep the test to the handshake crypto. */ + cctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method()); + zassert_not_null(cctx, "client CTX"); + wolfSSL_CTX_set_verify(cctx, WOLFSSL_VERIFY_NONE, NULL); + + wolfSSL_CTX_SetIORecv(sctx, mem_recv); + wolfSSL_CTX_SetIOSend(sctx, mem_send); + wolfSSL_CTX_SetIORecv(cctx, mem_recv); + wolfSSL_CTX_SetIOSend(cctx, mem_send); + + ssl_s = wolfSSL_new(sctx); + ssl_c = wolfSSL_new(cctx); + zassert_not_null(ssl_s, "server SSL"); + zassert_not_null(ssl_c, "client SSL"); + + /* Client reads server->client, writes client->server (and vice-versa). */ + wolfSSL_SetIOReadCtx(ssl_c, &g_s2c); + wolfSSL_SetIOWriteCtx(ssl_c, &g_c2s); + wolfSSL_SetIOReadCtx(ssl_s, &g_c2s); + wolfSSL_SetIOWriteCtx(ssl_s, &g_s2c); + + /* Pump both ends until the handshake completes on both sides. */ + for (i = 0; i < 20 && (cret != WOLFSSL_SUCCESS || sret != WOLFSSL_SUCCESS); + i++) { + if (cret != WOLFSSL_SUCCESS) { + cret = wolfSSL_connect(ssl_c); + } + if (sret != WOLFSSL_SUCCESS) { + sret = wolfSSL_accept(ssl_s); + } + } + + zassert_equal(cret, WOLFSSL_SUCCESS, "client handshake (err %d)", + wolfSSL_get_error(ssl_c, cret)); + zassert_equal(sret, WOLFSSL_SUCCESS, "server handshake (err %d)", + wolfSSL_get_error(ssl_s, sret)); + + /* The PSA provider still works right after a TLS handshake in one image. */ + zassert_equal(psa_generate_random(rnd, sizeof(rnd)), PSA_SUCCESS, + "PSA after TLS handshake"); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(cctx); + wolfSSL_CTX_free(sctx); + wolfSSL_Cleanup(); +} + +ZTEST_SUITE(psa_tls_coexist, NULL, NULL, NULL, NULL, NULL); diff --git a/zephyr/tests/psa_tls_coexist/testcase.yaml b/zephyr/tests/psa_tls_coexist/testcase.yaml new file mode 100644 index 0000000..41e71c5 --- /dev/null +++ b/zephyr/tests/psa_tls_coexist/testcase.yaml @@ -0,0 +1,12 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +common: + tags: + - wolfpsa + - psa + - tls + harness: ztest +tests: + crypto.wolfpsa.tls_coexist: + integration_platforms: + - native_sim/native/64 diff --git a/zephyr/tests/psa_transform/CMakeLists.txt b/zephyr/tests/psa_transform/CMakeLists.txt new file mode 100644 index 0000000..5e644f3 --- /dev/null +++ b/zephyr/tests/psa_transform/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(wolfpsa_psa_persistent) + +target_sources(app PRIVATE src/main.c) diff --git a/zephyr/tests/psa_transform/prj.conf b/zephyr/tests/psa_transform/prj.conf new file mode 100644 index 0000000..2e4bd35 --- /dev/null +++ b/zephyr/tests/psa_transform/prj.conf @@ -0,0 +1,36 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +CONFIG_ZTEST=y +CONFIG_ZTEST_STACK_SIZE=16384 +CONFIG_MAIN_STACK_SIZE=16384 + +# wolfPSA as the custom PSA provider, reusing the wolfSSL module's wolfCrypt. +CONFIG_WOLFSSL=y +CONFIG_PSA_CRYPTO=y +CONFIG_PSA_CRYPTO_PROVIDER_CUSTOM=y + +# Entropy for the wolfCrypt DRBG seed. +CONFIG_ENTROPY_GENERATOR=y + +# Persistent keys: PSA ITS via the secure_storage subsystem over Settings/NVS, +# using wolfPSA's wolfCrypt AES-GCM custom transform (not Zephyr's +# Mbed-TLS-coupled AEAD transform). +CONFIG_SECURE_STORAGE=y +CONFIG_SECURE_STORAGE_ITS_STORE_IMPLEMENTATION_SETTINGS=y +CONFIG_SECURE_STORAGE_ITS_TRANSFORM_IMPLEMENTATION_CUSTOM=y +CONFIG_SETTINGS=y +CONFIG_SETTINGS_NVS=y +CONFIG_NVS=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y + +# Capabilities used by the tests: AES for the persistent key + the transform. +CONFIG_PSA_WANT_KEY_TYPE_AES=y +CONFIG_PSA_WANT_ALG_CTR=y +CONFIG_PSA_WANT_ALG_GCM=y + +# Broad example wolfCrypt config so wolfPSA exposes a full PSA surface (the +# example is self-contained and crypto-only). A real application points this at +# its own config or uses the module Kconfig. +CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" diff --git a/zephyr/tests/psa_transform/src/main.c b/zephyr/tests/psa_transform/src/main.c new file mode 100644 index 0000000..8237146 --- /dev/null +++ b/zephyr/tests/psa_transform/src/main.c @@ -0,0 +1,149 @@ +/* main.c - wolfPSA secure_storage ITS transform unit test + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * Unit-tests wolfPSA's wolfCrypt AES-256-GCM custom ITS transform + * (zephyr/src/psa_its_transform_wolfcrypt.c) directly -- its two functions are + * global. This is wolfPSA-internal behavior that Zephyr's own suite cannot + * cover: Zephyr's custom-transform test is a plaintext passthrough that verifies + * neither confidentiality nor tamper detection. The generic persistent-key and + * ITS round-trip flows are covered instead by running Zephyr's own + * samples/psa/{its,persistent_key} and tests/subsys/secure_storage/psa/crypto + * against wolfPSA (see the sibling psa_its / psa_persistent_key / + * psa_secure_storage wrappers). + */ + +#include +#include +#include +#include + +/* Return non-zero if the byte pattern needle occurs anywhere in hay. */ +static int contains_bytes(const uint8_t *hay, size_t hay_len, + const uint8_t *needle, size_t needle_len) +{ + size_t i; + + if (needle_len == 0 || hay_len < needle_len) { + return 0; + } + for (i = 0; i + needle_len <= hay_len; i++) { + if (memcmp(hay + i, needle, needle_len) == 0) { + return 1; + } + } + return 0; +} + +/* Confidentiality at rest, tamper detection, and UID binding of the AES-GCM + * transform. */ +ZTEST(wolfpsa_transform, test_transform_authenticates) +{ + const secure_storage_its_uid_t uid = { + .uid = 0x1234, .caller_id = SECURE_STORAGE_ITS_CALLER_PSA_ITS + }; + const secure_storage_its_uid_t other = { + .uid = 0x9999, .caller_id = SECURE_STORAGE_ITS_CALLER_PSA_ITS + }; + const uint8_t secret[] = "top secret persistent key material"; + uint8_t stored[SECURE_STORAGE_ITS_TRANSFORM_MAX_STORED_DATA_SIZE]; + uint8_t tampered[SECURE_STORAGE_ITS_TRANSFORM_MAX_STORED_DATA_SIZE]; + uint8_t out[128]; + size_t stored_len = 0; + size_t out_len = 0; + psa_storage_create_flags_t flags = 0; + + zassert_equal(secure_storage_its_transform_to_store(uid, sizeof(secret), + secret, PSA_STORAGE_FLAG_NONE, stored, &stored_len), + PSA_SUCCESS, "encrypt for store"); + + /* Confidentiality: the plaintext must not appear in the stored blob. */ + zassert_false(contains_bytes(stored, stored_len, secret, sizeof(secret)), + "plaintext leaked into stored data"); + + /* Correct round-trip. */ + zassert_equal(secure_storage_its_transform_from_store(uid, stored_len, + stored, sizeof(out), out, &out_len, &flags), + PSA_SUCCESS, "decrypt from store"); + zassert_equal(out_len, sizeof(secret), "recovered length"); + zassert_mem_equal(out, secret, sizeof(secret), "recovered data"); + + /* Tamper one byte -> authentication fails. */ + memcpy(tampered, stored, stored_len); + tampered[stored_len - 1] ^= 0x01; + zassert_equal(secure_storage_its_transform_from_store(uid, stored_len, + tampered, sizeof(out), out, &out_len, &flags), + PSA_ERROR_INVALID_SIGNATURE, "tamper detected"); + + /* A different UID cannot decrypt (UID is salt + AAD). */ + zassert_equal(secure_storage_its_transform_from_store(other, stored_len, + stored, sizeof(out), out, &out_len, &flags), + PSA_ERROR_INVALID_SIGNATURE, "uid binding enforced"); +} + +/* A stored blob shorter than the minimum framing (flags + nonce + tag = 29 + * bytes) must be rejected as corrupt before any crypto runs. */ +ZTEST(wolfpsa_transform, test_from_store_short_input_is_corrupt) +{ + const secure_storage_its_uid_t uid = { + .uid = 0x1234, .caller_id = SECURE_STORAGE_ITS_CALLER_PSA_ITS + }; + uint8_t stored[SECURE_STORAGE_ITS_TRANSFORM_MAX_STORED_DATA_SIZE] = {0}; + uint8_t out[128]; + size_t out_len = 0; + psa_storage_create_flags_t flags = 0; + + /* 10 < 1 (flags) + 12 (nonce) + 16 (tag). */ + zassert_equal(secure_storage_its_transform_from_store(uid, 10, + stored, sizeof(out), out, &out_len, &flags), + PSA_ERROR_DATA_CORRUPT, "short input rejected as corrupt"); +} + +/* A destination buffer smaller than the recovered plaintext must be rejected + * with BUFFER_TOO_SMALL, and no bytes written past its end. */ +ZTEST(wolfpsa_transform, test_from_store_small_output_buffer) +{ + const secure_storage_its_uid_t uid = { + .uid = 0x1234, .caller_id = SECURE_STORAGE_ITS_CALLER_PSA_ITS + }; + const uint8_t secret[] = "top secret persistent key material"; + uint8_t stored[SECURE_STORAGE_ITS_TRANSFORM_MAX_STORED_DATA_SIZE]; + /* out_area is one byte larger than the too-small window we pass in; the + * trailing byte is a canary that must survive (no OOB write). */ + uint8_t out_area[sizeof(secret)]; + const size_t small = sizeof(secret) - 1; + size_t stored_len = 0; + size_t out_len = 0; + psa_storage_create_flags_t flags = 0; + + memset(out_area, 0xAA, sizeof(out_area)); + + zassert_equal(secure_storage_its_transform_to_store(uid, sizeof(secret), + secret, PSA_STORAGE_FLAG_NONE, stored, &stored_len), + PSA_SUCCESS, "encrypt for store"); + + zassert_equal(secure_storage_its_transform_from_store(uid, stored_len, + stored, small, out_area, &out_len, &flags), + PSA_ERROR_BUFFER_TOO_SMALL, "small output buffer rejected"); + zassert_equal(out_area[small], 0xAA, "no write past the output buffer"); +} + +ZTEST_SUITE(wolfpsa_transform, NULL, NULL, NULL, NULL, NULL); diff --git a/zephyr/tests/psa_transform/testcase.yaml b/zephyr/tests/psa_transform/testcase.yaml new file mode 100644 index 0000000..b7af5d5 --- /dev/null +++ b/zephyr/tests/psa_transform/testcase.yaml @@ -0,0 +1,18 @@ +# Copyright (C) 2026 wolfSSL Inc. +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Unit-tests wolfPSA's wolfCrypt AES-GCM secure_storage ITS transform directly: +# confidentiality at rest, tamper detection, and UID binding -- behavior Zephyr's +# own passthrough custom-transform test cannot cover. The generic persistent-key +# / ITS flows are covered by the psa_its, psa_persistent_key and +# psa_secure_storage wrappers (which run Zephyr's own apps against wolfPSA). +common: + filter: not CONFIG_BUILD_WITH_TFM + tags: + - wolfpsa + - psa + - secure_storage +tests: + crypto.wolfpsa.psa_transform: + integration_platforms: + - native_sim/native/64 diff --git a/zephyr/user_settings_example.h b/zephyr/user_settings_example.h new file mode 100644 index 0000000..ff0e089 --- /dev/null +++ b/zephyr/user_settings_example.h @@ -0,0 +1,176 @@ +/* user_settings_example.h + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * EXAMPLE broad wolfCrypt feature configuration for use with wolfPSA. + * + * wolfPSA does NOT apply this file automatically. wolfPSA follows whatever + * wolfCrypt configuration the wolfSSL module was built with; this header is just + * a ready-made, broad example you can opt into by pointing + * CONFIG_WOLFSSL_SETTINGS_FILE at it (the wolfPSA module root is on the include + * path, so this zephyr/-relative name resolves): + * + * CONFIG_WOLFSSL_SETTINGS_FILE="zephyr/user_settings_example.h" + * + * The wolfPSA samples/tests use it so their PSA surface stays broad. For a real + * application, point CONFIG_WOLFSSL_SETTINGS_FILE at your own user_settings.h + * (or configure wolfCrypt via the module's Kconfig) and wolfPSA will expose + * exactly what you enabled. + * + * This is FEATURE config only: the structural profile wolfPSA needs (the + * Hash-DRBG + seed callback, and single-threaded on a no-threads kernel) is + * applied via generic wolfSSL Kconfig knobs that the wolfPSA Kconfig selects, + * and WOLFSSL_PSA_ENGINE is defined only for wolfPSA's own translation units -- + * none of that lives here. WOLFCRYPT_ONLY is NOT part of that profile: wolfPSA + * coexists with the wolfSSL TLS layer, so building crypto-only is an optional + * lean-image choice (WOLFSSL_CRYPTO_ONLY), never a wolfPSA requirement. + */ + +#ifndef USER_SETTINGS_WOLFPSA_EXAMPLE_H +#define USER_SETTINGS_WOLFPSA_EXAMPLE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------------------------------------------------------------- */ +/* Platform / build model */ +/* ------------------------------------------------------------------------- */ +#define WOLFSSL_GENERAL_ALIGNMENT 4 /* 32-bit alignment on uint32_t */ +#define SIZEOF_LONG_LONG 8 +#define WOLFSSL_IGNORE_FILE_WARN +#define NO_FILESYSTEM /* wolfPSA uses the pluggable store, not files */ +#define NO_WRITEV + +/* Structural profile. wolfPSA no longer injects these via Kconfig, so a + * settings file must set them itself; this makes the example a self-contained + * crypto-only config. (For TLS + wolfPSA coexistence, use the module-default + * Kconfig config instead of this file.) */ +#define WOLFCRYPT_ONLY +#define HAVE_HASHDRBG /* wolfPSA requires the Hash-DRBG; the wolfSSL + * module's wc_GenerateSeed() seeds it from the + * HW entropy driver when one is present */ +#ifndef CONFIG_MULTITHREADING + #define SINGLE_THREADED /* no threading layer available */ +#endif + +/* ------------------------------------------------------------------------- */ +/* Math (Single Precision, all sizes) */ +/* ------------------------------------------------------------------------- */ +#define WOLFSSL_SP_MATH_ALL +#define WOLFSSL_HAVE_SP_RSA +#define WOLFSSL_HAVE_SP_ECC +#define WOLFSSL_SP_1024 +#define WOLFSSL_SP_384 +#define HAVE_SP_ECC + +/* ------------------------------------------------------------------------- */ +/* RSA */ +/* ------------------------------------------------------------------------- */ +#define RSA_MIN_SIZE 1024 +#define WOLFSSL_KEY_GEN +#define WC_RSA_BLINDING +#define WC_RSA_PSS +#define WOLFSSL_PSS_SALT_LEN_DISCOVER +#define WOLFSSL_RSA_OAEP + +/* Side-channel hardening for private-key ops. */ +#define TFM_TIMING_RESISTANT +#define ECC_TIMING_RESISTANT + +/* ------------------------------------------------------------------------- */ +/* Hashes */ +/* ------------------------------------------------------------------------- */ +#define WOLFSSL_MD5 +#define WOLFSSL_RIPEMD +#define WOLFSSL_SHA224 +#define WOLFSSL_SHA256 +#define WOLFSSL_SHA384 +#define WOLFSSL_SHA512 +#define WOLFSSL_SHA3 +#define WOLFSSL_SHAKE128 +#define WOLFSSL_SHAKE256 +#undef NO_MD5 +#undef NO_DES3 + +/* ------------------------------------------------------------------------- */ +/* ECC / EdDSA / Montgomery */ +/* ------------------------------------------------------------------------- */ +#define HAVE_ECC +#define HAVE_ECC384 +#define HAVE_ECC_KEY_EXPORT +#define HAVE_ECC_KEY_IMPORT +#define WOLFSSL_ECDSA_DETERMINISTIC_K +#define HAVE_CURVE25519 +#define HAVE_ED25519 +#define WOLFSSL_ED25519_STREAMING_VERIFY +#define HAVE_CURVE448 +#define HAVE_ED448 +#define WOLFSSL_ED448_STREAMING_VERIFY + +/* ------------------------------------------------------------------------- */ +/* Symmetric / AEAD / MAC */ +/* ------------------------------------------------------------------------- */ +#define WOLFSSL_DES3 +#define WOLFSSL_DES_ECB +#define HAVE_AESGCM +#define HAVE_AESCCM +#define HAVE_AES_ECB +#define WOLFSSL_AES_COUNTER +#define WOLFSSL_AES_CFB +#define WOLFSSL_AES_OFB +#define WOLFSSL_AES_DIRECT /* required by AES key wrap */ +#define HAVE_AES_KEYWRAP +#define WOLFSSL_CMAC +#define HAVE_CHACHA +#define HAVE_XCHACHA +#define HAVE_POLY1305 +#define HAVE_ONE_TIME_AUTH + +/* ------------------------------------------------------------------------- */ +/* KDFs */ +/* ------------------------------------------------------------------------- */ +#define WOLFSSL_HAVE_PRF +#define HAVE_HKDF +#define HAVE_PBKDF2 +#define HAVE_CMAC_KDF + +/* ------------------------------------------------------------------------- */ +/* PQC / stateful-hash */ +/* ------------------------------------------------------------------------- */ +#define WOLFSSL_HAVE_MLDSA +#define WOLFSSL_HAVE_MLKEM +#define WOLFSSL_HAVE_LMS +#define WOLFSSL_LMS_VERIFY_ONLY +#define WOLFSSL_HAVE_XMSS +#define WOLFSSL_XMSS_VERIFY_ONLY + +/* ------------------------------------------------------------------------- */ +/* Experimental (Ascon requires the opt-in) */ +/* ------------------------------------------------------------------------- */ +#define WOLFSSL_EXPERIMENTAL_SETTINGS +#define HAVE_ASCON + +#ifdef __cplusplus +} +#endif + +#endif /* USER_SETTINGS_WOLFPSA_EXAMPLE_H */ diff --git a/zephyr/zephyr_init.c b/zephyr/zephyr_init.c new file mode 100644 index 0000000..19a0396 --- /dev/null +++ b/zephyr/zephyr_init.c @@ -0,0 +1,80 @@ +/* zephyr_init.c - wolfPSA PSA Crypto provider boot initialisation + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfPSA. + * + * wolfPSA 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. + * + * wolfPSA 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* + * The in-tree mbedtls module only calls psa_crypto_init() when its own + * CONFIG_MBEDTLS_PSA_CRYPTO_CLIENT is set, so a custom provider must register + * its own SYS_INIT. psa_crypto_init() runs at POST_KERNEL / + * CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, matching the mbedtls module so PSA is + * ready by the time downstream PSA consumers initialise. + * + * When a CSPRNG is available (CONFIG_CSPRNG_ENABLED) the DRBG seed callback is + * registered in a SEPARATE, earlier PRE_KERNEL_2 init. Registration only stores + * a function pointer (no kernel or device dependency) and the callback is not + * invoked until the first RNG use, so doing it in PRE_KERNEL_2 guarantees it is + * in place before ANY POST_KERNEL consumer's first PSA RNG operation, avoiding + * a link-order race with same-priority consumers. + */ + +#include +#include +#include + +#include + +#include +#include "psa_lock.h" + +/* wolfPSA needs wolfCrypt's Hash-DRBG for its RNG. Fail the build with a clear + * message if the chosen wolfCrypt config (the module-default Kconfig, or a + * user-supplied CONFIG_WOLFSSL_SETTINGS_FILE) did not enable it -- rather than + * silently injecting it via Kconfig on top of the user's settings file. */ +#if !defined(HAVE_HASHDRBG) +#error "wolfPSA requires HAVE_HASHDRBG: enable it in your wolfCrypt config " \ + "(the module Kconfig default enables it; a custom " \ + "CONFIG_WOLFSSL_SETTINGS_FILE must define it itself)." +#endif +LOG_MODULE_REGISTER(wolfpsa, CONFIG_WOLFPSA_LOG_LEVEL); + +/* The global key-store mutex (WOLFPSA_THREAD_SAFE) is created inside + * psa_crypto_init() -- the platform-neutral PSA init point -- so it needs no + * Zephyr-specific bootstrap. The POST_KERNEL wolfpsa_init() below runs + * psa_crypto_init() at boot, single-threaded, before any consumer's first PSA + * call, so the lock is ready without an init-time race. */ + +/* wolfCrypt's DRBG seeding (from the hardware entropy driver when present, else + * the default) is owned by the wolfSSL module's zephyr_init.c, so wolfPSA no + * longer registers a seed callback of its own -- it just uses wc_InitRng(). */ + +static int wolfpsa_init(void) +{ + psa_status_t status = psa_crypto_init(); + + if (status != PSA_SUCCESS) { + LOG_ERR("psa_crypto_init() failed: %d", (int)status); + return -EIO; + } + + LOG_DBG("wolfPSA PSA Crypto provider initialised"); + return 0; +} + +SYS_INIT(wolfpsa_init, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);