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