diff --git a/.github/workflows/spdm-emu-pqc-test.yml b/.github/workflows/spdm-emu-pqc-test.yml new file mode 100644 index 0000000..2c50dd2 --- /dev/null +++ b/.github/workflows/spdm-emu-pqc-test.yml @@ -0,0 +1,195 @@ +name: SPDM Emulator PQC (ML-DSA) Test + +# Post-quantum interop, mirroring the classical SPDM Emulator Integration Test +# matrix (ubuntu 22.04/24.04 x64 + 24.04 aarch64, each with static and dynamic +# memory). The wc_MlDsaKey context API wolfSPDM verifies with lands +# post-v5.9.1-stable, so this job pins wolfSSL master. libspdm's ML-DSA is only +# in its OpenSSL backend (the mbedtls backend stubs it out), so spdm-emu is +# built with CRYPTO=openssl. + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + repository_dispatch: + types: [nightly-trigger] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + pqc-emu-test: + name: ${{ matrix.os }} (${{ matrix.arch }}) / dynamic-mem=${{ matrix.dynamic-mem }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + arch: x64 + dynamic-mem: 'no' + - os: ubuntu-22.04 + arch: x64 + dynamic-mem: 'yes' + - os: ubuntu-24.04 + arch: x64 + dynamic-mem: 'no' + - os: ubuntu-24.04 + arch: x64 + dynamic-mem: 'yes' + - os: ubuntu-24.04-arm + arch: aarch64 + dynamic-mem: 'no' + - os: ubuntu-24.04-arm + arch: aarch64 + dynamic-mem: 'yes' + runs-on: ${{ matrix.os }} + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool cmake + + - name: Compute cache period + id: cache-period + run: echo "biweekly=$(( $(date +%s) / 1296000 ))" >> $GITHUB_OUTPUT + + # --- wolfSSL master with ML-DSA (rebuilt to track upstream drift) --- + - name: Build wolfSSL master (--enable-mldsa) + run: | + cd ~ + git clone --depth 1 --branch master https://github.com/wolfSSL/wolfssl.git + cd wolfssl + ./autogen.sh + ./configure --enable-ecc --enable-sha384 --enable-aesgcm \ + --enable-hkdf --enable-sp --enable-mldsa \ + --prefix=$HOME/wolfssl-install + make -j"$(nproc)" + make install + grep LIBWOLFSSL_VERSION_STRING $HOME/wolfssl-install/include/wolfssl/version.h + + # --- wolfSPDM with ML-DSA asserted on, static or dynamic memory --- + - name: Build and install wolfSPDM (--enable-mldsa) + run: | + ./autogen.sh + ./configure --with-wolfssl=$HOME/wolfssl-install \ + --prefix=$HOME/wolfspdm-install --enable-mldsa \ + ${{ matrix.dynamic-mem == 'yes' && '--enable-dynamic-mem' || '' }} + make -j"$(nproc)" + make install + + - name: Run unit tests (includes ML-DSA verify) + run: make check + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/.libs:${{ github.workspace }}/src/.libs:${{ env.HOME }}/wolfssl-install/lib + + # --- spdm-emu with OpenSSL backend (ML-DSA), cached per OS/arch --- + # Cache the whole build tree, not just build/bin: the OpenSSL-backed + # responder depends on the bundled OpenSSL libraries/providers built + # elsewhere under build/, so a build/bin-only cache restores a binary + # that fails mid-handshake. The full tree makes a restored build behave + # identically to a fresh one. + - name: Cache spdm-emu (openssl) + id: cache-spdm-emu + uses: actions/cache@v4 + with: + path: ~/spdm-emu/build + key: spdm-emu-pqc-openssl-v3-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.cache-period.outputs.biweekly }} + + - name: Build spdm-emu (CRYPTO=openssl) + if: steps.cache-spdm-emu.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 --recurse-submodules https://github.com/DMTF/spdm-emu.git + cd spdm-emu + mkdir build && cd build + cmake -DARCH=${{ matrix.arch }} -DTOOLCHAIN=GCC -DTARGET=Release -DCRYPTO=openssl .. + make copy_sample_key + make -j"$(nproc)" spdm_responder_emu + + # --- ML-DSA interop: responder offers only ML-DSA, requester verifies. + # spdm-emu DataTransferSize is 0x1200 (4608 B): ML-DSA-44 (sig 2420) + # and ML-DSA-65 (3309) responses fit one message, so they complete + # without chunking. ML-DSA-87 (sig 4627) exceeds it and the responder + # chunks the response, which needs the SPDM 1.2 chunking engine + # (follow-on work) -- so 87 is covered by unit tests / cert parsing, + # not this over-the-wire job. + - name: ML-DSA 44/65 session + measurements + challenge + run: | + set -e + export LD_LIBRARY_PATH=$HOME/wolfspdm-install/lib:$HOME/wolfssl-install/lib + export SPDM_EMU_PATH=$HOME/spdm-emu/build/bin + DEMO=./examples/spdm_demo + + # Wait until the responder is accepting connections on port 2323. + wait_for_port() { + local i + for i in $(seq 1 50); do + if ss -ltn 2>/dev/null | grep -q ':2323 '; then return 0; fi + sleep 0.2 + done + return 1 + } + + # $1 ML_DSA_xx (responder), $2 cert dir, $3 demo label, then demo args. + # The OpenSSL-backed ML-DSA responder is slower to become ready than + # the mbedtls/ECDSA one, and it stops after a failed connection, so + # retry the whole case (restarting the responder) a few times. + run_case() { + local alg="$1" certdir="$2" label="$3"; shift 3 + export SPDM_EMU_CERT_DIR="$certdir" + echo "::group::$alg $label" + local attempt rc=1 emu + for attempt in 1 2 3; do + ( cd "$SPDM_EMU_PATH" && ./spdm_responder_emu --ver 1.4 \ + --hash SHA_384 --asym NONE --pqc_asym "$alg" \ + --dhe SECP_384_R1 --aead AES_256_GCM \ + >/tmp/pqc_emu_${alg}_${label}.log 2>&1 ) & + emu=$! + if wait_for_port; then + sleep 1 + if "$DEMO" "$@" --ver 1.4 --debug; then rc=0; else rc=$?; fi + else + rc=1 + fi + kill $emu 2>/dev/null || true + wait $emu 2>/dev/null || true + [ $rc -eq 0 ] && break + echo "--- responder log (attempt $attempt) ---" + cat /tmp/pqc_emu_${alg}_${label}.log || true + echo "attempt $attempt for $alg $label failed (rc=$rc), retrying" + sleep 1 + done + echo "::endgroup::" + if [ $rc -ne 0 ]; then + echo "::error::$alg $label failed after retries (rc=$rc)" + exit $rc + fi + echo "$alg $label: OK" + } + + for spec in "ML_DSA_44 mldsa44" "ML_DSA_65 mldsa65"; do + set -- $spec + alg="$1"; dir="$2" + run_case "$alg" "$dir" session --emu + run_case "$alg" "$dir" meas --meas + run_case "$alg" "$dir" challenge --challenge + done + + - name: Upload logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: spdm-emu-pqc-logs-${{ matrix.os }}-${{ matrix.arch }}-dynmem-${{ matrix.dynamic-mem }} + path: | + config.log + test/*.log + /tmp/pqc_emu_*.log + retention-days: 5 diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml new file mode 100644 index 0000000..fa9ce90 --- /dev/null +++ b/.github/workflows/wiki-sync.yml @@ -0,0 +1,24 @@ +name: Sync docs to wiki + +on: + push: + branches: [master, main] + paths: ['docs/**'] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: wiki-sync + cancel-in-progress: true + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Publish docs/ to wiki + uses: Andrew-Chen-Wang/github-wiki-action@50650fccf3a10f741995523cf9708c53cec8912a + with: + path: docs/ diff --git a/.github/workflows/wolfssl-versions.yml b/.github/workflows/wolfssl-versions.yml index 8224db8..3e0010c 100644 --- a/.github/workflows/wolfssl-versions.yml +++ b/.github/workflows/wolfssl-versions.yml @@ -42,11 +42,20 @@ jobs: fi echo "Latest stable wolfSSL: $LATEST" echo "latest-stable=$LATEST" >> "$GITHUB_OUTPUT" - MATRIX=$(jq -nc --arg latest "$LATEST" '{ + # ML-DSA (the wc_MlDsaKey context API wolfSPDM verifies with) lands + # post-v5.9.1-stable. Enable it for master and any latest-stable + # strictly newer than v5.9.1-stable; v5.8.0 floor stays classical. + PQC_LATEST=false + NEWEST=$(printf '%s\n%s\n' "v5.9.1-stable" "$LATEST" | sort -V | tail -n1) + if [ "$NEWEST" = "$LATEST" ] && [ "$LATEST" != "v5.9.1-stable" ]; then + PQC_LATEST=true + fi + echo "PQC for latest-stable: $PQC_LATEST" + MATRIX=$(jq -nc --arg latest "$LATEST" --argjson pqclatest "$PQC_LATEST" '{ include: [ - {"wolfssl-version":"v5.8.0-stable","wolfssl-ref":"v5.8.0-stable","cache-key":"wolfssl-spdm-v5.8.0-v1"}, - {"wolfssl-version":$latest,"wolfssl-ref":$latest,"cache-key":("wolfssl-spdm-" + $latest + "-v1")}, - {"wolfssl-version":"master","wolfssl-ref":"master","cache-key":""} + {"wolfssl-version":"v5.8.0-stable","wolfssl-ref":"v5.8.0-stable","cache-key":"wolfssl-spdm-v5.8.0-v2","pqc":false}, + {"wolfssl-version":$latest,"wolfssl-ref":$latest,"cache-key":("wolfssl-spdm-" + $latest + "-pqc" + ($pqclatest|tostring) + "-v2"),"pqc":$pqclatest}, + {"wolfssl-version":"master","wolfssl-ref":"master","cache-key":"","pqc":true} ] }') echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" @@ -84,9 +93,11 @@ jobs: git clone --depth 1 --branch ${{ matrix.wolfssl-ref }} \ https://github.com/wolfSSL/wolfssl.git cd wolfssl + PQC_FLAGS="" + if [ "${{ matrix.pqc }}" = "true" ]; then PQC_FLAGS="--enable-mldsa"; fi ./autogen.sh ./configure --enable-wolftpm --enable-ecc --enable-sha384 \ - --enable-aesgcm --enable-hkdf --enable-sp \ + --enable-aesgcm --enable-hkdf --enable-sp $PQC_FLAGS \ --prefix=$HOME/wolfssl-install make -j"$(nproc)" make install @@ -98,8 +109,13 @@ jobs: - name: Build wolfSPDM run: | + # On PQC-capable wolfSSL, pass --enable-mldsa so configure ERRORS if + # the wc_MlDsaKey API is somehow missing, instead of silently + # dropping ML-DSA coverage. + MLDSA_FLAG="" + if [ "${{ matrix.pqc }}" = "true" ]; then MLDSA_FLAG="--enable-mldsa"; fi ./autogen.sh - ./configure --with-wolfssl=$HOME/wolfssl-install + ./configure --with-wolfssl=$HOME/wolfssl-install $MLDSA_FLAG make -j"$(nproc)" - name: Run unit tests @@ -109,9 +125,11 @@ jobs: - name: Build with --enable-dynamic-mem run: | + MLDSA_FLAG="" + if [ "${{ matrix.pqc }}" = "true" ]; then MLDSA_FLAG="--enable-mldsa"; fi make distclean || true ./autogen.sh - ./configure --with-wolfssl=$HOME/wolfssl-install --enable-dynamic-mem + ./configure --with-wolfssl=$HOME/wolfssl-install --enable-dynamic-mem $MLDSA_FLAG make -j"$(nproc)" - name: Run unit tests (dynamic-mem) diff --git a/README.md b/README.md index 6e60108..0033322 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ wolfSPDM is a lightweight C library implementing [SPDM 1.2 / 1.3 / 1.4](https:// - **Standard SPDM 1.2 / 1.3 / 1.4 requester** per DMTF DSP0274 and DSP0277 - **Algorithm Set B fixed:** ECDSA P-384, ECDHE P-384, SHA-384, AES-256-GCM, HKDF-SHA384 +- **Post-quantum signatures (SPDM 1.4):** optional ML-DSA-44 / 65 / 87 (FIPS 204), dual-stacked with ECDSA P-384 — see the [Post-Quantum ML-DSA](https://github.com/aidangarske/wolfSPDM/wiki/Post-Quantum-ML-DSA) wiki page - **Zero-malloc by default:** static memory, ~32 KB context, ideal for constrained/embedded environments - **Optional `--enable-dynamic-mem`** for heap-allocated contexts on small-stack platforms - **Full session lifecycle:** key exchange, finish, encrypted messaging, heartbeat keep-alive, key update @@ -42,6 +43,8 @@ sudo ldconfig `--enable-sp` enables Single Precision math with optimized ECC P-384, required for SPDM Algorithm Set B on ARM64 and other constrained targets. `--enable-all` works as a superset. +For post-quantum ML-DSA signatures, add `--enable-mldsa` and use wolfSSL master (or a release that ships the `wc_MlDsaKey` context API). wolfSPDM then auto-enables ML-DSA; `./configure --disable-mldsa` forces it off. + ## Build ```bash diff --git a/config.h.in b/config.h.in index c20a396..61c7c1b 100644 --- a/config.h.in +++ b/config.h.in @@ -6,7 +6,7 @@ /* Define to 1 if you have the header file. */ #undef HAVE_INTTYPES_H -/* Define to 1 if you have the `wolfssl' library (-lwolfssl). */ +/* Define to 1 if you have the 'wolfssl' library (-lwolfssl). */ #undef HAVE_LIBWOLFSSL /* Define to 1 if you have the header file. */ @@ -57,7 +57,7 @@ /* Define to the version of this package. */ #undef PACKAGE_VERSION -/* Define to 1 if all of the C90 standard headers exist (not just the ones +/* Define to 1 if all of the C89 standard headers exist (not just the ones required in a freestanding environment). This macro is provided for backward compatibility; new code need not use it. */ #undef STDC_HEADERS @@ -71,6 +71,9 @@ /* Enable dynamic memory allocation */ #undef WOLFSPDM_DYNAMIC_MEMORY +/* Disable ML-DSA support */ +#undef WOLFSPDM_NO_MLDSA + /* Define for Solaris 2.5.1 so the uint32_t typedef from , , or is not used. If the typedef were allowed, the #define below would cause a syntax error. */ @@ -86,7 +89,7 @@ #define below would cause a syntax error. */ #undef _UINT8_T -/* Define to `unsigned int' if does not define. */ +/* Define as 'unsigned int' if doesn't define. */ #undef size_t /* Define to the type of an unsigned integer type of width exactly 16 bits if diff --git a/configure.ac b/configure.ac index 621a559..75bab2a 100644 --- a/configure.ac +++ b/configure.ac @@ -80,6 +80,60 @@ if test "x$enable_dynamic_mem" = "xyes"; then AC_DEFINE([WOLFSPDM_DYNAMIC_MEMORY], [1], [Enable dynamic memory allocation]) fi +# ML-DSA (FIPS 204) post-quantum signatures (DSP0274 1.4). Default: auto-follow +# the linked wolfSSL - on when it reports WOLFSSL_HAVE_MLDSA, off otherwise. +AC_ARG_ENABLE([mldsa], + [AS_HELP_STRING([--disable-mldsa], [Disable ML-DSA support even if wolfSSL has it])], + [enable_mldsa=$enableval], + [enable_mldsa=auto]) + +AC_MSG_CHECKING([whether wolfSSL provides ML-DSA]) +AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + #include + #ifndef WOLFSSL_HAVE_MLDSA + #error "no mldsa" + #endif + int main(void) { return 0; } +]])], +[have_wolfssl_mldsa=yes], +[have_wolfssl_mldsa=no]) +AC_MSG_RESULT([$have_wolfssl_mldsa]) + +# wolfSPDM uses the context-based wc_MlDsaKey_* verification API (FIPS 204 +# ML-DSA.Verify with a context). That API lands post-v5.9.1-stable; older +# wolfSSL releases ship only the legacy ML-DSA interface and report the same +# LIBWOLFSSL_VERSION_HEX as wolfSSL master, so we capability-test for the API +# instead of gating on a version number. +have_mldsa_api=no +if test "x$enable_mldsa" != "xno" && test "x$have_wolfssl_mldsa" = "xyes"; then + AC_MSG_CHECKING([for the wc_MlDsaKey context API]) + AC_LINK_IFELSE([AC_LANG_PROGRAM([[ + #include + #include + #include + ]], [[ + MlDsaKey k; int res = 0; word32 idx = 0; + (void)wc_MlDsaKey_VerifyCtx(&k, 0, 0, 0, 0, 0, 0, &res); + (void)wc_MlDsaKey_PublicKeyDecode(&k, 0, 0, &idx); + ]])], + [have_mldsa_api=yes], + [have_mldsa_api=no]) + AC_MSG_RESULT([$have_mldsa_api]) +fi + +if test "x$enable_mldsa" = "xno"; then + AC_DEFINE([WOLFSPDM_NO_MLDSA], [1], [Disable ML-DSA support]) + mldsa_status=disabled +elif test "x$have_mldsa_api" = "xyes"; then + mldsa_status=enabled +elif test "x$enable_mldsa" = "xyes"; then + AC_MSG_ERROR([--enable-mldsa requires a wolfSSL with the wc_MlDsaKey context API (wolfSSL master or a release that ships it, built with --enable-mldsa). Use --disable-mldsa or update wolfSSL.]) +else + AC_DEFINE([WOLFSPDM_NO_MLDSA], [1], [Disable ML-DSA support]) + mldsa_status=disabled +fi + # Output files AC_CONFIG_FILES([Makefile wolfspdm.pc]) AC_OUTPUT @@ -89,5 +143,6 @@ echo "wolfSPDM configuration summary:" echo " Version: $PACKAGE_VERSION" echo " Debug: $enable_debug" echo " Dynamic mem: $enable_dynamic_mem" +echo " ML-DSA: $mldsa_status" echo " wolfSSL: ${WOLFSSL_DIR:-system}" echo "" diff --git a/docs/API-Reference.md b/docs/API-Reference.md new file mode 100644 index 0000000..85f2c11 --- /dev/null +++ b/docs/API-Reference.md @@ -0,0 +1,78 @@ +# API Reference + +Public APIs are declared in `wolfspdm/spdm.h`. + +All APIs return `WOLFSPDM_SUCCESS` (`0`) on success unless documented otherwise; failures are negative error codes from `wolfspdm/spdm_error.h`. + +## Context and lifecycle + +- `wolfSPDM_Init` +- `wolfSPDM_InitStatic` +- `wolfSPDM_GetCtxSize` +- `wolfSPDM_Free` +- `wolfSPDM_New` *(only when built with `WOLFSPDM_DYNAMIC_MEMORY`)* + +## Configuration + +- `wolfSPDM_SetIO` +- `wolfSPDM_SetMaxVersion` +- `wolfSPDM_SetRequesterSessionId` +- `wolfSPDM_AllowUntrustedCerts` +- `wolfSPDM_SetTrustedCAs` +- `wolfSPDM_SetDebug` + +## Session establishment and state + +- `wolfSPDM_Connect` +- `wolfSPDM_IsConnected` +- `wolfSPDM_Disconnect` +- `wolfSPDM_GetSessionId` +- `wolfSPDM_GetNegotiatedVersion` +- `wolfSPDM_GetVersion_Negotiated` *(legacy compatibility symbol)* +- `wolfSPDM_GetLastPeerError` + +## Fine-grained handshake + +- `wolfSPDM_GetVersion` +- `wolfSPDM_GetCapabilities` +- `wolfSPDM_NegotiateAlgorithms` +- `wolfSPDM_GetDigests` +- `wolfSPDM_GetCertificate` +- `wolfSPDM_KeyExchange` +- `wolfSPDM_Finish` + +## Secured messaging + +- `wolfSPDM_SecuredExchange` +- `wolfSPDM_SendData` *(not in `WOLFSPDM_LEAN`)* +- `wolfSPDM_ReceiveData` *(not in `WOLFSPDM_LEAN`)* +- `wolfSPDM_EncryptMessage` *(not in `WOLFSPDM_LEAN`)* +- `wolfSPDM_DecryptMessage` *(not in `WOLFSPDM_LEAN`)* + +## Attestation + +- `wolfSPDM_GetMeasurements` *(not with `NO_WOLFSPDM_MEAS`)* +- `wolfSPDM_GetMeasurementCount` *(not with `NO_WOLFSPDM_MEAS`)* +- `wolfSPDM_GetMeasurementBlock` *(not with `NO_WOLFSPDM_MEAS`)* +- `wolfSPDM_Challenge` *(not with `NO_WOLFSPDM_CHALLENGE`)* + +## Session maintenance + +- `wolfSPDM_Heartbeat` +- `wolfSPDM_KeyUpdate` + +## Error utilities + +- `wolfSPDM_GetErrorString` + +## Common error codes + +Examples: +- `WOLFSPDM_E_INVALID_ARG` +- `WOLFSPDM_E_BAD_STATE` +- `WOLFSPDM_E_NOT_CONNECTED` +- `WOLFSPDM_E_IO_FAIL` +- `WOLFSPDM_E_PEER_ERROR` +- `WOLFSPDM_E_MEAS_SIG_FAIL` +- `WOLFSPDM_E_CHALLENGE` +- `WOLFSPDM_E_KEY_UPDATE` diff --git a/docs/Attestation-Notes.md b/docs/Attestation-Notes.md new file mode 100644 index 0000000..28b079c --- /dev/null +++ b/docs/Attestation-Notes.md @@ -0,0 +1,61 @@ +# Attestation Notes + +wolfSPDM supports SPDM attestation through both measurement retrieval and challenge authentication. + +## Measurement attestation (`GET_MEASUREMENTS`) + +Primary API: + +```c +int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, + int requestSignature); +``` + +Behavior: +- `requestSignature=1`: requests signed measurements; verifies signature when verification support is compiled in +- `requestSignature=0`: retrieves unsigned measurements (informational) + +Signature verification (measurements and `CHALLENGE_AUTH`) uses whichever +asymmetric algorithm was negotiated — ECDSA P-384 or, on SPDM 1.4 with ML-DSA +built in, ML-DSA-44/65/87. See [[Post-Quantum ML-DSA]]. + +Result access: +- `wolfSPDM_GetMeasurementCount` +- `wolfSPDM_GetMeasurementBlock` + +Relevant return codes: +- `WOLFSPDM_SUCCESS` +- `WOLFSPDM_E_MEAS_NOT_VERIFIED` +- `WOLFSPDM_E_MEAS_SIG_FAIL` +- `WOLFSPDM_E_MEASUREMENT` + +## Sessionless challenge attestation (`CHALLENGE_AUTH`) + +Primary API: + +```c +int wolfSPDM_Challenge(WOLFSPDM_CTX* ctx, int slotId, byte measHashType); +``` + +Typical prerequisite state: +- Version/capabilities/algorithms negotiated +- Digest and cert chain retrieved + +## Trust anchor handling + +Load trusted CA material with: + +```c +int wolfSPDM_SetTrustedCAs(WOLFSPDM_CTX* ctx, const byte* derCerts, + word32 derCertsSz); +``` + +`wolfSPDM_SetTrustedCAs` currently accepts a single DER certificate buffer for root-hash matching. + +## Feature toggles + +- `NO_WOLFSPDM_MEAS` disables measurements +- `NO_WOLFSPDM_MEAS_VERIFY` disables measurement signature verification +- `NO_WOLFSPDM_CHALLENGE` disables challenge API + +For deeper measurement details and test examples, see `docs/ATTESTATION.md`. diff --git a/docs/Configuration-and-Macros.md b/docs/Configuration-and-Macros.md new file mode 100644 index 0000000..e410c54 --- /dev/null +++ b/docs/Configuration-and-Macros.md @@ -0,0 +1,58 @@ +# Configuration and Macros + +## Configure-time options + +From `configure.ac`: + +| Option | Default | Effect | +|--------|---------|--------| +| `--with-wolfssl=PATH` | system paths | Adds wolfSSL include/library search paths | +| `--enable-debug` | off | Defines `WOLFSPDM_DEBUG`, builds with `-g -O0` | +| `--enable-dynamic-mem` | off | Defines `WOLFSPDM_DYNAMIC_MEMORY` and enables `wolfSPDM_New` | +| `--disable-mldsa` | auto | Force ML-DSA off (default follows wolfSSL — see [[Post-Quantum ML-DSA]]) | + +## Public feature macros + +Defined in `wolfspdm/spdm.h` depending on build flags: + +- `WOLFSPDM_HAS_MEASUREMENTS` *(not defined if `NO_WOLFSPDM_MEAS`)* +- `WOLFSPDM_HAS_CHALLENGE` *(not defined if `NO_WOLFSPDM_CHALLENGE`)* +- `WOLFSPDM_HAS_HEARTBEAT` +- `WOLFSPDM_HAS_KEY_UPDATE` +- `WOLFSPDM_HAVE_MLDSA` *(defined when ML-DSA is built in; follows wolfSSL's `WOLFSSL_HAVE_MLDSA`, suppress with `WOLFSPDM_NO_MLDSA`)* + +## Size and protocol constants + +From `wolfspdm/spdm.h` and `wolfspdm/spdm_types.h`. The buffer/context defaults +grow when ML-DSA is built in so ML-DSA-65 payloads fit a single message +(all three buffer caps are overridable with `-D`): + +| Constant | Classical | With ML-DSA | +|----------|-----------|-------------| +| `WOLFSPDM_CTX_STATIC_SIZE` | `32768` | `73728` | +| `WOLFSPDM_MAX_MSG_SIZE` | `4096` | `8192` | +| `WOLFSPDM_MAX_CERT_CHAIN` | `4096` | `24576` | +| `WOLFSPDM_MAX_TRANSCRIPT` | `4096` | `16384` | + +Version constants: +- `SPDM_VERSION_12`, `SPDM_VERSION_13`, `SPDM_VERSION_14` + +Measurement constants (when enabled): +- `SPDM_MEAS_OPERATION_ALL` +- `SPDM_MEAS_SUMMARY_HASH_NONE`, `_TCB`, `_ALL` + +## Common compile-time feature toggles + +These are used in source-level conditional compilation: + +| Macro | Effect | +|-------|--------| +| `NO_WOLFSPDM_MEAS` | Removes measurement APIs and related fields/code | +| `NO_WOLFSPDM_MEAS_VERIFY` | Keeps retrieval path but disables measurement signature verification | +| `NO_WOLFSPDM_CHALLENGE` | Removes challenge-attestation API/code | +| `WOLFSPDM_LEAN` | Excludes selected convenience secured-message helpers | + +## Notes + +- `wolfspdm/options.h` is auto-generated from `config.h` during build/install. +- API availability should be detected using feature macros rather than hard-coded assumptions. diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md new file mode 100644 index 0000000..20e0322 --- /dev/null +++ b/docs/Getting-Started.md @@ -0,0 +1,87 @@ +# Getting Started + +## Prerequisites + +wolfSPDM depends on wolfSSL/wolfCrypt with SPDM-required algorithms enabled. + +Minimum validated wolfSSL version: **v5.8.0-stable**. + +Example wolfSSL build: + +```bash +git clone https://github.com/wolfSSL/wolfssl.git +cd wolfssl +./autogen.sh +./configure --enable-wolftpm --enable-ecc --enable-sha384 \ + --enable-aesgcm --enable-hkdf --enable-sp +make +sudo make install +sudo ldconfig +``` + +For optional post-quantum ML-DSA support, add `--enable-mldsa` and use +wolfSSL master (or a release that ships the `wc_MlDsaKey` context API). See +[[Post-Quantum ML-DSA]]. + +## Build wolfSPDM + +```bash +./autogen.sh +./configure --with-wolfssl=/usr/local +make +make check +``` + +### Configure options + +| Option | Description | +|--------|-------------| +| `--with-wolfssl=PATH` | Path to wolfSSL headers/libs | +| `--enable-debug` | Enables debug build flags and `WOLFSPDM_DEBUG` | +| `--enable-dynamic-mem` | Enables heap-allocated context APIs (`wolfSPDM_New`) | +| `--disable-mldsa` | Forces ML-DSA off (default auto-follows wolfSSL) | + +## Memory modes + +### Static mode (default) + +Zero-malloc operation with caller-managed context memory: + +```c +byte spdmBuf[WOLFSPDM_CTX_STATIC_SIZE]; +WOLFSPDM_CTX* ctx = (WOLFSPDM_CTX*)spdmBuf; +wolfSPDM_InitStatic(ctx, sizeof(spdmBuf)); +``` + +`WOLFSPDM_CTX_STATIC_SIZE` is 32768 bytes. + +### Dynamic mode (optional) + +Enable with `--enable-dynamic-mem`, then: + +```c +WOLFSPDM_CTX* ctx = wolfSPDM_New(); +``` + +## Minimal connection flow + +1. Initialize context (`wolfSPDM_Init` or `wolfSPDM_InitStatic`) +2. Register transport callback with `wolfSPDM_SetIO` +3. Optionally set trust root with `wolfSPDM_SetTrustedCAs` +4. Establish session with `wolfSPDM_Connect` +5. Exchange secured data using `wolfSPDM_SecuredExchange` (or send/receive helpers) +6. End session with `wolfSPDM_Disconnect` +7. Cleanup via `wolfSPDM_Free` + +## Transport callback contract + +All transport is caller-owned through: + +```c +typedef int (*WOLFSPDM_IO_CB)(WOLFSPDM_CTX* ctx, + const byte* txBuf, word32 txSz, + byte* rxBuf, word32* rxSz, + void* userCtx); +``` + +The callback sends raw SPDM/SPDM-secured records and returns the responder message. diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 0000000..96d9b07 --- /dev/null +++ b/docs/Home.md @@ -0,0 +1,59 @@ +# wolfSPDM Documentation + +Welcome to the wolfSPDM wiki. This documentation covers wolfSPDM, a lightweight requester-only SPDM implementation for embedded systems and constrained environments. + +## What is wolfSPDM? + +wolfSPDM is a C library implementing: +- **SPDM 1.2 / 1.3 / 1.4** ([DMTF DSP0274](https://www.dmtf.org/sites/default/files/standards/documents/DSP0274_1.4.0.pdf)) +- **Secured Messages over MCTP** ([DMTF DSP0277](https://www.dmtf.org/sites/default/files/standards/documents/DSP0277_1.2.0.pdf)) + +It uses [wolfSSL / wolfCrypt](https://www.wolfssl.com/) as its crypto backend and is tested end-to-end against the DMTF [spdm-emu](https://github.com/DMTF/spdm-emu) responder emulator. + +## Key Features + +| Feature | Description | +|---------|-------------| +| Requester-only SPDM stack | Purpose-built initiator implementation | +| SPDM 1.2/1.3/1.4 | Standards-based negotiation and session setup | +| Fixed Algorithm Set B | ECDSA P-384, ECDHE P-384, SHA-384, AES-256-GCM, HKDF-SHA384 | +| Post-quantum signatures (1.4) | Optional ML-DSA-44/65/87 (FIPS 204), dual-stacked with ECDSA P-384 | +| Zero-malloc by default | Static context (`WOLFSPDM_CTX_STATIC_SIZE`, 32 KB; ~72 KB with ML-DSA) | +| Optional dynamic context | `--enable-dynamic-mem` enables `wolfSPDM_New()` | +| Attestation operations | Signed/unsigned `GET_MEASUREMENTS`, sessionless `CHALLENGE_AUTH` | +| Session operations | `HEARTBEAT`, `KEY_UPDATE`, secured app data transfer | +| CI + security coverage | Multi-compiler, static analysis, CodeQL, Valgrind, spdm-emu integration | + +## Documentation + +| Page | Description | +|------|-------------| +| [[Getting Started]] | Dependencies, build, install, and first connection flow | +| [[Supported Operations]] | Supported SPDM flows and operation/API mapping | +| [[Post-Quantum ML-DSA]] | SPDM 1.4 ML-DSA (FIPS 204) post-quantum signatures | +| [[API Reference]] | Public API grouped by lifecycle and purpose | +| [[Configuration and Macros]] | Configure flags and compile-time feature controls | +| [[Testing and CI]] | Unit tests, emulator tests, and CI workflow coverage | +| [[Project Structure]] | Repository layout and module responsibilities | +| [[Attestation Notes]] | Measurement and challenge attestation details | + +## Protocol Session Flow + +The primary session establishment sequence is: + +`GET_VERSION -> GET_CAPABILITIES -> NEGOTIATE_ALGORITHMS -> GET_DIGESTS -> GET_CERTIFICATE -> KEY_EXCHANGE -> FINISH` + +After `FINISH`, secured messaging and maintenance operations are available. + +## Quick Links + +- [GitHub Repository](https://github.com/aidangarske/wolfSPDM) +- [README](https://github.com/aidangarske/wolfSPDM/blob/main/README.md) +- [DMTF DSP0274 (SPDM)](https://www.dmtf.org/sites/default/files/standards/documents/DSP0274_1.4.0.pdf) +- [DMTF DSP0277 (Secured Messages)](https://www.dmtf.org/sites/default/files/standards/documents/DSP0277_1.2.0.pdf) +- [wolfSSL Website](https://www.wolfssl.com/) + +## License + +wolfSPDM is free software licensed under GPLv3. +For commercial licensing and support, contact [wolfSSL](https://www.wolfssl.com/contact/). diff --git a/docs/Post-Quantum-ML-DSA.md b/docs/Post-Quantum-ML-DSA.md new file mode 100644 index 0000000..048dca6 --- /dev/null +++ b/docs/Post-Quantum-ML-DSA.md @@ -0,0 +1,84 @@ +# Post-Quantum ML-DSA + +SPDM 1.4 (DMTF DSP0274 1.4.0) adds post-quantum cryptography: **ML-DSA** +(FIPS 204) for signatures and **ML-KEM** (FIPS 203) for key exchange. wolfSPDM +implements the requester side of **ML-DSA signature verification**, dual-stacked +alongside the classical ECDSA P-384 profile. + +ML-KEM hybrid key exchange and SPDM 1.2 message chunking (for the largest PQC +payloads) are tracked as follow-on work. + +## How negotiation works + +In `NEGOTIATE_ALGORITHMS`, wolfSPDM advertises ECDSA P-384 in `BaseAsymAlgo` +**and** ML-DSA-44 / ML-DSA-65 / ML-DSA-87 in the SPDM 1.4 `PqcAsymAlgo` field +(8-byte field at offset 16). Per DSP0274 1.4, the responder selects exactly one +signature algorithm across `BaseAsymSel` and `PqcAsymSel` (offset 20 in +`ALGORITHMS`). wolfSPDM records the choice, pins the certificate's parameter set +to it, and verifies `KEY_EXCHANGE_RSP`, `CHALLENGE_AUTH`, and signed +`MEASUREMENTS` with whichever family was negotiated. + +| PqcAsymSel bit | Algorithm | SigLen | Public key | +|----------------|-----------|--------|------------| +| `0x01` | ML-DSA-44 | 2420 B | 1312 B | +| `0x02` | ML-DSA-65 | 3309 B | 1952 B | +| `0x04` | ML-DSA-87 | 4627 B | 2592 B | + +## Signing construction (DSP0274 1.4 §15.5) + +ML-DSA uses **Algorithm 2 (pure `ML-DSA.Sign`)**, not the pre-hash variant: + +- `M = combined_spdm_prefix || message_hash` + - `combined_spdm_prefix` = `"dmtf-spdm-v1.4.*"` ×4, zero-pad, `spdm_context` + (100 bytes) + - `message_hash` = SHA-384 of the data to be signed (the negotiated + `BaseHashSel`) +- ML-DSA `ctx` parameter = `spdm_context` (the `"responder- signing"` + string) + +wolfSPDM verifies with `wc_MlDsaKey_VerifyCtx(key, sig, sigLen, ctx, ctxLen, M, +mLen, &res)`. Public keys are imported from the leaf certificate with +`wc_MlDsaKey_PublicKeyDecode`, pinned to the negotiated parameter set. + +## Building + +ML-DSA follows the linked wolfSSL automatically: it is enabled when wolfSSL +reports `WOLFSSL_HAVE_MLDSA` and provides the `wc_MlDsaKey` context API (wolfSSL +master or a release that ships it; build wolfSSL with `--enable-mldsa`). The +capability is detected at configure time — wolfSPDM does not gate on a wolfSSL +version number, since master and the matching stable can report the same +`LIBWOLFSSL_VERSION_HEX`. + +```sh +# wolfSSL with ML-DSA +./configure --enable-ecc --enable-sha384 --enable-aesgcm --enable-hkdf \ + --enable-sp --enable-mldsa --prefix=$HOME/wolfssl-install +make && make install + +# wolfSPDM (ML-DSA auto-enabled; --enable-mldsa asserts it, --disable-mldsa off) +./configure --with-wolfssl=$HOME/wolfssl-install +make && make check +``` + +The configure summary prints `ML-DSA: enabled|disabled`. + +## Memory note + +PQC signatures, public keys, and certificate chains are multi-kilobyte, so the +buffer and static-context sizes grow when ML-DSA is built in (see +[[Configuration and Macros]]). The signature-bearing responses also use larger +on-stack receive buffers in ML-DSA builds — `wolfSPDM_GetMeasurements` uses +`WOLFSPDM_MAX_MSG_SIZE` (8 KB) and `wolfSPDM_KeyExchange` / +`wolfSPDM_Challenge` use `WOLFSPDM_SIG_RSP_BUF` (~5.3 KB) — so size embedded +thread/task stacks accordingly. ML-DSA-44 and ML-DSA-65 responses fit a single +SPDM message at the common DataTransferSize (spdm-emu uses 4608 B), so they +complete over the wire today. ML-DSA-87 responses (sig 4627 B) exceed that and +the responder chunks them, which needs the SPDM 1.2 chunking engine (follow-on +work) — ML-DSA-87 negotiation, certificate parsing, and signature verification +are exercised by the unit tests in the meantime. + +## References + +- DMTF DSP0274 1.4.0 — SPDM Specification (§15 SPDMsign, §15.5 ML-DSA, Tables 19/20) +- NIST FIPS 204 — ML-DSA; FIPS 203 — ML-KEM +- wolfSSL `wc_mldsa.h` — `wc_MlDsaKey_*` API diff --git a/docs/Project-Structure.md b/docs/Project-Structure.md new file mode 100644 index 0000000..59f1e00 --- /dev/null +++ b/docs/Project-Structure.md @@ -0,0 +1,44 @@ +# Project Structure + +## Top-level layout + +| Path | Purpose | +|------|---------| +| `src/` | Core protocol, crypto glue, transcript, secured messaging, and session logic | +| `wolfspdm/` | Public headers (`spdm.h`, `spdm_types.h`, `spdm_error.h`) | +| `examples/` | Demo client and emulator integration script | +| `test/` | Unit tests and emulator smoke test | +| `docs/` | Project documentation (including attestation notes) | +| `.github/workflows/` | CI workflows | + +## Core source modules + +| File | Responsibility | +|------|----------------| +| `src/spdm_context.c` | Context init/free lifecycle and state setup | +| `src/spdm_msg.c` | SPDM message construction and parsing | +| `src/spdm_crypto.c` | Cryptographic helper operations | +| `src/spdm_kdf.c` | HKDF-based key derivation | +| `src/spdm_transcript.c` | Transcript management (TH computations) | +| `src/spdm_secured.c` | Secured message protection (AES-256-GCM) | +| `src/spdm_session.c` | Handshake/session flow and higher-level operations | +| `src/spdm_internal.h` | Internal types, constants, and internal APIs | + +## Public API surface + +| Header | Content | +|--------|---------| +| `wolfspdm/spdm.h` | Main public API and feature macros | +| `wolfspdm/spdm_types.h` | SPDM protocol constants and algorithm identifiers | +| `wolfspdm/spdm_error.h` | Error code enum and error-string helper | + +## Build/test assets + +| File | Purpose | +|------|---------| +| `configure.ac` | Autotools configure logic and options | +| `Makefile.am` | Library, test, and example build targets | +| `examples/spdm_demo.c` | CLI demo for session/measurement/challenge/heartbeat/key update | +| `examples/spdm_test.sh` | 18-case emulator integration driver | +| `test/unit_test.c` | Unit test coverage | +| `test/test_spdm.c` | SPDM smoke-test utility | diff --git a/docs/Supported-Operations.md b/docs/Supported-Operations.md new file mode 100644 index 0000000..c04df7a --- /dev/null +++ b/docs/Supported-Operations.md @@ -0,0 +1,53 @@ +# Supported Operations + +wolfSPDM implements requester-side SPDM operations for session establishment, secure data exchange, attestation, and session maintenance. + +## Operation coverage + +| Operation | SPDM area | wolfSPDM API | +|----------|-----------|--------------| +| Version negotiation | GET_VERSION | `wolfSPDM_GetVersion` | +| Capability negotiation | GET_CAPABILITIES | `wolfSPDM_GetCapabilities` | +| Algorithm negotiation | NEGOTIATE_ALGORITHMS | `wolfSPDM_NegotiateAlgorithms` | +| Certificate digest retrieval | GET_DIGESTS | `wolfSPDM_GetDigests` | +| Certificate chain retrieval | GET_CERTIFICATE | `wolfSPDM_GetCertificate` | +| Session key exchange | KEY_EXCHANGE | `wolfSPDM_KeyExchange` | +| Session finalization | FINISH | `wolfSPDM_Finish` | +| One-shot full connect | Full handshake | `wolfSPDM_Connect` | +| Secured app exchange | Secured messages | `wolfSPDM_SecuredExchange` | +| App send/receive helpers | Secured messages | `wolfSPDM_SendData`, `wolfSPDM_ReceiveData` | +| Measurements (signed/unsigned) | GET_MEASUREMENTS | `wolfSPDM_GetMeasurements` | +| Measurement block access | Measurement parsing | `wolfSPDM_GetMeasurementCount`, `wolfSPDM_GetMeasurementBlock` | +| Sessionless challenge auth | CHALLENGE / CHALLENGE_AUTH | `wolfSPDM_Challenge` | +| Keep-alive | HEARTBEAT | `wolfSPDM_Heartbeat` | +| Session key rotation | KEY_UPDATE | `wolfSPDM_KeyUpdate` | + +## Supported protocol versions + +- SPDM 1.2 (`0x12`) +- SPDM 1.3 (`0x13`) +- SPDM 1.4 (`0x14`) + +Maximum negotiated version can be capped with `wolfSPDM_SetMaxVersion`. + +## Fixed cryptographic profile (Algorithm Set B) + +- Hash: SHA-384 +- Asymmetric signature: ECDSA P-384 +- DHE: secp384r1 +- AEAD: AES-256-GCM +- Key schedule: SPDM key schedule + HKDF-SHA384 + +## Post-quantum signatures (SPDM 1.4, optional) + +When built against a wolfSSL with ML-DSA (FIPS 204), wolfSPDM additionally +advertises **ML-DSA-44 / ML-DSA-65 / ML-DSA-87** in the SPDM 1.4 `PqcAsymAlgo` +field (dual-stack alongside ECDSA P-384). The responder selects exactly one +signature algorithm; wolfSPDM verifies whichever was negotiated. See +[[Post-Quantum ML-DSA]]. + +## Notable implementation scope + +- Requester-only implementation (no responder role) +- Designed for standards-based SPDM peers and DMTF spdm-emu +- Trust anchor support via `wolfSPDM_SetTrustedCAs` (single DER CA cert buffer) diff --git a/docs/Testing-and-CI.md b/docs/Testing-and-CI.md new file mode 100644 index 0000000..cbc92fa --- /dev/null +++ b/docs/Testing-and-CI.md @@ -0,0 +1,63 @@ +# Testing and CI + +## Local tests + +### Unit tests + +```bash +make check +./test/unit_test +``` + +### Integration test with DMTF spdm-emu + +```bash +export SPDM_EMU_PATH=../spdm-emu/build/bin +./examples/spdm_test.sh +``` + +`spdm_test.sh` runs 18 scenarios: +- Session +- Signed measurements +- Unsigned measurements +- Challenge +- Heartbeat +- Key update + +Across SPDM versions 1.2, 1.3, and 1.4. + +## CI workflow coverage + +Documented workflows include: + +- Build and Test (OS/config matrix) +- Multiple Compilers (GCC 11-13, Clang 14-17) +- Compiler Warnings (`-Werror`, pedantic/conversion/shadow checks) +- Static Analysis (cppcheck + scan-build) +- Memory Check (Valgrind) +- Empty Brace Scope Scan +- CodeQL Security +- Codespell +- SPDM Emulator Test (integration matrix on x64 + aarch64) +- SPDM Emulator PQC (ML-DSA) Test — wolfSSL master + spdm-emu (OpenSSL backend), + ML-DSA-44/65 session/measurements/challenge over the wire + +See `.github/workflows/README.md` for workflow inventory details. + +## ML-DSA (post-quantum) test coverage + +- **Unit (`make check`, ML-DSA build):** PqcAsymAlgo/PqcAsymSel wire offsets and + the Base/Pqc mutual-exclusion; a real wolfSSL ML-DSA sign + verify round-trip + through `wolfSPDM_VerifyMeasurementSig` for ML-DSA-44/65/87 (with a tamper + negative); and KEY_EXCHANGE_RSP / CHALLENGE_AUTH signature-size guards. +- **Real-certificate validation:** `wolfSPDM_ExtractResponderPubKey` was + validated against the actual spdm-emu ML-DSA cert chains for all three levels + (44 -> WC_ML_DSA_44, 65 -> 65, 87 -> 87), plus a negative case where a + level-65 cert is rejected when ML-DSA-87 was negotiated (level pinning). +- **Over-the-wire (CI):** ML-DSA-44 and ML-DSA-65 complete against spdm-emu. + ML-DSA-87 responses exceed the 4608 B DataTransferSize and need the SPDM 1.2 + chunking engine (follow-on), so 87 is covered at the unit/cert level. + +## Validation caveat + +Building/tests require a compatible wolfSSL installation and may fail if `--with-wolfssl` is not provided or wolfSSL is absent from default search paths. diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md new file mode 100644 index 0000000..2a41cca --- /dev/null +++ b/docs/_Sidebar.md @@ -0,0 +1,11 @@ +### Documentation + +- [[Home]] +- [[Getting Started]] +- [[Supported Operations]] +- [[Post-Quantum ML-DSA]] +- [[API Reference]] +- [[Configuration and Macros]] +- [[Testing and CI]] +- [[Project Structure]] +- [[Attestation Notes]] diff --git a/examples/spdm_demo.c b/examples/spdm_demo.c index c71b70f..c02bde1 100644 --- a/examples/spdm_demo.c +++ b/examples/spdm_demo.c @@ -236,11 +236,12 @@ static void usage(const char* argv0) { fprintf(stderr, "Usage: %s {--emu|--meas|--challenge|--heartbeat|--key-update}\n" - " [--no-sig] [--ver 1.2|1.3|1.4]\n" + " [--no-sig] [--ver 1.2|1.3|1.4] [--debug]\n" "\n" "Env:\n" - " SPDM_EMU_PATH path to spdm-emu build/bin/ (used for trusted CA\n" - " lookup in --challenge mode)\n", + " SPDM_EMU_PATH path to spdm-emu build/bin/ (used for trusted CA\n" + " lookup in --challenge mode)\n" + " SPDM_EMU_CERT_DIR cert subdir (ecp384 default, mldsa65, ...)\n", argv0); } @@ -287,13 +288,23 @@ static int sanitize_emu_path(const char* emuPath, char* outReal, size_t outSz) static int load_trusted_ca(WOLFSPDM_CTX* ctx) { + /* Cert subdir matches the responder's selected algorithm. Only a fixed set + * of spdm-emu directory names is accepted; the env value is mapped to the + * matching string literal so no caller-controlled data reaches the fopen() + * path below (avoids path traversal). */ + static const char* const allowedCertDirs[] = { + "ecp256", "ecp384", "ecp521", "mldsa44", "mldsa65", "mldsa87" + }; const char* emuPath = getenv("SPDM_EMU_PATH"); + const char* certDir = getenv("SPDM_EMU_CERT_DIR"); + const char* safeDir = NULL; char realEmu[PATH_MAX]; char path[PATH_MAX]; byte* der; word32 derSz; int rc; int n; + unsigned int i; if (emuPath == NULL) { fprintf(stderr, "ERROR: SPDM_EMU_PATH not set; cannot locate " @@ -304,7 +315,20 @@ static int load_trusted_ca(WOLFSPDM_CTX* ctx) fprintf(stderr, "ERROR: SPDM_EMU_PATH is not a valid directory path\n"); return -1; } - n = snprintf(path, sizeof(path), "%s/ecp384/ca.cert.der", realEmu); + if (certDir == NULL || certDir[0] == '\0') { + certDir = "ecp384"; /* default: ECDSA P-384 */ + } + for (i = 0; i < sizeof(allowedCertDirs) / sizeof(allowedCertDirs[0]); i++) { + if (strcmp(certDir, allowedCertDirs[i]) == 0) { + safeDir = allowedCertDirs[i]; + break; + } + } + if (safeDir == NULL) { + fprintf(stderr, "ERROR: unsupported SPDM_EMU_CERT_DIR '%s'\n", certDir); + return -1; + } + n = snprintf(path, sizeof(path), "%s/%s/ca.cert.der", realEmu, safeDir); if (n < 0 || (size_t)n >= sizeof(path)) { fprintf(stderr, "ERROR: certificate path too long\n"); return -1; @@ -445,17 +469,19 @@ int main(int argc, char* argv[]) { "heartbeat", no_argument, 0, 'b' }, { "key-update", no_argument, 0, 'k' }, { "ver", required_argument, 0, 'v' }, + { "debug", no_argument, 0, 'd' }, { "help", no_argument, 0, 'h' }, { 0, 0, 0, 0 } }; int mode = 0; int withSig = 1; + int debug = 0; byte maxVer = 0; int opt; int rc; WOLFSPDM_CTX* ctx = (WOLFSPDM_CTX*)g_ctxBuf; - while ((opt = getopt_long(argc, argv, "emncbkv:h", longOpts, NULL)) != -1) { + while ((opt = getopt_long(argc, argv, "emncbkv:hd", longOpts, NULL)) != -1) { switch (opt) { case 'e': mode = MODE_SESSION; break; case 'm': mode = MODE_MEAS; break; @@ -463,6 +489,7 @@ int main(int argc, char* argv[]) case 'c': mode = MODE_CHALLENGE; break; case 'b': mode = MODE_HEARTBEAT; break; case 'k': mode = MODE_KEY_UPDATE; break; + case 'd': debug = 1; break; case 'v': maxVer = parse_version(optarg); if (maxVer == 0) { @@ -499,6 +526,10 @@ int main(int argc, char* argv[]) wolfSPDM_SetIO(ctx, tcp_io_callback, &g_tcpCtx); + if (debug) { + wolfSPDM_SetDebug(ctx, 1); + } + /* Demo runs against the DMTF spdm-emu, which uses self-signed test * certs. Explicitly opt in to operating without a trust anchor so the * default fail-closed behavior doesn't refuse the handshake. Real diff --git a/src/spdm_context.c b/src/spdm_context.c index 4f0d8f3..28f5abe 100644 --- a/src/spdm_context.c +++ b/src/spdm_context.c @@ -138,7 +138,7 @@ void wolfSPDM_Free(WOLFSPDM_CTX* ctx) /* Free responder public key (used for measurement/challenge verification) */ if (ctx->flags.hasResponderPubKey) { - wc_ecc_free(&ctx->responderPubKey); + wolfSPDM_FreeResponderPubKey(ctx); } #ifndef NO_WOLFSPDM_CHALLENGE @@ -210,7 +210,7 @@ int wolfSPDM_SetTrustedCAs(WOLFSPDM_CTX* ctx, const byte* derCerts, return WOLFSPDM_E_INVALID_ARG; } - if (derCertsSz > WOLFSPDM_MAX_CERT_CHAIN) { + if (derCertsSz > WOLFSPDM_MAX_TRUSTED_CA) { return WOLFSPDM_E_BUFFER_SMALL; } @@ -335,7 +335,7 @@ static int wolfSPDM_ConnectStandard(WOLFSPDM_CTX* ctx) * clear sessionId / seqNums so a partial prior attempt can't leak * state into the new handshake. */ if (ctx->flags.hasResponderPubKey) { - wc_ecc_free(&ctx->responderPubKey); + wolfSPDM_FreeResponderPubKey(ctx); ctx->flags.hasResponderPubKey = 0; } /* Wipe derived key material from any prior session before starting a @@ -467,7 +467,7 @@ int wolfSPDM_Disconnect(WOLFSPDM_CTX* ctx) * responder's certificate chain - otherwise KEY_EXCHANGE_RSP signature * verification on the reconnect would run against the old key. */ if (ctx->flags.hasResponderPubKey) { - wc_ecc_free(&ctx->responderPubKey); + wolfSPDM_FreeResponderPubKey(ctx); ctx->flags.hasResponderPubKey = 0; } /* Wipe every long-lived session secret so disconnected contexts cannot diff --git a/src/spdm_internal.h b/src/spdm_internal.h index 38a3172..22526ca 100644 --- a/src/spdm_internal.h +++ b/src/spdm_internal.h @@ -47,6 +47,9 @@ #include #include #include +#ifdef WOLFSPDM_HAVE_MLDSA + #include +#endif #if defined(LIBWOLFSSL_VERSION_HEX) && LIBWOLFSSL_VERSION_HEX < 0x05008004 /* wc_ForceZero added in wolfSSL v5.8.4; provide a stub for older releases. */ @@ -204,8 +207,8 @@ struct WOLFSPDM_CTX { word32 measBlockCount; byte measNonce[32]; /* Nonce for signed measurements */ byte measSummaryHash[WOLFSPDM_HASH_SIZE]; /* Summary hash from response */ - byte measSignature[WOLFSPDM_ECC_SIG_SIZE]; /* Captured signature (96 bytes P-384) */ - word32 measSignatureSize; /* 0 if unsigned, 96 if signed */ + byte measSignature[WOLFSPDM_MAX_SIG_SIZE]; /* Captured signature (ECDSA/ML-DSA) */ + word32 measSignatureSize; /* 0 if unsigned, else SigLen */ #ifndef NO_WOLFSPDM_MEAS_VERIFY /* Saved GET_MEASUREMENTS request for L1/L2 transcript */ @@ -214,11 +217,20 @@ struct WOLFSPDM_CTX { #endif /* !NO_WOLFSPDM_MEAS_VERIFY */ #endif /* !NO_WOLFSPDM_MEAS */ - /* Responder identity for signature verification (measurements + challenge) */ - ecc_key responderPubKey; /* Extracted from cert chain leaf */ + /* Responder identity for signature verification (measurements + challenge). + * asymType records which family the responder selected during + * NEGOTIATE_ALGORITHMS; only one key in the union is ever live. */ + byte asymType; /* WOLFSPDM_ASYM_ECDSA|_MLDSA */ + word32 pqcAsymSel; /* Selected PqcAsymSel (0=ECDSA) */ + union { + ecc_key ecc; /* ECDSA P-384 (Algorithm Set B) */ +#ifdef WOLFSPDM_HAVE_MLDSA + MlDsaKey mldsa; /* ML-DSA (DSP0274 1.4) */ +#endif + } responderPubKey; /* Extracted from cert chain leaf */ /* Certificate chain validation */ - byte trustedCAs[WOLFSPDM_MAX_CERT_CHAIN]; /* DER-encoded root CAs */ + byte trustedCAs[WOLFSPDM_MAX_TRUSTED_CA]; /* DER-encoded root CA */ word32 trustedCAsSz; #ifndef NO_WOLFSPDM_CHALLENGE @@ -243,6 +255,18 @@ struct WOLFSPDM_CTX { byte rspAppSecret[WOLFSPDM_HASH_SIZE]; /* 48 bytes */ }; +/* Free whichever responder verify key is live (union member by asymType). */ +static WC_INLINE void wolfSPDM_FreeResponderPubKey(WOLFSPDM_CTX* ctx) +{ +#ifdef WOLFSPDM_HAVE_MLDSA + if (ctx->asymType == WOLFSPDM_ASYM_MLDSA) { + wc_MlDsaKey_Free(&ctx->responderPubKey.mldsa); + return; + } +#endif + wc_ecc_free(&ctx->responderPubKey.ecc); +} + /* --- Byte-Order Helpers --- */ static WC_INLINE void SPDM_Set16LE(byte* buf, word16 val) { diff --git a/src/spdm_msg.c b/src/spdm_msg.c index 2c3b1a5..396ffe3 100644 --- a/src/spdm_msg.c +++ b/src/spdm_msg.c @@ -79,6 +79,18 @@ int wolfSPDM_BuildNegotiateAlgorithms(WOLFSPDM_CTX* ctx, byte* buf, word32* bufS /* BaseHashAlgo: SHA-384 (bit 1) */ buf[12] = 0x02; buf[13] = 0x00; buf[14] = 0x00; buf[15] = 0x00; +#ifdef WOLFSPDM_HAVE_MLDSA + /* DSP0274 1.4 Table 19: PqcAsymAlgo (8-byte field at offset 16). Advertise + * ML-DSA-44/65/87 alongside ECDSA (dual-stack); the responder selects + * exactly one across BaseAsymAlgo and PqcAsymAlgo. Bytes 16-31 are + * reserved-zero before 1.4, so only emit the selection there. */ + if (ctx->spdmVersion >= SPDM_VERSION_14) { + buf[16] = (byte)(SPDM_PQC_ASYM_ALGO_ML_DSA_44 | + SPDM_PQC_ASYM_ALGO_ML_DSA_65 | + SPDM_PQC_ASYM_ALGO_ML_DSA_87); + } +#endif + /* Struct tables start at offset 32 */ /* DHE: SECP_384_R1 */ buf[32] = 0x02; buf[33] = 0x20; buf[34] = 0x10; buf[35] = 0x00; @@ -205,57 +217,93 @@ int wolfSPDM_BuildKeyExchange(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) /* --- Shared Signing Helpers --- */ -/* Build SPDM 1.2+ signed hash per DSP0274: - * M = combined_spdm_prefix || zero_pad || context_str || inputDigest - * outputDigest = Hash(M) - * - * combined_spdm_prefix = "dmtf-spdm-v1.X.*" x4 = 64 bytes - * zero_pad = (36 - contextStrLen) bytes of 0x00 - * context_str = signing context string (variable length, max 36) */ -static int wolfSPDM_BuildSignedHash(byte spdmVersion, +/* Size of the negotiated signature field (DSP0274 1.4 Table 19 SigLen). */ +static word32 wolfSPDM_GetSigSize(const WOLFSPDM_CTX* ctx) +{ +#ifdef WOLFSPDM_HAVE_MLDSA + if (ctx->asymType == WOLFSPDM_ASYM_MLDSA) { + if (ctx->pqcAsymSel == SPDM_PQC_ASYM_ALGO_ML_DSA_44) { + return WOLFSPDM_MLDSA44_SIG_SIZE; + } + if (ctx->pqcAsymSel == SPDM_PQC_ASYM_ALGO_ML_DSA_87) { + return WOLFSPDM_MLDSA87_SIG_SIZE; + } + return WOLFSPDM_MLDSA65_SIG_SIZE; + } +#else + (void)ctx; +#endif + return WOLFSPDM_ECC_SIG_SIZE; +} + +/* Assemble the SPDM 1.2+ data_to_be_signed message M per DSP0274 Sec. 15: + * M = combined_spdm_prefix || message_hash + * combined_spdm_prefix = "dmtf-spdm-v1.X.*" x4 (64) || zero_pad || spdm_context + * (100 bytes total) + * message_hash = inputDigest (Hash of data_to_be_signed, 48 bytes SHA-384) + * contextStr already carries the "responder-"/"requester-" spdm_context prefix. + * outMsg must hold >= 148 bytes. */ +static int wolfSPDM_BuildSignedMsg(byte spdmVersion, const char* contextStr, word32 contextStrLen, - const byte* inputDigest, byte* outputDigest) + const byte* inputDigest, byte* outMsg, word32* outMsgLen) { - byte signMsg[200]; /* 64 + 36 + 48 = 148 bytes max */ word32 signMsgLen = 0; word32 zeroPadLen; byte majorVer, minorVer; - int i, rc; + int i; /* Reject overlong context strings before computing zeroPadLen (which * is 36 - contextStrLen and would underflow). */ - if (contextStr == NULL || contextStrLen > 36) { + if (contextStr == NULL || contextStrLen > 36 || + outMsg == NULL || outMsgLen == NULL) { return WOLFSPDM_E_INVALID_ARG; } majorVer = (byte)('0' + ((spdmVersion >> 4) & 0xF)); minorVer = (byte)('0' + (spdmVersion & 0xF)); - /* combined_spdm_prefix: "dmtf-spdm-v1.X.*" x4 = 64 bytes */ + /* spdm_prefix: "dmtf-spdm-v1.X.*" x4 = 64 bytes */ for (i = 0; i < 4; i++) { - XMEMCPY(&signMsg[signMsgLen], "dmtf-spdm-v1.2.*", 16); - signMsg[signMsgLen + 11] = majorVer; - signMsg[signMsgLen + 13] = minorVer; - signMsg[signMsgLen + 15] = '*'; + XMEMCPY(&outMsg[signMsgLen], "dmtf-spdm-v1.2.*", 16); + outMsg[signMsgLen + 11] = majorVer; + outMsg[signMsgLen + 13] = minorVer; + outMsg[signMsgLen + 15] = '*'; signMsgLen += 16; } - /* Zero padding: 36 - contextStrLen bytes */ + /* Zero padding: 36 - contextStrLen bytes (combined prefix is 100 bytes) */ zeroPadLen = 36 - contextStrLen; - XMEMSET(&signMsg[signMsgLen], 0x00, zeroPadLen); + XMEMSET(&outMsg[signMsgLen], 0x00, zeroPadLen); signMsgLen += zeroPadLen; - /* Signing context string */ - XMEMCPY(&signMsg[signMsgLen], contextStr, contextStrLen); + /* spdm_context string */ + XMEMCPY(&outMsg[signMsgLen], contextStr, contextStrLen); signMsgLen += contextStrLen; - /* Input digest */ - XMEMCPY(&signMsg[signMsgLen], inputDigest, WOLFSPDM_HASH_SIZE); + /* message_hash */ + XMEMCPY(&outMsg[signMsgLen], inputDigest, WOLFSPDM_HASH_SIZE); signMsgLen += WOLFSPDM_HASH_SIZE; - /* Hash M */ - rc = wolfSPDM_Sha384Hash(outputDigest, signMsg, signMsgLen, - NULL, 0, NULL, 0); + *outMsgLen = signMsgLen; + return WOLFSPDM_SUCCESS; +} + +/* Build the pre-hashed signing input used by RSA/ECDSA: outputDigest = Hash(M). + * ECDSA hashes M internally, so verify_hash takes Hash(M). */ +static int wolfSPDM_BuildSignedHash(byte spdmVersion, + const char* contextStr, word32 contextStrLen, + const byte* inputDigest, byte* outputDigest) +{ + byte signMsg[200]; /* 64 + 36 + 48 = 148 bytes max */ + word32 signMsgLen = 0; + int rc; + + rc = wolfSPDM_BuildSignedMsg(spdmVersion, contextStr, contextStrLen, + inputDigest, signMsg, &signMsgLen); + if (rc == WOLFSPDM_SUCCESS) { + rc = wolfSPDM_Sha384Hash(outputDigest, signMsg, signMsgLen, + NULL, 0, NULL, 0); + } /* signMsg embeds the inputDigest (a transcript-state hash). Wipe it * before returning so the assembled signing input does not linger on * the stack frame. */ @@ -284,7 +332,7 @@ static int wolfSPDM_VerifyEccSig(WOLFSPDM_CTX* ctx, } rc = wc_ecc_verify_hash(derSig, derSigSz, digest, digestSz, - &verified, &ctx->responderPubKey); + &verified, &ctx->responderPubKey.ecc); if (rc != 0) { wolfSPDM_DebugPrint(ctx, "ECC verify_hash failed: %d\n", rc); /* Internal wolfCrypt failure (memory pressure, missing curve, etc.) @@ -297,6 +345,70 @@ static int wolfSPDM_VerifyEccSig(WOLFSPDM_CTX* ctx, return (verified == 1) ? WOLFSPDM_SUCCESS : WOLFSPDM_E_BAD_SIGNATURE; } +/* ECDSA signing tail: hash M, then verify the raw r||s signature. */ +static int wolfSPDM_VerifyEcdsaSigned(WOLFSPDM_CTX* ctx, + const char* contextStr, word32 contextStrLen, + byte* digest, const byte* sig, word32 sigSz) +{ + int rc = wolfSPDM_BuildSignedHash(ctx->spdmVersion, contextStr, + contextStrLen, digest, digest); + if (rc == WOLFSPDM_SUCCESS) { + rc = wolfSPDM_VerifyEccSig(ctx, sig, sigSz, digest, WOLFSPDM_HASH_SIZE); + } + return rc; +} + +#ifdef WOLFSPDM_HAVE_MLDSA +/* Verify an SPDM ML-DSA signature against message_hash using the responder's + * ML-DSA public key. Per DSP0274 1.4 Sec. 15.5, SPDM uses Algorithm 2 (pure + * ML-DSA.Sign), NOT the pre-hash variant: M = combined_spdm_prefix || + * message_hash, and the ML-DSA ctx parameter is spdm_context (contextStr). */ +static int wolfSPDM_VerifyMlDsaSig(WOLFSPDM_CTX* ctx, + const char* contextStr, word32 contextStrLen, + const byte* messageHash, const byte* sig, word32 sigSz) +{ + byte signMsg[200]; /* combined_spdm_prefix(100) + message_hash(48) = 148 */ + word32 signMsgLen = 0; + int verified = 0; + int rc; + + rc = wolfSPDM_BuildSignedMsg(ctx->spdmVersion, contextStr, contextStrLen, + messageHash, signMsg, &signMsgLen); + if (rc == WOLFSPDM_SUCCESS) { + rc = wc_MlDsaKey_VerifyCtx(&ctx->responderPubKey.mldsa, sig, sigSz, + (const byte*)contextStr, (byte)contextStrLen, + signMsg, signMsgLen, &verified); + if (rc != 0) { + wolfSPDM_DebugPrint(ctx, "ML-DSA VerifyCtx failed: %d\n", rc); + rc = WOLFSPDM_E_CRYPTO_FAIL; + } + else { + rc = (verified == 1) ? WOLFSPDM_SUCCESS : WOLFSPDM_E_BAD_SIGNATURE; + } + } + /* signMsg embeds message_hash (transcript-state). Wipe before return. */ + wc_ForceZero(signMsg, sizeof(signMsg)); + return rc; +} +#endif /* WOLFSPDM_HAVE_MLDSA */ + +/* Verify an SPDM signature over message_hash using whichever asymmetric family + * the responder selected in NEGOTIATE_ALGORITHMS. digest holds message_hash; + * the ECDSA path overwrites it with Hash(M). Returns the raw verify rc. */ +static int wolfSPDM_VerifySig(WOLFSPDM_CTX* ctx, + const char* contextStr, word32 contextStrLen, + byte* digest, const byte* sig, word32 sigSz) +{ +#ifdef WOLFSPDM_HAVE_MLDSA + if (ctx->asymType == WOLFSPDM_ASYM_MLDSA) { + return wolfSPDM_VerifyMlDsaSig(ctx, contextStr, contextStrLen, + digest, sig, sigSz); + } +#endif + return wolfSPDM_VerifyEcdsaSigned(ctx, contextStr, contextStrLen, + digest, sig, sigSz); +} + int wolfSPDM_BuildFinish(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) { byte th2Hash[WOLFSPDM_HASH_SIZE]; @@ -471,6 +583,7 @@ int wolfSPDM_ParseAlgorithms(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) { word32 baseAsymAlgo; word32 baseHashAlgo; + word32 pqcAsymSel; word16 declaredLen; byte numAlgs; byte extAsymCount; @@ -523,13 +636,60 @@ int wolfSPDM_ParseAlgorithms(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) baseAsymAlgo = SPDM_Get32LE(&buf[12]); baseHashAlgo = SPDM_Get32LE(&buf[16]); - /* Per DSP0274 Table 18, BaseAsymSel / BaseHashSel carry the responder's - * SELECTED algorithm - exactly one bit. Strict equality enforces - * Algorithm Set B rather than accepting any superset. */ - if (baseAsymAlgo != SPDM_ASYM_ALGO_ECDSA_P384) { + /* Defensive: a second NEGOTIATE_ALGORITHMS via the fine-grained API could + * flip asymType while a responder key from a prior round is still live, + * which would later free the wrong union member. Drop any live key first + * so the union member always matches the asymType set below. */ + if (ctx->flags.hasResponderPubKey) { + wolfSPDM_FreeResponderPubKey(ctx); + ctx->flags.hasResponderPubKey = 0; + } + + /* DSP0274 1.4 Table 20: PqcAsymSel (offset 20). Present from 1.4; earlier + * versions leave these bytes reserved-zero. The spec caps the combined + * bit count of BaseAsymSel and PqcAsymSel at one, so exactly one of the + * two fields carries the selected signature algorithm. */ + pqcAsymSel = 0; + if (ctx->spdmVersion >= SPDM_VERSION_14) { + pqcAsymSel = SPDM_Get32LE(&buf[20]); + } + + if (pqcAsymSel != 0) { +#ifdef WOLFSPDM_HAVE_MLDSA + if (baseAsymAlgo != 0) { + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: BaseAsymSel and PqcAsymSel both set " + "(0x%08x/0x%08x)\n", baseAsymAlgo, pqcAsymSel); + return WOLFSPDM_E_ALGO_MISMATCH; + } + if (pqcAsymSel != SPDM_PQC_ASYM_ALGO_ML_DSA_44 && + pqcAsymSel != SPDM_PQC_ASYM_ALGO_ML_DSA_65 && + pqcAsymSel != SPDM_PQC_ASYM_ALGO_ML_DSA_87) { + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: unsupported PqcAsymSel (0x%08x)\n", pqcAsymSel); + return WOLFSPDM_E_ALGO_MISMATCH; + } + ctx->asymType = WOLFSPDM_ASYM_MLDSA; + ctx->pqcAsymSel = pqcAsymSel; +#else wolfSPDM_DebugPrint(ctx, - "ALGORITHMS: BaseAsymSel != ECDSA_P384 (0x%08x)\n", baseAsymAlgo); + "ALGORITHMS: PqcAsymSel set but ML-DSA not built in (0x%08x)\n", + pqcAsymSel); return WOLFSPDM_E_ALGO_MISMATCH; +#endif + } + else { + /* Per DSP0274 Table 18, BaseAsymSel carries the responder's SELECTED + * algorithm - exactly one bit. Strict equality enforces Algorithm + * Set B rather than accepting any superset. */ + if (baseAsymAlgo != SPDM_ASYM_ALGO_ECDSA_P384) { + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: BaseAsymSel != ECDSA_P384 (0x%08x)\n", + baseAsymAlgo); + return WOLFSPDM_E_ALGO_MISMATCH; + } + ctx->asymType = WOLFSPDM_ASYM_ECDSA; + ctx->pqcAsymSel = 0; } if (baseHashAlgo != SPDM_HASH_ALGO_SHA_384) { wolfSPDM_DebugPrint(ctx, @@ -680,7 +840,7 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS const byte* rspVerifyData; byte expectedHmac[WOLFSPDM_HASH_SIZE]; byte th1Partial[WOLFSPDM_HASH_SIZE]; - byte signedDigest[WOLFSPDM_HASH_SIZE]; + word32 sigSize; int rc; SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 140); @@ -694,6 +854,8 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS return WOLFSPDM_E_BAD_STATE; } + sigSize = wolfSPDM_GetSigSize(ctx); + /* MutAuthRequested (offset 6) per DSP0274 Table 35. We don't implement * the requester-signed FINISH path; refuse before committing sessionId * so a rejected handshake doesn't leak partial session state into ctx. */ @@ -709,7 +871,7 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS sigOffset = 138 + opaqueLen; keRspPartialLen = sigOffset; - if (bufSz < sigOffset + WOLFSPDM_ECC_SIG_SIZE + WOLFSPDM_HASH_SIZE) { + if (bufSz < sigOffset + sigSize + WOLFSPDM_HASH_SIZE) { return WOLFSPDM_E_BUFFER_SMALL; } @@ -722,7 +884,7 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS XMEMCPY(peerPubKeyY, &buf[88], WOLFSPDM_ECC_KEY_SIZE); signature = buf + sigOffset; - rspVerifyData = buf + sigOffset + WOLFSPDM_ECC_SIG_SIZE; + rspVerifyData = buf + sigOffset + sigSize; /* Add KEY_EXCHANGE_RSP partial (without sig/verify) to transcript */ rc = wolfSPDM_TranscriptAdd(ctx, buf, keRspPartialLen); @@ -730,19 +892,15 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS goto cleanup; } - /* Verify responder signature per DSP0274 Sec 14: signature is over - * BuildSignedHash("responder-key_exchange_rsp signing", Hash(partial - * transcript)). wolfSPDM_KeyExchange refuses to proceed without a - * parsed cert chain, so hasResponderPubKey is always true here. */ + /* Verify responder signature per DSP0274 Sec 14: signature is over the + * partial transcript hash with context "responder-key_exchange_rsp + * signing", using the negotiated asym family (ECDSA or ML-DSA). + * wolfSPDM_KeyExchange refuses to proceed without a parsed cert chain, + * so hasResponderPubKey is always true here. */ rc = wolfSPDM_TranscriptHash(ctx, th1Partial); if (rc == WOLFSPDM_SUCCESS) { - rc = wolfSPDM_BuildSignedHash(ctx->spdmVersion, - sigCtx, (word32)(sizeof(sigCtx) - 1), - th1Partial, signedDigest); - } - if (rc == WOLFSPDM_SUCCESS) { - rc = wolfSPDM_VerifyEccSig(ctx, signature, WOLFSPDM_ECC_SIG_SIZE, - signedDigest, WOLFSPDM_HASH_SIZE); + rc = wolfSPDM_VerifySig(ctx, sigCtx, (word32)(sizeof(sigCtx) - 1), + th1Partial, signature, sigSize); if (rc != WOLFSPDM_SUCCESS) { wolfSPDM_DebugPrint(ctx, "KEY_EXCHANGE_RSP signature verification failed (rc=%d)\n", @@ -758,7 +916,7 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS } /* Add signature to transcript (TH1 includes signature) */ - rc = wolfSPDM_TranscriptAdd(ctx, signature, WOLFSPDM_ECC_SIG_SIZE); + rc = wolfSPDM_TranscriptAdd(ctx, signature, sigSize); if (rc != WOLFSPDM_SUCCESS) { goto cleanup; } @@ -806,13 +964,12 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS cleanup: /* expectedHmac is derived from rspFinishedKey; wipe regardless of path. - * th1Partial / signedDigest are intermediate handshake material - wipe - * them too so they do not linger on the stack frame. */ + * th1Partial is intermediate handshake material - wipe it too so it does + * not linger on the stack frame. */ wc_ForceZero(expectedHmac, sizeof(expectedHmac)); wc_ForceZero(peerPubKeyX, sizeof(peerPubKeyX)); wc_ForceZero(peerPubKeyY, sizeof(peerPubKeyY)); wc_ForceZero(th1Partial, sizeof(th1Partial)); - wc_ForceZero(signedDigest, sizeof(signedDigest)); return rc; } @@ -938,6 +1095,7 @@ int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) word32 recordLen; word32 recordEnd; word32 blockIdx; + word32 sigSize; SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 8); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_MEASUREMENTS, WOLFSPDM_E_MEASUREMENT); @@ -1094,10 +1252,12 @@ int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) offset += 8; } - /* Signature (if present) */ - if (offset + WOLFSPDM_ECC_SIG_SIZE <= bufSz) { - XMEMCPY(ctx->measSignature, &buf[offset], WOLFSPDM_ECC_SIG_SIZE); - ctx->measSignatureSize = WOLFSPDM_ECC_SIG_SIZE; + /* Signature (if present). Size is the negotiated SigLen (ECDSA or + * ML-DSA); the stored copy bounds at WOLFSPDM_MAX_SIG_SIZE. */ + sigSize = wolfSPDM_GetSigSize(ctx); + if (offset + sigSize <= bufSz) { + XMEMCPY(ctx->measSignature, &buf[offset], sigSize); + ctx->measSignatureSize = sigSize; } } @@ -1113,15 +1273,13 @@ int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) /* Shared tail: BuildSignedHash -> VerifyEccSig -> debug print -> return */ static int wolfSPDM_VerifySignedDigest(WOLFSPDM_CTX* ctx, const char* contextStr, word32 contextStrLen, - byte* digest, /* in: hash, overwritten by BuildSignedHash */ + byte* digest, /* in: message_hash (ECDSA path overwrites it) */ const byte* sig, word32 sigSz, const char* passMsg, const char* failMsg, int failErr) { - int rc = wolfSPDM_BuildSignedHash(ctx->spdmVersion, - contextStr, contextStrLen, digest, digest); - if (rc != WOLFSPDM_SUCCESS) return rc; + int rc = wolfSPDM_VerifySig(ctx, contextStr, contextStrLen, + digest, sig, sigSz); - rc = wolfSPDM_VerifyEccSig(ctx, sig, sigSz, digest, WOLFSPDM_HASH_SIZE); if (rc == WOLFSPDM_SUCCESS) { wolfSPDM_DebugPrint(ctx, "%s\n", passMsg); return WOLFSPDM_SUCCESS; @@ -1139,6 +1297,7 @@ int wolfSPDM_VerifyMeasurementSig(WOLFSPDM_CTX* ctx, { byte digest[WOLFSPDM_HASH_SIZE]; word32 sigOffset; + word32 sigSize; int rc; if (ctx == NULL || rspBuf == NULL || reqMsg == NULL) { @@ -1149,11 +1308,12 @@ int wolfSPDM_VerifyMeasurementSig(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_MEAS_NOT_VERIFIED; } - /* Signature is the last WOLFSPDM_ECC_SIG_SIZE bytes of the response */ - if (rspBufSz < WOLFSPDM_ECC_SIG_SIZE) { + /* Signature is the last SigLen bytes of the response (ECDSA or ML-DSA) */ + sigSize = wolfSPDM_GetSigSize(ctx); + if (rspBufSz < sigSize) { return WOLFSPDM_E_MEASUREMENT; } - sigOffset = rspBufSz - WOLFSPDM_ECC_SIG_SIZE; + sigOffset = rspBufSz - sigSize; /* Compute L1||L2 hash per DSP0274 Section 10.11.1: * L1/L2 = VCA || GET_MEASUREMENTS_request || MEASUREMENTS_response(before sig) */ @@ -1164,7 +1324,7 @@ int wolfSPDM_VerifyMeasurementSig(WOLFSPDM_CTX* ctx, if (rc == WOLFSPDM_SUCCESS) { rc = wolfSPDM_VerifySignedDigest(ctx, "responder-measurements signing", 30, digest, - rspBuf + sigOffset, WOLFSPDM_ECC_SIG_SIZE, + rspBuf + sigOffset, sigSize, "Measurement signature VERIFIED", "Measurement signature INVALID", WOLFSPDM_E_MEAS_SIG_FAIL); @@ -1249,12 +1409,72 @@ static int wolfSPDM_FindLeafCert(const byte* certChain, word32 certChainLen, return WOLFSPDM_SUCCESS; } +/* Import the ECDSA P-384 public key from the parsed leaf cert. */ +static int wolfSPDM_ImportEccPubKey(WOLFSPDM_CTX* ctx, DecodedCert* cert) +{ + word32 idx = 0; + int rc = wc_ecc_init(&ctx->responderPubKey.ecc); + if (rc != 0) { + return WOLFSPDM_E_CRYPTO_FAIL; + } + rc = wc_EccPublicKeyDecode(cert->publicKey, &idx, + &ctx->responderPubKey.ecc, cert->pubKeySize); + if (rc != 0) { + wolfSPDM_DebugPrint(ctx, "ECC public key decode failed: %d\n", rc); + wc_ecc_free(&ctx->responderPubKey.ecc); + return WOLFSPDM_E_CERT_PARSE; + } + wolfSPDM_DebugPrint(ctx, "Extracted responder ECC P-384 public key\n"); + return WOLFSPDM_SUCCESS; +} + +#ifdef WOLFSPDM_HAVE_MLDSA +/* Import the ML-DSA public key from the parsed leaf cert. The parameter set + * is pinned from the negotiated PqcAsymSel, so a cert whose AlgorithmIdentifier + * OID names a different level is rejected by the decoder. */ +static int wolfSPDM_ImportMlDsaPubKey(WOLFSPDM_CTX* ctx, DecodedCert* cert) +{ + word32 idx = 0; + byte level; + int rc; + + if (ctx->pqcAsymSel == SPDM_PQC_ASYM_ALGO_ML_DSA_44) { + level = WC_ML_DSA_44; + } + else if (ctx->pqcAsymSel == SPDM_PQC_ASYM_ALGO_ML_DSA_87) { + level = WC_ML_DSA_87; + } + else { + level = WC_ML_DSA_65; + } + + rc = wc_MlDsaKey_Init(&ctx->responderPubKey.mldsa, NULL, INVALID_DEVID); + if (rc == 0) { + rc = wc_MlDsaKey_SetParams(&ctx->responderPubKey.mldsa, level); + } + if (rc != 0) { + wc_MlDsaKey_Free(&ctx->responderPubKey.mldsa); + return WOLFSPDM_E_CRYPTO_FAIL; + } + + rc = wc_MlDsaKey_PublicKeyDecode(&ctx->responderPubKey.mldsa, + cert->publicKey, cert->pubKeySize, &idx); + if (rc != 0) { + wolfSPDM_DebugPrint(ctx, "ML-DSA public key decode failed: %d\n", rc); + wc_MlDsaKey_Free(&ctx->responderPubKey.mldsa); + return WOLFSPDM_E_CERT_PARSE; + } + wolfSPDM_DebugPrint(ctx, "Extracted responder ML-DSA public key (level %u)\n", + level); + return WOLFSPDM_SUCCESS; +} +#endif /* WOLFSPDM_HAVE_MLDSA */ + int wolfSPDM_ExtractResponderPubKey(WOLFSPDM_CTX* ctx) { DecodedCert cert; const byte* leafCert; word32 leafCertSz; - word32 idx; int rc; if (ctx == NULL || ctx->certChainLen == 0) { @@ -1278,27 +1498,24 @@ int wolfSPDM_ExtractResponderPubKey(WOLFSPDM_CTX* ctx) return WOLFSPDM_E_CERT_PARSE; } - /* Extract public key from cert and import into ecc_key */ - rc = wc_ecc_init(&ctx->responderPubKey); - if (rc != 0) { - wc_FreeDecodedCert(&cert); - return WOLFSPDM_E_CRYPTO_FAIL; + /* Import the responder verify key for whichever family was negotiated. */ +#ifdef WOLFSPDM_HAVE_MLDSA + if (ctx->asymType == WOLFSPDM_ASYM_MLDSA) { + rc = wolfSPDM_ImportMlDsaPubKey(ctx, &cert); } - - idx = 0; - rc = wc_EccPublicKeyDecode(cert.publicKey, &idx, &ctx->responderPubKey, - cert.pubKeySize); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "ECC public key decode failed: %d\n", rc); - wc_ecc_free(&ctx->responderPubKey); - wc_FreeDecodedCert(&cert); - return WOLFSPDM_E_CERT_PARSE; + else { + rc = wolfSPDM_ImportEccPubKey(ctx, &cert); } +#else + rc = wolfSPDM_ImportEccPubKey(ctx, &cert); +#endif wc_FreeDecodedCert(&cert); - ctx->flags.hasResponderPubKey = 1; - wolfSPDM_DebugPrint(ctx, "Extracted responder ECC P-384 public key\n"); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } + ctx->flags.hasResponderPubKey = 1; return WOLFSPDM_SUCCESS; } @@ -1499,8 +1716,8 @@ int wolfSPDM_ParseChallengeAuth(WOLFSPDM_CTX* ctx, const byte* buf, offset += 8; } - /* Signature starts here */ - if (offset + WOLFSPDM_ECC_SIG_SIZE > bufSz) { + /* Signature starts here (ECDSA or ML-DSA SigLen) */ + if (offset + wolfSPDM_GetSigSize(ctx) > bufSz) { wolfSPDM_DebugPrint(ctx, "CHALLENGE_AUTH: no room for signature\n"); return WOLFSPDM_E_CHALLENGE; } @@ -1559,7 +1776,7 @@ int wolfSPDM_VerifyChallengeAuthSig(WOLFSPDM_CTX* ctx, rc = wolfSPDM_VerifySignedDigest(ctx, "responder-challenge_auth signing", 32, digest, - rspBuf + sigOffset, WOLFSPDM_ECC_SIG_SIZE, + rspBuf + sigOffset, wolfSPDM_GetSigSize(ctx), "CHALLENGE_AUTH signature VERIFIED", "CHALLENGE_AUTH signature INVALID", WOLFSPDM_E_CHALLENGE); diff --git a/src/spdm_session.c b/src/spdm_session.c index 082f2a8..c6ba0b8 100644 --- a/src/spdm_session.c +++ b/src/spdm_session.c @@ -275,7 +275,7 @@ int wolfSPDM_GetCertificate(WOLFSPDM_CTX* ctx, int slotId) int wolfSPDM_KeyExchange(WOLFSPDM_CTX* ctx) { byte txBuf[192]; /* KEY_EXCHANGE: ~158 bytes */ - byte rxBuf[384]; /* KEY_EXCHANGE_RSP: ~302 bytes */ + byte rxBuf[WOLFSPDM_SIG_RSP_BUF]; /* KEY_EXCHANGE_RSP (ECDSA ~302 / ML-DSA) */ word32 txSz = sizeof(txBuf); word32 rxSz = sizeof(rxBuf); int rc; @@ -520,7 +520,7 @@ int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, int wolfSPDM_Challenge(WOLFSPDM_CTX* ctx, int slotId, byte measHashType) { byte txBuf[48]; /* CHALLENGE: 36 bytes (1.2) or 44 bytes (1.3+) */ - byte rxBuf[512]; /* CHALLENGE_AUTH: variable, up to ~300+ bytes */ + byte rxBuf[WOLFSPDM_SIG_RSP_BUF]; /* CHALLENGE_AUTH (ECDSA ~300 / ML-DSA) */ word32 txSz = sizeof(txBuf); word32 rxSz = sizeof(rxBuf); word32 sigOffset = 0; diff --git a/test/unit_test.c b/test/unit_test.c index 3652767..32352bd 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -399,6 +399,264 @@ static int test_parse_algorithms_set_b_enforcement(void) TEST_PASS(); } +#ifdef WOLFSPDM_HAVE_MLDSA +static int test_negotiate_algorithms_pqc_build(void) +{ + byte buf[64]; + word32 bufSz; + TEST_CTX_SETUP(); + + printf("test_negotiate_algorithms_pqc_build...\n"); + + /* SPDM 1.4: PqcAsymAlgo (offset 16) advertises ML-DSA-44|65|87 = 0x07. */ + ctx->spdmVersion = SPDM_VERSION_14; + bufSz = sizeof(buf); + ASSERT_SUCCESS(wolfSPDM_BuildNegotiateAlgorithms(ctx, buf, &bufSz)); + ASSERT_EQ(buf[16], + (SPDM_PQC_ASYM_ALGO_ML_DSA_44 | SPDM_PQC_ASYM_ALGO_ML_DSA_65 | + SPDM_PQC_ASYM_ALGO_ML_DSA_87), + "1.4 PqcAsymAlgo must advertise ML-DSA 44/65/87 at offset 16"); + ASSERT_EQ(buf[8], 0x80, "BaseAsymAlgo ECDSA P-384 still advertised"); + + /* SPDM 1.2: offset 16 is reserved-zero (no PqcAsymAlgo). */ + ctx->spdmVersion = SPDM_VERSION_12; + bufSz = sizeof(buf); + ASSERT_SUCCESS(wolfSPDM_BuildNegotiateAlgorithms(ctx, buf, &bufSz)); + ASSERT_EQ(buf[16], 0x00, "1.2 must leave offset 16 reserved-zero"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +/* Build a minimal SPDM 1.4 ALGORITHMS response with the given BaseAsymSel + * (offset 12) and PqcAsymSel (offset 20). Returns total length (52). */ +static word32 build_algorithms_14(byte* rsp, word32 baseAsymSel, + word32 pqcAsymSel) +{ + XMEMSET(rsp, 0, 52); + rsp[0] = SPDM_VERSION_14; + rsp[1] = SPDM_ALGORITHMS; + rsp[2] = 4; /* AlgStructCount */ + SPDM_Set16LE(&rsp[4], 52); + rsp[6] = 0x01; /* MeasurementSpecificationSel = DMTF */ + rsp[7] = 0x02; /* OtherParamsSel = OpaqueDataFormat1 */ + SPDM_Set32LE(&rsp[12], baseAsymSel); + rsp[16] = SPDM_HASH_ALGO_SHA_384; + SPDM_Set32LE(&rsp[20], pqcAsymSel); + rsp[36] = 2; rsp[37] = 0x20; rsp[38] = 0x10; /* DHE SECP_384_R1 */ + rsp[40] = 3; rsp[41] = 0x20; rsp[42] = 0x02; /* AEAD AES_256_GCM */ + rsp[44] = 4; rsp[45] = 0x20; rsp[46] = 0x0F; /* ReqBaseAsym */ + rsp[48] = 5; rsp[49] = 0x20; rsp[50] = 0x01; /* KeySchedule SPDM */ + return 52; +} + +static int test_parse_algorithms_pqc_select(void) +{ + byte rsp[64]; + word32 levels[3]; + int i; + TEST_CTX_SETUP(); + + levels[0] = SPDM_PQC_ASYM_ALGO_ML_DSA_44; + levels[1] = SPDM_PQC_ASYM_ALGO_ML_DSA_65; + levels[2] = SPDM_PQC_ASYM_ALGO_ML_DSA_87; + + printf("test_parse_algorithms_pqc_select...\n"); + ctx->spdmVersion = SPDM_VERSION_14; + + /* Each ML-DSA level (44/65/87) with BaseAsymSel = 0: select ML-DSA. */ + for (i = 0; i < 3; i++) { + build_algorithms_14(rsp, 0, levels[i]); + ASSERT_SUCCESS(wolfSPDM_ParseAlgorithms(ctx, rsp, 52)); + ASSERT_EQ(ctx->asymType, WOLFSPDM_ASYM_MLDSA, "asymType must be ML-DSA"); + ASSERT_EQ(ctx->pqcAsymSel, levels[i], "pqcAsymSel must echo level"); + } + + /* Both BaseAsymSel and PqcAsymSel set: spec caps combined bits at one. */ + build_algorithms_14(rsp, SPDM_ASYM_ALGO_ECDSA_P384, + SPDM_PQC_ASYM_ALGO_ML_DSA_65); + ASSERT_EQ(wolfSPDM_ParseAlgorithms(ctx, rsp, 52), + WOLFSPDM_E_ALGO_MISMATCH, "both Base+Pqc selected must fail"); + + /* Unsupported PqcAsymSel bit (e.g. an SLH-DSA bit) must be rejected. */ + build_algorithms_14(rsp, 0, 0x00000008); + ASSERT_EQ(wolfSPDM_ParseAlgorithms(ctx, rsp, 52), + WOLFSPDM_E_ALGO_MISMATCH, "unsupported PqcAsymSel must fail"); + + /* No PqcAsymSel: classic ECDSA path still works. */ + build_algorithms_14(rsp, SPDM_ASYM_ALGO_ECDSA_P384, 0); + ASSERT_SUCCESS(wolfSPDM_ParseAlgorithms(ctx, rsp, 52)); + ASSERT_EQ(ctx->asymType, WOLFSPDM_ASYM_ECDSA, "asymType must be ECDSA"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +#ifndef NO_WOLFSPDM_MEAS_VERIFY +/* Mirror wolfSPDM's data_to_be_signed construction (DSP0274 Sec. 15): + * M = combined_spdm_prefix(100) || message_hash(48), so the test signs exactly + * what wolfSPDM_VerifyMlDsaSig assembles and verifies. */ +static word32 build_signed_msg(byte version, const char* ctxStr, + word32 ctxLen, const byte* msgHash, byte* out) +{ + word32 n = 0; + word32 pad; + byte maj = (byte)('0' + ((version >> 4) & 0xF)); + byte min = (byte)('0' + (version & 0xF)); + int i; + for (i = 0; i < 4; i++) { + XMEMCPY(&out[n], "dmtf-spdm-v1.2.*", 16); + out[n + 11] = maj; out[n + 13] = min; out[n + 15] = '*'; + n += 16; + } + pad = 36 - ctxLen; + XMEMSET(&out[n], 0, pad); n += pad; + XMEMCPY(&out[n], ctxStr, ctxLen); n += ctxLen; + XMEMCPY(&out[n], msgHash, WOLFSPDM_HASH_SIZE); n += WOLFSPDM_HASH_SIZE; + return n; +} + +/* Sign an SPDM "measurements" message with a fresh ML-DSA key at the given + * level and confirm wolfSPDM_VerifyMeasurementSig accepts it (and rejects a + * tampered copy). Exercises GetSigSize, BuildSignedMsg, and VerifyCtx for one + * parameter set. Returns 0 on pass. */ +static int mldsa_verify_one(byte level, word32 pqcSel, word32 expSigLen) +{ + static const char measCtx[] = "responder-measurements signing"; + WC_RNG rng; + byte reqMsg[8]; + byte rspBuf[64 + WOLFSPDM_MLDSA87_SIG_SIZE]; + byte msgHash[WOLFSPDM_HASH_SIZE]; + byte signMsg[200]; + word32 signMsgLen; + word32 sigLen; + word32 bodyLen = 16; + int rc; + TEST_CTX_SETUP(); + + ctx->spdmVersion = SPDM_VERSION_14; + ctx->asymType = WOLFSPDM_ASYM_MLDSA; + ctx->pqcAsymSel = pqcSel; + ctx->vcaLen = 0; + + ASSERT_EQ(wc_InitRng(&rng), 0, "InitRng"); + ASSERT_EQ(wc_MlDsaKey_Init(&ctx->responderPubKey.mldsa, NULL, INVALID_DEVID), + 0, "MlDsaKey_Init"); + ASSERT_EQ(wc_MlDsaKey_SetParams(&ctx->responderPubKey.mldsa, level), + 0, "SetParams"); + ASSERT_EQ(wc_MlDsaKey_MakeKey(&ctx->responderPubKey.mldsa, &rng), 0, + "MakeKey"); + ctx->flags.hasResponderPubKey = 1; + + XMEMSET(reqMsg, 0xA5, sizeof(reqMsg)); + XMEMSET(rspBuf, 0x5A, bodyLen); + + /* message_hash = SHA384(VCA(empty) || reqMsg || signed_body) */ + ASSERT_EQ(wolfSPDM_Sha384Hash(msgHash, NULL, 0, reqMsg, sizeof(reqMsg), + rspBuf, bodyLen), 0, "hash"); + signMsgLen = build_signed_msg(SPDM_VERSION_14, measCtx, + (word32)(sizeof(measCtx) - 1), msgHash, signMsg); + + sigLen = (word32)(sizeof(rspBuf) - bodyLen); + rc = wc_MlDsaKey_SignCtx(&ctx->responderPubKey.mldsa, + (const byte*)measCtx, (byte)(sizeof(measCtx) - 1), + &rspBuf[bodyLen], &sigLen, signMsg, signMsgLen, &rng); + ASSERT_EQ(rc, 0, "SignCtx"); + ASSERT_EQ(sigLen, expSigLen, "ML-DSA SigLen must match level"); + + /* Good signature verifies. */ + ASSERT_SUCCESS(wolfSPDM_VerifyMeasurementSig(ctx, rspBuf, bodyLen + sigLen, + reqMsg, sizeof(reqMsg))); + + /* Tamper a signed-body byte -> verification must fail. */ + rspBuf[0] ^= 0xFF; + ASSERT_FAIL(wolfSPDM_VerifyMeasurementSig(ctx, rspBuf, bodyLen + sigLen, + reqMsg, sizeof(reqMsg))); + + wc_FreeRng(&rng); + TEST_CTX_FREE(); + return 0; +} + +static int test_mldsa_measurement_verify(void) +{ + printf("test_mldsa_measurement_verify (ML-DSA 44/65/87)...\n"); + if (mldsa_verify_one(WC_ML_DSA_44, SPDM_PQC_ASYM_ALGO_ML_DSA_44, + WOLFSPDM_MLDSA44_SIG_SIZE) != 0) { + return -1; + } + if (mldsa_verify_one(WC_ML_DSA_65, SPDM_PQC_ASYM_ALGO_ML_DSA_65, + WOLFSPDM_MLDSA65_SIG_SIZE) != 0) { + return -1; + } + if (mldsa_verify_one(WC_ML_DSA_87, SPDM_PQC_ASYM_ALGO_ML_DSA_87, + WOLFSPDM_MLDSA87_SIG_SIZE) != 0) { + return -1; + } + TEST_PASS(); +} +#endif /* !NO_WOLFSPDM_MEAS_VERIFY */ + +/* The KEY_EXCHANGE_RSP / CHALLENGE_AUTH parsers derive the signature offset + * from wolfSPDM_GetSigSize(ctx). These confirm the ML-DSA SigLen (not the + * 96-byte ECDSA size) drives the buffer-bound check: a response only large + * enough for an ECDSA signature is rejected once ML-DSA is negotiated. */ +static int test_key_exchange_rsp_mldsa_sigsize(void) +{ + byte buf[300]; + TEST_CTX_SETUP(); + + printf("test_key_exchange_rsp_mldsa_sigsize...\n"); + ctx->spdmVersion = SPDM_VERSION_14; + ctx->asymType = WOLFSPDM_ASYM_MLDSA; + ctx->pqcAsymSel = SPDM_PQC_ASYM_ALGO_ML_DSA_65; + ASSERT_EQ(wc_MlDsaKey_Init(&ctx->responderPubKey.mldsa, NULL, INVALID_DEVID), + 0, "MlDsaKey_Init"); + ctx->flags.hasResponderPubKey = 1; + + XMEMSET(buf, 0, sizeof(buf)); + buf[0] = SPDM_VERSION_14; + buf[1] = SPDM_KEY_EXCHANGE_RSP; + /* buf[6] MutAuth = 0; opaqueLen at 136-137 = 0 -> sigOffset = 138. + * 282 fits an ECDSA sig (138+96+48) but not ML-DSA-65 (138+3309+48). */ + ASSERT_EQ(wolfSPDM_ParseKeyExchangeRsp(ctx, buf, 282), + WOLFSPDM_E_BUFFER_SMALL, "ML-DSA-65 KEY_EXCHANGE_RSP size guard"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +#ifndef NO_WOLFSPDM_CHALLENGE +static int test_challenge_auth_mldsa_sigsize(void) +{ + byte buf[200]; + word32 sigOff = 0; + TEST_CTX_SETUP(); + + printf("test_challenge_auth_mldsa_sigsize...\n"); + ctx->spdmVersion = SPDM_VERSION_12; + ctx->asymType = WOLFSPDM_ASYM_MLDSA; + ctx->pqcAsymSel = SPDM_PQC_ASYM_ALGO_ML_DSA_65; + ctx->challengeSlotId = 0; + ctx->challengeMeasHashType = SPDM_MEAS_SUMMARY_HASH_NONE; + + XMEMSET(buf, 0, sizeof(buf)); + buf[0] = SPDM_VERSION_12; + buf[1] = SPDM_CHALLENGE_AUTH; + buf[2] = 0; /* SlotID echo */ + XMEMSET(&buf[4], 0xCC, WOLFSPDM_HASH_SIZE); /* CertChainHash */ + XMEMSET(ctx->certChainHash, 0xCC, WOLFSPDM_HASH_SIZE); + /* Fixed tail ends at 4+48+32+2(opaqueLen=0) = 86; 182 fits an ECDSA sig + * but not ML-DSA-65 (86+3309), so the sig-room check must reject it. */ + ASSERT_EQ(wolfSPDM_ParseChallengeAuth(ctx, buf, 182, &sigOff), + WOLFSPDM_E_CHALLENGE, "ML-DSA-65 CHALLENGE_AUTH size guard"); + + TEST_CTX_FREE(); + TEST_PASS(); +} +#endif /* !NO_WOLFSPDM_CHALLENGE */ +#endif /* WOLFSPDM_HAVE_MLDSA */ + static int test_build_get_digests(void) { byte buf[16]; @@ -2073,6 +2331,17 @@ int main(void) test_build_get_capabilities(); test_build_negotiate_algorithms(); test_parse_algorithms_set_b_enforcement(); +#ifdef WOLFSPDM_HAVE_MLDSA + test_negotiate_algorithms_pqc_build(); + test_parse_algorithms_pqc_select(); +#ifndef NO_WOLFSPDM_MEAS_VERIFY + test_mldsa_measurement_verify(); +#endif + test_key_exchange_rsp_mldsa_sigsize(); +#ifndef NO_WOLFSPDM_CHALLENGE + test_challenge_auth_mldsa_sigsize(); +#endif +#endif test_build_get_digests(); test_build_get_certificate(); test_build_key_exchange_opaque_data(); diff --git a/wolfspdm/spdm.h b/wolfspdm/spdm.h index 0ca5198..3baef83 100644 --- a/wolfspdm/spdm.h +++ b/wolfspdm/spdm.h @@ -89,11 +89,16 @@ extern "C" { /* Compile-time size for static allocation of WOLFSPDM_CTX. * Use this when you need a buffer large enough to hold WOLFSPDM_CTX * without access to the struct definition (e.g., in wolfTPM). - * Actual struct size: ~31.3 KB (with measurements) / ~29.9 KB (NO_WOLFSPDM_MEAS). - * Rounded up to 32 KB for platform alignment. - * wolfSPDM_InitStatic() verifies at runtime that the provided buffer - * is large enough; returns WOLFSPDM_E_BUFFER_SMALL if not. */ + * Classical (Algorithm Set B) struct size: ~31.3 KB, rounded to 32 KB. + * With ML-DSA the larger PQC buffers (sigs, cert chains) and the ML-DSA verify + * key push it to ~67 KB, rounded to 72 KB. wolfSPDM_InitStatic() verifies at + * runtime that the provided buffer is large enough (WOLFSPDM_E_BUFFER_SMALL); + * a compile-time _Static_assert in spdm_context.c also guards this value. */ +#ifdef WOLFSPDM_HAVE_MLDSA +#define WOLFSPDM_CTX_STATIC_SIZE 73728 /* 72KB - fits CTX with ML-DSA buffers */ +#else #define WOLFSPDM_CTX_STATIC_SIZE 32768 /* 32KB - fits CTX with cert validation + challenge + key update fields */ +#endif /* Forward declaration */ struct WOLFSPDM_CTX; diff --git a/wolfspdm/spdm_types.h b/wolfspdm/spdm_types.h index dffc2ec..b310fd9 100644 --- a/wolfspdm/spdm_types.h +++ b/wolfspdm/spdm_types.h @@ -47,6 +47,19 @@ extern "C" { #include #endif +/* ML-DSA (FIPS 204) support follows wolfSSL: auto-on when the linked wolfSSL + * reports WOLFSSL_HAVE_MLDSA. Define WOLFSPDM_NO_MLDSA to force it off. + * wolfSPDM's configure capability-tests for the wc_MlDsaKey context API and + * defines WOLFSPDM_NO_MLDSA when only the legacy ML-DSA interface is present. + * Non-autoconf consumers (e.g. wolfTPM embedding) linking a pre-context-API + * wolfSSL that still reports WOLFSSL_HAVE_MLDSA must define WOLFSPDM_NO_MLDSA + * themselves to avoid a compile break. */ +#if defined(WOLFSSL_HAVE_MLDSA) && !defined(WOLFSPDM_NO_MLDSA) + #ifndef WOLFSPDM_HAVE_MLDSA + #define WOLFSPDM_HAVE_MLDSA + #endif +#endif + /* --- SPDM Protocol Constants (DMTF DSP0274 / DSP0277) --- */ /* SPDM Version Numbers */ @@ -127,6 +140,13 @@ extern "C" { /* Asymmetric Signature Algorithms */ #define SPDM_ASYM_ALGO_ECDSA_P384 0x00000080 /* ECDSA-ECC_NIST_P384 */ +/* PQC Asymmetric Signature Algorithms (DSP0274 1.4 Table 19 PqcAsymAlgo / + * Table 20 PqcAsymSel). Byte 0 bit mask; one selected, mutually exclusive + * with BaseAsymSel. */ +#define SPDM_PQC_ASYM_ALGO_ML_DSA_44 0x00000001 /* ML-DSA-44, SigLen 2420 */ +#define SPDM_PQC_ASYM_ALGO_ML_DSA_65 0x00000002 /* ML-DSA-65, SigLen 3309 */ +#define SPDM_PQC_ASYM_ALGO_ML_DSA_87 0x00000004 /* ML-DSA-87, SigLen 4627 */ + /* DHE (Diffie-Hellman Ephemeral) Algorithms */ #define SPDM_DHE_ALGO_SECP384R1 0x0010 /* secp384r1 */ @@ -141,6 +161,12 @@ extern "C" { #define SPDM_ALG_TYPE_AEAD 3 #define SPDM_ALG_TYPE_REQ_BASE_ASYM 4 #define SPDM_ALG_TYPE_KEY_SCHEDULE 5 +#define SPDM_ALG_TYPE_REQ_PQC_ASYM 6 /* DSP0274 1.4 Table 21 ReqPqcAsymAlg */ +#define SPDM_ALG_TYPE_KEM 7 /* DSP0274 1.4 KEM (ML-KEM) */ + +/* Which asymmetric family the responder selected (ctx->asymType) */ +#define WOLFSPDM_ASYM_ECDSA 0 /* BaseAsymSel = ECDSA P-384 */ +#define WOLFSPDM_ASYM_MLDSA 1 /* PqcAsymSel = ML-DSA */ /* Algorithm Set B Fixed Parameters */ #define WOLFSPDM_HASH_SIZE 48 /* SHA-384 output size */ @@ -152,6 +178,30 @@ extern "C" { #define WOLFSPDM_AEAD_TAG_SIZE 16 /* AES-GCM tag size */ #define WOLFSPDM_HMAC_SIZE 48 /* HMAC-SHA384 output size */ +#ifdef WOLFSPDM_HAVE_MLDSA +/* ML-DSA signature sizes (DSP0274 1.4 Table 19; FIPS 204). The largest sig we + * may need to verify bounds receive/transcript buffers below. */ +#define WOLFSPDM_MLDSA44_SIG_SIZE 2420 +#define WOLFSPDM_MLDSA65_SIG_SIZE 3309 +#define WOLFSPDM_MLDSA87_SIG_SIZE 4627 +#define WOLFSPDM_MAX_SIG_SIZE WOLFSPDM_MLDSA87_SIG_SIZE +#else +#define WOLFSPDM_MAX_SIG_SIZE WOLFSPDM_ECC_SIG_SIZE +#endif + +/* Receive-buffer size for the signature-bearing responses (KEY_EXCHANGE_RSP, + * CHALLENGE_AUTH). Sized for fixed fields + a small OpaqueData block + the + * negotiated SigLen + HMAC, NOT the full advertised DataTransferSize: like the + * pre-ML-DSA design (4096 advertised vs a 384-byte buffer), this assumes real + * responders keep OpaqueData in these two responses small. These are on-stack + * buffers in wolfSPDM_KeyExchange / wolfSPDM_Challenge, so the ML-DSA value + * (~5.3 KB) is the per-call stack cost on constrained targets. */ +#ifdef WOLFSPDM_HAVE_MLDSA +#define WOLFSPDM_SIG_RSP_BUF (640 + WOLFSPDM_MAX_SIG_SIZE) +#else +#define WOLFSPDM_SIG_RSP_BUF 512 +#endif + /* --- Capability Flags (per DSP0274) --- */ /* Requester Capabilities (GET_CAPABILITIES flags) */ @@ -182,9 +232,42 @@ extern "C" { /* --- Buffer/Message Size Limits --- */ -#define WOLFSPDM_MAX_MSG_SIZE 4096 /* Maximum SPDM message size */ -#define WOLFSPDM_MAX_CERT_CHAIN 4096 /* Maximum certificate chain size */ -#define WOLFSPDM_MAX_TRANSCRIPT 4096 /* Maximum transcript buffer */ +/* ML-DSA payloads (multi-KB sigs, pubkeys, cert chains) need larger buffers + * than Algorithm Set B. Defaults grow when ML-DSA is built in so ML-DSA-65 + * fits a single message; all three are overridable with -D. ML-DSA-87 and + * very large chains rely on the (future) chunking engine. */ +#ifndef WOLFSPDM_MAX_MSG_SIZE +#ifdef WOLFSPDM_HAVE_MLDSA +#define WOLFSPDM_MAX_MSG_SIZE 8192 /* Maximum SPDM message size */ +#else +#define WOLFSPDM_MAX_MSG_SIZE 4096 +#endif +#endif +#ifndef WOLFSPDM_MAX_CERT_CHAIN +#ifdef WOLFSPDM_HAVE_MLDSA +/* A full ML-DSA-65 responder cert chain (spdm-emu) is ~16.8 KB; ML-DSA-87 and + * alias chains run larger. 24 KB fits the common chains in one buffer. */ +#define WOLFSPDM_MAX_CERT_CHAIN 24576 /* Maximum certificate chain size */ +#else +#define WOLFSPDM_MAX_CERT_CHAIN 4096 +#endif +#endif +/* trustedCAs holds a single root CA cert, not a full chain, so it stays small + * even when ML-DSA grows the chain buffer. */ +#ifndef WOLFSPDM_MAX_TRUSTED_CA +#ifdef WOLFSPDM_HAVE_MLDSA +#define WOLFSPDM_MAX_TRUSTED_CA 8192 +#else +#define WOLFSPDM_MAX_TRUSTED_CA 4096 +#endif +#endif +#ifndef WOLFSPDM_MAX_TRANSCRIPT +#ifdef WOLFSPDM_HAVE_MLDSA +#define WOLFSPDM_MAX_TRANSCRIPT 16384 /* Maximum transcript buffer */ +#else +#define WOLFSPDM_MAX_TRANSCRIPT 4096 +#endif +#endif #define WOLFSPDM_RANDOM_SIZE 32 /* Random data in KEY_EXCHANGE */ /* --- MCTP Transport Constants (for TCP/socket transport) --- */