From f216e73c0b57a1202ae089e7775865dafd8f3480 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 13 May 2026 17:39:53 +0100 Subject: [PATCH 1/7] Standalone SPDM 1.2/1.3/1.4 requester + skoll security hardening Strips Nuvoton TPM mode and WOLFSPDM_MODE so wolfSPDM is a pure DSP0274/DSP0277 standard requester. Adds the missing spdm-emu integration (new examples/spdm_demo CLI + 18-test matrix driver) and rewrites the broken spdm-emu CI workflow. Then applies a deep skoll review-security pass on the diff and the full repo. Security: - KEY_EXCHANGE_RSP responder signature verified before key derivation - ResponderVerifyData HMAC mismatch fatal; constant-time compare - Seq-number wrap cap + explicit replay mismatch check (DSP0277 11.3) - Strict Algorithm Set B enforcement in ParseAlgorithms - Mutual-auth requested by responder refused before sessionId commit - 1.3+ RequesterContext echo verified in CHALLENGE_AUTH - Inner MCTP-type byte validated post-decrypt - Encrypt/DecryptInternal use goto-exit + wc_ForceZero on plaintext - wc_ForceZero of transient secrets in DeriveAppDataKeys/UpdatedKeys - Disconnect frees responderPubKey + clears measurement state - ConnectStandard resets stale ctx fields between retries - Compile-time _Static_assert on WOLFSPDM_CTX_STATIC_SIZE Standards / interop: - SPDM 1.2/1.3/1.4 negotiation, wolfSPDM_SetMaxVersion runtime clamp - 1.3+ RequesterContext in GET_MEASUREMENTS and CHALLENGE; 1.4 OpaqueLength in FINISH and FINISH_RSP (256B cap on incoming) - ParseAlgorithms walks AlgStruct via ExtAsymSelCount/ExtHashSelCount - Finish ERROR sniff uses explicit sessionId compare (no heuristic) - ExchangeMsg snapshots transcriptLen and rolls back on failure API: - Renamed wolfSPDM_GetVersion_Negotiated -> wolfSPDM_GetNegotiatedVersion (old name retained as exported ABI-compat alias) - New: wolfSPDM_SetMaxVersion, wolfSPDM_GetLastPeerError - Removed: SetRequesterKeyPair, SignHash, SetResponderPubKey, SetMode - GetSessionId returns id from KEY_EX onward (I/O callback needs it to tag the secured FINISH record) - WOLFSPDM_API visibility shim, bit-field packed flags struct Tests + CI: - 53 unit tests (was 27); 18/18 integration tests pass against spdm-emu (6 scenarios x SPDM 1.2/1.3/1.4) - CI drops --enable-nuvoton across all matrices, adds 3-arch spdm-emu integration job (ubuntu-22.04 x64, ubuntu-24.04 x64, ubuntu-24.04-arm aarch64) --- .github/workflows/README.md | 4 +- .github/workflows/build-test.yml | 6 +- .github/workflows/codeql.yml | 2 +- .github/workflows/compiler-warnings.yml | 4 +- .github/workflows/multi-compiler.yml | 3 +- .github/workflows/spdm-emu-test.yml | 212 +++-- .github/workflows/static-analysis.yml | 2 +- .gitignore | 4 + CLAUDE.md | 109 +++ Makefile.am | 19 +- README.md | 201 ++--- config.h.in | 3 - configure.ac | 12 - docs/ATTESTATION.md | 144 ++++ examples/spdm_demo.c | 459 ++++++++++ examples/spdm_test.sh | 260 ++++++ src/spdm_context.c | 242 +++--- src/spdm_crypto.c | 142 +--- src/spdm_internal.h | 136 +-- src/spdm_kdf.c | 177 ++-- src/spdm_msg.c | 656 ++++++++++----- src/spdm_nuvoton.c | 820 ------------------ src/spdm_secured.c | 364 +++----- src/spdm_session.c | 219 +++-- src/spdm_transcript.c | 1 - test/test_spdm.c | 2 +- test/unit_test.c | 1017 +++++++++++++++++++---- wolfspdm/spdm.h | 189 ++--- wolfspdm/spdm_error.h | 5 +- wolfspdm/spdm_nuvoton.h | 319 ------- wolfspdm/spdm_types.h | 15 +- 31 files changed, 3141 insertions(+), 2607 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/ATTESTATION.md create mode 100644 examples/spdm_demo.c create mode 100755 examples/spdm_test.sh delete mode 100644 src/spdm_nuvoton.c delete mode 100644 wolfspdm/spdm_nuvoton.h diff --git a/.github/workflows/README.md b/.github/workflows/README.md index c2e0234..b51b3a2 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -2,11 +2,11 @@ | Workflow | File | Description | |----------|------|-------------| -| Build and Test | `build-test.yml` | Matrix build across OS (ubuntu-latest, 22.04) and configure options (debug, nuvoton, dynamic-mem). Runs unit tests for each combination. | +| Build and Test | `build-test.yml` | Matrix build across OS (ubuntu-latest, 22.04) and configure options (debug, dynamic-mem). Runs unit tests for each combination. | | Multiple Compilers | `multi-compiler.yml` | Builds and tests with GCC 11-13 and Clang 14-17 using `-Wall -Wextra -Werror`. | | Compiler Warnings | `compiler-warnings.yml` | GCC strict warnings (`-Wpedantic -Wconversion -Wshadow -Werror`) and Clang `-Werror` build. | | Static Analysis | `static-analysis.yml` | Runs cppcheck (style, performance, portability) and Clang Static Analyzer (scan-build). | | Memory Check | `memory-check.yml` | Valgrind leak check with `--leak-check=full` for both static and dynamic memory modes. | | CodeQL Security | `codeql.yml` | GitHub CodeQL security-and-quality analysis. Runs on PRs and weekly (Monday 6 AM UTC). | | Codespell | `codespell.yml` | Spell-checks source files. | -| SPDM Emulator Test | `spdm-emu-test.yml` | End-to-end integration test against the DMTF libspdm emulator via wolfTPM. Runs 6 tests: session establishment, signed/unsigned measurements, challenge authentication, heartbeat, and key update. Dependencies (wolfSSL, spdm-emu, wolfTPM) are cached and refreshed every ~15 days. | +| SPDM Emulator Test | `spdm-emu-test.yml` | End-to-end integration test against the DMTF libspdm emulator. Runs 18 tests (6 scenarios x SPDM 1.2/1.3/1.4): session establishment, signed/unsigned measurements, challenge authentication, heartbeat, key update. Matrix across ubuntu-22.04 (x64), ubuntu-24.04 (x64), ubuntu-24.04-arm (aarch64). | diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 80ea218..945deea 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -8,14 +8,13 @@ on: jobs: build: - name: ${{ matrix.os }} / debug=${{ matrix.debug }} / nuvoton=${{ matrix.nuvoton }} / dynamic-mem=${{ matrix.dynamic-mem }} + name: ${{ matrix.os }} / debug=${{ matrix.debug }} / dynamic-mem=${{ matrix.dynamic-mem }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, ubuntu-22.04] debug: [yes, no] - nuvoton: [yes, no] dynamic-mem: [yes, no] steps: @@ -53,7 +52,6 @@ jobs: run: | ./configure --with-wolfssl=$HOME/wolfssl-install \ ${{ matrix.debug == 'yes' && '--enable-debug' || '' }} \ - ${{ matrix.nuvoton == 'yes' && '--enable-nuvoton' || '' }} \ ${{ matrix.dynamic-mem == 'yes' && '--enable-dynamic-mem' || '' }} - name: Build @@ -68,7 +66,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: test-logs-${{ matrix.os }}-debug-${{ matrix.debug }}-nuvoton-${{ matrix.nuvoton }}-dynmem-${{ matrix.dynamic-mem }} + name: test-logs-${{ matrix.os }}-debug-${{ matrix.debug }}-dynmem-${{ matrix.dynamic-mem }} path: | test/*.log config.log diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8076041..79c8373 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,7 +54,7 @@ jobs: - name: Build run: | ./autogen.sh - ./configure --with-wolfssl=$HOME/wolfssl-install --enable-nuvoton + ./configure --with-wolfssl=$HOME/wolfssl-install make -j$(nproc) - name: Perform CodeQL Analysis diff --git a/.github/workflows/compiler-warnings.yml b/.github/workflows/compiler-warnings.yml index f10fa09..619ed1c 100644 --- a/.github/workflows/compiler-warnings.yml +++ b/.github/workflows/compiler-warnings.yml @@ -42,7 +42,7 @@ jobs: - name: Build with strict warnings run: | ./autogen.sh - ./configure --with-wolfssl=$HOME/wolfssl-install --enable-nuvoton + ./configure --with-wolfssl=$HOME/wolfssl-install make -j$(nproc) CFLAGS="-Wall -Wextra -Wpedantic -Werror -Wconversion -Wshadow" clang: @@ -80,7 +80,7 @@ jobs: - name: Build with clang run: | ./autogen.sh - CC=clang ./configure --with-wolfssl=$HOME/wolfssl-install --enable-nuvoton + CC=clang ./configure --with-wolfssl=$HOME/wolfssl-install make -j$(nproc) CFLAGS="-Wall -Wextra -Werror" - name: Run unit tests diff --git a/.github/workflows/multi-compiler.yml b/.github/workflows/multi-compiler.yml index d0afc6f..a4b7e8c 100644 --- a/.github/workflows/multi-compiler.yml +++ b/.github/workflows/multi-compiler.yml @@ -57,8 +57,7 @@ jobs: - name: Build wolfSPDM with ${{ matrix.cc }} run: | ./autogen.sh - CC=${{ matrix.cc }} ./configure --with-wolfssl=$HOME/wolfssl-install \ - --enable-nuvoton + CC=${{ matrix.cc }} ./configure --with-wolfssl=$HOME/wolfssl-install make -j$(nproc) CFLAGS="-Wall -Wextra -Werror" - name: Run unit tests diff --git a/.github/workflows/spdm-emu-test.yml b/.github/workflows/spdm-emu-test.yml index fce5dd9..079cafb 100644 --- a/.github/workflows/spdm-emu-test.yml +++ b/.github/workflows/spdm-emu-test.yml @@ -8,105 +8,137 @@ on: jobs: spdm-emu-test: - name: SPDM emulator integration test - runs-on: ubuntu-latest - + 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 }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool cmake libmbedtls-dev - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y autoconf automake libtool cmake libmbedtls-dev + # Cache period rotates every ~15 days so dependencies stay fresh + - name: Compute cache period + id: cache-period + run: echo "biweekly=$(( $(date +%s) / 1296000 ))" >> $GITHUB_OUTPUT - # Cache period rotates every ~15 days so dependencies stay fresh - - name: Compute cache period - id: cache-period - run: | - echo "biweekly=$(( $(date +%s) / 1296000 ))" >> $GITHUB_OUTPUT + # --- wolfSSL (cached) --- + - name: Cache wolfSSL + id: cache-wolfssl + uses: actions/cache@v4 + with: + path: ~/wolfssl-install + key: wolfssl-spdm-${{ matrix.os }}-${{ steps.cache-period.outputs.biweekly }} - # --- Build wolfSSL (cached) --- - - name: Cache wolfSSL - id: cache-wolfssl - uses: actions/cache@v4 - with: - path: ~/wolfssl-install - key: wolfssl-${{ steps.cache-period.outputs.biweekly }} + - name: Build wolfSSL + if: steps.cache-wolfssl.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 https://github.com/wolfSSL/wolfssl.git + cd wolfssl + ./autogen.sh + ./configure --enable-wolftpm --enable-ecc --enable-sha384 \ + --enable-aesgcm --enable-hkdf --enable-sp \ + --prefix=$HOME/wolfssl-install + make -j$(nproc) + make install - - name: Build wolfSSL - if: steps.cache-wolfssl.outputs.cache-hit != 'true' - run: | - cd ~ - git clone --depth 1 https://github.com/wolfSSL/wolfssl.git - cd wolfssl - ./autogen.sh - ./configure --enable-wolftpm --enable-all \ - --prefix=$HOME/wolfssl-install - make -j$(nproc) - make install + # --- wolfSPDM (always rebuilt - this is what we're testing) --- + - name: Build and install wolfSPDM + run: | + ./autogen.sh + ./configure --with-wolfssl=$HOME/wolfssl-install \ + --prefix=$HOME/wolfspdm-install \ + ${{ matrix.dynamic-mem == 'yes' && '--enable-dynamic-mem' || '' }} + make -j$(nproc) + make install - # --- Build wolfSPDM (always, this is what we're testing) --- - - name: Build and install wolfSPDM - run: | - ./autogen.sh - ./configure --with-wolfssl=$HOME/wolfssl-install \ - --prefix=$HOME/wolfspdm-install - make -j$(nproc) - make install + - name: Run unit tests + run: make check + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/.libs:${{ github.workspace }}/src/.libs:${{ env.HOME }}/wolfssl-install/lib - # --- Build spdm-emu (cached) --- - - name: Cache spdm-emu - id: cache-spdm-emu - uses: actions/cache@v4 - with: - path: ~/spdm-emu - key: spdm-emu-${{ steps.cache-period.outputs.biweekly }} + # --- spdm-emu (cached) --- + - name: Cache spdm-emu + id: cache-spdm-emu + uses: actions/cache@v4 + with: + path: ~/spdm-emu/build/bin + key: spdm-emu-${{ matrix.os }}-${{ steps.cache-period.outputs.biweekly }} - - name: Build spdm-emu - if: steps.cache-spdm-emu.outputs.cache-hit != 'true' - run: | - cd ~ - git clone --depth 1 --recursive https://github.com/DMTF/spdm-emu.git - cd spdm-emu - mkdir build && cd build - cmake -DARCH=x64 -DTOOLCHAIN=GCC -DTARGET=Release -DCRYPTO=mbedtls .. - make copy_sample_key && make -j$(nproc) + - name: Build spdm-emu + if: steps.cache-spdm-emu.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 --recursive https://github.com/DMTF/spdm-emu.git + cd spdm-emu + mkdir build && cd build + cmake -DARCH=${{ matrix.arch }} -DTOOLCHAIN=GCC \ + -DTARGET=Release -DCRYPTO=mbedtls .. + make copy_sample_key + make -j$(nproc) - # --- Build wolfTPM (cached, dynamically links wolfspdm at runtime) --- - - name: Cache wolfTPM - id: cache-wolftpm - uses: actions/cache@v4 - with: - path: ~/wolfTPM - key: wolftpm-${{ steps.cache-period.outputs.biweekly }} + # --- Demo smoke (help text / arg parsing, no emulator) --- + - name: spdm_demo CLI smoke (no emulator) + run: | + export LD_LIBRARY_PATH=$HOME/wolfspdm-install/lib:$HOME/wolfssl-install/lib + ./examples/spdm_demo --help 2>&1 | head -20 || true - - name: Build wolfTPM - if: steps.cache-wolftpm.outputs.cache-hit != 'true' - run: | - cd ~ - # TODO: Switch to wolfSSL/wolfTPM once PR #453 is merged - git clone --depth 1 -b add-wolfspdm-backend \ - https://github.com/aidangarske/wolfTPM.git wolfTPM - cd wolfTPM - ./autogen.sh - ./configure --enable-spdm --enable-swtpm \ - --with-wolfspdm=$HOME/wolfspdm-install \ - --with-wolfcrypt=$HOME/wolfssl-install - make -j$(nproc) + # --- Legacy smoke test (test/test_spdm) — single session against emu --- + - name: test/test_spdm smoke (one session) + run: | + export LD_LIBRARY_PATH=$HOME/wolfspdm-install/lib:$HOME/wolfssl-install/lib + export SPDM_EMU_PATH=$HOME/spdm-emu/build/bin + # Start emulator in background, run the legacy smoke, then kill it + (cd "$SPDM_EMU_PATH" && ./spdm_responder_emu --ver 1.2 \ + --hash SHA_384 --asym ECDSA_P384 \ + --dhe SECP_384_R1 --aead AES_256_GCM \ + >/tmp/test_spdm_emu.log 2>&1) & + EMU_PID=$! + sleep 2 + ./test/test_spdm + RC=$? + kill $EMU_PID 2>/dev/null || true + wait $EMU_PID 2>/dev/null || true + exit $RC - # --- Run integration tests --- - - name: Run SPDM emulator tests - run: | - cd ~/wolfTPM - export LD_LIBRARY_PATH=$HOME/wolfspdm-install/lib:$HOME/wolfssl-install/lib - export SPDM_EMU_PATH=$HOME/spdm-emu/build/bin - ./examples/spdm/spdm_test.sh --emu + # --- Full integration matrix (18 tests: 6 scenarios x SPDM 1.2/1.3/1.4) --- + - name: Run SPDM emulator tests (18-test matrix) + run: | + export LD_LIBRARY_PATH=$HOME/wolfspdm-install/lib:$HOME/wolfssl-install/lib + export SPDM_EMU_PATH=$HOME/spdm-emu/build/bin + ./examples/spdm_test.sh - - name: Upload logs on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: spdm-emu-test-logs - path: | - config.log - ~/wolfTPM/config.log + - name: Upload logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: spdm-emu-test-logs-${{ matrix.os }}-${{ matrix.arch }}-dynmem-${{ matrix.dynamic-mem }} + path: | + config.log + test/*.log + /tmp/spdm_emu_*.log + /tmp/test_spdm_emu.log diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 44390a3..50ab404 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -86,7 +86,7 @@ jobs: - name: Configure run: | ./autogen.sh - ./configure --with-wolfssl=$HOME/wolfssl-install --enable-nuvoton + ./configure --with-wolfssl=$HOME/wolfssl-install - name: Run scan-build run: scan-build --status-bugs -o scan-results make -j$(nproc) diff --git a/.gitignore b/.gitignore index 8ca0239..0efba0d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,10 @@ test/.libs/ test/*.o test/unit_test test/test_spdm +examples/.libs/ +examples/*.o +examples/.dirstamp +examples/spdm_demo # pkg-config generated wolfspdm.pc diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..790b14a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,109 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +wolfSPDM is a lightweight SPDM 1.2/1.3/1.4 requester (initiator) implementation using wolfCrypt for cryptographic operations. It provides authenticated and encrypted communication with SPDM responders per DMTF DSP0274/DSP0277 specifications, tested against the DMTF spdm-emu emulator. + +**Key characteristics:** +- Standard SPDM 1.2 / 1.3 / 1.4 negotiation +- Algorithm Set B only: ECDSA P-384, ECDHE P-384, SHA-384, AES-256-GCM, HKDF-SHA384 +- Zero-malloc by default (static memory ~28KB context), optional `--enable-dynamic-mem` for heap allocation +- Requester-only - pair with any SPDM responder (e.g. spdm-emu) + +## Build Commands + +```bash +./autogen.sh # Generate configure script +./configure --with-wolfssl=/usr/local # Configure (adjust wolfSSL path as needed) +make # Build library +make check # Run unit tests +sudo make install # Install library +``` + +**Configure options:** +- `--enable-debug` - Debug output with -g -O0 (default: -O2) +- `--enable-dynamic-mem` - Use heap allocation for WOLFSPDM_CTX (default: static/zero-malloc) +- `--with-wolfssl=PATH` - wolfSSL installation path + +## Testing + +**Unit tests:** +```bash +make check # Runs test/unit_test +./test/unit_test # Run directly +``` + +**Integration test (requires DMTF spdm-emu):** +```bash +export SPDM_EMU_PATH=../spdm-emu/build/bin +./examples/spdm_test.sh # Runs 18 tests (6 scenarios x SPDM 1.2/1.3/1.4) +``` + +The script starts/stops `spdm_responder_emu` per test and runs session +establishment, signed measurements, unsigned measurements, challenge +authentication, heartbeat, and key update. + +## Architecture + +### Source Organization +``` +src/ # Core implementation + spdm_context.c # Context lifecycle (New/Init/Free) + spdm_msg.c # Message building/parsing + spdm_crypto.c # ECC operations + spdm_kdf.c # HKDF key derivation + spdm_transcript.c # Transcript buffer for TH1/TH2 + spdm_secured.c # AES-256-GCM encryption/decryption + spdm_session.c # Session management + spdm_internal.h # Internal declarations + +wolfspdm/ # Public API headers + spdm.h # Main API + spdm_types.h # Protocol constants + spdm_error.h # Error codes + +examples/ + spdm_demo.c # CLI demo: --emu/--meas/--challenge/--heartbeat/--key-update + spdm_test.sh # Integration test driver (runs 18 tests against spdm-emu) + +test/ + unit_test.c # Unit tests + test_spdm.c # Smoke test (single session against emu) +``` + +### Protocol State Machine +``` +INIT -> VERSION -> CAPS -> ALGO -> DIGESTS -> CERT -> KEY_EX -> FINISH -> CONNECTED -> MEASURED +``` + +### Key Patterns + +**Return codes:** 0 = success, negative = error (see `spdm_error.h`) + +**Buffer pattern:** Output size is input capacity and output actual size: +```c +int func(..., byte* buf, word32* bufSz); +``` + +**Transport abstraction:** I/O callback handles all transport: +```c +typedef int (*WOLFSPDM_IO_CB)(WOLFSPDM_CTX* ctx, + const byte* txBuf, word32 txSz, + byte* rxBuf, word32* rxSz, + void* userCtx); +``` + +**Memory:** Zero-malloc by default (static context via `wolfSPDM_InitStatic()`). With `--enable-dynamic-mem`, uses XMALLOC/XFREE macros (wolfCrypt convention) for heap allocation via `wolfSPDM_New()`. Fixed-size internal buffers (4KB max message/cert/transcript). + +### Cryptographic Constants +- Hash: 48 bytes (SHA-384) +- ECC key: 48 bytes, point: 96 bytes, signature: 96 bytes (P-384) +- AEAD: 32-byte key, 12-byte IV, 16-byte tag (AES-256-GCM) + +## Dependencies + +wolfSSL/wolfCrypt configured with: `--enable-wolftpm --enable-ecc --enable-sha384 --enable-aesgcm --enable-hkdf --enable-sp` + +**Build order:** wolfSSL (install) -> wolfSPDM (build). `WOLFSPDM_CTX_STATIC_SIZE` depends on wolfSSL internal struct sizes (`ecc_key`, `wc_Sha384`, `WC_RNG`), so wolfSPDM must be rebuilt whenever wolfSSL config changes. diff --git a/Makefile.am b/Makefile.am index a3e89b4..88c2c8f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -11,11 +11,6 @@ libwolfspdm_la_SOURCES = \ src/spdm_session.c \ src/spdm_transcript.c -# Nuvoton TPM support (TCG binding + vendor commands) -if BUILD_NUVOTON -libwolfspdm_la_SOURCES += src/spdm_nuvoton.c -endif - libwolfspdm_la_CPPFLAGS = -I$(srcdir)/wolfspdm -I$(srcdir)/src libwolfspdm_la_LIBADD = -lwolfssl @@ -23,7 +18,7 @@ libwolfspdm_la_LIBADD = -lwolfssl BUILT_SOURCES = wolfspdm/options.h wolfspdm/options.h: config.h @mkdir -p wolfspdm - @echo '/* wolfspdm/options.h — auto-generated from config.h, do not edit */' > $@ + @echo '/* wolfspdm/options.h - auto-generated from config.h, do not edit */' > $@ @echo '#ifndef WOLFSPDM_OPTIONS_H' >> $@ @echo '#define WOLFSPDM_OPTIONS_H' >> $@ @grep '^\#define WOLFSPDM_' config.h >> $@ || true @@ -36,11 +31,6 @@ nobase_include_HEADERS = \ wolfspdm/spdm_error.h \ wolfspdm/options.h -# Nuvoton header (always installed, conditionally useful) -if BUILD_NUVOTON -nobase_include_HEADERS += wolfspdm/spdm_nuvoton.h -endif - # Test programs check_PROGRAMS = test/unit_test test/test_spdm @@ -54,6 +44,13 @@ test_test_spdm_LDADD = libwolfspdm.la -lwolfssl TESTS = test/unit_test +# Example programs (built but not installed) +noinst_PROGRAMS = examples/spdm_demo + +examples_spdm_demo_SOURCES = examples/spdm_demo.c +examples_spdm_demo_CPPFLAGS = -I$(srcdir)/wolfspdm +examples_spdm_demo_LDADD = libwolfspdm.la -lwolfssl + # pkgconfig pkgconfigdir = $(libdir)/pkgconfig pkgconfig_DATA = wolfspdm.pc diff --git a/README.md b/README.md index 140e941..1ad2133 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,54 @@ # wolfSPDM +wolfSPDM is a lightweight C library implementing [SPDM 1.2 / 1.3 / 1.4](https://www.dmtf.org/sites/default/files/standards/documents/DSP0274_1.4.0.pdf) and [Secured Messages over MCTP (DSP0277)](https://www.dmtf.org/sites/default/files/standards/documents/DSP0277_1.2.0.pdf) using [wolfSSL](https://www.wolfssl.com/) as the crypto backend. It is a standalone, requester-only stack designed for embedded use, tested end-to-end against the DMTF [spdm-emu](https://github.com/DMTF/spdm-emu) emulator. -**NOTE: wolfSPDM is being moved directly into wolfTPM https://github.com/wolfSSL/wolfTPM/pull/458 to allow better support for TPM products like the NSING NS350 and Nuvoton NPCT75x. At this point wolfSPDM won't be maintained directly or recognized as a wolfSSL product but it serves as a good reference to how our wolfTPM code works since users can directly try it out without hardware using TCG commands plus spdm-emu** +## Main Features +- **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 +- **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 +- **Device attestation:** signed / unsigned `GET_MEASUREMENTS`, sessionless `CHALLENGE_AUTH`, certificate-chain validation against trusted root CAs +- **Compatible with DMTF spdm-emu** for interoperability testing (18-test matrix across 1.2 / 1.3 / 1.4) +- **Path to FIPS 140-3** via wolfCrypt FIPS Certificate #4718 (sole crypto dependency) -Lightweight SPDM 1.2+ requester-only stack implementation using wolfSSL/wolfCrypt with no dynamic memory allocations +## Supported Operations (RFC / DSP0274) -## Overview +| Operation | DSP0274 | wolfSPDM API | +|---|---|---| +| Session establishment | Sec. 10.7 | `wolfSPDM_Connect`, `wolfSPDM_KeyExchange`, `wolfSPDM_Finish` | +| Encrypted application data | DSP0277 | `wolfSPDM_SecuredExchange`, `wolfSPDM_SendData`, `wolfSPDM_ReceiveData` | +| Measurements (signed/unsigned) | Sec. 10.11 | `wolfSPDM_GetMeasurements`, `wolfSPDM_GetMeasurementBlock` | +| Challenge authentication (sessionless) | Sec. 10.8 | `wolfSPDM_Challenge` | +| Session keep-alive | Sec. 10.10 | `wolfSPDM_Heartbeat` | +| Session key rotation | Sec. 10.9 | `wolfSPDM_KeyUpdate` | +| Trust anchor | Sec. 10.6 | `wolfSPDM_SetTrustedCAs` | -- SPDM 1.2 requester implementation -- Algorithm Set B (FIPS 140-3 Level 3): ECDSA/ECDHE P-384, SHA-384, AES-256-GCM, HKDF-SHA384 -- **Zero-malloc by default** — fully static memory (~32 KB context), ideal for constrained/embedded environments -- Optional `--enable-dynamic-mem` for heap-allocated contexts (useful for small-stack platforms) -- Session establishment with full key exchange and encrypted messaging -- Device attestation via signed/unsigned measurements (GET_MEASUREMENTS) -- Sessionless attestation via CHALLENGE/CHALLENGE_AUTH with signature verification -- Certificate chain validation against trusted root CAs -- Session keep-alive via HEARTBEAT/HEARTBEAT_ACK -- Session key rotation via KEY_UPDATE/KEY_UPDATE_ACK (DSP0277) -- Hardware SPDM via wolfTPM + Nuvoton TPM -- Full transcript tracking for TH1/TH2 computation -- Compatible with DMTF spdm-emu for interoperability testing -- **FIPS 140-3** (Certificate #4718) via wolfCrypt FIPS -- **DO-178C DAL A** via wolfCrypt DO-178 with wolfTPM +## Prerequisites (wolfSSL) -wolfSPDM supports hardware-backed SPDM through wolfTPM with Nuvoton TPM -integration, and requires no external dependencies beyond wolfSSL/wolfCrypt. - -## Prerequisites - -wolfSSL with the required crypto algorithms: +wolfSPDM requires [wolfSSL](https://www.wolfssl.com/) configured with ECC P-384, SHA-384, AES-GCM, and HKDF: ```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 +./configure --enable-wolftpm --enable-ecc --enable-sha384 \ + --enable-aesgcm --enable-hkdf --enable-sp make sudo make install sudo ldconfig ``` -The `--enable-sp` flag enables Single Precision math with optimized ECC P-384 -support, which is required for SPDM Algorithm Set B on platforms like ARM64. -For a broader feature set, `--enable-all` can be used instead. +`--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. -## Building +## Build ```bash ./autogen.sh ./configure make +make check ``` ### Configure Options @@ -58,16 +56,12 @@ make | Option | Description | |---|---| | `--enable-debug` | Debug output with `-g -O0` (default: `-O2`) | -| `--enable-nuvoton` | Enable Nuvoton TPM support | -| `--enable-dynamic-mem` | Use heap allocation for WOLFSPDM_CTX (default: static) | +| `--enable-dynamic-mem` | Use heap allocation for `WOLFSPDM_CTX` (default: static) | | `--with-wolfssl=PATH` | wolfSSL installation path | ### Memory Modes -**Static (default):** Zero heap allocation. The caller provides a buffer -(`WOLFSPDM_CTX_STATIC_SIZE` bytes, ~32 KB) and wolfSPDM operates entirely -within it. This is ideal for embedded and constrained environments where -malloc is unavailable or undesirable. +**Static (default):** zero heap allocation. The caller provides a buffer (`WOLFSPDM_CTX_STATIC_SIZE` bytes, ~32 KB) and wolfSPDM operates entirely within it. Ideal for embedded and constrained environments where malloc is unavailable or undesirable. ```c #include @@ -79,9 +73,7 @@ wolfSPDM_InitStatic(ctx, sizeof(spdmBuf)); wolfSPDM_Free(ctx); ``` -**Dynamic (`--enable-dynamic-mem`):** Context is heap-allocated via -`wolfSPDM_New()`. Useful on platforms with small stacks where a ~32 KB -local variable is impractical. +**Dynamic (`--enable-dynamic-mem`):** context is heap-allocated via `wolfSPDM_New()`. Useful on platforms with small stacks where a ~32 KB local variable is impractical. ```c #include @@ -91,80 +83,23 @@ WOLFSPDM_CTX* ctx = wolfSPDM_New(); wolfSPDM_Free(ctx); /* frees heap memory */ ``` -## Build Order - -wolfSPDM depends on wolfSSL, and wolfTPM depends on both. When changing -wolfSSL configuration, **all three must be rebuilt in order** because -wolfSPDM's static context size (`WOLFSPDM_CTX_STATIC_SIZE`) depends on -wolfSSL internal struct sizes (`ecc_key`, `wc_Sha384`, `WC_RNG`, etc.): - -``` -wolfSSL (sudo make install) → wolfSPDM (make) → wolfTPM (make) -``` +## Quick Start -## Testing with spdm-emu Emulator +`examples/spdm_demo` is a CLI driver that exercises each SPDM operation against `spdm-emu` over TCP/MCTP: ```bash -# Build emulator -git clone https://github.com/DMTF/spdm-emu.git +# Build the DMTF spdm-emu emulator +git clone --recursive https://github.com/DMTF/spdm-emu.git cd spdm-emu && mkdir build && cd build cmake -DARCH=x64 -DTOOLCHAIN=GCC -DTARGET=Release -DCRYPTO=mbedtls .. make copy_sample_key && make -# Build wolfSSL -cd wolfssl -./autogen.sh -./configure --enable-wolftpm --enable-ecc --enable-sha384 --enable-aesgcm --enable-hkdf --enable-sp -make -sudo make install -sudo ldconfig - -# Build wolfSPDM -cd wolfSPDM -./autogen.sh -./configure -make - -# Build wolfTPM (point --with-wolfspdm to wolfSPDM source directory) -cd wolfTPM -./autogen.sh -./configure --enable-spdm --enable-swtpm --with-wolfspdm=../wolfSPDM -make - -# Run emulator tests (starts/stops emulator automatically) -./examples/spdm/spdm_test.sh --emu +# Run the 18-test integration matrix from this repo +export SPDM_EMU_PATH=../spdm-emu/build/bin +./examples/spdm_test.sh ``` -The test script automatically finds `spdm_responder_emu` in `../spdm-emu/build/bin/`, -starts it for each test, and runs session establishment, signed measurements, -unsigned measurements, challenge authentication, heartbeat, and key update. - -## Testing with Nuvoton NPCT75x - -```bash -# Build wolfSSL -cd wolfssl -./autogen.sh -./configure --enable-wolftpm --enable-ecc --enable-sha384 --enable-aesgcm --enable-hkdf --enable-sp -make -sudo make install -sudo ldconfig - -# Build wolfSPDM with Nuvoton support -cd wolfSPDM -./autogen.sh -./configure --enable-nuvoton -make - -# Build wolfTPM (point --with-wolfspdm to wolfSPDM source directory) -cd wolfTPM -./autogen.sh -./configure --enable-spdm --enable-nuvoton --with-wolfspdm=../wolfSPDM -make - -# Run Nuvoton test suite -./examples/spdm/spdm_test.sh --nuvoton -``` +The driver starts/stops `spdm_responder_emu` per test and runs six scenarios — Session, Signed Measurements, Unsigned Measurements, Challenge, Heartbeat, Key Update — across SPDM 1.2, 1.3, and 1.4 (18 tests total). ## API Reference @@ -177,22 +112,56 @@ make | `wolfSPDM_GetCtxSize()` | Return `sizeof(WOLFSPDM_CTX)` at runtime | | `wolfSPDM_SetIO()` | Set transport I/O callback | | `wolfSPDM_SetDebug()` | Enable/disable debug output | -| `wolfSPDM_Connect()` | Full SPDM handshake | +| `wolfSPDM_SetMaxVersion()` | Runtime cap on the highest SPDM version to negotiate | +| `wolfSPDM_Connect()` | Full SPDM handshake (`GET_VERSION` -> `FINISH`) | | `wolfSPDM_IsConnected()` | Check session status | | `wolfSPDM_Disconnect()` | End session | -| `wolfSPDM_EncryptMessage()` | Encrypt outgoing message | -| `wolfSPDM_DecryptMessage()` | Decrypt incoming message | -| `wolfSPDM_SecuredExchange()` | Combined send/receive | -| `wolfSPDM_SetTrustedCAs()` | Load trusted root CA certificates for chain validation | +| `wolfSPDM_SecuredExchange()` | Combined encrypted send/receive | +| `wolfSPDM_SendData()` / `wolfSPDM_ReceiveData()` | Application data over an established session | +| `wolfSPDM_SetTrustedCAs()` | Load the trusted root CA certificate for chain validation | | `wolfSPDM_GetMeasurements()` | Retrieve device measurements with optional signature verification | -| `wolfSPDM_GetMeasurementCount()` | Get number of measurement blocks retrieved | -| `wolfSPDM_GetMeasurementBlock()` | Access individual measurement block data | -| `wolfSPDM_Challenge()` | Sessionless device attestation via CHALLENGE/CHALLENGE_AUTH | -| `wolfSPDM_Heartbeat()` | Session keep-alive (HEARTBEAT/HEARTBEAT_ACK) | -| `wolfSPDM_KeyUpdate()` | Rotate session encryption keys (KEY_UPDATE/KEY_UPDATE_ACK) | -| `wolfSPDM_SendData()` | Send application data over established session | -| `wolfSPDM_ReceiveData()` | Receive application data over established session | +| `wolfSPDM_GetMeasurementCount()` / `wolfSPDM_GetMeasurementBlock()` | Access individual measurement block data | +| `wolfSPDM_Challenge()` | Sessionless device attestation via `CHALLENGE` / `CHALLENGE_AUTH` | +| `wolfSPDM_Heartbeat()` | Session keep-alive (`HEARTBEAT` / `HEARTBEAT_ACK`) | +| `wolfSPDM_KeyUpdate()` | Rotate session encryption keys (`KEY_UPDATE` / `KEY_UPDATE_ACK`) | +| `wolfSPDM_GetSessionId()` | Combined req/rsp session ID; available from `KEY_EXCHANGE_RSP` onward so I/O callbacks can distinguish the encrypted `FINISH` record from plaintext handshake messages | +| `wolfSPDM_GetNegotiatedVersion()` | Negotiated SPDM version (e.g. 0x12, 0x13, 0x14). The old spelling `wolfSPDM_GetVersion_Negotiated` is kept as an exported ABI-compat alias | +| `wolfSPDM_GetLastPeerError()` | Last `SPDM_ERROR` code received from the responder, for retry/backoff logic | + +## CI / Testing + +Runs on every push and PR: + +- **Build + Test**: Ubuntu 22.04 / 24.04, debug and release, static-mem and `--enable-dynamic-mem` +- **Multi-compiler**: GCC 11-13 and Clang 14-17 with `-Wall -Wextra -Werror` +- **Compiler Warnings**: strict `-Wpedantic -Werror -Wconversion -Wshadow` +- **Static Analysis**: cppcheck and Clang Static Analyzer (`scan-build`) +- **CodeQL Security**: weekly + per-PR analysis +- **Memory Check**: Valgrind `--leak-check=full` (static and dynamic mem) +- **SPDM Emulator Integration**: 18-test matrix (6 scenarios x SPDM 1.2 / 1.3 / 1.4) across ubuntu-22.04 x64, ubuntu-24.04 x64, and ubuntu-24.04-arm aarch64 +- **Skoll review**: wolfSSL deep-review pipeline, pre-merge security and code review + +## Relationship to wolfTPM's SPDM + +wolfTPM ships its own SPDM implementation in `src/spdm/` for hardware-backed responders (Nuvoton NPCT75x, NSING NS350) with PSK / TCG-binding extensions. **wolfSPDM is a separate implementation** focused on the standard DSP0274 / DSP0277 requester for embedded use with `spdm-emu` and any standards-compliant peer. The two share heritage but solve different problems: + +| | wolfSPDM | wolfTPM `src/spdm/` | +|---|---|---| +| Role | Requester only | Requester + responder | +| Scope | Pure standard SPDM 1.2 / 1.3 / 1.4 | Same, plus PSK / TCG / Nuvoton / Nations vendor bindings | +| Target | Embedded / spdm-emu / generic SPDM peer | TPM hardware (Nuvoton, NS350) | +| Footprint | ~32 KB context, zero-malloc | Larger; includes TPM stack | + +Either library can be used standalone; they aren't link-time compatible. ## License -GPLv3 — see LICENSE file. Copyright (C) 2006-2025 wolfSSL Inc. +wolfSPDM is free software licensed under the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html). + +Copyright (C) 2006-2026 wolfSSL Inc. + +## Support + +> **Note:** wolfSPDM is currently maintained by wolfSSL developers but is not yet classified as an officially supported product. It was designed from the ground up to meet the same quality standards as the rest of the wolfSSL suite with future adoption in mind. We are eager to transition this to a fully supported product as demand grows; if your organization requires official support, has specific feature requirements, or just has general questions or guidance with the product, please reach out. + +For commercial licensing, professional support contracts, or to discuss moving wolfSPDM into your production environment, contact [wolfSSL](https://www.wolfssl.com/contact/). diff --git a/config.h.in b/config.h.in index cd10d1b..c20a396 100644 --- a/config.h.in +++ b/config.h.in @@ -71,9 +71,6 @@ /* Enable dynamic memory allocation */ #undef WOLFSPDM_DYNAMIC_MEMORY -/* Enable Nuvoton TPM support */ -#undef WOLFSPDM_NUVOTON - /* 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. */ diff --git a/configure.ac b/configure.ac index 4884765..66ab506 100644 --- a/configure.ac +++ b/configure.ac @@ -54,17 +54,6 @@ else CFLAGS="$CFLAGS -O2" fi -# Nuvoton TPM support (TCG binding + vendor commands) -AC_ARG_ENABLE([nuvoton], - [AS_HELP_STRING([--enable-nuvoton], [Enable Nuvoton TPM support (TCG binding headers, vendor commands)])], - [enable_nuvoton=$enableval], - [enable_nuvoton=no]) - -if test "x$enable_nuvoton" = "xyes"; then - AC_DEFINE([WOLFSPDM_NUVOTON], [1], [Enable Nuvoton TPM support]) -fi -AM_CONDITIONAL([BUILD_NUVOTON], [test "x$enable_nuvoton" = "xyes"]) - # Dynamic memory allocation (enables wolfSPDM_New/XMALLOC) AC_ARG_ENABLE([dynamic-mem], [AS_HELP_STRING([--enable-dynamic-mem], [Enable dynamic memory allocation (wolfSPDM_New)])], @@ -83,7 +72,6 @@ echo "" echo "wolfSPDM configuration summary:" echo " Version: $PACKAGE_VERSION" echo " Debug: $enable_debug" -echo " Nuvoton: $enable_nuvoton" echo " Dynamic mem: $enable_dynamic_mem" echo " wolfSSL: ${WOLFSSL_DIR:-system}" echo "" diff --git a/docs/ATTESTATION.md b/docs/ATTESTATION.md new file mode 100644 index 0000000..a8aa05e --- /dev/null +++ b/docs/ATTESTATION.md @@ -0,0 +1,144 @@ +# wolfSPDM Attestation (GET_MEASUREMENTS) + +## Overview + +wolfSPDM supports SPDM 1.2 device attestation via GET_MEASUREMENTS with +optional cryptographic signature verification. This enables a requester to +retrieve firmware/hardware measurement blocks from any SPDM-capable device +and verify their authenticity. + +The same protocol is used by: +- **GPUs** - NVIDIA uses SPDM over MCTP for GPU attestation +- **NICs** - BMC/iLO verify network cards +- **SSDs** - NVMe SPDM over DOE (Solidigm D7 series, etc.) +- **CXL memory devices** +- **Any PCIe device with CMA** (Component Measurement and Authentication) + +## API Reference + +### wolfSPDM_GetMeasurements + +```c +int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, + int requestSignature); +``` + +Retrieves measurements from the SPDM responder. + +**Parameters:** +- `measOperation` - `SPDM_MEAS_OPERATION_ALL` (0xFF) for all measurements, + or a specific 1-based index +- `requestSignature` - 1 to request signed measurements, 0 for unsigned + +**Return values:** +- `WOLFSPDM_SUCCESS` - Measurements retrieved and signature **verified** +- `WOLFSPDM_E_MEAS_NOT_VERIFIED` - Measurements retrieved but not verified + (unsigned request, or `NO_WOLFSPDM_MEAS_VERIFY` compiled out) +- `WOLFSPDM_E_MEAS_SIG_FAIL` - Signature verification **failed** + +### wolfSPDM_GetMeasurementCount / wolfSPDM_GetMeasurementBlock + +```c +int wolfSPDM_GetMeasurementCount(WOLFSPDM_CTX* ctx); +int wolfSPDM_GetMeasurementBlock(WOLFSPDM_CTX* ctx, int blockIdx, + byte* measIndex, byte* measType, byte* value, word32* valueSz); +``` + +Access individual measurement blocks after retrieval. + +### wolfSPDM_SendData / wolfSPDM_ReceiveData + +```c +int wolfSPDM_SendData(WOLFSPDM_CTX* ctx, const byte* data, word32 dataSz); +int wolfSPDM_ReceiveData(WOLFSPDM_CTX* ctx, byte* data, word32* dataSz); +``` + +Send/receive application data over an established SPDM session. + +## Build Configurations + +| Configuration | Flag | Effect | +|---|---|---| +| Full (default) | - | Measurement retrieval + signature verification | +| No verification | `-DNO_WOLFSPDM_MEAS_VERIFY` | Retrieval only, no sig verify | +| No measurements | `-DNO_WOLFSPDM_MEAS` | Zero measurement code/RAM | + +## Signature Verification + +When `requestSignature=1` and verification is compiled in: + +1. wolfSPDM sends GET_MEASUREMENTS with a 32-byte nonce +2. Responder returns measurements + ECDSA P-384 signature +3. wolfSPDM computes the L1/L2 transcript hash: + - L1 = GET_MEASUREMENTS request (complete message) + - L2 = MEASUREMENTS response (everything before the Signature field) + - Per DSP0274: M = combined_spdm_prefix || zero_pad || context_str || Hash(L1||L2) + - SignedData = Hash(M) +4. Verifies the signature using the responder's public key (extracted + from the certificate chain during `wolfSPDM_Connect()`) + +The nonce prevents replay attacks - each measurement request uses a fresh +random nonce that is included in the signed response. + +## Testing with spdm-emu + +Build and install [spdm-emu](https://github.com/DMTF/spdm-emu): + +```bash +git clone https://github.com/DMTF/spdm-emu.git +cd spdm-emu && mkdir build && cd build +cmake .. && make +``` + +Run the automated test script in this repo: + +```bash +# Runs 18 tests: session, signed/unsigned measurements, challenge, +# heartbeat, key update, each across SPDM 1.2 / 1.3 / 1.4. +export SPDM_EMU_PATH=../spdm-emu/build/bin +./examples/spdm_test.sh +``` + +The script automatically finds `spdm_responder_emu` via `SPDM_EMU_PATH` (or +`../spdm-emu/build/bin/`), starts/stops it per test, and verifies all the +test cases. + +Expected output with `--meas`: +``` +=== SPDM GET_MEASUREMENTS === +Measurements retrieved and signature VERIFIED +Measurement blocks: 8 + [1] type=0x00 size=48: + [2] type=0x01 size=48: + ... +``` + +With `--meas --no-sig`: +``` +=== SPDM GET_MEASUREMENTS === +Measurements retrieved (not signature-verified) +Measurement blocks: 8 + ... +``` + +## Standalone Demo + +`examples/spdm_demo` exercises the measurement flow against spdm-emu: + +| Flag | Description | +|---|---| +| `--meas` | Establish session + retrieve all measurements with signature verification | +| `--no-sig` | Skip signature verification (use with `--meas`) | + +## Security Notes + +- **Signed + verified measurements** provide cryptographic proof that + firmware/hardware state came from the authenticated responder +- **Replay protection** via fresh 32-byte nonce per request +- **Certificate chain validation** (chain-of-trust to root CA, expiry, + revocation) is NOT yet implemented - signature is verified against the + leaf certificate from session establishment. Full chain validation is + a follow-up item +- **Parser security**: All fields from the untrusted responder are + bounds-checked before use. Malformed responses return + `WOLFSPDM_E_MEASUREMENT` diff --git a/examples/spdm_demo.c b/examples/spdm_demo.c new file mode 100644 index 0000000..f35f833 --- /dev/null +++ b/examples/spdm_demo.c @@ -0,0 +1,459 @@ +/* spdm_demo.c + * + * wolfSPDM emulator demo - drives spdm-emu over TCP/MCTP for end-to-end + * testing of session, measurements, challenge, heartbeat, and key update. + * + * Usage: + * spdm_demo --emu [--ver 1.2|1.3|1.4] + * spdm_demo --meas [--no-sig] [--ver ...] + * spdm_demo --challenge [--ver ...] + * spdm_demo --heartbeat [--ver ...] + * spdm_demo --key-update [--ver ...] + * + * Picks up the spdm-emu install dir from $SPDM_EMU_PATH (used to find the + * ca.cert.der for --challenge). + */ + +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#include +#include +#include +#include +#include +#define HAS_SOCKET 1 +#endif + +#define EMU_HOST "127.0.0.1" +#define EMU_PORT 2323 + +#ifdef HAS_SOCKET + +typedef struct { + int sockFd; +} TCP_CTX; + +static TCP_CTX g_tcpCtx = { -1 }; + +/* A secured SPDM record starts with the 4-byte session ID; a plain SPDM + * message starts with a version byte. Look up the live session ID from + * the wolfSPDM context: before KEY_EXCHANGE_RSP it's 0, after it matches + * the first 4 bytes of every secured record. Robust against non-default + * reqSessionId picks, unlike a buf[0] range check. */ +static int is_secured_spdm(WOLFSPDM_CTX* ctx, const byte* buf, word32 sz) +{ + word32 sid; + word32 b0; + if (sz < 4) return 0; + sid = wolfSPDM_GetSessionId(ctx); + if (sid == 0) return 0; + b0 = (word32)buf[0] | ((word32)buf[1] << 8) | + ((word32)buf[2] << 16) | ((word32)buf[3] << 24); + return b0 == sid; +} + +/* MCTP transport I/O callback for spdm-emu (--trans MCTP, the default) */ +static int tcp_io_callback(WOLFSPDM_CTX* ctx, + const byte* txBuf, word32 txSz, + byte* rxBuf, word32* rxSz, + void* userCtx) +{ + TCP_CTX* tcpCtx = (TCP_CTX*)userCtx; + byte sendBuf[4096]; + byte recvHdr[12]; + ssize_t sent, recvd; + word32 payloadSz, respSize; + + if (tcpCtx == NULL || tcpCtx->sockFd < 0) { + return -1; + } + + /* Bound txSz first so the +1/+12 additions can't overflow word32. */ + if (txSz > sizeof(sendBuf) - 13) { + return -1; + } + payloadSz = 1 + txSz; + + /* Socket header: command(4,BE) + transport_type(4,BE) + size(4,BE) */ + sendBuf[0] = 0x00; sendBuf[1] = 0x00; sendBuf[2] = 0x00; sendBuf[3] = 0x01; + sendBuf[4] = 0x00; sendBuf[5] = 0x00; sendBuf[6] = 0x00; sendBuf[7] = 0x01; + sendBuf[8] = (byte)(payloadSz >> 24); + sendBuf[9] = (byte)(payloadSz >> 16); + sendBuf[10] = (byte)(payloadSz >> 8); + sendBuf[11] = (byte)(payloadSz & 0xFF); + + /* MCTP message type: 0x05 = SPDM, 0x06 = Secured SPDM. */ + sendBuf[12] = is_secured_spdm(ctx, txBuf, txSz) ? 0x06 : 0x05; + + if (txSz > 0) { + memcpy(sendBuf + 13, txBuf, txSz); + } + + sent = send(tcpCtx->sockFd, sendBuf, 12 + payloadSz, 0); + if (sent != (ssize_t)(12 + payloadSz)) { + return -1; + } + + recvd = recv(tcpCtx->sockFd, recvHdr, 12, MSG_WAITALL); + if (recvd != 12) return -1; + + respSize = ((word32)recvHdr[8] << 24) | ((word32)recvHdr[9] << 16) | + ((word32)recvHdr[10] << 8) | (word32)recvHdr[11]; + + if (respSize < 1 || respSize - 1 > *rxSz) { + return -1; + } + + /* Skip MCTP header byte */ + { + byte mctpHdr; + recvd = recv(tcpCtx->sockFd, &mctpHdr, 1, MSG_WAITALL); + if (recvd != 1) return -1; + } + + *rxSz = respSize - 1; + if (*rxSz > 0) { + recvd = recv(tcpCtx->sockFd, rxBuf, *rxSz, MSG_WAITALL); + if (recvd != (ssize_t)*rxSz) return -1; + } + return 0; +} + +static int tcp_connect(const char* host, int port) +{ + int sockFd; + struct sockaddr_in addr; + int optVal = 1; + + sockFd = socket(AF_INET, SOCK_STREAM, 0); + if (sockFd < 0) return -1; + + setsockopt(sockFd, IPPROTO_TCP, TCP_NODELAY, &optVal, sizeof(optVal)); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { + close(sockFd); + return -1; + } + if (connect(sockFd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + close(sockFd); + return -1; + } + g_tcpCtx.sockFd = sockFd; + return 0; +} + +static void tcp_disconnect(void) +{ + if (g_tcpCtx.sockFd >= 0) { + close(g_tcpCtx.sockFd); + g_tcpCtx.sockFd = -1; + } +} + +/* Load DER from file. Returns malloc'd buffer; caller frees. */ +static byte* load_der(const char* path, word32* outSz) +{ + FILE* f = fopen(path, "rb"); + long sz; + byte* buf; + size_t r; + + if (f == NULL) return NULL; + if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; } + sz = ftell(f); + if (sz <= 0) { fclose(f); return NULL; } + rewind(f); + + buf = (byte*)malloc((size_t)sz); + if (buf == NULL) { fclose(f); return NULL; } + + r = fread(buf, 1, (size_t)sz, f); + fclose(f); + if (r != (size_t)sz) { free(buf); return NULL; } + + *outSz = (word32)sz; + return buf; +} + +/* Static-mode buffer for the SPDM context; sized by the public header. */ +static byte g_ctxBuf[WOLFSPDM_CTX_STATIC_SIZE]; +#define CTX_BUF_SIZE ((int)sizeof(g_ctxBuf)) + +enum { + MODE_SESSION = 1, /* --emu */ + MODE_MEAS, /* --meas */ + MODE_CHALLENGE, /* --challenge */ + MODE_HEARTBEAT, /* --heartbeat */ + MODE_KEY_UPDATE /* --key-update */ +}; + +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" + "\n" + "Env:\n" + " SPDM_EMU_PATH path to spdm-emu build/bin/ (used for trusted CA\n" + " lookup in --challenge mode)\n", + argv0); +} + +/* Map "1.2"/"1.3"/"1.4" -> 0x12/0x13/0x14. Returns 0 on parse error. */ +static byte parse_version(const char* s) +{ + if (s == NULL) return 0; + if (strcmp(s, "1.2") == 0) return SPDM_VERSION_12; + if (strcmp(s, "1.3") == 0) return SPDM_VERSION_13; + if (strcmp(s, "1.4") == 0) return SPDM_VERSION_14; + return 0; +} + +static int load_trusted_ca(WOLFSPDM_CTX* ctx) +{ + const char* emuPath = getenv("SPDM_EMU_PATH"); + char path[512]; + byte* der; + word32 derSz; + int rc; + + if (emuPath == NULL) { + fprintf(stderr, "ERROR: SPDM_EMU_PATH not set; cannot locate " + "ca.cert.der for --challenge\n"); + return -1; + } + snprintf(path, sizeof(path), "%s/ecp384/ca.cert.der", emuPath); + + der = load_der(path, &derSz); + if (der == NULL) { + fprintf(stderr, "ERROR: cannot read %s\n", path); + return -1; + } + rc = wolfSPDM_SetTrustedCAs(ctx, der, derSz); + free(der); + if (rc != WOLFSPDM_SUCCESS) { + fprintf(stderr, "ERROR: wolfSPDM_SetTrustedCAs: %s (%d)\n", + wolfSPDM_GetErrorString(rc), rc); + return -1; + } + return 0; +} + +static int do_session(WOLFSPDM_CTX* ctx) +{ + int rc = wolfSPDM_Connect(ctx); + if (rc != WOLFSPDM_SUCCESS) { + fprintf(stderr, "wolfSPDM_Connect: %s (%d)\n", + wolfSPDM_GetErrorString(rc), rc); + return rc; + } + printf("Session established (id=0x%08x, version=0x%02x)\n", + wolfSPDM_GetSessionId(ctx), + wolfSPDM_GetNegotiatedVersion(ctx)); + return WOLFSPDM_SUCCESS; +} + +static int do_meas(WOLFSPDM_CTX* ctx, int withSig) +{ + int rc; + + rc = do_session(ctx); + if (rc != WOLFSPDM_SUCCESS) return rc; + + rc = wolfSPDM_GetMeasurements(ctx, SPDM_MEAS_OPERATION_ALL, withSig); + if (withSig) { + if (rc != WOLFSPDM_SUCCESS) { + fprintf(stderr, "GetMeasurements (signed): %s (%d)\n", + wolfSPDM_GetErrorString(rc), rc); + return rc; + } + printf("Signed measurements verified (%d blocks)\n", + wolfSPDM_GetMeasurementCount(ctx)); + } + else { + /* Unsigned: NOT_VERIFIED is the expected success return */ + if (rc != WOLFSPDM_SUCCESS && rc != WOLFSPDM_E_MEAS_NOT_VERIFIED) { + fprintf(stderr, "GetMeasurements (unsigned): %s (%d)\n", + wolfSPDM_GetErrorString(rc), rc); + return rc; + } + printf("Unsigned measurements received (%d blocks)\n", + wolfSPDM_GetMeasurementCount(ctx)); + rc = WOLFSPDM_SUCCESS; + } + return rc; +} + +static int do_challenge(WOLFSPDM_CTX* ctx) +{ + int rc; + + /* Sessionless: walk through GET_VERSION -> CAPABILITIES -> ALGORITHMS -> + * GET_DIGESTS -> GET_CERTIFICATE, then CHALLENGE. We don't call + * wolfSPDM_Connect() because that does KEY_EXCHANGE + FINISH. */ + rc = wolfSPDM_GetVersion(ctx); + if (rc != WOLFSPDM_SUCCESS) goto done; + rc = wolfSPDM_GetCapabilities(ctx); + if (rc != WOLFSPDM_SUCCESS) goto done; + rc = wolfSPDM_NegotiateAlgorithms(ctx); + if (rc != WOLFSPDM_SUCCESS) goto done; + rc = wolfSPDM_GetDigests(ctx); + if (rc != WOLFSPDM_SUCCESS) goto done; + rc = wolfSPDM_GetCertificate(ctx, 0); + if (rc != WOLFSPDM_SUCCESS) goto done; + + rc = load_trusted_ca(ctx); + if (rc != 0) { rc = WOLFSPDM_E_INVALID_ARG; goto done; } + + /* wolfSPDM_Challenge internally validates the cert chain against the + * loaded CAs when flags.hasTrustedCAs is set. */ + rc = wolfSPDM_Challenge(ctx, 0, SPDM_MEAS_SUMMARY_HASH_ALL); + if (rc == WOLFSPDM_SUCCESS) { + printf("Challenge succeeded (signature verified)\n"); + } +done: + if (rc != WOLFSPDM_SUCCESS) { + fprintf(stderr, "Challenge flow failed: %s (%d)\n", + wolfSPDM_GetErrorString(rc), rc); + } + return rc; +} + +static int do_heartbeat(WOLFSPDM_CTX* ctx) +{ + int rc = do_session(ctx); + if (rc != WOLFSPDM_SUCCESS) return rc; + rc = wolfSPDM_Heartbeat(ctx); + if (rc == WOLFSPDM_SUCCESS) { + printf("Heartbeat ACK received\n"); + } + else { + fprintf(stderr, "Heartbeat: %s (%d)\n", + wolfSPDM_GetErrorString(rc), rc); + } + return rc; +} + +static int do_key_update(WOLFSPDM_CTX* ctx) +{ + int rc = do_session(ctx); + if (rc != WOLFSPDM_SUCCESS) return rc; + rc = wolfSPDM_KeyUpdate(ctx, 1); /* rotate both directions */ + if (rc == WOLFSPDM_SUCCESS) { + printf("Key update succeeded\n"); + } + else { + fprintf(stderr, "KeyUpdate: %s (%d)\n", + wolfSPDM_GetErrorString(rc), rc); + } + return rc; +} + +int main(int argc, char* argv[]) +{ + static const struct option longOpts[] = { + { "emu", no_argument, 0, 'e' }, + { "meas", no_argument, 0, 'm' }, + { "no-sig", no_argument, 0, 'n' }, + { "challenge", no_argument, 0, 'c' }, + { "heartbeat", no_argument, 0, 'b' }, + { "key-update", no_argument, 0, 'k' }, + { "ver", required_argument, 0, 'v' }, + { "help", no_argument, 0, 'h' }, + { 0, 0, 0, 0 } + }; + int mode = 0; + int withSig = 1; + 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) { + switch (opt) { + case 'e': mode = MODE_SESSION; break; + case 'm': mode = MODE_MEAS; break; + case 'n': withSig = 0; break; + case 'c': mode = MODE_CHALLENGE; break; + case 'b': mode = MODE_HEARTBEAT; break; + case 'k': mode = MODE_KEY_UPDATE; break; + case 'v': + maxVer = parse_version(optarg); + if (maxVer == 0) { + fprintf(stderr, "Invalid --ver %s (expected 1.2/1.3/1.4)\n", + optarg); + return 1; + } + break; + case 'h': usage(argv[0]); return 0; + default: usage(argv[0]); return 1; + } + } + if (mode == 0) { usage(argv[0]); return 1; } + + if (wolfSPDM_GetCtxSize() > CTX_BUF_SIZE) { + fprintf(stderr, "ERROR: CTX_BUF_SIZE too small (%d needed)\n", + wolfSPDM_GetCtxSize()); + return 1; + } + + if (tcp_connect(EMU_HOST, EMU_PORT) < 0) { + fprintf(stderr, "ERROR: cannot connect to %s:%d (is spdm-emu running?)\n", + EMU_HOST, EMU_PORT); + return 1; + } + + rc = wolfSPDM_InitStatic(ctx, CTX_BUF_SIZE); + if (rc != WOLFSPDM_SUCCESS) { + fprintf(stderr, "wolfSPDM_InitStatic: %s\n", + wolfSPDM_GetErrorString(rc)); + tcp_disconnect(); + return 1; + } + + wolfSPDM_SetIO(ctx, tcp_io_callback, &g_tcpCtx); + + if (maxVer != 0) { + rc = wolfSPDM_SetMaxVersion(ctx, maxVer); + if (rc != WOLFSPDM_SUCCESS) { + fprintf(stderr, "wolfSPDM_SetMaxVersion: %s\n", + wolfSPDM_GetErrorString(rc)); + goto done; + } + } + + switch (mode) { + case MODE_SESSION: rc = do_session(ctx); break; + case MODE_MEAS: rc = do_meas(ctx, withSig); break; + case MODE_CHALLENGE: rc = do_challenge(ctx); break; + case MODE_HEARTBEAT: rc = do_heartbeat(ctx); break; + case MODE_KEY_UPDATE: rc = do_key_update(ctx); break; + default: rc = -1; break; + } + + if (rc == WOLFSPDM_SUCCESS && wolfSPDM_IsConnected(ctx)) { + wolfSPDM_Disconnect(ctx); + } + +done: + wolfSPDM_Free(ctx); + tcp_disconnect(); + return (rc == WOLFSPDM_SUCCESS) ? 0 : 1; +} + +#else /* !HAS_SOCKET */ +int main(void) +{ + fprintf(stderr, "spdm_demo: socket support unavailable on this platform\n"); + return 1; +} +#endif diff --git a/examples/spdm_test.sh b/examples/spdm_test.sh new file mode 100755 index 0000000..99551db --- /dev/null +++ b/examples/spdm_test.sh @@ -0,0 +1,260 @@ +#!/bin/bash +# +# spdm_test.sh - SPDM emulator test script +# +# Tests SPDM protocol with libspdm emulator (session + measurements + challenge +# + heartbeat + key update) across SPDM versions 1.2, 1.3, and 1.4. +# +# Usage: +# ./spdm_test.sh # Run emulator tests +# ./spdm_test.sh [path-to-spdm_demo] # Custom spdm_demo path +# + +SPDM_DEMO="./examples/spdm_demo" +PASS=0 +FAIL=0 +TOTAL=0 +EMU_PID="" +EMU_LOG="/tmp/spdm_emu_$$.log" + +# Colors (if terminal supports it) +if [ -t 1 ]; then + GREEN='\033[0;32m' + RED='\033[0;31m' + YELLOW='\033[0;33m' + NC='\033[0m' +else + GREEN='' + RED='' + YELLOW='' + NC='' +fi + +usage() { + echo "Usage: $0 [path-to-spdm_demo]" + echo "" + echo "Runs SPDM emulator tests (session, measurements, challenge," + echo "heartbeat, key update) across SPDM versions 1.2, 1.3, and 1.4." + echo "" + echo "Expects spdm_responder_emu to be found via:" + echo " 1. SPDM_EMU_PATH environment variable" + echo " 2. ../spdm-emu/build/bin/ (cloned next to wolfSPDM)" + echo " 3. spdm_responder_emu in PATH" +} + +# Parse arguments +for arg in "$@"; do + case "$arg" in + -h|--help) + usage + exit 0 + ;; + *) + # Treat as path to spdm_demo + SPDM_DEMO="$arg" + ;; + esac +done + +# Find spdm_responder_emu +find_emu() { + # 1. Check SPDM_EMU_PATH + if [ -n "$SPDM_EMU_PATH" ]; then + if [ -x "$SPDM_EMU_PATH/spdm_responder_emu" ]; then + EMU_DIR="$SPDM_EMU_PATH" + EMU_BIN="$SPDM_EMU_PATH/spdm_responder_emu" + return 0 + elif [ -x "$SPDM_EMU_PATH" ]; then + EMU_DIR="$(dirname "$SPDM_EMU_PATH")" + EMU_BIN="$SPDM_EMU_PATH" + return 0 + fi + fi + + # 2. Check common relative paths + for dir in \ + "../spdm-emu/build/bin" \ + "../../spdm-emu/build/bin" \ + "$HOME/spdm-emu/build/bin"; do + if [ -x "$dir/spdm_responder_emu" ]; then + EMU_DIR="$dir" + EMU_BIN="$dir/spdm_responder_emu" + return 0 + fi + done + + # 3. Check PATH + if command -v spdm_responder_emu >/dev/null 2>&1; then + EMU_BIN="$(command -v spdm_responder_emu)" + EMU_DIR="$(dirname "$EMU_BIN")" + return 0 + fi + + return 1 +} + +# Start the emulator (must run from its bin dir for cert files) +# Usage: start_emu [version] +start_emu() { + local ver="${1:-1.2}" + echo " Starting spdm_responder_emu (SPDM $ver)..." + + # Reap any emulator we started ourselves earlier (do NOT kill unrelated + # spdm_responder_emu instances - a developer may have one in another + # shell). Only the previous $EMU_PID is fair game. + if [ -n "$EMU_PID" ] && kill -0 "$EMU_PID" 2>/dev/null; then + kill -9 "$EMU_PID" 2>/dev/null + wait "$EMU_PID" 2>/dev/null + EMU_PID="" + sleep 1 + fi + + # If port 2323 is still occupied, it isn't ours - surface that clearly + # rather than kicking the unrelated holder off the port. + if ss -tlnp 2>/dev/null | grep -q ":2323 "; then + echo -e " ${RED}ERROR: Port 2323 already in use by another process${NC}" + ss -tlnp 2>/dev/null | grep ":2323 " + return 1 + fi + + # Verify cert/key files exist in EMU_DIR (spdm-emu uses lowercase 'ecp384') + if [ ! -d "$EMU_DIR/ecp384" ] && [ ! -d "$EMU_DIR/EcP384" ]; then + echo -e " ${YELLOW}WARNING: Certificate files may be missing in $EMU_DIR${NC}" + echo " Run 'make copy_sample_key' in the spdm-emu build directory" + fi + + (cd "$EMU_DIR" && ./spdm_responder_emu --ver "$ver" \ + --hash SHA_384 --asym ECDSA_P384 \ + --dhe SECP_384_R1 --aead AES_256_GCM >"$EMU_LOG" 2>&1) & + EMU_PID=$! + sleep 2 + + # Verify it started + if ! kill -0 "$EMU_PID" 2>/dev/null; then + echo -e " ${RED}ERROR: Emulator failed to start${NC}" + if [ -s "$EMU_LOG" ]; then + echo " Emulator output:" + sed 's/^/ /' "$EMU_LOG" | head -20 + fi + EMU_PID="" + return 1 + fi + return 0 +} + +# Stop the emulator +stop_emu() { + if [ -n "$EMU_PID" ]; then + kill "$EMU_PID" 2>/dev/null + wait "$EMU_PID" 2>/dev/null + EMU_PID="" + fi +} + +# Cleanup on exit +cleanup() { + stop_emu + rm -f "$EMU_LOG" +} +trap cleanup EXIT + +# Run a test (start/stop emulator around each test) +# Usage: run_test +run_test() { + local name="$1" + local emu_ver="$2" + shift 2 + + TOTAL=$((TOTAL + 1)) + echo "[$TOTAL] $name" + + if ! start_emu "$emu_ver"; then + echo -e " ${RED}FAIL (emulator start)${NC}" + FAIL=$((FAIL + 1)) + echo "" + return 1 + fi + + if "$@"; then + echo -e " ${GREEN}PASS${NC}" + PASS=$((PASS + 1)) + else + echo -e " ${RED}FAIL${NC}" + FAIL=$((FAIL + 1)) + fi + + stop_emu + sleep 1 # Let port release + echo "" +} + +# Check spdm_demo exists +if [ ! -x "$SPDM_DEMO" ]; then + echo "Error: $SPDM_DEMO not found or not executable" + usage + exit 1 +fi + +# ========================================================================== +# Emulator Tests +# ========================================================================== +echo "=== SPDM Emulator Tests ===" + +if ! find_emu; then + echo -e "${RED}ERROR: spdm_responder_emu not found${NC}" + echo "" + echo "Set SPDM_EMU_PATH or clone spdm-emu next to wolfSPDM:" + echo " git clone https://github.com/DMTF/spdm-emu.git ../spdm-emu" + echo " cd ../spdm-emu && mkdir build && cd build" + echo " cmake -DARCH=x64 -DTOOLCHAIN=GCC -DTARGET=Release -DCRYPTO=mbedtls .." + echo " make copy_sample_key && make" + exit 1 +fi + +echo "Using emulator: $EMU_BIN" +echo "Using demo: $SPDM_DEMO" +echo "" + +# Test each SPDM version (1.2, 1.3, 1.4) against the emulator +for VER in 1.2 1.3 1.4; do + echo "--- SPDM $VER ---" + + # Session establishment + run_test "Session (SPDM $VER)" "$VER" \ + "$SPDM_DEMO" --emu --ver "$VER" + + # Session + signed measurements + run_test "Signed measurements (SPDM $VER)" "$VER" \ + "$SPDM_DEMO" --meas --ver "$VER" + + # Session + unsigned measurements + run_test "Unsigned measurements (SPDM $VER)" "$VER" \ + "$SPDM_DEMO" --meas --no-sig --ver "$VER" + + # Challenge authentication (sessionless) + run_test "Challenge (SPDM $VER)" "$VER" \ + "$SPDM_DEMO" --challenge --ver "$VER" + + # Session + heartbeat + run_test "Heartbeat (SPDM $VER)" "$VER" \ + "$SPDM_DEMO" --emu --heartbeat --ver "$VER" + + # Session + key update + run_test "Key update (SPDM $VER)" "$VER" \ + "$SPDM_DEMO" --emu --key-update --ver "$VER" + + echo "" +done + +# ========================================================================== +# Summary +# ========================================================================== +echo "=== Results ===" +echo "Total: $TOTAL Passed: $PASS Failed: $FAIL" +if [ $FAIL -eq 0 ]; then + echo -e "${GREEN}ALL TESTS PASSED${NC}" + exit 0 +else + echo -e "${RED}$FAIL TEST(S) FAILED${NC}" + exit 1 +fi diff --git a/src/spdm_context.c b/src/spdm_context.c index f9b498d..19b18c2 100644 --- a/src/spdm_context.c +++ b/src/spdm_context.c @@ -20,8 +20,6 @@ */ #include "spdm_internal.h" -#include -#include #include #include @@ -35,7 +33,7 @@ int wolfSPDM_Init(WOLFSPDM_CTX* ctx) return WOLFSPDM_E_INVALID_ARG; } - /* Clean slate — do NOT read any fields before this (could be garbage) */ + /* Clean slate - do NOT read any fields before this (could be garbage) */ XMEMSET(ctx, 0, sizeof(WOLFSPDM_CTX)); ctx->state = WOLFSPDM_STATE_INIT; @@ -44,7 +42,7 @@ int wolfSPDM_Init(WOLFSPDM_CTX* ctx) if (rc != 0) { return WOLFSPDM_E_CRYPTO_FAIL; } - ctx->rngInitialized = 1; + ctx->flags.rngInitialized = 1; /* Set default requester capabilities */ ctx->reqCaps = WOLFSPDM_DEFAULT_REQ_CAPS; @@ -52,8 +50,8 @@ int wolfSPDM_Init(WOLFSPDM_CTX* ctx) /* Set default session ID (0x0001 is valid; 0x0000/0xFFFF are reserved) */ ctx->reqSessionId = 0x0001; - ctx->initialized = 1; - /* isDynamic remains 0 — only wolfSPDM_New sets it */ + ctx->flags.initialized = 1; + /* isDynamic remains 0 - only wolfSPDM_New sets it */ return WOLFSPDM_SUCCESS; } @@ -73,7 +71,7 @@ WOLFSPDM_CTX* wolfSPDM_New(void) XFREE(ctx, NULL, DYNAMIC_TYPE_TMP_BUFFER); return NULL; } - ctx->isDynamic = 1; /* Tag AFTER Init so it isn't wiped */ + ctx->flags.isDynamic = 1; /* Tag AFTER Init so it isn't wiped */ return ctx; } @@ -87,29 +85,29 @@ void wolfSPDM_Free(WOLFSPDM_CTX* ctx) #ifdef WOLFSPDM_DYNAMIC_MEMORY { - int wasDynamic = ctx->isDynamic; + int wasDynamic = ctx->flags.isDynamic; #endif /* Free RNG */ - if (ctx->rngInitialized) { + if (ctx->flags.rngInitialized) { wc_FreeRng(&ctx->rng); } /* Free ephemeral key */ - if (ctx->ephemeralKeyInitialized) { + if (ctx->flags.ephemeralKeyInit) { wc_ecc_free(&ctx->ephemeralKey); } /* Free responder public key (used for measurement/challenge verification) */ - if (ctx->hasResponderPubKey) { + if (ctx->flags.hasResponderPubKey) { wc_ecc_free(&ctx->responderPubKey); } #ifndef NO_WOLFSPDM_CHALLENGE /* Free M1/M2 challenge hash if still initialized */ - if (ctx->m1m2HashInit) { + if (ctx->flags.m1m2HashInit) { wc_Sha384Free(&ctx->m1m2Hash); - ctx->m1m2HashInit = 0; + ctx->flags.m1m2HashInit = 0; } #endif @@ -129,6 +127,18 @@ int wolfSPDM_GetCtxSize(void) return (int)sizeof(WOLFSPDM_CTX); } +/* Catch struct growth past the public WOLFSPDM_CTX_STATIC_SIZE at compile + * time rather than at wolfSPDM_InitStatic runtime. Negative array size if + * the static buffer is no longer sufficient. */ +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +_Static_assert(sizeof(struct WOLFSPDM_CTX) <= WOLFSPDM_CTX_STATIC_SIZE, + "WOLFSPDM_CTX_STATIC_SIZE must be >= sizeof(struct WOLFSPDM_CTX); " + "bump the public macro in wolfspdm/spdm.h"); +#else +typedef char wolfSPDM_ctx_static_size_check + [(sizeof(struct WOLFSPDM_CTX) <= WOLFSPDM_CTX_STATIC_SIZE) ? 1 : -1]; +#endif + int wolfSPDM_InitStatic(WOLFSPDM_CTX* ctx, int size) { if (ctx == NULL) { @@ -156,62 +166,6 @@ int wolfSPDM_SetIO(WOLFSPDM_CTX* ctx, WOLFSPDM_IO_CB ioCb, void* userCtx) return WOLFSPDM_SUCCESS; } -int wolfSPDM_SetResponderPubKey(WOLFSPDM_CTX* ctx, - const byte* pubKey, word32 pubKeySz) -{ - if (ctx == NULL || pubKey == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - if (pubKeySz != WOLFSPDM_ECC_POINT_SIZE) { - return WOLFSPDM_E_INVALID_ARG; - } - - XMEMCPY(ctx->rspPubKey, pubKey, pubKeySz); - ctx->rspPubKeyLen = pubKeySz; - ctx->hasRspPubKey = 1; - - return WOLFSPDM_SUCCESS; -} - -int wolfSPDM_SetRequesterKeyPair(WOLFSPDM_CTX* ctx, - const byte* privKey, word32 privKeySz, - const byte* pubKey, word32 pubKeySz) -{ - if (ctx == NULL || privKey == NULL || pubKey == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - if (privKeySz != WOLFSPDM_ECC_KEY_SIZE || - pubKeySz != WOLFSPDM_ECC_POINT_SIZE) { - return WOLFSPDM_E_INVALID_ARG; - } - - XMEMCPY(ctx->reqPrivKey, privKey, privKeySz); - ctx->reqPrivKeyLen = privKeySz; - XMEMCPY(ctx->reqPubKey, pubKey, pubKeySz); - ctx->reqPubKeyLen = pubKeySz; - ctx->hasReqKeyPair = 1; - - return WOLFSPDM_SUCCESS; -} - -#ifdef WOLFSPDM_NUVOTON -int wolfSPDM_SetRequesterKeyTPMT(WOLFSPDM_CTX* ctx, - const byte* tpmtPub, word32 tpmtPubSz) -{ - if (ctx == NULL || tpmtPub == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - if (tpmtPubSz > sizeof(ctx->reqPubKeyTPMT)) { - return WOLFSPDM_E_INVALID_ARG; - } - XMEMCPY(ctx->reqPubKeyTPMT, tpmtPub, tpmtPubSz); - ctx->reqPubKeyTPMTLen = tpmtPubSz; - return WOLFSPDM_SUCCESS; -} -#endif /* WOLFSPDM_NUVOTON */ - int wolfSPDM_SetTrustedCAs(WOLFSPDM_CTX* ctx, const byte* derCerts, word32 derCertsSz) { @@ -225,7 +179,7 @@ int wolfSPDM_SetTrustedCAs(WOLFSPDM_CTX* ctx, const byte* derCerts, XMEMCPY(ctx->trustedCAs, derCerts, derCertsSz); ctx->trustedCAsSz = derCertsSz; - ctx->hasTrustedCAs = 1; + ctx->flags.hasTrustedCAs = 1; return WOLFSPDM_SUCCESS; } @@ -233,38 +187,44 @@ int wolfSPDM_SetTrustedCAs(WOLFSPDM_CTX* ctx, const byte* derCerts, void wolfSPDM_SetDebug(WOLFSPDM_CTX* ctx, int enable) { if (ctx != NULL) { - ctx->debug = enable; + ctx->flags.debug = enable ? 1 : 0; } } -int wolfSPDM_SetMode(WOLFSPDM_CTX* ctx, WOLFSPDM_MODE mode) +byte wolfSPDM_GetLastPeerError(WOLFSPDM_CTX* ctx) +{ + return (ctx != NULL) ? ctx->lastPeerErrorCode : 0; +} + +/* Backwards-compat: the old function name from before the rename to + * wolfSPDM_GetNegotiatedVersion. Kept so binaries already linked against + * the old symbol still resolve. */ +byte wolfSPDM_GetVersion_Negotiated(WOLFSPDM_CTX* ctx) +{ + return wolfSPDM_GetNegotiatedVersion(ctx); +} + +int wolfSPDM_SetMaxVersion(WOLFSPDM_CTX* ctx, byte maxVersion) { if (ctx == NULL) { return WOLFSPDM_E_INVALID_ARG; } - if (mode == WOLFSPDM_MODE_NUVOTON) { -#ifdef WOLFSPDM_NUVOTON - ctx->mode = WOLFSPDM_MODE_NUVOTON; - /* Initialize Nuvoton-specific fields */ - ctx->connectionHandle = WOLFSPDM_NUVOTON_CONN_HANDLE_DEFAULT; - ctx->fipsIndicator = WOLFSPDM_NUVOTON_FIPS_DEFAULT; + /* 0 means reset to compile-time default */ + if (maxVersion == 0) { + ctx->maxVersion = 0; return WOLFSPDM_SUCCESS; -#else - return WOLFSPDM_E_INVALID_ARG; /* Nuvoton support not compiled in */ -#endif } - ctx->mode = WOLFSPDM_MODE_STANDARD; - return WOLFSPDM_SUCCESS; -} - -WOLFSPDM_MODE wolfSPDM_GetMode(WOLFSPDM_CTX* ctx) -{ - if (ctx == NULL) { - return WOLFSPDM_MODE_STANDARD; + /* Validate range. WOLFSPDM_MAX_SPDM_VERSION is the build-time ceiling + * and is authoritative: the runtime setter cannot raise it. */ + if (maxVersion < WOLFSPDM_MIN_SPDM_VERSION || + maxVersion > WOLFSPDM_MAX_SPDM_VERSION) { + return WOLFSPDM_E_INVALID_ARG; } - return ctx->mode; + + ctx->maxVersion = maxVersion; + return WOLFSPDM_SUCCESS; } /* --- Session Status --- */ @@ -279,13 +239,20 @@ int wolfSPDM_IsConnected(WOLFSPDM_CTX* ctx) word32 wolfSPDM_GetSessionId(WOLFSPDM_CTX* ctx) { - if (ctx == NULL || ctx->state != WOLFSPDM_STATE_CONNECTED) { + /* Return the negotiated session ID once KEY_EXCHANGE_RSP has set it + * (I/O callbacks need it between KEY_EXCHANGE and FINISH to tag the + * encrypted FINISH record). Restrict the exposure window to states + * where the value is actually meaningful: from KEY_EX through CONNECTED + * / MEASURED. Pre-KEY_EX or in the error state, return 0 so callers + * that test "GetSessionId() != 0" don't see a stale or transitional id. */ + if (ctx == NULL || ctx->state < WOLFSPDM_STATE_KEY_EX || + ctx->state == WOLFSPDM_STATE_ERROR) { return 0; } return ctx->sessionId; } -byte wolfSPDM_GetVersion_Negotiated(WOLFSPDM_CTX* ctx) +byte wolfSPDM_GetNegotiatedVersion(WOLFSPDM_CTX* ctx) { if (ctx == NULL || ctx->state < WOLFSPDM_STATE_VERSION) { return 0; @@ -293,24 +260,6 @@ byte wolfSPDM_GetVersion_Negotiated(WOLFSPDM_CTX* ctx) return ctx->spdmVersion; } -#ifdef WOLFSPDM_NUVOTON -word32 wolfSPDM_GetConnectionHandle(WOLFSPDM_CTX* ctx) -{ - if (ctx == NULL) { - return 0; - } - return ctx->connectionHandle; -} - -word16 wolfSPDM_GetFipsIndicator(WOLFSPDM_CTX* ctx) -{ - if (ctx == NULL) { - return 0; - } - return ctx->fipsIndicator; -} -#endif - /* --- Session Establishment - Connect (Full Handshake) --- */ /* Standard SPDM 1.2 connection flow (for libspdm emulator, etc.) */ @@ -318,8 +267,32 @@ static int wolfSPDM_ConnectStandard(WOLFSPDM_CTX* ctx) { int rc; - /* Reset state for new connection */ + /* Reset state for new connection. Drop any cached responder public + * key from a prior attempt - GetCertificate's guard otherwise skips + * re-extraction, and KEY_EXCHANGE_RSP signature verification would + * then run against the stale key from the previous responder. Also + * 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); + ctx->flags.hasResponderPubKey = 0; + } ctx->state = WOLFSPDM_STATE_INIT; + ctx->sessionId = 0; + /* Re-pick a non-reserved reqSessionId; DSP0277 reserves 0x0000 and + * 0xFFFF, so use the same default Init picked. */ + ctx->reqSessionId = 0x0001; + ctx->rspSessionId = 0; + ctx->reqSeqNum = 0; + ctx->rspSeqNum = 0; + ctx->lastPeerErrorCode = 0; +#ifndef NO_WOLFSPDM_MEAS + /* Drop stale measurement state from a prior connect so reconnect- + * without-disconnect doesn't surface old blocks. */ + ctx->measBlockCount = 0; + ctx->measSignatureSize = 0; + ctx->flags.hasMeasurements = 0; +#endif wolfSPDM_TranscriptReset(ctx); SPDM_CONNECT_STEP(ctx, "Step 1: GET_VERSION\n", @@ -333,13 +306,16 @@ static int wolfSPDM_ConnectStandard(WOLFSPDM_CTX* ctx) SPDM_CONNECT_STEP(ctx, "Step 5: GET_CERTIFICATE\n", wolfSPDM_GetCertificate(ctx, 0)); - /* Validate certificate chain if trusted CAs are loaded */ - if (ctx->hasTrustedCAs) { - SPDM_CONNECT_STEP(ctx, "", wolfSPDM_ValidateCertChain(ctx)); + /* Validate certificate chain if trusted CAs are loaded. GetCertificate + * already guarantees flags.hasResponderPubKey is set on success (returns + * an error otherwise), so we only need to gate on the CA-bundle. */ + if (ctx->flags.hasTrustedCAs) { + SPDM_CONNECT_STEP(ctx, "Validating certificate chain\n", + wolfSPDM_ValidateCertChain(ctx)); } - else if (!ctx->hasResponderPubKey) { + else { wolfSPDM_DebugPrint(ctx, - "Warning: No trusted CAs loaded — chain not validated\n"); + "Warning: No trusted CAs loaded - chain not validated\n"); } SPDM_CONNECT_STEP(ctx, "Step 6: KEY_EXCHANGE\n", @@ -360,7 +336,7 @@ int wolfSPDM_Connect(WOLFSPDM_CTX* ctx) return WOLFSPDM_E_INVALID_ARG; } - if (!ctx->initialized) { + if (!ctx->flags.initialized) { return WOLFSPDM_E_BAD_STATE; } @@ -368,13 +344,6 @@ int wolfSPDM_Connect(WOLFSPDM_CTX* ctx) return WOLFSPDM_E_IO_FAIL; } - /* Dispatch based on mode */ -#ifdef WOLFSPDM_NUVOTON - if (ctx->mode == WOLFSPDM_MODE_NUVOTON) { - return wolfSPDM_ConnectNuvoton(ctx); - } -#endif - return wolfSPDM_ConnectStandard(ctx); } @@ -404,11 +373,26 @@ int wolfSPDM_Disconnect(WOLFSPDM_CTX* ctx) rxSz = sizeof(rxBuf); rc = wolfSPDM_SecuredExchange(ctx, txBuf, txSz, rxBuf, &rxSz); - /* Reset state regardless of result */ + /* Reset state regardless of result. Free the cached responder public + * key so the next Connect re-extracts it from the (potentially new) + * 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); + ctx->flags.hasResponderPubKey = 0; + } ctx->state = WOLFSPDM_STATE_INIT; ctx->sessionId = 0; ctx->reqSeqNum = 0; ctx->rspSeqNum = 0; + ctx->lastPeerErrorCode = 0; +#ifndef NO_WOLFSPDM_MEAS + /* Drop stale measurement state so callers can't accidentally read + * blocks from the previous session after a reconnect. */ + ctx->measBlockCount = 0; + ctx->measSignatureSize = 0; + ctx->flags.hasMeasurements = 0; +#endif return (rc == WOLFSPDM_SUCCESS) ? WOLFSPDM_SUCCESS : rc; } @@ -439,7 +423,7 @@ void wolfSPDM_DebugPrint(WOLFSPDM_CTX* ctx, const char* fmt, ...) { va_list args; - if (ctx == NULL || !ctx->debug) { + if (ctx == NULL || !ctx->flags.debug) { return; } @@ -455,7 +439,7 @@ void wolfSPDM_DebugHex(WOLFSPDM_CTX* ctx, const char* label, { word32 i; - if (ctx == NULL || !ctx->debug || data == NULL) { + if (ctx == NULL || !ctx->flags.debug || data == NULL) { return; } @@ -476,7 +460,7 @@ void wolfSPDM_DebugHex(WOLFSPDM_CTX* ctx, const char* label, int wolfSPDM_GetMeasurementCount(WOLFSPDM_CTX* ctx) { - if (ctx == NULL || !ctx->hasMeasurements) { + if (ctx == NULL || !ctx->flags.hasMeasurements) { return 0; } return (int)ctx->measBlockCount; @@ -487,7 +471,7 @@ int wolfSPDM_GetMeasurementBlock(WOLFSPDM_CTX* ctx, int blockIdx, { const WOLFSPDM_MEAS_BLOCK* blk; - if (ctx == NULL || !ctx->hasMeasurements) { + if (ctx == NULL || !ctx->flags.hasMeasurements) { return WOLFSPDM_E_INVALID_ARG; } if (blockIdx < 0 || blockIdx >= (int)ctx->measBlockCount) { diff --git a/src/spdm_crypto.c b/src/spdm_crypto.c index 4018628..e6c96e3 100644 --- a/src/spdm_crypto.c +++ b/src/spdm_crypto.c @@ -20,7 +20,24 @@ */ #include "spdm_internal.h" -#include + +/* Left-pad a buffer in-place to targetSz with leading zeros. + * Returns WOLFSPDM_E_CRYPTO_FAIL if currentSz exceeds targetSz - that + * would indicate a wolfCrypt routine wrote more bytes than the protocol + * allows (P-384 should never produce more than 48 raw bytes); silently + * truncating would hide an internal-invariant violation. */ +static int wolfSPDM_LeftPadToSize(byte* buf, word32 currentSz, word32 targetSz) +{ + if (currentSz > targetSz) { + return WOLFSPDM_E_CRYPTO_FAIL; + } + if (currentSz < targetSz) { + word32 padLen = targetSz - currentSz; + XMEMMOVE(buf + padLen, buf, currentSz); + XMEMSET(buf, 0, padLen); + } + return WOLFSPDM_SUCCESS; +} /* --- Random Number Generation --- */ @@ -32,7 +49,7 @@ int wolfSPDM_GetRandom(WOLFSPDM_CTX* ctx, byte* out, word32 outSz) return WOLFSPDM_E_INVALID_ARG; } - if (!ctx->rngInitialized) { + if (!ctx->flags.rngInitialized) { return WOLFSPDM_E_BAD_STATE; } @@ -54,14 +71,14 @@ int wolfSPDM_GenerateEphemeralKey(WOLFSPDM_CTX* ctx) return WOLFSPDM_E_INVALID_ARG; } - if (!ctx->rngInitialized) { + if (!ctx->flags.rngInitialized) { return WOLFSPDM_E_BAD_STATE; } /* Free existing key if any */ - if (ctx->ephemeralKeyInitialized) { + if (ctx->flags.ephemeralKeyInit) { wc_ecc_free(&ctx->ephemeralKey); - ctx->ephemeralKeyInitialized = 0; + ctx->flags.ephemeralKeyInit = 0; } /* Initialize new key */ @@ -77,7 +94,7 @@ int wolfSPDM_GenerateEphemeralKey(WOLFSPDM_CTX* ctx) return WOLFSPDM_E_CRYPTO_FAIL; } - ctx->ephemeralKeyInitialized = 1; + ctx->flags.ephemeralKeyInit = 1; wolfSPDM_DebugPrint(ctx, "Generated P-384 ephemeral key\n"); return WOLFSPDM_SUCCESS; @@ -94,7 +111,7 @@ int wolfSPDM_ExportEphemeralPubKey(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_INVALID_ARG; } - if (!ctx->ephemeralKeyInitialized) { + if (!ctx->flags.ephemeralKeyInit) { return WOLFSPDM_E_BAD_STATE; } @@ -109,6 +126,18 @@ int wolfSPDM_ExportEphemeralPubKey(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_CRYPTO_FAIL; } + /* Left-pad coordinates to full size (wolfSSL may strip leading zeros) */ + rc = wolfSPDM_LeftPadToSize(pubKeyX, *pubKeyXSz, WOLFSPDM_ECC_KEY_SIZE); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } + *pubKeyXSz = WOLFSPDM_ECC_KEY_SIZE; + rc = wolfSPDM_LeftPadToSize(pubKeyY, *pubKeyYSz, WOLFSPDM_ECC_KEY_SIZE); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } + *pubKeyYSz = WOLFSPDM_ECC_KEY_SIZE; + return WOLFSPDM_SUCCESS; } @@ -125,7 +154,7 @@ int wolfSPDM_ComputeSharedSecret(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_INVALID_ARG; } - if (!ctx->ephemeralKeyInitialized) { + if (!ctx->flags.ephemeralKeyInit) { return WOLFSPDM_E_BAD_STATE; } @@ -156,12 +185,12 @@ int wolfSPDM_ComputeSharedSecret(WOLFSPDM_CTX* ctx, } /* Zero-pad if needed (P-384 should always return 48 bytes, but just in case) */ - if (ctx->sharedSecretSz < WOLFSPDM_ECC_KEY_SIZE) { - word32 padLen = WOLFSPDM_ECC_KEY_SIZE - ctx->sharedSecretSz; - XMEMMOVE(ctx->sharedSecret + padLen, ctx->sharedSecret, ctx->sharedSecretSz); - XMEMSET(ctx->sharedSecret, 0, padLen); - ctx->sharedSecretSz = WOLFSPDM_ECC_KEY_SIZE; + rc = wolfSPDM_LeftPadToSize(ctx->sharedSecret, ctx->sharedSecretSz, + WOLFSPDM_ECC_KEY_SIZE); + if (rc != WOLFSPDM_SUCCESS) { + goto cleanup; } + ctx->sharedSecretSz = WOLFSPDM_ECC_KEY_SIZE; wolfSPDM_DebugPrint(ctx, "ECDH shared secret computed (%u bytes)\n", ctx->sharedSecretSz); @@ -175,90 +204,3 @@ int wolfSPDM_ComputeSharedSecret(WOLFSPDM_CTX* ctx, return (rc == 0) ? WOLFSPDM_SUCCESS : WOLFSPDM_E_CRYPTO_FAIL; } - -/* --- ECDSA Signing (P-384) --- */ - -int wolfSPDM_SignHash(WOLFSPDM_CTX* ctx, const byte* hash, word32 hashSz, - byte* sig, word32* sigSz) -{ - ecc_key sigKey; - int rc; - int keyInit = 0; - byte derSig[104]; /* P-384 DER sig max: 2 + (2+49) + (2+49) = 104 */ - word32 derSigSz = sizeof(derSig); - word32 rLen, sLen; - - if (ctx == NULL || hash == NULL || sig == NULL || sigSz == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - if (!ctx->hasReqKeyPair || ctx->reqPrivKeyLen == 0) { - wolfSPDM_DebugPrint(ctx, "No requester key pair for signing\n"); - return WOLFSPDM_E_BAD_STATE; - } - - /* Need at least 96 bytes for P-384 signature (R||S) */ - if (*sigSz < WOLFSPDM_ECC_POINT_SIZE) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Initialize key structure */ - rc = wc_ecc_init(&sigKey); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "wc_ecc_init failed: %d\n", rc); - return WOLFSPDM_E_CRYPTO_FAIL; - } - keyInit = 1; - - /* Import private key and public key */ - rc = wc_ecc_import_unsigned(&sigKey, - ctx->reqPubKey, /* X coordinate */ - ctx->reqPubKey + WOLFSPDM_ECC_KEY_SIZE, /* Y coordinate */ - ctx->reqPrivKey, /* Private key (d) */ - ECC_SECP384R1); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "wc_ecc_import_unsigned failed: %d\n", rc); - goto cleanup; - } - - /* Sign the hash - wolfSSL returns DER encoded signature */ - rc = wc_ecc_sign_hash(hash, hashSz, derSig, &derSigSz, &ctx->rng, &sigKey); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "wc_ecc_sign_hash failed: %d\n", rc); - goto cleanup; - } - - /* Convert DER signature to raw R||S format (96 bytes for P-384) */ - rLen = WOLFSPDM_ECC_KEY_SIZE; - sLen = WOLFSPDM_ECC_KEY_SIZE; - rc = wc_ecc_sig_to_rs(derSig, derSigSz, sig, &rLen, - sig + WOLFSPDM_ECC_KEY_SIZE, &sLen); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "wc_ecc_sig_to_rs failed: %d\n", rc); - goto cleanup; - } - - /* Pad R and S to 48 bytes if needed */ - if (rLen < WOLFSPDM_ECC_KEY_SIZE) { - XMEMMOVE(sig + (WOLFSPDM_ECC_KEY_SIZE - rLen), sig, rLen); - XMEMSET(sig, 0, WOLFSPDM_ECC_KEY_SIZE - rLen); - } - if (sLen < WOLFSPDM_ECC_KEY_SIZE) { - XMEMMOVE(sig + WOLFSPDM_ECC_KEY_SIZE + (WOLFSPDM_ECC_KEY_SIZE - sLen), - sig + WOLFSPDM_ECC_KEY_SIZE, sLen); - XMEMSET(sig + WOLFSPDM_ECC_KEY_SIZE, 0, WOLFSPDM_ECC_KEY_SIZE - sLen); - } - - *sigSz = WOLFSPDM_ECC_POINT_SIZE; /* 96 bytes */ - - wolfSPDM_DebugPrint(ctx, "Signed hash with P-384 key (sig=%u bytes)\n", *sigSz); - - rc = 0; - -cleanup: - if (keyInit) { - wc_ecc_free(&sigKey); - } - - return (rc == 0) ? WOLFSPDM_SUCCESS : WOLFSPDM_E_CRYPTO_FAIL; -} diff --git a/src/spdm_internal.h b/src/spdm_internal.h index 0fcccfc..f9bda43 100644 --- a/src/spdm_internal.h +++ b/src/spdm_internal.h @@ -47,6 +47,19 @@ #include #include +/* Constant-time byte comparison: returns 0 iff a==b for the full length. + * Used for MAC/HMAC equality so we don't leak match position via timing. */ +static WC_INLINE int wolfSPDM_ConstCompare(const byte* a, const byte* b, + word32 len) +{ + byte diff = 0; + word32 i; + for (i = 0; i < len; i++) { + diff |= (byte)(a[i] ^ b[i]); + } + return diff; +} + #ifdef __cplusplus extern "C" { #endif @@ -67,6 +80,15 @@ extern "C" { #define WOLFSPDM_STATE_MEASURED 10 /* Measurements retrieved */ #endif +/* SPDM version bounds. Override with -DWOLFSPDM_MIN/MAX_SPDM_VERSION at + * compile time. The runtime wolfSPDM_SetMaxVersion clamps against these. */ +#ifndef WOLFSPDM_MAX_SPDM_VERSION +#define WOLFSPDM_MAX_SPDM_VERSION SPDM_VERSION_14 +#endif +#ifndef WOLFSPDM_MIN_SPDM_VERSION +#define WOLFSPDM_MIN_SPDM_VERSION SPDM_VERSION_12 +#endif + /* --- Measurement Block Structure --- */ #ifndef NO_WOLFSPDM_MEAS @@ -85,42 +107,39 @@ struct WOLFSPDM_CTX { /* State machine */ int state; - /* Configuration flags */ - int debug; - int initialized; - int isDynamic; /* Set by wolfSPDM_New(), checked by wolfSPDM_Free() */ - - /* Protocol mode (standard SPDM or Nuvoton) */ - WOLFSPDM_MODE mode; + /* Boolean flags - packed into a small bit-field struct (one 4-byte + * unsigned int holding 9 booleans, vs. 9 separate ints = 36 bytes). + * Use unsigned int (not byte) since C11 Sec. 6.7.2.1 only guarantees + * bit-field support for _Bool, signed int, and unsigned int - this + * keeps the struct portable under -Wpedantic -Werror. */ + struct { + unsigned int debug : 1; + unsigned int initialized : 1; + unsigned int isDynamic : 1; /* Set by wolfSPDM_New(), checked by Free */ + unsigned int rngInitialized : 1; + unsigned int ephemeralKeyInit : 1; + unsigned int hasMeasurements : 1; + unsigned int hasResponderPubKey : 1; + unsigned int hasTrustedCAs : 1; + unsigned int m1m2HashInit : 1; + } flags; /* I/O callback */ WOLFSPDM_IO_CB ioCb; void* ioUserCtx; -#ifdef WOLFSPDM_NUVOTON - /* Nuvoton-specific: TCG binding fields */ - word32 connectionHandle; /* Connection handle (usually 0) */ - word16 fipsIndicator; /* FIPS service indicator */ - - /* Nuvoton-specific: Host's public key in TPMT_PUBLIC format */ - byte reqPubKeyTPMT[128]; /* TPMT_PUBLIC serialized (~120 bytes) */ - word32 reqPubKeyTPMTLen; -#endif - /* Random number generator */ WC_RNG rng; - int rngInitialized; /* Negotiated parameters */ + byte maxVersion; /* Runtime max version cap (0 = use compile-time default) */ byte spdmVersion; /* Negotiated SPDM version */ + byte lastPeerErrorCode; /* Last SPDM_ERROR Param1 from responder (0 = none) */ word32 rspCaps; /* Responder capabilities */ word32 reqCaps; /* Our (requester) capabilities */ - byte mutAuthRequested; /* MutAuthRequested from KEY_EXCHANGE_RSP (offset 6) */ - byte reqSlotId; /* ReqSlotIDParam from KEY_EXCHANGE_RSP (offset 7) */ /* Ephemeral ECDHE key (generated for KEY_EXCHANGE) */ ecc_key ephemeralKey; - int ephemeralKeyInitialized; /* ECDH shared secret (P-384 X-coordinate = 48 bytes) */ byte sharedSecret[WOLFSPDM_ECC_KEY_SIZE]; @@ -162,22 +181,6 @@ struct WOLFSPDM_CTX { word16 rspSessionId; /* Responder's session ID */ word32 sessionId; /* Combined: reqSessionId | (rspSessionId << 16) */ - /* Responder's identity public key (for cert-less mode like Nuvoton) */ - byte rspPubKey[128]; /* TPMT_PUBLIC (120 bytes for P-384) or raw X||Y (96) */ - word32 rspPubKeyLen; - int hasRspPubKey; - - /* Requester's identity key pair (for mutual auth) */ - byte reqPrivKey[WOLFSPDM_ECC_KEY_SIZE]; - word32 reqPrivKeyLen; - byte reqPubKey[WOLFSPDM_ECC_POINT_SIZE]; - word32 reqPubKeyLen; - int hasReqKeyPair; - - /* Message buffers */ - byte sendBuf[WOLFSPDM_MAX_MSG_SIZE + WOLFSPDM_AEAD_TAG_SIZE]; - byte recvBuf[WOLFSPDM_MAX_MSG_SIZE + WOLFSPDM_AEAD_TAG_SIZE]; - #ifndef NO_WOLFSPDM_MEAS /* Measurement data */ WOLFSPDM_MEAS_BLOCK measBlocks[WOLFSPDM_MAX_MEAS_BLOCKS]; @@ -186,7 +189,6 @@ struct WOLFSPDM_CTX { 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 */ - int hasMeasurements; #ifndef NO_WOLFSPDM_MEAS_VERIFY /* Saved GET_MEASUREMENTS request for L1/L2 transcript */ @@ -197,16 +199,15 @@ struct WOLFSPDM_CTX { /* Responder identity for signature verification (measurements + challenge) */ ecc_key responderPubKey; /* Extracted from cert chain leaf */ - int hasResponderPubKey; /* 1 if key extracted successfully */ /* Certificate chain validation */ byte trustedCAs[WOLFSPDM_MAX_CERT_CHAIN]; /* DER-encoded root CAs */ word32 trustedCAsSz; - int hasTrustedCAs; /* 1 if CAs loaded */ #ifndef NO_WOLFSPDM_CHALLENGE /* Challenge authentication */ byte challengeNonce[32]; /* Saved nonce from CHALLENGE request */ + byte challengeReqCtx[8]; /* RequesterContext sent (1.3+) */ byte challengeMeasHashType; /* MeasurementSummaryHashType from req */ /* Running M1/M2 hash for CHALLENGE_AUTH signature verification. @@ -217,10 +218,9 @@ struct WOLFSPDM_CTX { * This hash accumulates A+B during NegAlgo/GetDigests/GetCertificate, * then C is added in VerifyChallengeAuthSig. */ wc_Sha384 m1m2Hash; - int m1m2HashInit; /* 1 if m1m2Hash is initialized */ #endif - /* Key update state — app secrets for re-derivation */ + /* Key update state - app secrets for re-derivation */ byte reqAppSecret[WOLFSPDM_HASH_SIZE]; /* 48 bytes */ byte rspAppSecret[WOLFSPDM_HASH_SIZE]; /* 48 bytes */ }; @@ -270,22 +270,11 @@ static WC_INLINE word64 SPDM_Get64LE(const byte* buf) { /* Build IV: BaseIV XOR zero-extended sequence number (DSP0277) */ static WC_INLINE void wolfSPDM_BuildIV(byte* iv, const byte* baseIv, - word64 seqNum, int nuvotonMode) + word64 seqNum) { XMEMCPY(iv, baseIv, WOLFSPDM_AEAD_IV_SIZE); -#ifdef WOLFSPDM_NUVOTON - if (nuvotonMode) { - byte seq[8]; int i; - SPDM_Set64LE(seq, seqNum); - for (i = 0; i < 8; i++) iv[i] ^= seq[i]; - } - else -#endif - { - (void)nuvotonMode; - iv[0] ^= (byte)(seqNum & 0xFF); - iv[1] ^= (byte)((seqNum >> 8) & 0xFF); - } + iv[0] ^= (byte)(seqNum & 0xFF); + iv[1] ^= (byte)((seqNum >> 8) & 0xFF); } /* --- Connect Step Macro --- */ @@ -299,12 +288,26 @@ static WC_INLINE void wolfSPDM_BuildIV(byte* iv, const byte* baseIv, /* --- Argument Validation Macros --- */ #define SPDM_CHECK_BUILD_ARGS(ctx, buf, bufSz, minSz) \ - if ((ctx) == NULL || (buf) == NULL || (bufSz) == NULL || *(bufSz) < (minSz)) \ - return WOLFSPDM_E_BUFFER_SMALL + do { \ + if ((ctx) == NULL || (buf) == NULL || (bufSz) == NULL) \ + return WOLFSPDM_E_INVALID_ARG; \ + if (*(bufSz) < (minSz)) \ + return WOLFSPDM_E_BUFFER_SMALL; \ + } while (0) -#define SPDM_CHECK_PARSE_ARGS(ctx, buf, bufSz, minSz) \ - if ((ctx) == NULL || (buf) == NULL || (bufSz) < (minSz)) \ - return WOLFSPDM_E_INVALID_ARG +/* Validate parser inputs. The 4-byte SPDM header (version + code + Param1 + + * Param2) must always be present. A response shorter than minSz that turns + * out to be SPDM_ERROR is *not* rejected here so the matching + * SPDM_CHECK_RESPONSE call can surface it as WOLFSPDM_E_PEER_ERROR. */ +#define SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, minSz) \ + do { \ + if ((ctx) == NULL || (buf) == NULL) \ + return WOLFSPDM_E_INVALID_ARG; \ + if ((bufSz) < 4) \ + return WOLFSPDM_E_INVALID_ARG; \ + if ((bufSz) < (minSz) && (buf)[1] != SPDM_ERROR) \ + return WOLFSPDM_E_INVALID_ARG; \ + } while (0) /* --- Response Code Check Macro --- */ @@ -313,6 +316,7 @@ static WC_INLINE void wolfSPDM_BuildIV(byte* iv, const byte* baseIv, if ((buf)[1] != (expected)) { \ int _ec; \ if (wolfSPDM_CheckError((buf), (bufSz), &_ec)) { \ + (ctx)->lastPeerErrorCode = (byte)_ec; \ wolfSPDM_DebugPrint((ctx), "SPDM error: 0x%02x\n", _ec); \ return WOLFSPDM_E_PEER_ERROR; \ } \ @@ -360,10 +364,6 @@ int wolfSPDM_ComputeSharedSecret(WOLFSPDM_CTX* ctx, /* Generate random bytes */ int wolfSPDM_GetRandom(WOLFSPDM_CTX* ctx, byte* out, word32 outSz); -/* Sign hash with requester's private key (for mutual auth FINISH) */ -int wolfSPDM_SignHash(WOLFSPDM_CTX* ctx, const byte* hash, word32 hashSz, - byte* sig, word32* sigSz); - /* --- Internal Function Declarations - Key Derivation --- */ /* Derive all keys from shared secret and TH1 */ @@ -455,7 +455,11 @@ int wolfSPDM_SendReceive(WOLFSPDM_CTX* ctx, byte* rxBuf, word32* rxSz); /* Debug print (if enabled) */ -void wolfSPDM_DebugPrint(WOLFSPDM_CTX* ctx, const char* fmt, ...); +void wolfSPDM_DebugPrint(WOLFSPDM_CTX* ctx, const char* fmt, ...) +#ifdef __GNUC__ + __attribute__((format(printf, 2, 3))) +#endif + ; /* Hex dump for debugging */ void wolfSPDM_DebugHex(WOLFSPDM_CTX* ctx, const char* label, diff --git a/src/spdm_kdf.c b/src/spdm_kdf.c index c6e5a6b..378582d 100644 --- a/src/spdm_kdf.c +++ b/src/spdm_kdf.c @@ -20,7 +20,6 @@ */ #include "spdm_internal.h" -#include /* * SPDM Key Derivation (DSP0277) @@ -61,9 +60,7 @@ int wolfSPDM_HkdfExpandLabel(byte spdmVersion, const byte* secret, word32 secret prefix = SPDM_BIN_CONCAT_PREFIX_12; /* "spdm1.2 " */ } - /* BinConcat format: Length (2 LE) || "spdmX.Y " || Label || Context - * Note: SPDM spec references TLS 1.3 (BE), but Nuvoton uses LE. - * The ResponderVerifyData match proves LE is correct for this TPM. */ + /* BinConcat format: Length (2 LE) || "spdmX.Y " || Label || Context */ info[infoLen++] = (byte)(outSz & 0xFF); info[infoLen++] = (byte)((outSz >> 8) & 0xFF); @@ -93,21 +90,45 @@ int wolfSPDM_ComputeVerifyData(const byte* finishedKey, const byte* thHash, return WOLFSPDM_E_INVALID_ARG; } + rc = wc_HmacInit(&hmac, NULL, INVALID_DEVID); + if (rc != 0) { + return WOLFSPDM_E_CRYPTO_FAIL; + } + rc = wc_HmacSetKey(&hmac, WC_SHA384, finishedKey, WOLFSPDM_HASH_SIZE); if (rc != 0) { + wc_HmacFree(&hmac); return WOLFSPDM_E_CRYPTO_FAIL; } rc = wc_HmacUpdate(&hmac, thHash, WOLFSPDM_HASH_SIZE); if (rc != 0) { + wc_HmacFree(&hmac); return WOLFSPDM_E_CRYPTO_FAIL; } rc = wc_HmacFinal(&hmac, verifyData); + wc_HmacFree(&hmac); return (rc == 0) ? WOLFSPDM_SUCCESS : WOLFSPDM_E_CRYPTO_FAIL; } +/* Derive both data key (AES-256) and IV from a secret using HKDF-Expand */ +static int wolfSPDM_DeriveKeyIvPair(byte spdmVersion, const byte* secret, + byte* key, byte* iv) +{ + int rc; + rc = wolfSPDM_HkdfExpandLabel(spdmVersion, secret, + WOLFSPDM_HASH_SIZE, SPDM_LABEL_KEY, NULL, 0, + key, WOLFSPDM_AEAD_KEY_SIZE); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } + return wolfSPDM_HkdfExpandLabel(spdmVersion, secret, + WOLFSPDM_HASH_SIZE, SPDM_LABEL_IV, NULL, 0, + iv, WOLFSPDM_AEAD_IV_SIZE); +} + int wolfSPDM_DeriveHandshakeKeys(WOLFSPDM_CTX* ctx, const byte* th1Hash) { byte salt[WOLFSPDM_HASH_SIZE]; @@ -157,37 +178,15 @@ int wolfSPDM_DeriveHandshakeKeys(WOLFSPDM_CTX* ctx, const byte* th1Hash) return rc; } - /* Data encryption keys (AES-256-GCM) */ - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, ctx->reqHsSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_KEY, NULL, 0, - ctx->reqDataKey, WOLFSPDM_AEAD_KEY_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, ctx->rspHsSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_KEY, NULL, 0, - ctx->rspDataKey, WOLFSPDM_AEAD_KEY_SIZE); + /* Data encryption keys + IVs (AES-256-GCM) */ + rc = wolfSPDM_DeriveKeyIvPair(ctx->spdmVersion, ctx->reqHsSecret, + ctx->reqDataKey, ctx->reqDataIv); if (rc != WOLFSPDM_SUCCESS) { return rc; } - /* IVs */ - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, ctx->reqHsSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_IV, NULL, 0, - ctx->reqDataIv, WOLFSPDM_AEAD_IV_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, ctx->rspHsSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_IV, NULL, 0, - ctx->rspDataIv, WOLFSPDM_AEAD_IV_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - return WOLFSPDM_SUCCESS; + return wolfSPDM_DeriveKeyIvPair(ctx->spdmVersion, ctx->rspHsSecret, + ctx->rspDataKey, ctx->rspDataIv); } int wolfSPDM_DeriveAppDataKeys(WOLFSPDM_CTX* ctx) @@ -206,74 +205,46 @@ int wolfSPDM_DeriveAppDataKeys(WOLFSPDM_CTX* ctx) /* Compute TH2_final = Hash(full transcript including FINISH + FINISH_RSP) */ rc = wolfSPDM_TranscriptHash(ctx, th2Hash); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + if (rc != WOLFSPDM_SUCCESS) goto exit; + /* salt = HKDF-Expand(HandshakeSecret, BinConcat("derived"), 48) * Per DSP0277: "derived" label has NO context (unlike TLS 1.3 which uses Hash("")) * libspdm confirms: bin_concat("derived", context=NULL) */ rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, ctx->handshakeSecret, WOLFSPDM_HASH_SIZE, "derived", NULL, 0, salt, WOLFSPDM_HASH_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + if (rc != WOLFSPDM_SUCCESS) goto exit; /* MasterSecret = HKDF-Extract(salt, 0^hashSize) */ XMEMSET(zeroIkm, 0, sizeof(zeroIkm)); rc = wc_HKDF_Extract(WC_SHA384, salt, WOLFSPDM_HASH_SIZE, zeroIkm, WOLFSPDM_HASH_SIZE, masterSecret); - if (rc != 0) { - return WOLFSPDM_E_CRYPTO_FAIL; - } + if (rc != 0) { rc = WOLFSPDM_E_CRYPTO_FAIL; goto exit; } + /* reqAppSecret = HKDF-Expand(MasterSecret, "req app data" || TH2, 48) */ rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, masterSecret, WOLFSPDM_HASH_SIZE, SPDM_LABEL_REQ_DATA, th2Hash, WOLFSPDM_HASH_SIZE, reqAppSecret, WOLFSPDM_HASH_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + if (rc != WOLFSPDM_SUCCESS) goto exit; /* rspAppSecret = HKDF-Expand(MasterSecret, "rsp app data" || TH2, 48) */ rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, masterSecret, WOLFSPDM_HASH_SIZE, SPDM_LABEL_RSP_DATA, th2Hash, WOLFSPDM_HASH_SIZE, rspAppSecret, WOLFSPDM_HASH_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + if (rc != WOLFSPDM_SUCCESS) goto exit; /* Save app secrets for KEY_UPDATE re-derivation */ XMEMCPY(ctx->reqAppSecret, reqAppSecret, WOLFSPDM_HASH_SIZE); XMEMCPY(ctx->rspAppSecret, rspAppSecret, WOLFSPDM_HASH_SIZE); - /* Derive new encryption keys from app data secrets */ - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, reqAppSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_KEY, NULL, 0, - ctx->reqDataKey, WOLFSPDM_AEAD_KEY_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + /* Derive new encryption keys + IVs from app data secrets */ + rc = wolfSPDM_DeriveKeyIvPair(ctx->spdmVersion, reqAppSecret, + ctx->reqDataKey, ctx->reqDataIv); + if (rc != WOLFSPDM_SUCCESS) goto exit; - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, rspAppSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_KEY, NULL, 0, - ctx->rspDataKey, WOLFSPDM_AEAD_KEY_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, reqAppSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_IV, NULL, 0, - ctx->reqDataIv, WOLFSPDM_AEAD_IV_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, rspAppSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_IV, NULL, 0, - ctx->rspDataIv, WOLFSPDM_AEAD_IV_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + rc = wolfSPDM_DeriveKeyIvPair(ctx->spdmVersion, rspAppSecret, + ctx->rspDataKey, ctx->rspDataIv); + if (rc != WOLFSPDM_SUCCESS) goto exit; /* Reset sequence numbers for application phase */ ctx->reqSeqNum = 0; @@ -281,7 +252,14 @@ int wolfSPDM_DeriveAppDataKeys(WOLFSPDM_CTX* ctx) wolfSPDM_DebugPrint(ctx, "App data keys derived, seq nums reset to 0\n"); - return WOLFSPDM_SUCCESS; +exit: + /* Wipe transient secret material from the stack. ctx-resident copies + * are still live and will be zeroed when wolfSPDM_Free runs. */ + wc_ForceZero(salt, sizeof(salt)); + wc_ForceZero(masterSecret, sizeof(masterSecret)); + wc_ForceZero(reqAppSecret, sizeof(reqAppSecret)); + wc_ForceZero(rspAppSecret, sizeof(rspAppSecret)); + return rc; } /* --- Key Update Re-derivation (DSP0277) --- */ @@ -289,6 +267,7 @@ int wolfSPDM_DeriveAppDataKeys(WOLFSPDM_CTX* ctx) int wolfSPDM_DeriveUpdatedKeys(WOLFSPDM_CTX* ctx, int updateAll) { byte newReqAppSecret[WOLFSPDM_HASH_SIZE]; + byte newRspAppSecret[WOLFSPDM_HASH_SIZE]; int rc; if (ctx == NULL) { @@ -302,55 +281,33 @@ int wolfSPDM_DeriveUpdatedKeys(WOLFSPDM_CTX* ctx, int updateAll) rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, ctx->reqAppSecret, WOLFSPDM_HASH_SIZE, SPDM_LABEL_UPDATE, NULL, 0, newReqAppSecret, WOLFSPDM_HASH_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, newReqAppSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_KEY, NULL, 0, - ctx->reqDataKey, WOLFSPDM_AEAD_KEY_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + if (rc != WOLFSPDM_SUCCESS) goto exit; - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, newReqAppSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_IV, NULL, 0, - ctx->reqDataIv, WOLFSPDM_AEAD_IV_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + rc = wolfSPDM_DeriveKeyIvPair(ctx->spdmVersion, newReqAppSecret, + ctx->reqDataKey, ctx->reqDataIv); + if (rc != WOLFSPDM_SUCCESS) goto exit; /* Save new requester secret for future updates */ XMEMCPY(ctx->reqAppSecret, newReqAppSecret, WOLFSPDM_HASH_SIZE); /* Optionally update responder key */ if (updateAll) { - byte newRspAppSecret[WOLFSPDM_HASH_SIZE]; - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, ctx->rspAppSecret, WOLFSPDM_HASH_SIZE, SPDM_LABEL_UPDATE, NULL, 0, newRspAppSecret, WOLFSPDM_HASH_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, newRspAppSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_KEY, NULL, 0, - ctx->rspDataKey, WOLFSPDM_AEAD_KEY_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - rc = wolfSPDM_HkdfExpandLabel(ctx->spdmVersion, newRspAppSecret, - WOLFSPDM_HASH_SIZE, SPDM_LABEL_IV, NULL, 0, - ctx->rspDataIv, WOLFSPDM_AEAD_IV_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + if (rc != WOLFSPDM_SUCCESS) goto exit; + + rc = wolfSPDM_DeriveKeyIvPair(ctx->spdmVersion, newRspAppSecret, + ctx->rspDataKey, ctx->rspDataIv); + if (rc != WOLFSPDM_SUCCESS) goto exit; /* Save new responder secret for future updates */ XMEMCPY(ctx->rspAppSecret, newRspAppSecret, WOLFSPDM_HASH_SIZE); } - return WOLFSPDM_SUCCESS; +exit: + /* Wipe transient secret material from the stack. */ + wc_ForceZero(newReqAppSecret, sizeof(newReqAppSecret)); + wc_ForceZero(newRspAppSecret, sizeof(newRspAppSecret)); + return rc; } diff --git a/src/spdm_msg.c b/src/spdm_msg.c index 1a73497..b237bff 100644 --- a/src/spdm_msg.c +++ b/src/spdm_msg.c @@ -20,7 +20,6 @@ */ #include "spdm_internal.h" -#include #include int wolfSPDM_BuildGetVersion(byte* buf, word32* bufSz) @@ -54,9 +53,9 @@ int wolfSPDM_BuildGetCapabilities(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) SPDM_Set32LE(&buf[8], ctx->reqCaps); /* DataTransferSize (4 LE) */ - buf[12] = 0x00; buf[13] = 0x10; buf[14] = 0x00; buf[15] = 0x00; + SPDM_Set32LE(&buf[12], WOLFSPDM_MAX_MSG_SIZE); /* MaxSPDMmsgSize (4 LE) */ - buf[16] = 0x00; buf[17] = 0x10; buf[18] = 0x00; buf[19] = 0x00; + SPDM_Set32LE(&buf[16], WOLFSPDM_MAX_MSG_SIZE); *bufSz = 20; return WOLFSPDM_SUCCESS; @@ -155,11 +154,7 @@ int wolfSPDM_BuildKeyExchange(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) buf[offset++] = ctx->spdmVersion; buf[offset++] = SPDM_KEY_EXCHANGE; buf[offset++] = 0x00; /* MeasurementSummaryHashType = None */ -#ifdef WOLFSPDM_NUVOTON - buf[offset++] = 0xFF; /* SlotID = 0xFF (no cert, use provisioned public key) */ -#else buf[offset++] = 0x00; /* SlotID = 0 (certificate slot 0) */ -#endif /* ReqSessionID (2 LE) */ buf[offset++] = (byte)(ctx->reqSessionId & 0xFF); @@ -181,35 +176,24 @@ int wolfSPDM_BuildKeyExchange(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) XMEMCPY(&buf[offset], pubKeyY, WOLFSPDM_ECC_KEY_SIZE); offset += WOLFSPDM_ECC_KEY_SIZE; - /* OpaqueData for secured message version negotiation */ -#ifdef WOLFSPDM_NUVOTON - /* Nuvoton format: 12 bytes per spec Rev 1.11 page 19-20 - * OpaqueLength(2 LE) + OpaqueData(12 bytes) = 14 bytes total */ - buf[offset++] = 0x0c; /* OpaqueLength = 12 (LE) */ - buf[offset++] = 0x00; - buf[offset++] = 0x00; buf[offset++] = 0x00; /* SMDataID = 0 */ - buf[offset++] = 0x05; buf[offset++] = 0x00; /* DataSize = 5 (LE) */ - buf[offset++] = 0x01; /* Registry ID = 1 (DMTF) */ - buf[offset++] = 0x01; /* VendorLen = 1 */ - buf[offset++] = 0x01; buf[offset++] = 0x00; /* VersionCount = 1, Reserved = 0 */ - buf[offset++] = 0x10; buf[offset++] = 0x00; /* Version 1.0 (0x0010 LE) */ - buf[offset++] = 0x00; buf[offset++] = 0x00; /* Padding to make OpaqueData 12 bytes */ -#else - /* Standard SPDM 1.2+ OpaqueData format: 20 bytes */ + /* OpaqueData for secured message version negotiation. DSP0277 v1.2 is + * the current spec, defining secured-message versions 1.0, 1.1, 1.2. + * Higher SPDM control versions reuse the 1.2 secured-message format; + * there is no DSP0277 1.3 or 1.4. OpaqueLength must be a multiple of + * 4 per DSP0274 - the 20-byte fixed block satisfies that. */ buf[offset++] = 0x14; /* OpaqueLength = 20 */ buf[offset++] = 0x00; buf[offset++] = 0x01; buf[offset++] = 0x00; /* TotalElements */ buf[offset++] = 0x00; buf[offset++] = 0x00; /* Reserved */ buf[offset++] = 0x00; buf[offset++] = 0x00; buf[offset++] = 0x09; buf[offset++] = 0x00; /* DataSize */ - buf[offset++] = 0x01; /* Registry ID */ + buf[offset++] = 0x01; /* Registry ID = DMTF */ buf[offset++] = 0x01; /* VendorLen */ buf[offset++] = 0x03; buf[offset++] = 0x00; /* VersionCount */ buf[offset++] = 0x10; buf[offset++] = 0x00; /* 1.0 */ buf[offset++] = 0x11; buf[offset++] = 0x00; /* 1.1 */ buf[offset++] = 0x12; buf[offset++] = 0x00; /* 1.2 */ - buf[offset++] = 0x00; buf[offset++] = 0x00; /* Padding */ -#endif + buf[offset++] = 0x00; buf[offset++] = 0x00; /* Padding to mult of 4 */ *bufSz = offset; return WOLFSPDM_SUCCESS; @@ -234,6 +218,12 @@ static int wolfSPDM_BuildSignedHash(byte spdmVersion, byte majorVer, minorVer; int i, rc; + /* Reject overlong context strings before computing zeroPadLen (which + * is 36 - contextStrLen and would underflow). */ + if (contextStr == NULL || contextStrLen > 36) { + return WOLFSPDM_E_INVALID_ARG; + } + majorVer = (byte)('0' + ((spdmVersion >> 4) & 0xF)); minorVer = (byte)('0' + (spdmVersion & 0xF)); @@ -291,74 +281,51 @@ static int wolfSPDM_VerifyEccSig(WOLFSPDM_CTX* ctx, &verified, &ctx->responderPubKey); if (rc != 0) { wolfSPDM_DebugPrint(ctx, "ECC verify_hash failed: %d\n", rc); + /* Internal wolfCrypt failure (memory pressure, missing curve, etc.) + * - distinguish from an actual bad signature so the caller doesn't + * impeach the responder identity over a transient infra issue. */ return WOLFSPDM_E_CRYPTO_FAIL; } - return verified == 1 ? WOLFSPDM_SUCCESS : WOLFSPDM_E_CRYPTO_FAIL; + /* verified == 1 means the signature is good; 0 means tamper/wrong key. */ + return (verified == 1) ? WOLFSPDM_SUCCESS : WOLFSPDM_E_BAD_SIGNATURE; } int wolfSPDM_BuildFinish(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) { byte th2Hash[WOLFSPDM_HASH_SIZE]; byte verifyData[WOLFSPDM_HASH_SIZE]; - byte signature[WOLFSPDM_ECC_POINT_SIZE]; /* 96 bytes for P-384 */ - word32 sigSz = sizeof(signature); word32 offset = 4; /* Start after header */ - int mutualAuth = 0; int rc; -#ifdef WOLFSPDM_NUVOTON - /* Nuvoton requires mutual authentication when we have a requester key */ - if (ctx->mode == WOLFSPDM_MODE_NUVOTON && ctx->hasReqKeyPair) { - mutualAuth = 1; - wolfSPDM_DebugPrint(ctx, "Nuvoton: Mutual auth ENABLED (required after GIVE_PUB)\n"); - } -#endif - - /* Check buffer size: 148 bytes for mutual auth, 52 bytes otherwise */ + /* Check arguments first before any ctx dereference */ if (ctx == NULL || buf == NULL || bufSz == NULL) { return WOLFSPDM_E_INVALID_ARG; } - if (mutualAuth && *bufSz < 148) { - return WOLFSPDM_E_BUFFER_SMALL; - } - if (!mutualAuth && *bufSz < 52) { - return WOLFSPDM_E_BUFFER_SMALL; + + /* Check buffer size: header(4) + [OpaqueLength(2) for 1.4+] + HMAC(48) */ + { + word32 minSz = 4 + WOLFSPDM_HASH_SIZE; /* header + HMAC */ + if (ctx->spdmVersion >= SPDM_VERSION_14) + minSz += 2; /* OpaqueLength */ + if (*bufSz < minSz) + return WOLFSPDM_E_BUFFER_SMALL; } - /* Build FINISH header */ + /* Build FINISH header (mutual auth not supported in standard requester) */ buf[0] = ctx->spdmVersion; buf[1] = SPDM_FINISH; - if (mutualAuth) { - buf[2] = 0x01; /* Param1: Signature field is included */ - buf[3] = 0xFF; /* Param2: 0xFF = requester public key provisioned in trusted environment (GIVE_PUB_KEY) */ + buf[2] = 0x00; /* Param1: No signature */ + buf[3] = 0x00; /* Param2: SlotID = 0 when no signature */ + + /* SPDM 1.4 adds OpaqueLength(2) + OpaqueData(var) after header */ + if (ctx->spdmVersion >= SPDM_VERSION_14) { + buf[offset++] = 0x00; /* OpaqueLength = 0 (LE) */ + buf[offset++] = 0x00; } - else { - buf[2] = 0x00; /* Param1: No signature */ - buf[3] = 0x00; /* Param2: SlotID = 0 when no signature */ - } - - /* Per DSP0274 / libspdm: When mutual auth is requested, the transcript - * for TH2 must include Hash(Cm_requester) - the hash of the requester's - * public key/cert chain - BETWEEN message_k and message_f (FINISH header). - * - * TH2 = Hash(VCA || Ct || message_k || Hash(Cm_req) || FINISH_header) - * - * For Nuvoton with PUB_KEY_ID (SlotID=0xFF), Cm is the TPMT_PUBLIC - * structure that was sent via GIVE_PUB_KEY. */ -#ifdef WOLFSPDM_NUVOTON - if (mutualAuth && ctx->reqPubKeyTPMTLen > 0) { - byte cmHash[WOLFSPDM_HASH_SIZE]; - rc = wolfSPDM_Sha384Hash(cmHash, ctx->reqPubKeyTPMT, - ctx->reqPubKeyTPMTLen, NULL, 0, NULL, 0); - if (rc != WOLFSPDM_SUCCESS) return rc; - rc = wolfSPDM_TranscriptAdd(ctx, cmHash, WOLFSPDM_HASH_SIZE); - if (rc != WOLFSPDM_SUCCESS) return rc; - } -#endif - - /* Add FINISH header to transcript for TH2 */ - rc = wolfSPDM_TranscriptAdd(ctx, buf, 4); + + /* Add FINISH header (+ OpaqueLength for 1.4) to transcript for TH2 */ + rc = wolfSPDM_TranscriptAdd(ctx, buf, offset); if (rc != WOLFSPDM_SUCCESS) { return rc; } @@ -371,46 +338,8 @@ int wolfSPDM_BuildFinish(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) XMEMCPY(ctx->th2, th2Hash, WOLFSPDM_HASH_SIZE); - /* For mutual auth, use SPDM 1.2+ signing context format per DSP0274 */ - if (mutualAuth) { - byte signMsgHash[WOLFSPDM_HASH_SIZE]; - - rc = wolfSPDM_BuildSignedHash(ctx->spdmVersion, - "requester-finish signing", 24, th2Hash, signMsgHash); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - /* Sign Hash(M) */ - rc = wolfSPDM_SignHash(ctx, signMsgHash, WOLFSPDM_HASH_SIZE, signature, &sigSz); - if (rc != WOLFSPDM_SUCCESS) { - wolfSPDM_DebugPrint(ctx, "Failed to sign FINISH: %d\n", rc); - return rc; - } - - /* Copy signature to buffer (96 bytes) */ - XMEMCPY(&buf[offset], signature, WOLFSPDM_ECC_POINT_SIZE); - offset += WOLFSPDM_ECC_POINT_SIZE; - - /* Per DSP0274: TH2 for RequesterVerifyData MUST include the signature. - * TH2_sign = Hash(transcript || FINISH_header[4]) - used above for signature - * TH2_hmac = Hash(transcript || FINISH_header[4] || Signature[96]) - used for HMAC - * Add signature to transcript and recompute TH2 for HMAC. */ - rc = wolfSPDM_TranscriptAdd(ctx, signature, WOLFSPDM_ECC_POINT_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - rc = wolfSPDM_TranscriptHash(ctx, th2Hash); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - } - - /* RequesterVerifyData = HMAC(reqFinishedKey, TH2_hmac) - * For mutual auth: th2Hash now includes the signature (TH2_hmac) - * For no mutual auth: th2Hash is just Hash(transcript || FINISH_header) */ + /* RequesterVerifyData = HMAC(reqFinishedKey, TH2) where TH2 is the + * transcript hash through the FINISH header. */ rc = wolfSPDM_ComputeVerifyData(ctx->reqFinishedKey, th2Hash, verifyData); if (rc != WOLFSPDM_SUCCESS) { return rc; @@ -453,10 +382,11 @@ int wolfSPDM_CheckError(const byte* buf, word32 bufSz, int* errorCode) int wolfSPDM_ParseVersion(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) { word16 entryCount; + word16 maxEntries; word32 i; - byte highestVersion = SPDM_VERSION_12; /* Start at 1.2, find highest supported (capped at 1.3) */ + byte highestVersion = 0; /* No version found yet */ - SPDM_CHECK_PARSE_ARGS(ctx, buf, bufSz, 6); + SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 6); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_VERSION, WOLFSPDM_E_VERSION_MISMATCH); /* Parse VERSION response: @@ -464,21 +394,38 @@ int wolfSPDM_ParseVersion(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) * Offset 6+: VersionNumberEntry array (2 bytes each, LE) */ entryCount = SPDM_Get16LE(&buf[4]); - /* Find highest supported version from entries (capped at 1.3 for now) - * - * TODO: SPDM 1.4 fails at FINISH step with libspdm emulator returning - * InvalidRequest (0x01). KEY_EXCHANGE and key derivation work correctly - * with "spdm1.4 " prefix, but FINISH message format may differ in 1.4. - * Investigate OpaqueData format or FINISH requirements for 1.4 support. - */ - for (i = 0; i < entryCount && (6 + i * 2 + 1) < bufSz; i++) { - byte ver = buf[6 + i * 2 + 1]; /* Major.Minor in high byte */ - /* Cap at 1.3 (0x13) - SPDM 1.4 FINISH handling needs work */ - if (ver > highestVersion && ver <= SPDM_VERSION_13) { - highestVersion = ver; + /* Cap entryCount to what actually fits in the buffer to prevent + * overflow on exotic compilers where i*2 could wrap */ + maxEntries = (word16)((bufSz - 6) / 2); + if (entryCount > maxEntries) { + entryCount = maxEntries; + } + + /* Find highest mutually supported version. + * Per DSP0274, negotiated version must be the highest version + * that both sides support. We support WOLFSPDM_MIN_SPDM_VERSION + * through WOLFSPDM_MAX_SPDM_VERSION (or ctx->maxVersion if set). */ + { + byte maxVer = (ctx->maxVersion != 0) ? ctx->maxVersion + : WOLFSPDM_MAX_SPDM_VERSION; + for (i = 0; i < entryCount; i++) { + /* Each entry is 2 bytes; high byte (offset +1) is Major.Minor */ + byte ver = buf[6 + i * 2 + 1]; + if (ver >= WOLFSPDM_MIN_SPDM_VERSION && + ver <= maxVer && + ver > highestVersion) { + highestVersion = ver; + } } } + /* If no mutually supported version found, fail */ + if (highestVersion == 0) { + wolfSPDM_DebugPrint(ctx, "No mutually supported SPDM version found " + "(require >= 0x%02x)\n", WOLFSPDM_MIN_SPDM_VERSION); + return WOLFSPDM_E_VERSION_MISMATCH; + } + ctx->spdmVersion = highestVersion; ctx->state = WOLFSPDM_STATE_VERSION; @@ -488,7 +435,7 @@ int wolfSPDM_ParseVersion(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) int wolfSPDM_ParseCapabilities(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) { - SPDM_CHECK_PARSE_ARGS(ctx, buf, bufSz, 12); + SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 12); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_CAPABILITIES, WOLFSPDM_E_CAPS_MISMATCH); ctx->rspCaps = SPDM_Get32LE(&buf[8]); @@ -500,16 +447,114 @@ int wolfSPDM_ParseCapabilities(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) int wolfSPDM_ParseAlgorithms(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) { - SPDM_CHECK_PARSE_ARGS(ctx, buf, bufSz, 4); + word32 baseAsymAlgo; + word32 baseHashAlgo; + + SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 36); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_ALGORITHMS, WOLFSPDM_E_ALGO_MISMATCH); + /* Validate negotiated algorithms match Algorithm Set B. + * ALGORITHMS response layout (DSP0274 Table 18): + * Offset 8-11: MeasurementHashAlgo (4 LE) + * Offset 12-15: BaseAsymSel (4 LE) + * Offset 16-19: BaseHashSel (4 LE) + * Note: Response has MeasurementHashAlgo before BaseAsymSel, + * unlike the request which has BaseAsymAlgo at offset 8. */ + 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) { + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: BaseAsymSel != ECDSA_P384 (0x%08x)\n", baseAsymAlgo); + return WOLFSPDM_E_ALGO_MISMATCH; + } + if (baseHashAlgo != SPDM_HASH_ALGO_SHA_384) { + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: BaseHashSel != SHA_384 (0x%08x)\n", baseHashAlgo); + return WOLFSPDM_E_ALGO_MISMATCH; + } + + /* AlgStruct tables follow the fixed-size response header. Walk them + * and confirm DHE, AEAD, and KeySchedule selections are Algorithm + * Set B (SECP_384_R1 / AES_256_GCM / SPDM). Require all three to be + * present and match - a responder offering AlgStructCount=0 must not + * bypass the Set-B contract. Layout per DSP0274 Table 18: + * each struct: AlgType(1) | AlgCount(1) | AlgSupported(2 LE) | ext... */ + { + /* DSP0274 Table 18: ExtAsymSelCount (buf[32]) + ExtHashSelCount + * (buf[33]) push the AlgStruct array past the fixed 36 bytes. + * Skip both ExtAsym/ExtHash tables (each entry is 4 bytes) before + * walking AlgStructs. */ + byte numAlgs = buf[2]; /* Param1 = AlgStructCount */ + byte extAsymCount = (bufSz >= 33) ? buf[32] : 0; + byte extHashCount = (bufSz >= 34) ? buf[33] : 0; + word32 algStart = (word32)36 + + (word32)extAsymCount * 4 + (word32)extHashCount * 4; + byte ai; + word32 off; + int dheOk = 0, aeadOk = 0, ksOk = 0; + if (algStart > bufSz) { + return WOLFSPDM_E_ALGO_MISMATCH; + } + off = algStart; + for (ai = 0; ai < numAlgs && off + 4 <= bufSz; ai++) { + byte algType = buf[off]; + byte algCount = buf[off + 1]; + word16 algSel = SPDM_Get16LE(&buf[off + 2]); + /* Per DSP0274 Table 16: AlgCount low nibble = ExtAlgCount + * (each ExtAlg is 4 bytes); high nibble = fixed-size marker + * (= 2 in current spec). Use the LOW nibble for extLen. */ + word32 extLen = ((word32)(algCount & 0x0F)) * 4; + switch (algType) { + case 2: /* DHE - selected, single bit */ + if (algSel != 0x0010) { /* SECP_384_R1 = bit 4 */ + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: DHE not SECP_384_R1 (0x%04x)\n", algSel); + return WOLFSPDM_E_ALGO_MISMATCH; + } + dheOk = 1; + break; + case 3: /* AEAD - selected, single bit */ + if (algSel != 0x0002) { /* AES_256_GCM = bit 1 */ + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: AEAD not AES_256_GCM (0x%04x)\n", algSel); + return WOLFSPDM_E_ALGO_MISMATCH; + } + aeadOk = 1; + break; + case 5: /* KeySchedule - selected, single bit */ + if (algSel != 0x0001) { /* SPDM = bit 0 */ + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: KeySchedule not SPDM (0x%04x)\n", algSel); + return WOLFSPDM_E_ALGO_MISMATCH; + } + ksOk = 1; + break; + default: break; + } + off += 4 + extLen; + } + if (!dheOk || !aeadOk || !ksOk) { + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: missing required AlgStruct(s) dhe=%d aead=%d ks=%d\n", + dheOk, aeadOk, ksOk); + return WOLFSPDM_E_ALGO_MISMATCH; + } + } + + wolfSPDM_DebugPrint(ctx, "ALGORITHMS: BaseAsym=0x%08x BaseHash=0x%08x\n", + baseAsymAlgo, baseHashAlgo); + ctx->state = WOLFSPDM_STATE_ALGO; return WOLFSPDM_SUCCESS; } int wolfSPDM_ParseDigests(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) { - SPDM_CHECK_PARSE_ARGS(ctx, buf, bufSz, 4); + SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 4); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_DIGESTS, WOLFSPDM_E_CERT_FAIL); ctx->state = WOLFSPDM_STATE_DIGESTS; @@ -553,31 +598,44 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS byte expectedHmac[WOLFSPDM_HASH_SIZE]; int rc; - SPDM_CHECK_PARSE_ARGS(ctx, buf, bufSz, 140); + SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 140); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_KEY_EXCHANGE_RSP, WOLFSPDM_E_KEY_EXCHANGE); - ctx->rspSessionId = SPDM_Get16LE(&buf[4]); - ctx->sessionId = (word32)ctx->reqSessionId | ((word32)ctx->rspSessionId << 16); - - /* Parse MutAuthRequested (offset 6) and ReqSlotIDParam (offset 7) per DSP0274 */ - ctx->mutAuthRequested = buf[6]; - ctx->reqSlotId = buf[7]; + /* Defensive: the new in-parser signature verification dereferences + * ctx->responderPubKey. Internal callers (wolfSPDM_KeyExchange) gate + * on hasResponderPubKey, but enforce it here too so any future direct + * caller fails cleanly instead of dereferencing an uninitialized key. */ + if (!ctx->flags.hasResponderPubKey) { + return WOLFSPDM_E_BAD_STATE; + } - /* Extract responder's ephemeral public key (offset 40 = 4+2+1+1+32) */ - XMEMCPY(peerPubKeyX, &buf[40], WOLFSPDM_ECC_KEY_SIZE); - XMEMCPY(peerPubKeyY, &buf[88], WOLFSPDM_ECC_KEY_SIZE); + /* 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. */ + if (buf[6] != 0) { + wolfSPDM_DebugPrint(ctx, "Responder requested mutual auth (%02x); " + "not supported in this build\n", buf[6]); + return WOLFSPDM_E_KEY_EXCHANGE; + } - /* OpaqueLen at offset 136 */ + /* Compute and validate the layout BEFORE committing any ctx fields so + * a truncated response doesn't leak partial sessionId/peer-key state. */ opaqueLen = SPDM_Get16LE(&buf[136]); sigOffset = 138 + opaqueLen; keRspPartialLen = sigOffset; - (void)opaqueLen; - if (bufSz < sigOffset + WOLFSPDM_ECC_SIG_SIZE + WOLFSPDM_HASH_SIZE) { return WOLFSPDM_E_BUFFER_SMALL; } + /* Now safe to commit session state. */ + ctx->rspSessionId = SPDM_Get16LE(&buf[4]); + ctx->sessionId = (word32)ctx->reqSessionId | ((word32)ctx->rspSessionId << 16); + + /* Extract responder's ephemeral public key (offset 40 = 4+2+1+1+32) */ + XMEMCPY(peerPubKeyX, &buf[40], WOLFSPDM_ECC_KEY_SIZE); + XMEMCPY(peerPubKeyY, &buf[88], WOLFSPDM_ECC_KEY_SIZE); + signature = buf + sigOffset; rspVerifyData = buf + sigOffset + WOLFSPDM_ECC_SIG_SIZE; @@ -587,6 +645,38 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS return rc; } + /* 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. */ + { + static const char sigCtx[] = "responder-key_exchange_rsp signing"; + byte th1Partial[WOLFSPDM_HASH_SIZE]; + byte signedDigest[WOLFSPDM_HASH_SIZE]; + + rc = wolfSPDM_TranscriptHash(ctx, th1Partial); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } + rc = wolfSPDM_BuildSignedHash(ctx->spdmVersion, + sigCtx, (word32)(sizeof(sigCtx) - 1), + th1Partial, signedDigest); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } + rc = wolfSPDM_VerifyEccSig(ctx, signature, WOLFSPDM_ECC_SIG_SIZE, + signedDigest, WOLFSPDM_HASH_SIZE); + if (rc != WOLFSPDM_SUCCESS) { + wolfSPDM_DebugPrint(ctx, + "KEY_EXCHANGE_RSP signature verification failed (rc=%d)\n", rc); + /* Preserve the distinction wolfSPDM_VerifyEccSig draws between + * BAD_SIGNATURE (peer-level violation) and CRYPTO_FAIL + * (transient wolfCrypt issue). */ + return rc; + } + wolfSPDM_DebugPrint(ctx, "KEY_EXCHANGE_RSP signature verified\n"); + } + /* Add signature to transcript (TH1 includes signature) */ rc = wolfSPDM_TranscriptAdd(ctx, signature, WOLFSPDM_ECC_SIG_SIZE); if (rc != WOLFSPDM_SUCCESS) { @@ -616,11 +706,13 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS return rc; } - if (XMEMCMP(expectedHmac, rspVerifyData, WOLFSPDM_HASH_SIZE) != 0) { + /* Constant-time compare to avoid leaking HMAC bytes via timing. */ + if (wolfSPDM_ConstCompare(expectedHmac, rspVerifyData, + WOLFSPDM_HASH_SIZE) != 0) { wolfSPDM_DebugPrint(ctx, "ResponderVerifyData MISMATCH\n"); - } else { - wolfSPDM_DebugPrint(ctx, "ResponderVerifyData VERIFIED OK\n"); + return WOLFSPDM_E_BAD_HMAC; } + wolfSPDM_DebugPrint(ctx, "ResponderVerifyData VERIFIED OK\n"); /* Add ResponderVerifyData to transcript (per SPDM spec, always included) */ rc = wolfSPDM_TranscriptAdd(ctx, rspVerifyData, WOLFSPDM_HASH_SIZE); @@ -634,12 +726,35 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS int wolfSPDM_ParseFinishRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) { - SPDM_CHECK_PARSE_ARGS(ctx, buf, bufSz, 4); + SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 4); if (buf[1] == SPDM_FINISH_RSP) { int addRc; - /* Add FINISH_RSP to transcript for TH2_final (app data key derivation) */ - addRc = wolfSPDM_TranscriptAdd(ctx, buf, 4); + word32 rspMsgLen = 4; + + /* SPDM 1.4 adds OpaqueLength(2) + OpaqueData(var) to FINISH_RSP. + * Cap accepted OpaqueData size to keep wolfSPDM_Finish's decBuf + * footprint bounded. Per DSP0274 the field is u16 (theoretical + * 65535) but real responders keep it small. */ + if (ctx->spdmVersion >= SPDM_VERSION_14) { + word16 opaqueLen; + if (bufSz < 6) { + return WOLFSPDM_E_BUFFER_SMALL; + } + opaqueLen = SPDM_Get16LE(&buf[4]); + if (opaqueLen > 256) { + wolfSPDM_DebugPrint(ctx, + "FINISH_RSP: OpaqueLength %u exceeds 256B cap\n", opaqueLen); + return WOLFSPDM_E_BUFFER_SMALL; + } + rspMsgLen = 4 + 2 + opaqueLen; + if (bufSz < rspMsgLen) { + return WOLFSPDM_E_BUFFER_SMALL; + } + } + + /* Add FINISH_RSP (header + OpaqueData for 1.4) to transcript */ + addRc = wolfSPDM_TranscriptAdd(ctx, buf, rspMsgLen); if (addRc != WOLFSPDM_SUCCESS) { return addRc; } @@ -669,17 +784,24 @@ int wolfSPDM_BuildGetMeasurements(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz, return WOLFSPDM_E_INVALID_ARG; } - /* Size: 4 header + (requestSig ? 32 nonce + 1 slotId : 0) */ - if (requestSig && *bufSz < 37) { - return WOLFSPDM_E_BUFFER_SMALL; - } - if (!requestSig && *bufSz < 4) { - return WOLFSPDM_E_BUFFER_SMALL; + /* Size: 4 header + (requestSig ? 32 nonce + 1 slotId : 0) + * SPDM 1.3+ adds RequesterContext(8) always. OpaqueDataLength is NOT + * part of GET_MEASUREMENTS request per DSP0274 Table 51 / libspdm. */ + { + word32 minSz = 4; + if (requestSig) { + minSz += 32 + 1; /* Nonce + SlotIDParam */ + } + if (ctx->spdmVersion >= SPDM_VERSION_13) { + minSz += 8; /* RequesterContext (always for 1.3+) */ + } + if (*bufSz < minSz) + return WOLFSPDM_E_BUFFER_SMALL; } buf[offset++] = ctx->spdmVersion; buf[offset++] = SPDM_GET_MEASUREMENTS; - /* Param1: bits [7:1] = MeasurementSummaryHashType, bit 0 = signature requested */ + /* Param1: bit 0 = signature requested */ buf[offset++] = requestSig ? SPDM_MEAS_REQUEST_SIG_BIT : 0x00; /* Param2: MeasurementOperation */ buf[offset++] = operation; @@ -693,10 +815,24 @@ int wolfSPDM_BuildGetMeasurements(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz, XMEMCPY(ctx->measNonce, &buf[offset], 32); offset += 32; - /* SlotIDParam (1 byte) — slot 0 */ + /* SlotIDParam (1 byte) - slot 0 */ buf[offset++] = 0x00; } + /* DSP0274 v1.3.0 Table 50 / v1.4.0 Table 49: RequesterContext (8 bytes) + * is appended to GET_MEASUREMENTS for SPDM 1.3 and above, REGARDLESS of + * whether a signature was requested. The MEASUREMENTS response echoes + * these bytes back between OpaqueData and Signature; ParseMeasurements + * skips over them. The signature already covers RequesterContext, so + * we don't separately verify the echo here. */ + if (ctx->spdmVersion >= SPDM_VERSION_13) { + int rc = wolfSPDM_GetRandom(ctx, &buf[offset], 8); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } + offset += 8; + } + *bufSz = offset; return WOLFSPDM_SUCCESS; } @@ -709,7 +845,7 @@ int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) word32 recordEnd; word32 blockIdx; - SPDM_CHECK_PARSE_ARGS(ctx, buf, bufSz, 8); + SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 8); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_MEASUREMENTS, WOLFSPDM_E_MEASUREMENT); numBlocks = buf[4]; @@ -809,12 +945,24 @@ int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) offset += WOLFSPDM_MEAS_BLOCK_HDR_SIZE + measSize; } - /* After measurement record: Nonce(32) + OpaqueDataLength(2) + OpaqueData + Signature */ - /* Nonce is present only if signature was requested */ + /* After measurement record: Nonce(32) + OpaqueDataLength(2) + OpaqueData + * + [RequesterContext(8) for 1.3+] + Signature(96). Nonce/Sig only + * appear when signature was requested. Distinguish the two cases by + * whether the response carries ANY tail bytes: + * - offset == bufSz: unsigned request, no tail. Accepted. + * - bufSz > offset: signed request - the FULL tail must be present; + * a partial tail is a truncated/malformed response. */ ctx->measSignatureSize = 0; - if (offset + 32 + 2 <= bufSz) { - /* Nonce (32 bytes) — skip, we already have our own in ctx->measNonce */ + if (offset == bufSz) { + /* Unsigned measurement response - no tail expected. */ + } + else if (offset + 32 + 2 > bufSz) { + wolfSPDM_DebugPrint(ctx, "MEASUREMENTS: signed tail truncated\n"); + return WOLFSPDM_E_MEASUREMENT; + } + else { + /* Nonce (32 bytes) - skip, we already have our own in ctx->measNonce */ offset += 32; /* OpaqueDataLength (2 LE) */ @@ -828,6 +976,20 @@ int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) } offset += opaqueLen; + /* DSP0274 1.3+ Table 50 mandates an 8-byte RequesterContext echo + * between OpaqueData and Signature. Require room for the 8 bytes + * but NOT for the signature - unsigned measurements omit the sig + * tail and must still parse. The Signature copy below remains + * conditional on its own bufSz check. */ + if (ctx->spdmVersion >= SPDM_VERSION_13) { + if (offset + 8 > bufSz) { + wolfSPDM_DebugPrint(ctx, + "MEASUREMENTS: 1.3+ response missing RequesterContext\n"); + return WOLFSPDM_E_MEASUREMENT; + } + offset += 8; + } + /* Signature (if present) */ if (offset + WOLFSPDM_ECC_SIG_SIZE <= bufSz) { XMEMCPY(ctx->measSignature, &buf[offset], WOLFSPDM_ECC_SIG_SIZE); @@ -835,7 +997,7 @@ int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) } } - ctx->hasMeasurements = 1; + ctx->flags.hasMeasurements = 1; wolfSPDM_DebugPrint(ctx, "MEASUREMENTS: parsed %u blocks\n", ctx->measBlockCount); @@ -844,6 +1006,29 @@ int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) #ifndef NO_WOLFSPDM_MEAS_VERIFY +/* 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 */ + 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; + + rc = wolfSPDM_VerifyEccSig(ctx, sig, sigSz, digest, WOLFSPDM_HASH_SIZE); + if (rc == WOLFSPDM_SUCCESS) { + wolfSPDM_DebugPrint(ctx, "%s\n", passMsg); + return WOLFSPDM_SUCCESS; + } + wolfSPDM_DebugPrint(ctx, "%s\n", failMsg); + /* Preserve CRYPTO_FAIL (transient infra) vs BAD_SIGNATURE (peer-level + * violation) - only the latter gets mapped to the caller's domain + * error code (MEAS_SIG_FAIL / CHALLENGE). */ + return (rc == WOLFSPDM_E_BAD_SIGNATURE) ? failErr : rc; +} + int wolfSPDM_VerifyMeasurementSig(WOLFSPDM_CTX* ctx, const byte* rspBuf, word32 rspBufSz, const byte* reqMsg, word32 reqMsgSz) @@ -856,7 +1041,7 @@ int wolfSPDM_VerifyMeasurementSig(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_INVALID_ARG; } - if (!ctx->hasResponderPubKey) { + if (!ctx->flags.hasResponderPubKey) { return WOLFSPDM_E_MEAS_NOT_VERIFIED; } @@ -866,18 +1051,6 @@ int wolfSPDM_VerifyMeasurementSig(WOLFSPDM_CTX* ctx, } sigOffset = rspBufSz - WOLFSPDM_ECC_SIG_SIZE; - /* Build SPDM 1.2 signing context: - * M = combined_spdm_prefix || zero_pad || signing_context || Hash(L1||L2) - * - * Per DSP0274: - * - combined_spdm_prefix = "dmtf-spdm-v1.X.*" x4 = 64 bytes - * - zero_pad = 36 - strlen("responder-measurements signing") = 6 bytes - * - signing_context = "responder-measurements signing" (30 bytes) - * - Hash(L1||L2) = SHA-384(reqMsg || rspBuf[0..sigOffset-1]) = 48 bytes - * - * Total M = 64 + 6 + 30 + 48 = 148 bytes - * Then sign Hash(M) */ - /* Compute L1||L2 hash per DSP0274 Section 10.11.1: * L1/L2 = VCA || GET_MEASUREMENTS_request || MEASUREMENTS_response(before sig) */ rc = wolfSPDM_Sha384Hash(digest, @@ -886,21 +1059,12 @@ int wolfSPDM_VerifyMeasurementSig(WOLFSPDM_CTX* ctx, rspBuf, sigOffset); if (rc != WOLFSPDM_SUCCESS) return rc; - /* Build M = prefix || zero_pad || context_str || L1L2_hash, then hash it */ - rc = wolfSPDM_BuildSignedHash(ctx->spdmVersion, - "responder-measurements signing", 30, digest, digest); - if (rc != WOLFSPDM_SUCCESS) return rc; - - /* Verify ECDSA signature (raw r||s format) */ - rc = wolfSPDM_VerifyEccSig(ctx, rspBuf + sigOffset, WOLFSPDM_ECC_SIG_SIZE, - digest, WOLFSPDM_HASH_SIZE); - if (rc == WOLFSPDM_SUCCESS) { - wolfSPDM_DebugPrint(ctx, "Measurement signature VERIFIED\n"); - return WOLFSPDM_SUCCESS; - } - - wolfSPDM_DebugPrint(ctx, "Measurement signature INVALID\n"); - return WOLFSPDM_E_MEAS_SIG_FAIL; + return wolfSPDM_VerifySignedDigest(ctx, + "responder-measurements signing", 30, digest, + rspBuf + sigOffset, WOLFSPDM_ECC_SIG_SIZE, + "Measurement signature VERIFIED", + "Measurement signature INVALID", + WOLFSPDM_E_MEAS_SIG_FAIL); } #endif /* !NO_WOLFSPDM_MEAS_VERIFY */ @@ -1026,7 +1190,7 @@ int wolfSPDM_ExtractResponderPubKey(WOLFSPDM_CTX* ctx) } wc_FreeDecodedCert(&cert); - ctx->hasResponderPubKey = 1; + ctx->flags.hasResponderPubKey = 1; wolfSPDM_DebugPrint(ctx, "Extracted responder ECC P-384 public key\n"); return WOLFSPDM_SUCCESS; @@ -1044,7 +1208,7 @@ int wolfSPDM_ValidateCertChain(WOLFSPDM_CTX* ctx) return WOLFSPDM_E_CERT_PARSE; } - if (!ctx->hasTrustedCAs) { + if (!ctx->flags.hasTrustedCAs) { return WOLFSPDM_E_CERT_PARSE; } @@ -1061,16 +1225,21 @@ int wolfSPDM_ValidateCertChain(WOLFSPDM_CTX* ctx) chainRootHash = ctx->certChain + 4; /* Skip Length(2) + Reserved(2) */ if (XMEMCMP(caHash, chainRootHash, WOLFSPDM_HASH_SIZE) != 0) { wolfSPDM_DebugPrint(ctx, - "Root cert hash mismatch — chain not from trusted CA\n"); + "Root cert hash mismatch - chain not from trusted CA\n"); return WOLFSPDM_E_CERT_PARSE; } wolfSPDM_DebugPrint(ctx, "Root certificate hash VERIFIED against trusted CA\n"); - /* Extract public key from the leaf cert (reuses FindLeafCert internally) */ - rc = wolfSPDM_ExtractResponderPubKey(ctx); - if (rc != WOLFSPDM_SUCCESS) { - return rc; + /* Extract public key from the leaf cert. GetCertificate already + * ran ExtractResponderPubKey, so skip the re-init - calling + * wc_ecc_init on an already-initialized key leaks the previous + * key's internal allocations. */ + if (!ctx->flags.hasResponderPubKey) { + rc = wolfSPDM_ExtractResponderPubKey(ctx); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } } wolfSPDM_DebugPrint(ctx, "Certificate chain validated\n"); @@ -1085,9 +1254,19 @@ int wolfSPDM_BuildChallenge(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz, int slotId, byte measHashType) { word32 offset = 0; + word32 minSz; int rc; - SPDM_CHECK_BUILD_ARGS(ctx, buf, bufSz, 36); + if (ctx == NULL || buf == NULL || bufSz == NULL) { + return WOLFSPDM_E_INVALID_ARG; + } + + /* SPDM 1.3+ adds RequesterContext(8) per DSP0274 Table 46 */ + minSz = 36; + if (ctx->spdmVersion >= SPDM_VERSION_13) + minSz += 8; /* RequesterContext */ + if (*bufSz < minSz) + return WOLFSPDM_E_BUFFER_SMALL; buf[offset++] = ctx->spdmVersion; buf[offset++] = SPDM_CHALLENGE; @@ -1105,6 +1284,18 @@ int wolfSPDM_BuildChallenge(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz, XMEMCPY(ctx->challengeNonce, &buf[offset], 32); offset += 32; + /* SPDM 1.3+ adds RequesterContext(8) per DSP0274 Table 46. + * Save it so ParseChallengeAuth can verify the responder echoed it. + * Note: OpaqueDataLength is NOT part of the CHALLENGE request. */ + if (ctx->spdmVersion >= SPDM_VERSION_13) { + rc = wolfSPDM_GetRandom(ctx, &buf[offset], 8); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } + XMEMCPY(ctx->challengeReqCtx, &buf[offset], 8); + offset += 8; + } + *bufSz = offset; return WOLFSPDM_SUCCESS; } @@ -1171,6 +1362,26 @@ int wolfSPDM_ParseChallengeAuth(WOLFSPDM_CTX* ctx, const byte* buf, } offset += opaqueLen; + /* SPDM 1.3+ adds RequesterContext (8 bytes echoed from request). + * Per DSP0274, this comes AFTER OpaqueData and BEFORE Signature. + * Verify the responder echoed the same value we sent - the signature + * already covers it, so a tampered value would fail signature check, + * but the explicit echo check catches responder-side bugs / cached + * stale responses early. */ + if (ctx->spdmVersion >= SPDM_VERSION_13) { + if (offset + 8 > bufSz) { + wolfSPDM_DebugPrint(ctx, + "CHALLENGE_AUTH: too short for RequesterContext\n"); + return WOLFSPDM_E_CHALLENGE; + } + if (XMEMCMP(&buf[offset], ctx->challengeReqCtx, 8) != 0) { + wolfSPDM_DebugPrint(ctx, + "CHALLENGE_AUTH: RequesterContext echo mismatch\n"); + return WOLFSPDM_E_CHALLENGE; + } + offset += 8; + } + /* Signature starts here */ if (offset + WOLFSPDM_ECC_SIG_SIZE > bufSz) { wolfSPDM_DebugPrint(ctx, "CHALLENGE_AUTH: no room for signature\n"); @@ -1194,43 +1405,44 @@ int wolfSPDM_VerifyChallengeAuthSig(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_INVALID_ARG; } - if (!ctx->hasResponderPubKey) { + if (!ctx->flags.hasResponderPubKey) { return WOLFSPDM_E_CHALLENGE; } /* Build M1/M2 hash per DSP0274 Section 10.8.3: * A+B are already accumulated in ctx->m1m2Hash. Now add C and finalize. */ - if (!ctx->m1m2HashInit) { + if (!ctx->flags.m1m2HashInit) { wolfSPDM_DebugPrint(ctx, "CHALLENGE: M1/M2 hash not initialized\n"); return WOLFSPDM_E_CHALLENGE; } - /* Add C: CHALLENGE request + CHALLENGE_AUTH response (before sig) */ + /* Add C: CHALLENGE request + CHALLENGE_AUTH response (before sig). + * If a step fails, free the hash state and clear the init flag so a + * retry rebuilds from scratch instead of using a partially-updated hash. */ rc = wc_Sha384Update(&ctx->m1m2Hash, reqMsg, reqMsgSz); - if (rc != 0) return WOLFSPDM_E_CRYPTO_FAIL; + if (rc != 0) { + wc_Sha384Free(&ctx->m1m2Hash); + ctx->flags.m1m2HashInit = 0; + return WOLFSPDM_E_CRYPTO_FAIL; + } rc = wc_Sha384Update(&ctx->m1m2Hash, rspBuf, sigOffset); - if (rc != 0) return WOLFSPDM_E_CRYPTO_FAIL; + if (rc != 0) { + wc_Sha384Free(&ctx->m1m2Hash); + ctx->flags.m1m2HashInit = 0; + return WOLFSPDM_E_CRYPTO_FAIL; + } /* Finalize M1/M2 hash */ rc = wc_Sha384Final(&ctx->m1m2Hash, digest); - ctx->m1m2HashInit = 0; /* Hash consumed */ + ctx->flags.m1m2HashInit = 0; /* Hash consumed regardless */ if (rc != 0) return WOLFSPDM_E_CRYPTO_FAIL; - /* Build M = prefix || zero_pad || context_str || hash, then hash it */ - rc = wolfSPDM_BuildSignedHash(ctx->spdmVersion, - "responder-challenge_auth signing", 32, digest, digest); - if (rc != WOLFSPDM_SUCCESS) return rc; - - /* Verify ECDSA signature (raw r||s format) */ - rc = wolfSPDM_VerifyEccSig(ctx, rspBuf + sigOffset, WOLFSPDM_ECC_SIG_SIZE, - digest, WOLFSPDM_HASH_SIZE); - if (rc == WOLFSPDM_SUCCESS) { - wolfSPDM_DebugPrint(ctx, "CHALLENGE_AUTH signature VERIFIED\n"); - return WOLFSPDM_SUCCESS; - } - - wolfSPDM_DebugPrint(ctx, "CHALLENGE_AUTH signature INVALID\n"); - return WOLFSPDM_E_CHALLENGE; + return wolfSPDM_VerifySignedDigest(ctx, + "responder-challenge_auth signing", 32, digest, + rspBuf + sigOffset, WOLFSPDM_ECC_SIG_SIZE, + "CHALLENGE_AUTH signature VERIFIED", + "CHALLENGE_AUTH signature INVALID", + WOLFSPDM_E_CHALLENGE); } #endif /* !NO_WOLFSPDM_CHALLENGE */ @@ -1245,7 +1457,7 @@ int wolfSPDM_BuildHeartbeat(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) int wolfSPDM_ParseHeartbeatAck(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) { - SPDM_CHECK_PARSE_ARGS(ctx, buf, bufSz, 4); + SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 4); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_HEARTBEAT_ACK, WOLFSPDM_E_BAD_STATE); wolfSPDM_DebugPrint(ctx, "HEARTBEAT_ACK received\n"); @@ -1281,7 +1493,7 @@ int wolfSPDM_BuildKeyUpdate(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz, int wolfSPDM_ParseKeyUpdateAck(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz, byte operation, byte tag) { - SPDM_CHECK_PARSE_ARGS(ctx, buf, bufSz, 4); + SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 4); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_KEY_UPDATE_ACK, WOLFSPDM_E_KEY_UPDATE); /* Verify echoed operation and tag */ diff --git a/src/spdm_nuvoton.c b/src/spdm_nuvoton.c deleted file mode 100644 index 6738151..0000000 --- a/src/spdm_nuvoton.c +++ /dev/null @@ -1,820 +0,0 @@ -/* spdm_nuvoton.c - * - * Copyright (C) 2006-2025 wolfSSL Inc. - * - * This file is part of wolfSPDM. - * - * wolfSPDM 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. - * - * wolfSPDM 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 - */ - -/* Nuvoton TPM SPDM Support - * - * This file implements Nuvoton-specific SPDM functionality: - * - TCG SPDM Binding message framing (per TCG SPDM Binding Spec v1.0) - * - Nuvoton vendor-defined commands (GET_PUBK, GIVE_PUB, GET_STS_, SPDMONLY) - * - Nuvoton SPDM handshake flow - * - * Reference: Nuvoton SPDM Guidance Rev 1.11 - */ - -#include "spdm_internal.h" - -#ifdef WOLFSPDM_NUVOTON - -#include -#include - -/* Check for SPDM ERROR in response payload */ -#define SPDM_CHECK_ERROR_RSP(ctx, buf, sz, label) \ - if ((sz) >= 4 && (buf)[1] == SPDM_ERROR) { \ - wolfSPDM_DebugPrint(ctx, label ": SPDM ERROR 0x%02x 0x%02x\n", \ - (buf)[2], (buf)[3]); \ - return WOLFSPDM_E_PEER_ERROR; \ - } - -/* --- TCG SPDM Binding Message Framing --- */ - -int wolfSPDM_BuildTcgClearMessage( - WOLFSPDM_CTX* ctx, - const byte* spdmPayload, word32 spdmPayloadSz, - byte* outBuf, word32 outBufSz) -{ - word32 totalSz; - - if (ctx == NULL || spdmPayload == NULL || outBuf == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - /* TCG binding header (16 bytes per Nuvoton spec): - * tag(2/BE) + size(4/BE) + connHandle(4/BE) + fips(2/BE) + reserved(4) */ - totalSz = WOLFSPDM_TCG_HEADER_SIZE + spdmPayloadSz; - - if (outBufSz < totalSz) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Tag (2 bytes BE) */ - SPDM_Set16BE(outBuf, WOLFSPDM_TCG_TAG_CLEAR); - /* Size (4 bytes BE, total including header) */ - SPDM_Set32BE(outBuf + 2, totalSz); - /* Connection Handle (4 bytes BE) */ - SPDM_Set32BE(outBuf + 6, ctx->connectionHandle); - /* FIPS Service Indicator (2 bytes BE) */ - SPDM_Set16BE(outBuf + 10, ctx->fipsIndicator); - /* Reserved (4 bytes, must be 0) */ - XMEMSET(outBuf + 12, 0, 4); - /* SPDM Payload */ - XMEMCPY(outBuf + WOLFSPDM_TCG_HEADER_SIZE, spdmPayload, spdmPayloadSz); - - return (int)totalSz; -} - -int wolfSPDM_ParseTcgClearMessage( - const byte* inBuf, word32 inBufSz, - byte* spdmPayload, word32* spdmPayloadSz, - WOLFSPDM_TCG_CLEAR_HDR* hdr) -{ - word16 tag; - word32 msgSize; - word32 payloadSz; - - if (inBuf == NULL || spdmPayload == NULL || spdmPayloadSz == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - if (inBufSz < WOLFSPDM_TCG_HEADER_SIZE) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Parse header */ - tag = SPDM_Get16BE(inBuf); - if (tag != WOLFSPDM_TCG_TAG_CLEAR) { - return WOLFSPDM_E_PEER_ERROR; - } - - msgSize = SPDM_Get32BE(inBuf + 2); - if (msgSize > inBufSz) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - payloadSz = msgSize - WOLFSPDM_TCG_HEADER_SIZE; - if (*spdmPayloadSz < payloadSz) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Fill header if requested */ - if (hdr != NULL) { - hdr->tag = tag; - hdr->size = msgSize; - hdr->connectionHandle = SPDM_Get32BE(inBuf + 6); - hdr->fipsIndicator = SPDM_Get16BE(inBuf + 10); - hdr->reserved = SPDM_Get32BE(inBuf + 12); - } - - /* Extract payload */ - XMEMCPY(spdmPayload, inBuf + WOLFSPDM_TCG_HEADER_SIZE, payloadSz); - *spdmPayloadSz = payloadSz; - - return (int)payloadSz; -} - -int wolfSPDM_BuildTcgSecuredMessage( - WOLFSPDM_CTX* ctx, - const byte* encPayload, word32 encPayloadSz, - const byte* mac, word32 macSz, - byte* outBuf, word32 outBufSz) -{ - word32 totalSz; - word32 offset; - word16 recordLen; - - if (ctx == NULL || encPayload == NULL || mac == NULL || outBuf == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - /* Total: TCG header(16) + sessionId(4/LE) + seqNum(8/LE) + - * length(2/LE) + encPayload + MAC */ - totalSz = WOLFSPDM_TCG_HEADER_SIZE + WOLFSPDM_TCG_SECURED_HDR_SIZE + - encPayloadSz + macSz; - - if (outBufSz < totalSz) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* TCG binding header (16 bytes, all BE) */ - SPDM_Set16BE(outBuf, WOLFSPDM_TCG_TAG_SECURED); - SPDM_Set32BE(outBuf + 2, totalSz); - SPDM_Set32BE(outBuf + 6, ctx->connectionHandle); - SPDM_Set16BE(outBuf + 10, ctx->fipsIndicator); - XMEMSET(outBuf + 12, 0, 4); - - offset = WOLFSPDM_TCG_HEADER_SIZE; - - /* Session ID (4 bytes LE per DSP0277): - * ReqSessionId(2/LE) || RspSessionId(2/LE) */ - SPDM_Set16LE(outBuf + offset, ctx->reqSessionId); - offset += 2; - SPDM_Set16LE(outBuf + offset, ctx->rspSessionId); - offset += 2; - - /* Sequence Number (8 bytes LE per DSP0277) */ - SPDM_Set64LE(outBuf + offset, ctx->reqSeqNum); - offset += 8; - - /* Length (2 bytes LE per DSP0277) = encrypted data + MAC */ - recordLen = (word16)(encPayloadSz + macSz); - SPDM_Set16LE(outBuf + offset, recordLen); - offset += 2; - - /* Encrypted payload */ - XMEMCPY(outBuf + offset, encPayload, encPayloadSz); - offset += encPayloadSz; - - /* MAC (AES-256-GCM tag) */ - XMEMCPY(outBuf + offset, mac, macSz); - - /* Note: Sequence number increment is handled by caller */ - - return (int)totalSz; -} - -int wolfSPDM_ParseTcgSecuredMessage( - const byte* inBuf, word32 inBufSz, - word32* sessionId, word64* seqNum, - byte* encPayload, word32* encPayloadSz, - byte* mac, word32* macSz, - WOLFSPDM_TCG_SECURED_HDR* hdr) -{ - word16 tag; - word32 msgSize; - word32 offset; - word16 recordLen; - word32 payloadSz; - - if (inBuf == NULL || sessionId == NULL || seqNum == NULL || - encPayload == NULL || encPayloadSz == NULL || - mac == NULL || macSz == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - if (inBufSz < WOLFSPDM_TCG_HEADER_SIZE + WOLFSPDM_TCG_SECURED_HDR_SIZE + - WOLFSPDM_AEAD_TAG_SIZE) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Parse TCG binding header (16 bytes, all BE) */ - tag = SPDM_Get16BE(inBuf); - if (tag != WOLFSPDM_TCG_TAG_SECURED) { - return WOLFSPDM_E_PEER_ERROR; - } - - msgSize = SPDM_Get32BE(inBuf + 2); - if (msgSize > inBufSz) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Fill header if requested */ - if (hdr != NULL) { - hdr->tag = tag; - hdr->size = msgSize; - hdr->connectionHandle = SPDM_Get32BE(inBuf + 6); - hdr->fipsIndicator = SPDM_Get16BE(inBuf + 10); - hdr->reserved = SPDM_Get32BE(inBuf + 12); - } - - offset = WOLFSPDM_TCG_HEADER_SIZE; - - /* Session ID (4 bytes LE per DSP0277): - * ReqSessionId(2/LE) || RspSessionId(2/LE) */ - { - word16 reqSid = SPDM_Get16LE(inBuf + offset); - word16 rspSid = SPDM_Get16LE(inBuf + offset + 2); - *sessionId = ((word32)reqSid << 16) | rspSid; - } - offset += 4; - - /* Sequence Number (8 bytes LE per DSP0277) */ - *seqNum = SPDM_Get64LE(inBuf + offset); - offset += 8; - - /* Length (2 bytes LE per DSP0277) = encrypted data + MAC */ - recordLen = SPDM_Get16LE(inBuf + offset); - offset += 2; - - /* Validate record length */ - if (recordLen < WOLFSPDM_AEAD_TAG_SIZE) { - return WOLFSPDM_E_BUFFER_SMALL; - } - if (offset + recordLen > inBufSz) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Encrypted payload size = recordLen - MAC */ - payloadSz = recordLen - WOLFSPDM_AEAD_TAG_SIZE; - if (*encPayloadSz < payloadSz || *macSz < WOLFSPDM_AEAD_TAG_SIZE) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Encrypted payload */ - XMEMCPY(encPayload, inBuf + offset, payloadSz); - *encPayloadSz = payloadSz; - offset += payloadSz; - - /* MAC */ - XMEMCPY(mac, inBuf + offset, WOLFSPDM_AEAD_TAG_SIZE); - *macSz = WOLFSPDM_AEAD_TAG_SIZE; - - return (int)payloadSz; -} - -/* --- SPDM Vendor Defined Message Helpers --- */ - -/* SPDM message codes */ -#define SPDM_VERSION_1_3 0x13 -#define SPDM_VENDOR_DEFINED_REQUEST 0xFE -#define SPDM_VENDOR_DEFINED_RESPONSE 0x7E - -int wolfSPDM_BuildVendorDefined( - const char* vdCode, - const byte* payload, word32 payloadSz, - byte* outBuf, word32 outBufSz) -{ - word32 totalSz; - word32 offset = 0; - - if (vdCode == NULL || outBuf == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - /* SPDM VENDOR_DEFINED_REQUEST format (per Nuvoton SPDM Guidance): - * SPDMVersion(1) + reqRspCode(1) + param1(1) + param2(1) + - * standardId(2/LE) + vendorIdLen(1) + reqLength(2/LE) + - * vdCode(8) + payload */ - totalSz = 1 + 1 + 1 + 1 + 2 + 1 + 2 + WOLFSPDM_VDCODE_LEN + payloadSz; - - if (outBufSz < totalSz) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* SPDM Version (v1.3 = 0x13) */ - outBuf[offset++] = SPDM_VERSION_1_3; - /* Request/Response Code */ - outBuf[offset++] = SPDM_VENDOR_DEFINED_REQUEST; - /* Param1, Param2 */ - outBuf[offset++] = 0x00; - outBuf[offset++] = 0x00; - /* Standard ID (0x0001 = TCG, little-endian per Nuvoton spec) */ - SPDM_Set16LE(outBuf + offset, 0x0001); - offset += 2; - /* Vendor ID Length (0 for TCG) */ - outBuf[offset++] = 0x00; - /* Request Length (vdCode + payload, little-endian per Nuvoton spec) */ - SPDM_Set16LE(outBuf + offset, (word16)(WOLFSPDM_VDCODE_LEN + payloadSz)); - offset += 2; - /* VdCode (8-byte ASCII) */ - XMEMCPY(outBuf + offset, vdCode, WOLFSPDM_VDCODE_LEN); - offset += WOLFSPDM_VDCODE_LEN; - /* Payload */ - if (payload != NULL && payloadSz > 0) { - XMEMCPY(outBuf + offset, payload, payloadSz); - offset += payloadSz; - } - - return (int)offset; -} - -int wolfSPDM_ParseVendorDefined( - const byte* inBuf, word32 inBufSz, - char* vdCode, - byte* payload, word32* payloadSz) -{ - word32 offset = 0; - word16 reqLength; - word32 dataLen; - byte vendorIdLen; - - if (inBuf == NULL || vdCode == NULL || payload == NULL || - payloadSz == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - /* Minimum: version(1) + code(1) + param1(1) + param2(1) + stdId(2/LE) + - * vidLen(1) + reqLen(2/LE) + vdCode(8) = 17 */ - if (inBufSz < 17) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Skip SPDM version */ - offset += 1; - /* Skip request/response code + params */ - offset += 3; - /* Skip standard ID (2 bytes LE) */ - offset += 2; - /* Vendor ID length and vendor ID data */ - vendorIdLen = inBuf[offset]; - offset += 1 + vendorIdLen; - - if (offset + 2 > inBufSz) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Request/Response Length (2 bytes LE per Nuvoton spec) */ - reqLength = SPDM_Get16LE(inBuf + offset); - offset += 2; - - if (reqLength < WOLFSPDM_VDCODE_LEN) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - if (offset + reqLength > inBufSz) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* VdCode */ - XMEMCPY(vdCode, inBuf + offset, WOLFSPDM_VDCODE_LEN); - vdCode[WOLFSPDM_VDCODE_LEN] = '\0'; /* Null-terminate */ - offset += WOLFSPDM_VDCODE_LEN; - - /* Payload */ - dataLen = reqLength - WOLFSPDM_VDCODE_LEN; - if (*payloadSz < dataLen) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - if (dataLen > 0) { - XMEMCPY(payload, inBuf + offset, dataLen); - } - *payloadSz = dataLen; - - return (int)dataLen; -} - -/* --- Nuvoton-Specific SPDM Functions --- */ - -/* Helper: Send TCG clear message and receive response */ -static int wolfSPDM_Nuvoton_SendClear( - WOLFSPDM_CTX* ctx, - const byte* spdmPayload, word32 spdmPayloadSz, - byte* rxBuf, word32* rxSz) -{ - int rc; - byte txBuf[WOLFSPDM_MAX_MSG_SIZE]; - int txSz; - - if (ctx == NULL || spdmPayload == NULL || rxBuf == NULL || rxSz == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - if (ctx->ioCb == NULL) { - return WOLFSPDM_E_IO_FAIL; - } - - /* Build TCG clear message wrapper */ - txSz = wolfSPDM_BuildTcgClearMessage(ctx, spdmPayload, spdmPayloadSz, - txBuf, sizeof(txBuf)); - if (txSz < 0) { - return txSz; - } - - /* Send via IO callback and receive response */ - rc = ctx->ioCb(ctx, txBuf, (word32)txSz, rxBuf, rxSz, ctx->ioUserCtx); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "Nuvoton I/O failed: %d\n", rc); - return WOLFSPDM_E_IO_FAIL; - } - - return WOLFSPDM_SUCCESS; -} - -int wolfSPDM_Nuvoton_GetPubKey( - WOLFSPDM_CTX* ctx, - byte* pubKey, word32* pubKeySz) -{ - int rc; - byte spdmMsg[256]; - int spdmMsgSz; - byte rxBuf[512]; - word32 rxSz; - byte spdmPayload[256]; - word32 spdmPayloadSz; - byte rspPayload[256]; - word32 rspPayloadSz; - char rspVdCode[WOLFSPDM_VDCODE_LEN + 1]; - WOLFSPDM_TCG_CLEAR_HDR tcgHdr; - - if (ctx == NULL || pubKey == NULL || pubKeySz == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - wolfSPDM_DebugPrint(ctx, "Nuvoton: GET_PUBK\n"); - - /* Build GET_PUBK vendor-defined request */ - spdmMsgSz = wolfSPDM_BuildVendorDefined(WOLFSPDM_VDCODE_GET_PUBK, - NULL, 0, spdmMsg, sizeof(spdmMsg)); - if (spdmMsgSz < 0) { - return spdmMsgSz; - } - - /* Send via TCG clear message */ - rxSz = sizeof(rxBuf); - rc = wolfSPDM_Nuvoton_SendClear(ctx, spdmMsg, (word32)spdmMsgSz, - rxBuf, &rxSz); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - /* Parse TCG clear response and capture fipsIndicator from header */ - spdmPayloadSz = sizeof(spdmPayload); - XMEMSET(&tcgHdr, 0, sizeof(tcgHdr)); - rc = wolfSPDM_ParseTcgClearMessage(rxBuf, rxSz, spdmPayload, - &spdmPayloadSz, &tcgHdr); - - /* Capture fipsIndicator from response for use in subsequent messages */ - if (rc >= 0 && tcgHdr.fipsIndicator != 0) { - ctx->fipsIndicator = tcgHdr.fipsIndicator; - wolfSPDM_DebugPrint(ctx, "GET_PUBK: Captured FipsIndicator=0x%04x from response\n", - ctx->fipsIndicator); - } - if (rc < 0) { - wolfSPDM_DebugPrint(ctx, "GET_PUBK: ParseClearMessage failed %d\n", rc); - return rc; - } - - SPDM_CHECK_ERROR_RSP(ctx, spdmPayload, spdmPayloadSz, "GET_PUBK"); - - /* Parse vendor-defined response */ - rspPayloadSz = sizeof(rspPayload); - XMEMSET(rspVdCode, 0, sizeof(rspVdCode)); - rc = wolfSPDM_ParseVendorDefined(spdmPayload, spdmPayloadSz, - rspVdCode, rspPayload, &rspPayloadSz); - if (rc < 0) { - wolfSPDM_DebugPrint(ctx, "GET_PUBK: ParseVendorDefined failed %d\n", rc); - return rc; - } - - /* Verify VdCode */ - if (XMEMCMP(rspVdCode, WOLFSPDM_VDCODE_GET_PUBK, WOLFSPDM_VDCODE_LEN) != 0) { - wolfSPDM_DebugPrint(ctx, "GET_PUBK: Unexpected VdCode '%.8s'\n", rspVdCode); - return WOLFSPDM_E_PEER_ERROR; - } - - wolfSPDM_DebugPrint(ctx, "GET_PUBK: Got TPMT_PUBLIC (%u bytes)\n", rspPayloadSz); - - /* Copy public key to output */ - if (*pubKeySz < rspPayloadSz) { - return WOLFSPDM_E_BUFFER_SMALL; - } - XMEMCPY(pubKey, rspPayload, rspPayloadSz); - *pubKeySz = rspPayloadSz; - - /* Store for KEY_EXCHANGE cert_chain_buffer_hash computation. - * Per Nuvoton SPDM Guidance: cert_chain_buffer_hash = SHA-384(TPMT_PUBLIC) */ - if (rspPayloadSz <= sizeof(ctx->rspPubKey)) { - XMEMCPY(ctx->rspPubKey, rspPayload, rspPayloadSz); - ctx->rspPubKeyLen = rspPayloadSz; - ctx->hasRspPubKey = 1; - } - - return WOLFSPDM_SUCCESS; -} - -int wolfSPDM_Nuvoton_GivePubKey( - WOLFSPDM_CTX* ctx, - const byte* pubKey, word32 pubKeySz) -{ - int rc; - byte spdmMsg[256]; - int spdmMsgSz; - byte decBuf[256]; - word32 decSz; - - if (ctx == NULL || pubKey == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - if (ctx->state < WOLFSPDM_STATE_KEY_EX) { - return WOLFSPDM_E_BAD_STATE; - } - - wolfSPDM_DebugPrint(ctx, "Nuvoton: GIVE_PUB (%u bytes) - sending ENCRYPTED\n", pubKeySz); - - /* Build GIVE_PUB vendor-defined request */ - spdmMsgSz = wolfSPDM_BuildVendorDefined(WOLFSPDM_VDCODE_GIVE_PUB, - pubKey, pubKeySz, spdmMsg, sizeof(spdmMsg)); - if (spdmMsgSz < 0) { - return spdmMsgSz; - } - - /* GIVE_PUB is sent as a SECURED (encrypted) message per Nuvoton spec Rev 1.11. - * Section 4.2.4 shows GIVE_PUB_KEY uses tag 0x8201 (secured), not 0x8101 (clear). */ - decSz = sizeof(decBuf); - rc = wolfSPDM_SecuredExchange(ctx, spdmMsg, (word32)spdmMsgSz, - decBuf, &decSz); - if (rc != WOLFSPDM_SUCCESS) { - wolfSPDM_DebugPrint(ctx, "GIVE_PUB: SecuredExchange failed %d\n", rc); - return rc; - } - - SPDM_CHECK_ERROR_RSP(ctx, decBuf, decSz, "GIVE_PUB"); - - wolfSPDM_DebugPrint(ctx, "GIVE_PUB: Success\n"); - return WOLFSPDM_SUCCESS; -} - -int wolfSPDM_Nuvoton_GetStatus( - WOLFSPDM_CTX* ctx, - WOLFSPDM_NUVOTON_STATUS* status) -{ - int rc; - byte spdmMsg[256]; - int spdmMsgSz; - byte rxBuf[256]; - word32 rxSz; - byte spdmPayload[128]; - word32 spdmPayloadSz; - byte rspPayload[64]; - word32 rspPayloadSz; - char rspVdCode[WOLFSPDM_VDCODE_LEN + 1]; - byte statusType[4] = {0x00, 0x00, 0x00, 0x00}; /* All */ - - if (ctx == NULL || status == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - XMEMSET(status, 0, sizeof(*status)); - - wolfSPDM_DebugPrint(ctx, "Nuvoton: GET_STS_\n"); - - /* Build GET_STS_ vendor-defined request */ - spdmMsgSz = wolfSPDM_BuildVendorDefined(WOLFSPDM_VDCODE_GET_STS, - statusType, sizeof(statusType), spdmMsg, sizeof(spdmMsg)); - if (spdmMsgSz < 0) { - return spdmMsgSz; - } - - /* Send via TCG clear message */ - rxSz = sizeof(rxBuf); - rc = wolfSPDM_Nuvoton_SendClear(ctx, spdmMsg, (word32)spdmMsgSz, - rxBuf, &rxSz); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - /* Parse TCG clear response */ - spdmPayloadSz = sizeof(spdmPayload); - rc = wolfSPDM_ParseTcgClearMessage(rxBuf, rxSz, spdmPayload, - &spdmPayloadSz, NULL); - if (rc < 0) { - return rc; - } - - SPDM_CHECK_ERROR_RSP(ctx, spdmPayload, spdmPayloadSz, "GET_STS_"); - - /* Parse vendor-defined response */ - rspPayloadSz = sizeof(rspPayload); - XMEMSET(rspVdCode, 0, sizeof(rspVdCode)); - rc = wolfSPDM_ParseVendorDefined(spdmPayload, spdmPayloadSz, - rspVdCode, rspPayload, &rspPayloadSz); - if (rc < 0) { - return rc; - } - - wolfSPDM_DebugPrint(ctx, "GET_STS_: VdCode='%.8s', %u bytes\n", - rspVdCode, rspPayloadSz); - - /* Parse status fields per Nuvoton spec page 9: - * Byte 0: SpecVersionMajor (0 for SPDM 1.x) - * Byte 1: SpecVersionMinor (1 = SPDM 1.1, 3 = SPDM 1.3) - * Byte 2: Reserved - * Byte 3: SPDMOnly lock state (0 = unlocked, 1 = locked) */ - if (rspPayloadSz >= 4) { - byte specMajor = rspPayload[0]; - byte specMinor = rspPayload[1]; - byte spdmOnly = rspPayload[3]; - - status->specVersionMajor = specMajor; - status->specVersionMinor = specMinor; - status->spdmOnlyLocked = (spdmOnly != 0); - status->spdmEnabled = 1; /* If GET_STS works, SPDM is enabled */ - - /* Session active can't be determined from GET_STS alone - - * if we're getting a response, SPDM is working */ - status->sessionActive = 0; - - wolfSPDM_DebugPrint(ctx, "GET_STS_: SpecVersion=%u.%u, SPDMOnly=%s\n", - specMajor, specMinor, spdmOnly ? "LOCKED" : "unlocked"); - } - else if (rspPayloadSz >= 1) { - /* Minimal response - just SPDMOnly */ - status->spdmOnlyLocked = (rspPayload[0] != 0); - status->spdmEnabled = 1; - wolfSPDM_DebugPrint(ctx, "GET_STS_: SPDMOnly=%s (minimal response)\n", - status->spdmOnlyLocked ? "LOCKED" : "unlocked"); - } - return WOLFSPDM_SUCCESS; -} - -int wolfSPDM_Nuvoton_SetOnlyMode( - WOLFSPDM_CTX* ctx, - int lock) -{ - int rc; - byte spdmMsg[256]; - int spdmMsgSz; - byte decBuf[256]; - word32 decSz; - byte param[1]; - - if (ctx == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - if (ctx->state != WOLFSPDM_STATE_CONNECTED) { - return WOLFSPDM_E_NOT_CONNECTED; - } - - param[0] = lock ? WOLFSPDM_SPDMONLY_LOCK : WOLFSPDM_SPDMONLY_UNLOCK; - - wolfSPDM_DebugPrint(ctx, "Nuvoton: SPDMONLY %s\n", - lock ? "LOCK" : "UNLOCK"); - - /* Build SPDMONLY vendor-defined request */ - spdmMsgSz = wolfSPDM_BuildVendorDefined(WOLFSPDM_VDCODE_SPDMONLY, - param, sizeof(param), spdmMsg, sizeof(spdmMsg)); - if (spdmMsgSz < 0) { - return spdmMsgSz; - } - - /* Send encrypted via SecuredExchange */ - decSz = sizeof(decBuf); - rc = wolfSPDM_SecuredExchange(ctx, spdmMsg, (word32)spdmMsgSz, - decBuf, &decSz); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - SPDM_CHECK_ERROR_RSP(ctx, decBuf, decSz, "SPDMONLY"); - - wolfSPDM_DebugPrint(ctx, "SPDMONLY: Success\n"); - return WOLFSPDM_SUCCESS; -} - -/* --- Nuvoton SPDM Connection Flow --- */ - -/* Nuvoton-specific connection flow: - * GET_VERSION -> GET_PUB_KEY -> KEY_EXCHANGE -> GIVE_PUB_KEY -> FINISH - * - * Key differences from standard SPDM: - * - No GET_CAPABILITIES or NEGOTIATE_ALGORITHMS (Algorithm Set B is fixed) - * - Uses GET_PUBK vendor command instead of GET_CERTIFICATE - * - Uses GIVE_PUB vendor command for mutual authentication - * - All messages wrapped in TCG binding headers - */ -int wolfSPDM_ConnectNuvoton(WOLFSPDM_CTX* ctx) -{ - int rc; - byte pubKey[256]; - word32 pubKeySz; - - if (ctx == NULL) { - return WOLFSPDM_E_INVALID_ARG; - } - - if (!ctx->initialized) { - return WOLFSPDM_E_BAD_STATE; - } - - if (ctx->ioCb == NULL) { - return WOLFSPDM_E_IO_FAIL; - } - - wolfSPDM_DebugPrint(ctx, "Nuvoton: Starting SPDM connection\n"); - - /* Reset state for new connection */ - ctx->state = WOLFSPDM_STATE_INIT; - wolfSPDM_TranscriptReset(ctx); - - /* Step 1: GET_VERSION / VERSION */ - SPDM_CONNECT_STEP(ctx, "Nuvoton Step 1: GET_VERSION\n", - wolfSPDM_GetVersion(ctx)); - - /* Step 2: GET_PUBK (Nuvoton vendor command) - * Gets the TPM's SPDM-Identity public key (TPMT_PUBLIC format) */ - wolfSPDM_DebugPrint(ctx, "Nuvoton Step 2: GET_PUBK\n"); - pubKeySz = sizeof(pubKey); - rc = wolfSPDM_Nuvoton_GetPubKey(ctx, pubKey, &pubKeySz); - if (rc != WOLFSPDM_SUCCESS) { - wolfSPDM_DebugPrint(ctx, "GET_PUBK failed: %d\n", rc); - ctx->state = WOLFSPDM_STATE_ERROR; - return rc; - } - ctx->state = WOLFSPDM_STATE_CERT; - - /* Step 2.5: Compute Ct = SHA-384(TPMT_PUBLIC) and add to transcript - * For Nuvoton, the cert_chain_buffer_hash is SHA-384(TPMT_PUBLIC) - * instead of the standard certificate chain hash */ - if (ctx->hasRspPubKey && ctx->rspPubKeyLen > 0) { - wolfSPDM_DebugPrint(ctx, "Nuvoton: Computing Ct = SHA-384(TPMT_PUBLIC[%u])\n", - ctx->rspPubKeyLen); - rc = wolfSPDM_Sha384Hash(ctx->certChainHash, - ctx->rspPubKey, ctx->rspPubKeyLen, NULL, 0, NULL, 0); - if (rc != WOLFSPDM_SUCCESS) { - ctx->state = WOLFSPDM_STATE_ERROR; - return rc; - } - rc = wolfSPDM_TranscriptAdd(ctx, ctx->certChainHash, WOLFSPDM_HASH_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - ctx->state = WOLFSPDM_STATE_ERROR; - return rc; - } - } - else { - wolfSPDM_DebugPrint(ctx, "Nuvoton: Warning - no responder public key for Ct\n"); - } - - SPDM_CONNECT_STEP(ctx, "Nuvoton Step 3: KEY_EXCHANGE\n", - wolfSPDM_KeyExchange(ctx)); - - /* Step 4: GIVE_PUB (Nuvoton vendor command) - sent as SECURED message - * Gives the host's SPDM-Identity public key to the TPM. - * Per Nuvoton spec Rev 1.11 section 4.2.4, GIVE_PUB uses tag 0x8201 (secured). */ - if (ctx->hasReqKeyPair && ctx->reqPubKeyTPMTLen > 0) { - wolfSPDM_DebugPrint(ctx, "Nuvoton Step 4: GIVE_PUB\n"); - rc = wolfSPDM_Nuvoton_GivePubKey(ctx, ctx->reqPubKeyTPMT, - ctx->reqPubKeyTPMTLen); - if (rc != WOLFSPDM_SUCCESS) { - wolfSPDM_DebugPrint(ctx, "GIVE_PUB failed: %d\n", rc); - /* Don't fail - continue to FINISH for debug */ - } - else { - wolfSPDM_DebugPrint(ctx, "GIVE_PUB succeeded!\n"); - } - } - else { - wolfSPDM_DebugPrint(ctx, "Nuvoton Step 4: GIVE_PUB (skipped, no host key)\n"); - } - - /* Step 5: FINISH */ - SPDM_CONNECT_STEP(ctx, "Nuvoton Step 5: FINISH\n", - wolfSPDM_Finish(ctx)); - - ctx->state = WOLFSPDM_STATE_CONNECTED; - wolfSPDM_DebugPrint(ctx, "Nuvoton: SPDM Session Established! " - "SessionID=0x%08x\n", ctx->sessionId); - - return WOLFSPDM_SUCCESS; -} - -#endif /* WOLFSPDM_NUVOTON */ diff --git a/src/spdm_secured.c b/src/spdm_secured.c index e36034c..873ccae 100644 --- a/src/spdm_secured.c +++ b/src/spdm_secured.c @@ -20,80 +20,17 @@ */ #include "spdm_internal.h" -#include /* * SPDM Secured Message Format (DSP0277): * * MCTP transport: * Header/AAD: SessionID(4 LE) + SeqNum(2 LE) + Length(2 LE) = 8 bytes - * IV XOR: Leftmost 2 bytes (bytes 0-1) with 2-byte LE sequence number (DSP0277) - * - * Nuvoton TCG binding (Rev 1.11): - * Header/AAD: SessionID(4 LE) + SeqNum(8 LE) + Length(2 LE) = 14 bytes - * IV XOR: Leftmost 8 bytes (bytes 0-7) with 8-byte LE sequence number (DSP0277 1.2) - * Plaintext: AppDataLength(2 LE) + SPDM msg + RandomData (pad to 16) + * IV XOR: Leftmost 2 bytes (bytes 0-1) with 2-byte LE sequence number * * Full message: Header || Ciphertext || Tag (16) */ -#ifdef WOLFSPDM_NUVOTON -/* Self-test: verify AES-GCM encrypt/decrypt round-trip with current keys. - * Called before first encrypted message to confirm crypto parameters. */ -static int wolfSPDM_AesGcmSelfTest(WOLFSPDM_CTX* ctx) -{ - Aes aesEnc, aesDec; - byte testPlain[] = "wolfSPDM AES-GCM self-test 1234"; /* 31 bytes */ - byte testCipher[32]; - byte testDecrypted[32]; - byte testTag[WOLFSPDM_AEAD_TAG_SIZE]; - byte testAad[14]; - word32 testPlainSz = sizeof(testPlain); - int rc; - - /* Build AAD matching what we'd use for SeqNum=0 */ - SPDM_Set32LE(&testAad[0], ctx->sessionId); - XMEMSET(&testAad[4], 0, 8); /* SeqNum = 0 */ - SPDM_Set16LE(&testAad[12], (word16)(testPlainSz + 16)); - - /* Encrypt */ - rc = wc_AesGcmSetKey(&aesEnc, ctx->reqDataKey, WOLFSPDM_AEAD_KEY_SIZE); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "Self-test: AesGcmSetKey (enc) failed: %d\n", rc); - return rc; - } - rc = wc_AesGcmEncrypt(&aesEnc, testCipher, testPlain, testPlainSz, - ctx->reqDataIv, WOLFSPDM_AEAD_IV_SIZE, - testTag, WOLFSPDM_AEAD_TAG_SIZE, testAad, 14); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "Self-test: AesGcmEncrypt failed: %d\n", rc); - return rc; - } - - /* Decrypt with same key */ - rc = wc_AesGcmSetKey(&aesDec, ctx->reqDataKey, WOLFSPDM_AEAD_KEY_SIZE); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "Self-test: AesGcmSetKey (dec) failed: %d\n", rc); - return rc; - } - rc = wc_AesGcmDecrypt(&aesDec, testDecrypted, testCipher, testPlainSz, - ctx->reqDataIv, WOLFSPDM_AEAD_IV_SIZE, - testTag, WOLFSPDM_AEAD_TAG_SIZE, testAad, 14); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "Self-test: AesGcmDecrypt FAILED: %d\n", rc); - return rc; - } - - /* Verify plaintext matches */ - if (XMEMCMP(testPlain, testDecrypted, testPlainSz) != 0) { - wolfSPDM_DebugPrint(ctx, "Self-test: Plaintext mismatch!\n"); - return -1; - } - - wolfSPDM_DebugPrint(ctx, "Self-test: AES-GCM round-trip PASSED\n"); - return 0; -} -#endif /* WOLFSPDM_NUVOTON */ int wolfSPDM_EncryptInternal(WOLFSPDM_CTX* ctx, const byte* plain, word32 plainSz, @@ -101,7 +38,7 @@ int wolfSPDM_EncryptInternal(WOLFSPDM_CTX* ctx, { Aes aes; byte iv[WOLFSPDM_AEAD_IV_SIZE]; - byte aad[16]; /* Up to 14 bytes for TCG format */ + byte aad[8]; byte plainBuf[WOLFSPDM_MAX_MSG_SIZE + 16]; byte tag[WOLFSPDM_AEAD_TAG_SIZE]; word32 plainBufSz; @@ -114,66 +51,20 @@ int wolfSPDM_EncryptInternal(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_INVALID_ARG; } -#ifdef WOLFSPDM_NUVOTON - if (ctx->mode == WOLFSPDM_MODE_NUVOTON) { - /* Nuvoton TCG binding format per Rev 1.11 spec page 25: - * Header/AAD: SessionID(4 LE) + SeqNum(8 LE) + Length(2 LE) = 14 bytes - * IV XOR: Rightmost 8 bytes (bytes 4-11) with 8-byte sequence number - */ - word16 appDataLen = (word16)plainSz; - - /* Run self-test before first encrypted message */ - if (ctx->reqSeqNum == 0) { - rc = wolfSPDM_AesGcmSelfTest(ctx); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "AES-GCM self-test FAILED: %d\n", rc); - return WOLFSPDM_E_CRYPTO_FAIL; - } - } - word16 unpadded = (word16)(2 + appDataLen); /* AppDataLength + SPDM msg */ - word16 padLen = (word16)((16 - (unpadded % 16)) % 16); /* Pad to 16-byte boundary */ - word16 encPayloadSz = (word16)(unpadded + padLen); - - plainBufSz = encPayloadSz; - /* Length field = ciphertext + MAC (per Nuvoton spec page 25: Length=160=144+16) */ - recordLen = (word16)(encPayloadSz + WOLFSPDM_AEAD_TAG_SIZE); - hdrSz = 14; /* 4 + 8 + 2 (TCG binding format) */ - - if (*encSz < hdrSz + plainBufSz + WOLFSPDM_AEAD_TAG_SIZE) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Build plaintext: AppDataLength(2 LE) || SPDM message || RandomData */ - SPDM_Set16LE(plainBuf, appDataLen); - XMEMCPY(&plainBuf[2], plain, plainSz); - /* Fill RandomData with actual random bytes per Nuvoton spec */ - if (padLen > 0) { - WC_RNG rng; - if (wc_InitRng(&rng) == 0) { - wc_RNG_GenerateBlock(&rng, &plainBuf[unpadded], padLen); - wc_FreeRng(&rng); - } else { - /* Fallback to zeros if RNG fails */ - XMEMSET(&plainBuf[unpadded], 0, padLen); - } - } - - /* Build header/AAD: SessionID(4 LE) + SeqNum(8 LE) + Length(2 LE) = 14 bytes */ - SPDM_Set32LE(&enc[0], ctx->sessionId); - SPDM_Set64LE(&enc[4], ctx->reqSeqNum); - SPDM_Set16LE(&enc[12], recordLen); - - aadSz = 14; - XMEMCPY(aad, enc, aadSz); + /* DSP0277 Sec. 11.3: the sequence number shall not wrap. The wire field is + * 16-bit and wolfSPDM_BuildIV mixes only the low 16 bits into the AES-GCM + * IV, so a wrap would reuse an IV under the same key. Refuse to encrypt + * once the counter reaches 0x10000 - caller must wolfSPDM_KeyUpdate. */ + if (ctx->reqSeqNum > 0xFFFF) { + return WOLFSPDM_E_SEQUENCE; } - else -#endif + + /* MCTP format (per DSP0277): + * Plaintext: AppDataLen(2 LE) + MCTP header(0x05) + SPDM message + * Header: SessionID(4 LE) + SeqNum(2 LE) + Length(2 LE) = 8 bytes + * AAD = Header + */ { - /* MCTP format (per DSP0277): - * Plaintext: AppDataLen(2 LE) + MCTP header(0x05) + SPDM message - * Header: SessionID(4 LE) + SeqNum(2 LE) + Length(2 LE) = 8 bytes - * AAD = Header - */ word16 appDataLen = (word16)(1 + plainSz); word16 encDataLen = (word16)(2 + appDataLen); @@ -200,19 +91,25 @@ int wolfSPDM_EncryptInternal(WOLFSPDM_CTX* ctx, } /* Build IV: BaseIV XOR sequence number (DSP0277) */ - wolfSPDM_BuildIV(iv, ctx->reqDataIv, ctx->reqSeqNum, - ctx->mode == WOLFSPDM_MODE_NUVOTON); + wolfSPDM_BuildIV(iv, ctx->reqDataIv, ctx->reqSeqNum); + rc = wc_AesInit(&aes, NULL, INVALID_DEVID); + if (rc != 0) { + rc = WOLFSPDM_E_CRYPTO_FAIL; + goto exit; + } rc = wc_AesGcmSetKey(&aes, ctx->reqDataKey, WOLFSPDM_AEAD_KEY_SIZE); if (rc != 0) { - return WOLFSPDM_E_CRYPTO_FAIL; + rc = WOLFSPDM_E_CRYPTO_FAIL; + goto exit; } /* Encrypt directly into output buffer (enc + hdrSz) to avoid a copy */ rc = wc_AesGcmEncrypt(&aes, &enc[hdrSz], plainBuf, plainBufSz, iv, WOLFSPDM_AEAD_IV_SIZE, tag, WOLFSPDM_AEAD_TAG_SIZE, aad, aadSz); if (rc != 0) { - return WOLFSPDM_E_CRYPTO_FAIL; + rc = WOLFSPDM_E_CRYPTO_FAIL; + goto exit; } XMEMCPY(&enc[hdrSz + plainBufSz], tag, WOLFSPDM_AEAD_TAG_SIZE); @@ -223,7 +120,13 @@ int wolfSPDM_EncryptInternal(WOLFSPDM_CTX* ctx, wolfSPDM_DebugPrint(ctx, "Encrypted %u bytes -> %u bytes (seq=%llu)\n", plainSz, *encSz, (unsigned long long)(ctx->reqSeqNum - 1)); - return WOLFSPDM_SUCCESS; + rc = WOLFSPDM_SUCCESS; +exit: + /* Wipe the plaintext buffer so the outgoing payload doesn't linger + * on the stack frame after this call returns. */ + wc_ForceZero(plainBuf, sizeof(plainBuf)); + wc_AesFree(&aes); + return rc; } int wolfSPDM_DecryptInternal(WOLFSPDM_CTX* ctx, @@ -232,7 +135,7 @@ int wolfSPDM_DecryptInternal(WOLFSPDM_CTX* ctx, { Aes aes; byte iv[WOLFSPDM_AEAD_IV_SIZE]; - byte aad[16]; + byte aad[8]; byte decrypted[WOLFSPDM_MAX_MSG_SIZE + 16]; const byte* ciphertext; const byte* tag; @@ -241,151 +144,122 @@ int wolfSPDM_DecryptInternal(WOLFSPDM_CTX* ctx, word16 rspLen; word16 cipherLen; word16 appDataLen; - word32 hdrSz; - word32 aadSz; + word32 hdrSz = 8; + word32 aadSz = 8; int rc; if (ctx == NULL || enc == NULL || plain == NULL || plainSz == NULL) { return WOLFSPDM_E_INVALID_ARG; } -#ifdef WOLFSPDM_NUVOTON - if (ctx->mode == WOLFSPDM_MODE_NUVOTON) { - /* Nuvoton TCG binding format per Rev 1.11 spec page 25: - * Header/AAD: SessionID(4 LE) + SeqNum(8 LE) + Length(2 LE) = 14 bytes - * Encrypted: AppDataLength(2 LE) + SPDM message + RandomData padding - * MAC: 16 bytes - */ - word64 rspSeqNum64; - hdrSz = 14; - aadSz = 14; - - if (encSz < hdrSz + WOLFSPDM_AEAD_TAG_SIZE) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Parse header: SessionID(4) + SeqNum(8) + Length(2) */ - rspSessionId = SPDM_Get32LE(&enc[0]); - rspSeqNum64 = SPDM_Get64LE(&enc[4]); - rspLen = SPDM_Get16LE(&enc[12]); - rspSeqNum = (word16)(rspSeqNum64 & 0xFFFF); /* For debug output */ - - if (rspSessionId != ctx->sessionId) { - wolfSPDM_DebugPrint(ctx, "Session ID mismatch: 0x%08x != 0x%08x\n", - rspSessionId, ctx->sessionId); - return WOLFSPDM_E_SESSION_INVALID; - } - - /* Length field = ciphertext + MAC (per Nuvoton spec) */ - if (rspLen < WOLFSPDM_AEAD_TAG_SIZE || encSz < hdrSz + rspLen) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - cipherLen = (word16)(rspLen - WOLFSPDM_AEAD_TAG_SIZE); - ciphertext = enc + hdrSz; - tag = enc + hdrSz + cipherLen; - - XMEMCPY(aad, enc, aadSz); - - /* Build IV: BaseIV XOR sequence number (DSP0277 1.2) */ - wolfSPDM_BuildIV(iv, ctx->rspDataIv, rspSeqNum64, 1); - - rc = wc_AesGcmSetKey(&aes, ctx->rspDataKey, WOLFSPDM_AEAD_KEY_SIZE); - if (rc != 0) { - return WOLFSPDM_E_CRYPTO_FAIL; - } - - rc = wc_AesGcmDecrypt(&aes, decrypted, ciphertext, cipherLen, - iv, WOLFSPDM_AEAD_IV_SIZE, tag, WOLFSPDM_AEAD_TAG_SIZE, aad, aadSz); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "AES-GCM decrypt failed: %d\n", rc); - return WOLFSPDM_E_DECRYPT_FAIL; - } - - /* Parse decrypted: AppDataLen (2 LE) || SPDM message || RandomData */ - appDataLen = SPDM_Get16LE(decrypted); + /* DSP0277 Sec. 11.3: refuse to decrypt past wire-counter exhaustion (matches + * the encrypt-side cap; prevents AES-GCM IV reuse). Caller must + * wolfSPDM_KeyUpdate before the responder's seqNum reaches 0x10000. */ + if (ctx->rspSeqNum > 0xFFFF) { + return WOLFSPDM_E_SEQUENCE; + } - if (cipherLen < (word32)(2 + appDataLen)) { - return WOLFSPDM_E_BUFFER_SMALL; - } + /* MCTP format */ + if (encSz < hdrSz + WOLFSPDM_AEAD_TAG_SIZE) { + return WOLFSPDM_E_BUFFER_SMALL; + } - if (*plainSz < appDataLen) { - return WOLFSPDM_E_BUFFER_SMALL; - } + /* Parse header: SessionID(4) + SeqNum(2) + Length(2) */ + rspSessionId = SPDM_Get32LE(&enc[0]); + rspSeqNum = SPDM_Get16LE(&enc[4]); + rspLen = SPDM_Get16LE(&enc[6]); - /* Copy SPDM message (no MCTP header to skip) */ - XMEMCPY(plain, &decrypted[2], appDataLen); - *plainSz = appDataLen; + if (rspSessionId != ctx->sessionId) { + wolfSPDM_DebugPrint(ctx, "Session ID mismatch: 0x%08x != 0x%08x\n", + rspSessionId, ctx->sessionId); + return WOLFSPDM_E_SESSION_INVALID; } - else -#endif - { - /* MCTP format */ - hdrSz = 8; - aadSz = 8; - if (encSz < hdrSz + WOLFSPDM_AEAD_TAG_SIZE) { - return WOLFSPDM_E_BUFFER_SMALL; - } - - /* Parse header: SessionID(4) + SeqNum(2) + Length(2) */ - rspSessionId = SPDM_Get32LE(&enc[0]); - rspSeqNum = SPDM_Get16LE(&enc[4]); - rspLen = SPDM_Get16LE(&enc[6]); + /* Validate sequence number matches expected (DSP0277 replay protection). + * The wire field is 16-bit. Casting the 64-bit counter to word16 already + * truncates, matching what the encrypt side wrote. */ + if (rspSeqNum != (word16)ctx->rspSeqNum) { + wolfSPDM_DebugPrint(ctx, "Sequence number mismatch: %u != %llu\n", + (unsigned)rspSeqNum, (unsigned long long)ctx->rspSeqNum); + return WOLFSPDM_E_SEQUENCE; + } - if (rspSessionId != ctx->sessionId) { - wolfSPDM_DebugPrint(ctx, "Session ID mismatch: 0x%08x != 0x%08x\n", - rspSessionId, ctx->sessionId); - return WOLFSPDM_E_SESSION_INVALID; - } + if (rspLen < WOLFSPDM_AEAD_TAG_SIZE || encSz < (word32)(hdrSz + rspLen)) { + return WOLFSPDM_E_BUFFER_SMALL; + } - if (rspLen < WOLFSPDM_AEAD_TAG_SIZE || encSz < (word32)(hdrSz + rspLen)) { - return WOLFSPDM_E_BUFFER_SMALL; - } + cipherLen = (word16)(rspLen - WOLFSPDM_AEAD_TAG_SIZE); + ciphertext = enc + hdrSz; + tag = enc + hdrSz + cipherLen; - cipherLen = (word16)(rspLen - WOLFSPDM_AEAD_TAG_SIZE); - ciphertext = enc + hdrSz; - tag = enc + hdrSz + cipherLen; + XMEMCPY(aad, enc, aadSz); - XMEMCPY(aad, enc, aadSz); + /* Build IV: BaseIV XOR sequence number (DSP0277) */ + wolfSPDM_BuildIV(iv, ctx->rspDataIv, (word64)rspSeqNum); - /* Build IV: BaseIV XOR sequence number (DSP0277) */ - wolfSPDM_BuildIV(iv, ctx->rspDataIv, (word64)rspSeqNum, 0); + rc = wc_AesInit(&aes, NULL, INVALID_DEVID); + if (rc != 0) { + rc = WOLFSPDM_E_CRYPTO_FAIL; + goto exit; + } + rc = wc_AesGcmSetKey(&aes, ctx->rspDataKey, WOLFSPDM_AEAD_KEY_SIZE); + if (rc != 0) { + rc = WOLFSPDM_E_CRYPTO_FAIL; + goto exit; + } - rc = wc_AesGcmSetKey(&aes, ctx->rspDataKey, WOLFSPDM_AEAD_KEY_SIZE); - if (rc != 0) { - return WOLFSPDM_E_CRYPTO_FAIL; - } + rc = wc_AesGcmDecrypt(&aes, decrypted, ciphertext, cipherLen, + iv, WOLFSPDM_AEAD_IV_SIZE, tag, WOLFSPDM_AEAD_TAG_SIZE, aad, aadSz); + if (rc != 0) { + wolfSPDM_DebugPrint(ctx, "AES-GCM decrypt failed: %d\n", rc); + rc = WOLFSPDM_E_DECRYPT_FAIL; + goto exit; + } - rc = wc_AesGcmDecrypt(&aes, decrypted, ciphertext, cipherLen, - iv, WOLFSPDM_AEAD_IV_SIZE, tag, WOLFSPDM_AEAD_TAG_SIZE, aad, aadSz); - if (rc != 0) { - wolfSPDM_DebugPrint(ctx, "AES-GCM decrypt failed: %d\n", rc); - return WOLFSPDM_E_DECRYPT_FAIL; - } + /* Need at least AppDataLen(2) + MCTP(1) bytes in the decrypted output. */ + if (cipherLen < 3) { + rc = WOLFSPDM_E_BUFFER_SMALL; + goto exit; + } - /* Parse decrypted: AppDataLen (2) || MCTP (1) || SPDM msg */ - appDataLen = SPDM_Get16LE(decrypted); + /* Parse decrypted: AppDataLen (2) || MCTP (1) || SPDM msg */ + appDataLen = SPDM_Get16LE(decrypted); - if (appDataLen < 1 || cipherLen < (word32)(2 + appDataLen)) { - return WOLFSPDM_E_BUFFER_SMALL; - } + if (appDataLen < 1 || cipherLen < (word32)(2 + appDataLen)) { + rc = WOLFSPDM_E_BUFFER_SMALL; + goto exit; + } - /* Skip MCTP header, copy SPDM message */ - if (*plainSz < (word32)(appDataLen - 1)) { - return WOLFSPDM_E_BUFFER_SMALL; - } + /* Validate the inner MCTP type byte matches what the encrypt side + * writes - catches responder-side framing bugs early. */ + if (decrypted[2] != MCTP_MESSAGE_TYPE_SPDM) { + wolfSPDM_DebugPrint(ctx, "Inner MCTP type mismatch: 0x%02x\n", + decrypted[2]); + rc = WOLFSPDM_E_DECRYPT_FAIL; + goto exit; + } - XMEMCPY(plain, &decrypted[3], appDataLen - 1); - *plainSz = appDataLen - 1; + /* Skip MCTP header, copy SPDM message */ + if (*plainSz < (word32)(appDataLen - 1)) { + rc = WOLFSPDM_E_BUFFER_SMALL; + goto exit; } + XMEMCPY(plain, &decrypted[3], appDataLen - 1); + *plainSz = appDataLen - 1; + ctx->rspSeqNum++; wolfSPDM_DebugPrint(ctx, "Decrypted %u bytes -> %u bytes (seq=%u)\n", encSz, *plainSz, rspSeqNum); - return WOLFSPDM_SUCCESS; + rc = WOLFSPDM_SUCCESS; +exit: + /* Wipe the decrypted plaintext so secured-channel payloads don't + * linger on the stack frame after this call returns. */ + wc_ForceZero(decrypted, sizeof(decrypted)); + wc_AesFree(&aes); + return rc; } #ifndef WOLFSPDM_LEAN diff --git a/src/spdm_session.c b/src/spdm_session.c index ad4143a..7810e2e 100644 --- a/src/spdm_session.c +++ b/src/spdm_session.c @@ -20,80 +20,86 @@ */ #include "spdm_internal.h" -#include -int wolfSPDM_GetVersion(WOLFSPDM_CTX* ctx) +/* Callback types for build/parse functions */ +typedef int (*wolfSPDM_BuildFn)(WOLFSPDM_CTX*, byte*, word32*); +typedef int (*wolfSPDM_ParseFn)(WOLFSPDM_CTX*, const byte*, word32); + +/* Exchange helper: build -> transcript(tx) -> sendrecv -> transcript(rx) -> parse. + * Snapshot transcriptLen on entry and roll back if anything after the + * first TranscriptAdd fails - otherwise a transient failure would leave a + * partial TX/RX pair committed and corrupt TH1/TH2 on retry. */ +static int wolfSPDM_ExchangeMsg(WOLFSPDM_CTX* ctx, + wolfSPDM_BuildFn buildFn, wolfSPDM_ParseFn parseFn, + byte* txBuf, word32 txBufSz, byte* rxBuf, word32 rxBufSz) { - byte txBuf[8]; - byte rxBuf[32]; /* VERSION: 4 hdr + 2 count + up to 8 entries * 2 = 22 */ - word32 txSz = sizeof(txBuf); - word32 rxSz = sizeof(rxBuf); + word32 txSz = txBufSz; + word32 rxSz = rxBufSz; + word32 transcriptSnapshot; int rc; - rc = wolfSPDM_BuildGetVersion(txBuf, &txSz); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + rc = buildFn(ctx, txBuf, &txSz); + if (rc != WOLFSPDM_SUCCESS) return rc; - wolfSPDM_TranscriptAdd(ctx, txBuf, txSz); + transcriptSnapshot = ctx->transcriptLen; + + rc = wolfSPDM_TranscriptAdd(ctx, txBuf, txSz); + if (rc != WOLFSPDM_SUCCESS) goto rollback; rc = wolfSPDM_SendReceive(ctx, txBuf, txSz, rxBuf, &rxSz); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + if (rc != WOLFSPDM_SUCCESS) goto rollback; + + rc = wolfSPDM_TranscriptAdd(ctx, rxBuf, rxSz); + if (rc != WOLFSPDM_SUCCESS) goto rollback; - wolfSPDM_TranscriptAdd(ctx, rxBuf, rxSz); + rc = parseFn(ctx, rxBuf, rxSz); + if (rc != WOLFSPDM_SUCCESS) goto rollback; + + return WOLFSPDM_SUCCESS; - return wolfSPDM_ParseVersion(ctx, rxBuf, rxSz); +rollback: + ctx->transcriptLen = transcriptSnapshot; + return rc; } -int wolfSPDM_GetCapabilities(WOLFSPDM_CTX* ctx) +/* Adapter: BuildGetVersion doesn't take ctx */ +static int wolfSPDM_BuildGetVersionAdapter(WOLFSPDM_CTX* ctx, byte* buf, + word32* bufSz) { - byte txBuf[24]; /* GET_CAPABILITIES: 20 bytes */ - byte rxBuf[40]; /* CAPABILITIES: 20-36 bytes */ - word32 txSz = sizeof(txBuf); - word32 rxSz = sizeof(rxBuf); - int rc; - - rc = wolfSPDM_BuildGetCapabilities(ctx, txBuf, &txSz); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + (void)ctx; + return wolfSPDM_BuildGetVersion(buf, bufSz); +} - wolfSPDM_TranscriptAdd(ctx, txBuf, txSz); +int wolfSPDM_GetVersion(WOLFSPDM_CTX* ctx) +{ + byte txBuf[8]; + byte rxBuf[64]; /* VERSION: 4 hdr + 2 count + up to ~29 entries * 2 */ - rc = wolfSPDM_SendReceive(ctx, txBuf, txSz, rxBuf, &rxSz); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } + return wolfSPDM_ExchangeMsg(ctx, wolfSPDM_BuildGetVersionAdapter, + wolfSPDM_ParseVersion, txBuf, sizeof(txBuf), rxBuf, sizeof(rxBuf)); +} - wolfSPDM_TranscriptAdd(ctx, rxBuf, rxSz); +int wolfSPDM_GetCapabilities(WOLFSPDM_CTX* ctx) +{ + byte txBuf[24]; /* GET_CAPABILITIES: 20 bytes */ + byte rxBuf[40]; /* CAPABILITIES: 20-36 bytes */ - return wolfSPDM_ParseCapabilities(ctx, rxBuf, rxSz); + return wolfSPDM_ExchangeMsg(ctx, wolfSPDM_BuildGetCapabilities, + wolfSPDM_ParseCapabilities, txBuf, sizeof(txBuf), rxBuf, sizeof(rxBuf)); } int wolfSPDM_NegotiateAlgorithms(WOLFSPDM_CTX* ctx) { byte txBuf[52]; /* NEGOTIATE_ALGORITHMS: 48 bytes */ byte rxBuf[80]; /* ALGORITHMS: ~56 bytes with struct tables */ - word32 txSz = sizeof(txBuf); - word32 rxSz = sizeof(rxBuf); int rc; - rc = wolfSPDM_BuildNegotiateAlgorithms(ctx, txBuf, &txSz); - if (rc != WOLFSPDM_SUCCESS) { - return rc; - } - - wolfSPDM_TranscriptAdd(ctx, txBuf, txSz); - - rc = wolfSPDM_SendReceive(ctx, txBuf, txSz, rxBuf, &rxSz); + rc = wolfSPDM_ExchangeMsg(ctx, wolfSPDM_BuildNegotiateAlgorithms, + wolfSPDM_ParseAlgorithms, txBuf, sizeof(txBuf), rxBuf, sizeof(rxBuf)); if (rc != WOLFSPDM_SUCCESS) { return rc; } - wolfSPDM_TranscriptAdd(ctx, rxBuf, rxSz); - /* Save VCA transcript length (GET_VERSION through ALGORITHMS). * Used by measurement signature verification per DSP0274. */ ctx->vcaLen = ctx->transcriptLen; @@ -102,12 +108,19 @@ int wolfSPDM_NegotiateAlgorithms(WOLFSPDM_CTX* ctx) /* Initialize M1/M2 running hash for potential CHALLENGE auth. * Start with VCA (A portion of the M1/M2 transcript per DSP0274). */ { - int hashRc = wc_InitSha384(&ctx->m1m2Hash); + int hashRc; + /* Free a stale hash from a prior call so wc_InitSha384 doesn't + * leak whatever wolfCrypt allocated previously. */ + if (ctx->flags.m1m2HashInit) { + wc_Sha384Free(&ctx->m1m2Hash); + ctx->flags.m1m2HashInit = 0; + } + hashRc = wc_InitSha384(&ctx->m1m2Hash); if (hashRc == 0) { hashRc = wc_Sha384Update(&ctx->m1m2Hash, ctx->transcript, ctx->vcaLen); if (hashRc == 0) { - ctx->m1m2HashInit = 1; + ctx->flags.m1m2HashInit = 1; } else { wc_Sha384Free(&ctx->m1m2Hash); @@ -117,7 +130,7 @@ int wolfSPDM_NegotiateAlgorithms(WOLFSPDM_CTX* ctx) } #endif - return wolfSPDM_ParseAlgorithms(ctx, rxBuf, rxSz); + return WOLFSPDM_SUCCESS; } int wolfSPDM_GetDigests(WOLFSPDM_CTX* ctx) @@ -142,7 +155,7 @@ int wolfSPDM_GetDigests(WOLFSPDM_CTX* ctx) #ifndef NO_WOLFSPDM_CHALLENGE /* Feed GET_DIGESTS request + DIGESTS response to M1/M2 challenge hash */ - if (ctx->m1m2HashInit) { + if (ctx->flags.m1m2HashInit) { wc_Sha384Update(&ctx->m1m2Hash, txBuf, txSz); wc_Sha384Update(&ctx->m1m2Hash, rxBuf, rxSz); } @@ -177,7 +190,7 @@ int wolfSPDM_GetCertificate(WOLFSPDM_CTX* ctx, int slotId) #ifndef NO_WOLFSPDM_CHALLENGE /* Feed each GET_CERTIFICATE/CERTIFICATE chunk to M1/M2 challenge hash */ - if (ctx->m1m2HashInit) { + if (ctx->flags.m1m2HashInit) { wc_Sha384Update(&ctx->m1m2Hash, txBuf, txSz); wc_Sha384Update(&ctx->m1m2Hash, rxBuf, rxSz); } @@ -204,14 +217,16 @@ int wolfSPDM_GetCertificate(WOLFSPDM_CTX* ctx, int slotId) return rc; } - /* Auto-extract responder public key from leaf cert. - * Needed by both measurement signature verification and challenge auth. - * Non-fatal: caller can still proceed, but signature ops will fail. */ - if (!ctx->hasResponderPubKey) { + /* Auto-extract responder public key from leaf cert. Required by every + * downstream signature check (KEY_EXCHANGE_RSP, MEASUREMENTS, CHALLENGE). + * Fail hard: a chain we couldn't bind to a public key gives us no + * identity assurance, so refusing the session is the safe default. */ + if (!ctx->flags.hasResponderPubKey) { int keyRc = wolfSPDM_ExtractResponderPubKey(ctx); if (keyRc != WOLFSPDM_SUCCESS) { wolfSPDM_DebugPrint(ctx, - "Warning: Could not extract responder public key (%d)\n", keyRc); + "Could not extract responder public key (%d)\n", keyRc); + return WOLFSPDM_E_CERT_PARSE; } } @@ -226,6 +241,20 @@ int wolfSPDM_KeyExchange(WOLFSPDM_CTX* ctx) word32 rxSz = sizeof(rxBuf); int rc; + if (ctx == NULL) { + return WOLFSPDM_E_INVALID_ARG; + } + + /* Refuse KEY_EXCHANGE without an extracted responder public key: + * the responder's ECDSA signature would silently skip otherwise, + * leaving only the HMAC ResponderVerifyData (which proves the peer + * derived the same DHE secret but not its long-term identity). */ + if (!ctx->flags.hasResponderPubKey) { + wolfSPDM_DebugPrint(ctx, + "KEY_EXCHANGE refused: GET_CERTIFICATE must run first\n"); + return WOLFSPDM_E_BAD_STATE; + } + rc = wolfSPDM_BuildKeyExchange(ctx, txBuf, &txSz); if (rc != WOLFSPDM_SUCCESS) { return rc; @@ -247,10 +276,14 @@ int wolfSPDM_KeyExchange(WOLFSPDM_CTX* ctx) int wolfSPDM_Finish(WOLFSPDM_CTX* ctx) { - byte finishBuf[152]; /* 148 bytes max for mutual auth FINISH */ + /* Cap stack pressure for embedded callers: 1.4 FINISH_RSP carries a + * u16 OpaqueLength in theory, but spec-aligned responders keep it + * small. ParseFinishRsp enforces FINISH_RSP_MAX_OPAQUE so we know the + * decrypted size up front. */ + byte finishBuf[64]; /* FINISH: 4 hdr + 2 OpaqueLen (1.4) + 48 HMAC = 54 */ byte encBuf[256]; /* Encrypted: hdr(14) + padded(160) + tag(16) = 190 max */ - byte rxBuf[128]; /* Encrypted FINISH_RSP: ~94 bytes max */ - byte decBuf[64]; /* Decrypted FINISH_RSP: 4 hdr + 48 verify = 52 */ + byte rxBuf[768]; /* Encrypted FINISH_RSP: hdr + ciphertext + tag */ + byte decBuf[512]; /* Decrypted: 4 hdr + 2 OpaqueLen + up to ~500B OpaqueData */ word32 finishSz = sizeof(finishBuf); word32 encSz = sizeof(encBuf); word32 rxSz = sizeof(rxBuf); @@ -275,12 +308,15 @@ int wolfSPDM_Finish(WOLFSPDM_CTX* ctx) return rc; } - /* Check if response is unencrypted SPDM message - * SPDM messages start with version byte (0x10-0x1F). - * Encrypted records start with session ID. */ - if (rxSz >= 2 && rxBuf[0] >= 0x10 && rxBuf[0] <= 0x1F) { - /* Unencrypted SPDM message - check for ERROR */ - if (rxBuf[1] == 0x7F) { /* SPDM_ERROR */ + /* Classify the response: an encrypted record's first 4 bytes are the + * session ID we just negotiated. Anything else is an unencrypted SPDM + * message (typically an SPDM_ERROR from the peer). Compare against the + * session id explicitly rather than relying on the version-byte range + * heuristic, which can collide if reqSessionId's low byte falls in + * 0x10-0x1F. */ + if (rxSz >= 4 && SPDM_Get32LE(rxBuf) != ctx->sessionId) { + if (rxBuf[1] == SPDM_ERROR) { + ctx->lastPeerErrorCode = rxBuf[2]; wolfSPDM_DebugPrint(ctx, "FINISH: peer returned SPDM ERROR 0x%02x\n", rxBuf[2]); return WOLFSPDM_E_PEER_ERROR; @@ -347,8 +383,10 @@ int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, } #endif - /* Send/receive: use secured exchange if session established, else cleartext */ - if (ctx->state == WOLFSPDM_STATE_CONNECTED) { + /* Send/receive: use secured exchange once FINISH has installed session + * keys (STATE_FINISH and beyond - includes both end-of-Connect and + * step-by-step callers), else cleartext. */ + if (ctx->state >= WOLFSPDM_STATE_FINISH) { rc = wolfSPDM_SecuredExchange(ctx, txBuf, txSz, rxBuf, &rxSz); } else { @@ -359,6 +397,22 @@ int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, return rc; } + /* Check for SPDM_ERROR before parsing - SPDM error responses are only + * 4 bytes, which would be rejected by ParseMeasurements's minimum-size + * check (8 bytes) as WOLFSPDM_E_INVALID_ARG. Catch it here so the + * caller sees the more accurate PEER_ERROR. Stash the responder's + * error code so callers can retrieve it via wolfSPDM_GetLastPeerError + * (e.g. back off on BUSY, abort on UNSUPPORTED_REQUEST). */ + { + int errCode = 0; + if (wolfSPDM_CheckError(rxBuf, rxSz, &errCode)) { + ctx->lastPeerErrorCode = (byte)errCode; + wolfSPDM_DebugPrint(ctx, + "GET_MEASUREMENTS: responder error 0x%02x\n", errCode); + return WOLFSPDM_E_PEER_ERROR; + } + } + /* Parse the response */ rc = wolfSPDM_ParseMeasurements(ctx, rxBuf, rxSz); if (rc != WOLFSPDM_SUCCESS) { @@ -368,16 +422,17 @@ int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, #ifndef NO_WOLFSPDM_MEAS_VERIFY /* Verify signature if requested and signature was captured */ if (requestSignature && ctx->measSignatureSize > 0) { - if (!ctx->hasResponderPubKey) { + if (!ctx->flags.hasResponderPubKey) { wolfSPDM_DebugPrint(ctx, - "No responder public key — cannot verify signature\n"); + "No responder public key - cannot verify signature\n"); return WOLFSPDM_E_MEAS_NOT_VERIFIED; } rc = wolfSPDM_VerifyMeasurementSig(ctx, rxBuf, rxSz, ctx->measReqMsg, ctx->measReqMsgSz); if (rc != WOLFSPDM_SUCCESS) { - return WOLFSPDM_E_MEAS_SIG_FAIL; + /* Pass through CRYPTO_FAIL vs MEAS_SIG_FAIL distinction. */ + return rc; } ctx->state = WOLFSPDM_STATE_MEASURED; @@ -400,7 +455,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 */ + byte txBuf[48]; /* CHALLENGE: 36 bytes (1.2) or 44 bytes (1.3+) */ byte rxBuf[512]; /* CHALLENGE_AUTH: variable, up to ~300+ bytes */ word32 txSz = sizeof(txBuf); word32 rxSz = sizeof(rxBuf); @@ -416,12 +471,24 @@ int wolfSPDM_Challenge(WOLFSPDM_CTX* ctx, int slotId, byte measHashType) return WOLFSPDM_E_BAD_STATE; } - if (!ctx->hasResponderPubKey) { + if (!ctx->flags.hasResponderPubKey) { wolfSPDM_DebugPrint(ctx, "CHALLENGE: No responder public key for verification\n"); return WOLFSPDM_E_CHALLENGE; } + /* If trusted CAs are loaded, anchor the responder's leaf cert against + * the trust store before CHALLENGE issues. Otherwise the caller is + * trusting whatever leaf cert the responder shipped. */ + if (ctx->flags.hasTrustedCAs) { + rc = wolfSPDM_ValidateCertChain(ctx); + if (rc != WOLFSPDM_SUCCESS) { + wolfSPDM_DebugPrint(ctx, + "CHALLENGE: cert chain validation failed (%d)\n", rc); + return rc; + } + } + /* Build CHALLENGE request */ rc = wolfSPDM_BuildChallenge(ctx, txBuf, &txSz, slotId, measHashType); if (rc != WOLFSPDM_SUCCESS) { @@ -556,8 +623,14 @@ int wolfSPDM_KeyUpdate(WOLFSPDM_CTX* ctx, int updateAll) wolfSPDM_DebugPrint(ctx, "KEY_UPDATE: DeriveUpdatedKeys failed: %d\n", rc); return rc; } + /* Per DSP0277 Sec 11: reset only the seqNum for directions whose + * keys actually rotated. updateAll=0 (UpdateKey) only rotates the + * requester's send-direction; the responder keeps incrementing its + * old rspSeqNum until UpdateAll happens. */ ctx->reqSeqNum = 0; - ctx->rspSeqNum = 0; + if (updateAll) { + ctx->rspSeqNum = 0; + } /* Decrypt ACK with new rsp key */ rxSz = sizeof(rxBuf); diff --git a/src/spdm_transcript.c b/src/spdm_transcript.c index 0bf422c..893e5d9 100644 --- a/src/spdm_transcript.c +++ b/src/spdm_transcript.c @@ -20,7 +20,6 @@ */ #include "spdm_internal.h" -#include /* --- Transcript Management --- * VCA = GET_VERSION || VERSION || GET_CAPS || CAPS || NEG_ALGO || ALGO diff --git a/test/test_spdm.c b/test/test_spdm.c index 1b1dd10..bb8e203 100644 --- a/test/test_spdm.c +++ b/test/test_spdm.c @@ -139,7 +139,7 @@ static void tcp_disconnect(void) } } -/* Static context buffer — sized via wolfSPDM_GetCtxSize() at runtime, +/* Static context buffer - sized via wolfSPDM_GetCtxSize() at runtime, * but we need a compile-time upper bound. 16KB is generous. */ #define CTX_BUF_SIZE 16384 static byte g_ctxBuf[CTX_BUF_SIZE]; diff --git a/test/unit_test.c b/test/unit_test.c index f13dd4a..4edff6b 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -42,6 +42,19 @@ static int g_testsFailed = 0; #define ASSERT_EQ(a, b, msg) TEST_ASSERT((a) == (b), msg) #define ASSERT_NE(a, b, msg) TEST_ASSERT((a) != (b), msg) +/* Test context setup/cleanup macros */ +#define TEST_CTX_SETUP() \ + WOLFSPDM_CTX ctxBuf; \ + WOLFSPDM_CTX* ctx = &ctxBuf; \ + wolfSPDM_Init(ctx) + +#define TEST_CTX_SETUP_V12() \ + TEST_CTX_SETUP(); \ + ctx->spdmVersion = SPDM_VERSION_12 + +#define TEST_CTX_FREE() \ + wolfSPDM_Free(ctx) + /* Dummy I/O callback for testing */ static int dummy_io_cb(WOLFSPDM_CTX* ctx, const byte* txBuf, word32 txSz, byte* rxBuf, word32* rxSz, void* userCtx) @@ -65,7 +78,7 @@ static int test_context_new_free(void) ctx = wolfSPDM_New(); TEST_ASSERT(ctx != NULL, "wolfSPDM_New returned NULL"); ASSERT_EQ(ctx->state, WOLFSPDM_STATE_INIT, "Initial state wrong"); - ASSERT_EQ(ctx->initialized, 1, "Should be initialized by New()"); + ASSERT_EQ(ctx->flags.initialized, 1, "Should be initialized by New()"); wolfSPDM_Free(ctx); wolfSPDM_Free(NULL); /* Should not crash */ @@ -76,17 +89,14 @@ static int test_context_new_free(void) static int test_context_init(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; + TEST_CTX_SETUP(); printf("test_context_init...\n"); - - ASSERT_SUCCESS(wolfSPDM_Init(ctx)); - ASSERT_EQ(ctx->initialized, 1, "Not marked initialized"); - ASSERT_EQ(ctx->rngInitialized, 1, "RNG not initialized"); + ASSERT_EQ(ctx->flags.initialized, 1, "Not marked initialized"); + ASSERT_EQ(ctx->flags.rngInitialized, 1, "RNG not initialized"); ASSERT_EQ(ctx->reqCaps, WOLFSPDM_DEFAULT_REQ_CAPS, "Default caps wrong"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } @@ -100,7 +110,7 @@ static int test_context_static_alloc(void) ASSERT_EQ(wolfSPDM_GetCtxSize(), (int)sizeof(WOLFSPDM_CTX), "GetCtxSize mismatch"); ASSERT_EQ(wolfSPDM_InitStatic(ctx, 10), WOLFSPDM_E_BUFFER_SMALL, "Should fail on small buffer"); ASSERT_SUCCESS(wolfSPDM_InitStatic(ctx, sizeof(buffer))); - ASSERT_EQ(ctx->initialized, 1, "Static ctx not initialized"); + ASSERT_EQ(ctx->flags.initialized, 1, "Static ctx not initialized"); wolfSPDM_Free(ctx); TEST_PASS(); @@ -108,20 +118,17 @@ static int test_context_static_alloc(void) static int test_context_set_io(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; int dummy = 42; + TEST_CTX_SETUP(); printf("test_context_set_io...\n"); - wolfSPDM_Init(ctx); - ASSERT_SUCCESS(wolfSPDM_SetIO(ctx, dummy_io_cb, &dummy)); ASSERT_EQ(ctx->ioCb, dummy_io_cb, "IO callback not set"); ASSERT_EQ(ctx->ioUserCtx, &dummy, "User context not set"); ASSERT_EQ(wolfSPDM_SetIO(ctx, NULL, NULL), WOLFSPDM_E_INVALID_ARG, "NULL callback should fail"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } @@ -131,14 +138,11 @@ static int test_context_set_io(void) static int test_transcript_add_reset(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte data1[] = {0x01, 0x02, 0x03, 0x04}; byte data2[] = {0x05, 0x06, 0x07, 0x08}; + TEST_CTX_SETUP(); printf("test_transcript_add_reset...\n"); - - wolfSPDM_Init(ctx); ASSERT_EQ(ctx->transcriptLen, 0, "Transcript should start empty"); ASSERT_SUCCESS(wolfSPDM_TranscriptAdd(ctx, data1, sizeof(data1))); @@ -152,47 +156,41 @@ static int test_transcript_add_reset(void) wolfSPDM_TranscriptReset(ctx); ASSERT_EQ(ctx->transcriptLen, 0, "Reset should clear length"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_transcript_hash(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte data[] = "test data for hashing"; byte hash[WOLFSPDM_HASH_SIZE]; byte zeros[WOLFSPDM_HASH_SIZE]; + TEST_CTX_SETUP(); printf("test_transcript_hash...\n"); - - wolfSPDM_Init(ctx); wolfSPDM_TranscriptAdd(ctx, data, sizeof(data) - 1); ASSERT_SUCCESS(wolfSPDM_TranscriptHash(ctx, hash)); XMEMSET(zeros, 0, sizeof(zeros)); ASSERT_NE(memcmp(hash, zeros, WOLFSPDM_HASH_SIZE), 0, "Hash should be non-zero"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_certchain_hash(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte certData[] = {0x30, 0x82, 0x01, 0x00, 0xAA, 0xBB, 0xCC, 0xDD}; byte zeros[WOLFSPDM_HASH_SIZE]; + TEST_CTX_SETUP(); printf("test_certchain_hash...\n"); - - wolfSPDM_Init(ctx); ASSERT_SUCCESS(wolfSPDM_CertChainAdd(ctx, certData, sizeof(certData))); ASSERT_EQ(ctx->certChainLen, sizeof(certData), "CertChain len wrong"); ASSERT_SUCCESS(wolfSPDM_ComputeCertChainHash(ctx)); XMEMSET(zeros, 0, sizeof(zeros)); ASSERT_NE(memcmp(ctx->certChainHash, zeros, WOLFSPDM_HASH_SIZE), 0, "Ct should be non-zero"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } @@ -202,43 +200,37 @@ static int test_certchain_hash(void) static int test_random_generation(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte buf1[32], buf2[32]; + TEST_CTX_SETUP(); printf("test_random_generation...\n"); - - wolfSPDM_Init(ctx); ASSERT_SUCCESS(wolfSPDM_GetRandom(ctx, buf1, sizeof(buf1))); ASSERT_SUCCESS(wolfSPDM_GetRandom(ctx, buf2, sizeof(buf2))); ASSERT_NE(memcmp(buf1, buf2, sizeof(buf1)), 0, "Random outputs should differ"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_ephemeral_key_generation(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte pubKeyX[WOLFSPDM_ECC_KEY_SIZE]; byte pubKeyY[WOLFSPDM_ECC_KEY_SIZE]; byte zeros[WOLFSPDM_ECC_KEY_SIZE]; word32 xSz = sizeof(pubKeyX); word32 ySz = sizeof(pubKeyY); + TEST_CTX_SETUP(); printf("test_ephemeral_key_generation...\n"); - - wolfSPDM_Init(ctx); ASSERT_SUCCESS(wolfSPDM_GenerateEphemeralKey(ctx)); - ASSERT_EQ(ctx->ephemeralKeyInitialized, 1, "Key not marked initialized"); + ASSERT_EQ(ctx->flags.ephemeralKeyInit, 1, "Key not marked initialized"); ASSERT_SUCCESS(wolfSPDM_ExportEphemeralPubKey(ctx, pubKeyX, &xSz, pubKeyY, &ySz)); ASSERT_EQ(xSz, WOLFSPDM_ECC_KEY_SIZE, "X coordinate wrong size"); ASSERT_EQ(ySz, WOLFSPDM_ECC_KEY_SIZE, "Y coordinate wrong size"); XMEMSET(zeros, 0, sizeof(zeros)); ASSERT_NE(memcmp(pubKeyX, zeros, WOLFSPDM_ECC_KEY_SIZE), 0, "Public key X should be non-zero"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } @@ -309,99 +301,320 @@ static int test_build_get_version(void) static int test_build_get_capabilities(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte buf[32]; word32 bufSz = sizeof(buf); + TEST_CTX_SETUP_V12(); printf("test_build_get_capabilities...\n"); - - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; ASSERT_SUCCESS(wolfSPDM_BuildGetCapabilities(ctx, buf, &bufSz)); ASSERT_EQ(bufSz, 20, "GET_CAPABILITIES should be 20 bytes"); ASSERT_EQ(buf[0], SPDM_VERSION_12, "Version should be 0x12"); ASSERT_EQ(buf[1], SPDM_GET_CAPABILITIES, "Code should be 0xE1"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_build_negotiate_algorithms(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte buf[64]; word32 bufSz = sizeof(buf); + TEST_CTX_SETUP_V12(); printf("test_build_negotiate_algorithms...\n"); - - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; ASSERT_SUCCESS(wolfSPDM_BuildNegotiateAlgorithms(ctx, buf, &bufSz)); ASSERT_EQ(bufSz, 48, "NEGOTIATE_ALGORITHMS should be 48 bytes"); ASSERT_EQ(buf[1], SPDM_NEGOTIATE_ALGORITHMS, "Code should be 0xE3"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_parse_algorithms_set_b_enforcement(void) +{ + /* Synthesise a minimal ALGORITHMS response and verify each Set-B + * constraint (BaseAsym=ECDSA-P384, BaseHash=SHA-384, DHE=SECP_384_R1, + * AEAD=AES_256_GCM, KeySchedule=SPDM) is enforced. */ + byte rsp[64]; + TEST_CTX_SETUP_V12(); + + printf("test_parse_algorithms_set_b_enforcement...\n"); + + /* Build a response with Set-B selections. */ + XMEMSET(rsp, 0, sizeof(rsp)); + rsp[0] = SPDM_VERSION_12; + rsp[1] = SPDM_ALGORITHMS; + rsp[2] = 4; /* Param1 = AlgStructCount */ + /* BaseAsymSel = ECDSA_P384 (bit 7) at offset 12 */ + rsp[12] = SPDM_ASYM_ALGO_ECDSA_P384 & 0xFF; + rsp[13] = (SPDM_ASYM_ALGO_ECDSA_P384 >> 8) & 0xFF; + /* BaseHashSel = SHA_384 (bit 1) at offset 16 */ + rsp[16] = SPDM_HASH_ALGO_SHA_384; + /* AlgStruct table starts at offset 36, each entry 4 bytes: + * AlgType(1) + AlgCount(1=0x20) + AlgSel(2 LE) */ + rsp[36] = 2; rsp[37] = 0x20; rsp[38] = 0x10; rsp[39] = 0x00; /* DHE SECP_384_R1 */ + rsp[40] = 3; rsp[41] = 0x20; rsp[42] = 0x02; rsp[43] = 0x00; /* AEAD AES_256_GCM */ + rsp[44] = 4; rsp[45] = 0x20; rsp[46] = 0x0F; rsp[47] = 0x00; /* ReqBaseAsym */ + rsp[48] = 5; rsp[49] = 0x20; rsp[50] = 0x01; rsp[51] = 0x00; /* KeySchedule SPDM */ + + ASSERT_SUCCESS(wolfSPDM_ParseAlgorithms(ctx, rsp, 52)); + + /* Tamper AEAD selection to AES_128_GCM (bit 0) - must be rejected. */ + rsp[42] = 0x01; + ASSERT_EQ(wolfSPDM_ParseAlgorithms(ctx, rsp, 52), + WOLFSPDM_E_ALGO_MISMATCH, "Non-AES-256-GCM AEAD must fail Set-B"); + rsp[42] = 0x02; + + /* Tamper DHE to SECP_256_R1 (bit 3) - must be rejected. */ + rsp[38] = 0x08; + ASSERT_EQ(wolfSPDM_ParseAlgorithms(ctx, rsp, 52), + WOLFSPDM_E_ALGO_MISMATCH, "Non-SECP_384_R1 DHE must fail Set-B"); + rsp[38] = 0x10; + + /* Tamper KeySchedule to 0 - must be rejected. */ + rsp[50] = 0x00; + ASSERT_EQ(wolfSPDM_ParseAlgorithms(ctx, rsp, 52), + WOLFSPDM_E_ALGO_MISMATCH, "Non-SPDM KeySchedule must fail Set-B"); + + TEST_CTX_FREE(); TEST_PASS(); } static int test_build_get_digests(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte buf[16]; word32 bufSz = sizeof(buf); + TEST_CTX_SETUP_V12(); printf("test_build_get_digests...\n"); - - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; ASSERT_SUCCESS(wolfSPDM_BuildGetDigests(ctx, buf, &bufSz)); ASSERT_EQ(bufSz, 4, "GET_DIGESTS should be 4 bytes"); ASSERT_EQ(buf[1], SPDM_GET_DIGESTS, "Code should be 0x81"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_build_get_certificate(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte buf[16]; word32 bufSz = sizeof(buf); + TEST_CTX_SETUP_V12(); printf("test_build_get_certificate...\n"); - - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; ASSERT_SUCCESS(wolfSPDM_BuildGetCertificate(ctx, buf, &bufSz, 0, 0, 1024)); ASSERT_EQ(bufSz, 8, "GET_CERTIFICATE should be 8 bytes"); ASSERT_EQ(buf[1], SPDM_GET_CERTIFICATE, "Code should be 0x82"); ASSERT_EQ(buf[2], 0x00, "SlotID should be 0"); TEST_ASSERT(buf[6] == 0x00 && buf[7] == 0x04, "Length should be 1024"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_build_end_session(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte buf[16]; word32 bufSz = sizeof(buf); + TEST_CTX_SETUP_V12(); printf("test_build_end_session...\n"); - - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; ASSERT_SUCCESS(wolfSPDM_BuildEndSession(ctx, buf, &bufSz)); ASSERT_EQ(bufSz, 4, "END_SESSION should be 4 bytes"); ASSERT_EQ(buf[1], SPDM_END_SESSION, "Code should be 0xEA"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_parse_finish_rsp_14_opaque_length(void) +{ + /* SPDM 1.4 FINISH_RSP: header(4) + OpaqueLength(2 LE) + OpaqueData(var). + * Exercise both the happy path (correctly consuming the variable-length + * tail into the transcript) and the size-guard (truncated buffer must + * return BUFFER_SMALL). */ + byte rsp[64]; + word32 startTranscriptLen; + TEST_CTX_SETUP(); + + printf("test_parse_finish_rsp_14_opaque_length...\n"); + + ctx->spdmVersion = SPDM_VERSION_14; + + /* Empty OpaqueData (opaqueLen=0): rspMsgLen=6, fits in 4-byte header + * tail. The transcript should advance by 6 bytes. */ + XMEMSET(rsp, 0, sizeof(rsp)); + rsp[0] = SPDM_VERSION_14; + rsp[1] = SPDM_FINISH_RSP; + /* OpaqueLength = 0 at offset 4..5 (already zeroed). */ + startTranscriptLen = ctx->transcriptLen; + ASSERT_SUCCESS(wolfSPDM_ParseFinishRsp(ctx, rsp, 6)); + ASSERT_EQ(ctx->transcriptLen - startTranscriptLen, (word32)6, + "Transcript should grow by 6 (hdr+OpaqueLen) for empty opaque"); + + /* Non-zero OpaqueData (5 bytes) - rspMsgLen = 4+2+5 = 11. */ + ctx->transcriptLen = 0; + rsp[4] = 0x05; rsp[5] = 0x00; + rsp[6] = 'h'; rsp[7] = 'e'; rsp[8] = 'l'; rsp[9] = 'l'; rsp[10] = 'o'; + ctx->state = WOLFSPDM_STATE_FINISH; /* reset for re-parse */ + ASSERT_SUCCESS(wolfSPDM_ParseFinishRsp(ctx, rsp, 11)); + ASSERT_EQ(ctx->transcriptLen, (word32)11, + "Transcript should grow by hdr+OpaqueLen+OpaqueData = 11"); + + /* Truncated: 5 bytes (header + 1 byte of OpaqueLength) must fail. */ + ASSERT_EQ(wolfSPDM_ParseFinishRsp(ctx, rsp, 5), WOLFSPDM_E_BUFFER_SMALL, + "1.4 FINISH_RSP with truncated OpaqueLength must fail"); + + /* Truncated: 9 bytes (claimed opaqueLen=5 but only 3 bytes follow). */ + ASSERT_EQ(wolfSPDM_ParseFinishRsp(ctx, rsp, 9), WOLFSPDM_E_BUFFER_SMALL, + "1.4 FINISH_RSP with truncated OpaqueData must fail"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_build_finish_opaque_length_14(void) +{ + /* SPDM 1.4 adds OpaqueLength(2) to FINISH at offset 4. Verify both the + * grown size requirement and that the field is actually written. */ + byte buf[128]; + word32 bufSz; + int i; + TEST_CTX_SETUP_V12(); + + printf("test_build_finish_opaque_length_14...\n"); + + /* Populate reqFinishedKey so the HMAC step doesn't fault. The HMAC + * value itself isn't validated here - we're checking the header. */ + for (i = 0; i < WOLFSPDM_HASH_SIZE; i++) { + ctx->reqFinishedKey[i] = (byte)i; + } + + /* 1.4: header(4) + OpaqueLen(2) = 0x0000 + HMAC(48) = 54 bytes. */ + ctx->spdmVersion = SPDM_VERSION_14; + bufSz = sizeof(buf); + ASSERT_SUCCESS(wolfSPDM_BuildFinish(ctx, buf, &bufSz)); + ASSERT_EQ(bufSz, 54, "1.4 FINISH should be 54 bytes"); + ASSERT_EQ(buf[1], SPDM_FINISH, "Code should be SPDM_FINISH"); + ASSERT_EQ(buf[4], 0x00, "OpaqueLength byte 0 should be 0"); + ASSERT_EQ(buf[5], 0x00, "OpaqueLength byte 1 should be 0"); + + /* 1.4 size guard: 53 bytes must be refused. */ + { + byte tinyBuf[53]; + word32 tinySz = sizeof(tinyBuf); + ASSERT_EQ(wolfSPDM_BuildFinish(ctx, tinyBuf, &tinySz), + WOLFSPDM_E_BUFFER_SMALL, + "1.4 FINISH should refuse 53 bytes (needs 54)"); + } + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_build_key_exchange_opaque_data(void) +{ + /* OpaqueData carries the SecuredMessage version negotiation. DSP0277 + * only defines versions 1.0, 1.1, 1.2, so the block is the same length + * regardless of the negotiated SPDM control version. */ + byte buf[256]; + word32 bufSz; + word16 opaqueLen; + word32 opaqueOffset = 4 + 2 + 1 + 1 + 32 + 96; /* hdr + ResSI + pol + res + RND + ExchData */ + TEST_CTX_SETUP_V12(); + + printf("test_build_key_exchange_opaque_data...\n"); + + bufSz = sizeof(buf); + ASSERT_SUCCESS(wolfSPDM_BuildKeyExchange(ctx, buf, &bufSz)); + opaqueLen = (word16)(buf[opaqueOffset] | ((word16)buf[opaqueOffset + 1] << 8)); + ASSERT_EQ(opaqueLen, 20, "OpaqueLength should be 20 bytes"); + /* SecuredMessageVersions block at offset+12: 0x10 0x00 0x11 0x00 0x12 0x00 */ + ASSERT_EQ(buf[opaqueOffset + 14], 0x10, "Version[0] should be 0x10 (1.0)"); + ASSERT_EQ(buf[opaqueOffset + 16], 0x11, "Version[1] should be 0x11 (1.1)"); + ASSERT_EQ(buf[opaqueOffset + 18], 0x12, "Version[2] should be 0x12 (1.2)"); + + /* SPDM 1.4 reuses the same 1.2 secured-message format. */ + ctx->spdmVersion = SPDM_VERSION_14; + bufSz = sizeof(buf); + ASSERT_SUCCESS(wolfSPDM_BuildKeyExchange(ctx, buf, &bufSz)); + opaqueLen = (word16)(buf[opaqueOffset] | ((word16)buf[opaqueOffset + 1] << 8)); + ASSERT_EQ(opaqueLen, 20, "1.4 OpaqueLength should still be 20"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_key_exchange_requires_cert(void) +{ + /* wolfSPDM_KeyExchange must refuse to proceed without an extracted + * responder public key (signature verification would otherwise be + * skipped, regressing MITM resistance to HMAC-only). */ + TEST_CTX_SETUP_V12(); + + printf("test_key_exchange_requires_cert...\n"); + + /* hasResponderPubKey is 0 by default; KeyExchange must reject. */ + ASSERT_EQ(wolfSPDM_KeyExchange(ctx), WOLFSPDM_E_BAD_STATE, + "KeyExchange without cert must return BAD_STATE"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_parse_key_exchange_rsp_mutual_auth_refused(void) +{ + /* Responder sets MutAuthRequested (offset 6) - the parser must refuse + * before committing ctx->sessionId so a rejected handshake doesn't + * leak partial state. */ + byte rsp[240]; /* full KEY_EXCHANGE_RSP minimum + sig + hmac */ + TEST_CTX_SETUP_V12(); + + printf("test_parse_key_exchange_rsp_mutual_auth_refused...\n"); + + XMEMSET(rsp, 0, sizeof(rsp)); + rsp[0] = SPDM_VERSION_12; + rsp[1] = SPDM_KEY_EXCHANGE_RSP; + rsp[6] = 0x01; /* MutAuthRequested - we don't support it */ + ASSERT_EQ(wc_ecc_init(&ctx->responderPubKey), 0, "ecc_init"); + ctx->flags.hasResponderPubKey = 1; + ctx->sessionId = 0; + ASSERT_EQ(wolfSPDM_ParseKeyExchangeRsp(ctx, rsp, sizeof(rsp)), + WOLFSPDM_E_KEY_EXCHANGE, "MutAuthRequested must be refused"); + ASSERT_EQ(ctx->sessionId, (word32)0, + "sessionId must not be committed on mutual-auth refusal"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_parse_key_exchange_rsp_too_short(void) +{ + /* Verify ParseKeyExchangeRsp's size guard runs before the signature + * verification path. A truncated response (no room for the 96-byte + * signature) must be rejected up front rather than reaching the + * verifier with garbage bytes. The actual signature-verify failure + * path is exercised end-to-end by the integration tests against + * spdm-emu (a bad signature there returns WOLFSPDM_E_BAD_SIGNATURE). */ + byte rsp[140]; /* below the sigOffset(138) + sig(96) minimum */ + int rc; + TEST_CTX_SETUP_V12(); + + printf("test_parse_key_exchange_rsp_too_short...\n"); + + XMEMSET(rsp, 0, sizeof(rsp)); + rsp[0] = SPDM_VERSION_12; + rsp[1] = SPDM_KEY_EXCHANGE_RSP; + /* Properly init the ecc_key the flag references; wolfSPDM_Free's + * wc_ecc_free should run against a wolfCrypt-initialized state, not + * a zeroed-but-never-initialized struct. */ + ASSERT_EQ(wc_ecc_init(&ctx->responderPubKey), 0, "ecc_init"); + ctx->flags.hasResponderPubKey = 1; + rc = wolfSPDM_ParseKeyExchangeRsp(ctx, rsp, sizeof(rsp)); + ASSERT_NE(rc, WOLFSPDM_SUCCESS, + "Truncated KEY_EXCHANGE_RSP must not succeed"); + + TEST_CTX_FREE(); TEST_PASS(); } @@ -449,17 +662,13 @@ static int test_error_strings(void) static int test_build_get_measurements(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte buf[64]; byte zeros[32]; word32 bufSz; + TEST_CTX_SETUP_V12(); printf("test_build_get_measurements...\n"); - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; - /* Build without signature */ bufSz = sizeof(buf); ASSERT_SUCCESS(wolfSPDM_BuildGetMeasurements(ctx, buf, &bufSz, SPDM_MEAS_OPERATION_ALL, 0)); @@ -476,25 +685,33 @@ static int test_build_get_measurements(void) ASSERT_NE(memcmp(&buf[4], zeros, 32), 0, "Nonce should be non-zero"); ASSERT_EQ(memcmp(ctx->measNonce, &buf[4], 32), 0, "Nonce should match context"); - wolfSPDM_Free(ctx); + /* DSP0274 v1.3.0 Table 50 / v1.4.0 Table 49: RequesterContext is + * appended for 1.3+ regardless of whether signature was requested. */ + ctx->spdmVersion = SPDM_VERSION_13; + bufSz = sizeof(buf); + ASSERT_SUCCESS(wolfSPDM_BuildGetMeasurements(ctx, buf, &bufSz, SPDM_MEAS_OPERATION_ALL, 0)); + ASSERT_EQ(bufSz, 12, "1.3 unsigned should be 4 + 8 = 12 bytes"); + + bufSz = sizeof(buf); + ASSERT_SUCCESS(wolfSPDM_BuildGetMeasurements(ctx, buf, &bufSz, SPDM_MEAS_OPERATION_ALL, 1)); + ASSERT_EQ(bufSz, 45, "1.3 signed should be 4 + 32 + 1 + 8 = 45 bytes"); + + TEST_CTX_FREE(); TEST_PASS(); } static int test_measurement_accessors(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte measIdx, measType; byte value[64]; word32 valueSz; + TEST_CTX_SETUP(); printf("test_measurement_accessors...\n"); - - wolfSPDM_Init(ctx); ASSERT_EQ(wolfSPDM_GetMeasurementCount(ctx), 0, "Count should be 0 before measurements"); /* Manually populate 2 test blocks */ - ctx->hasMeasurements = 1; + ctx->flags.hasMeasurements = 1; ctx->measBlockCount = 2; ctx->measBlocks[0].index = 1; ctx->measBlocks[0].dmtfType = SPDM_MEAS_VALUE_TYPE_IMMUTABLE_ROM; @@ -526,14 +743,13 @@ static int test_measurement_accessors(void) ASSERT_FAIL(wolfSPDM_GetMeasurementBlock(ctx, 2, &measIdx, &measType, value, &valueSz)); ASSERT_FAIL(wolfSPDM_GetMeasurementBlock(ctx, -1, &measIdx, &measType, value, &valueSz)); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_parse_measurements(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; + TEST_CTX_SETUP(); /* Fake MEASUREMENTS response: 2 blocks, recordLen=20 */ byte rsp[] = { 0x12, 0x60, 0x00, 0x00, /* header */ @@ -547,10 +763,9 @@ static int test_parse_measurements(void) printf("test_parse_measurements...\n"); - wolfSPDM_Init(ctx); ASSERT_SUCCESS(wolfSPDM_ParseMeasurements(ctx, rsp, sizeof(rsp))); ASSERT_EQ(ctx->measBlockCount, 2, "Should have 2 blocks"); - ASSERT_EQ(ctx->hasMeasurements, 1, "hasMeasurements should be set"); + ASSERT_EQ(ctx->flags.hasMeasurements, 1, "hasMeasurements should be set"); ASSERT_EQ(ctx->measBlocks[0].index, 1, "Block 0 index wrong"); ASSERT_EQ(ctx->measBlocks[0].dmtfType, 0x00, "Block 0 type wrong"); ASSERT_EQ(ctx->measBlocks[0].valueSize, 4, "Block 0 valueSize wrong"); @@ -561,7 +776,7 @@ static int test_parse_measurements(void) /* Test truncated buffer */ ASSERT_FAIL(wolfSPDM_ParseMeasurements(ctx, rsp, 5)); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } @@ -569,8 +784,6 @@ static int test_parse_measurements(void) static int test_measurement_sig_verification(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; ecc_key sigKey; WC_RNG rng; /* Construct a minimal GET_MEASUREMENTS request (L1) */ @@ -612,12 +825,10 @@ static int test_measurement_sig_verification(void) word32 rSz = sizeof(rawR); word32 sSz = sizeof(rawS); int rc; + TEST_CTX_SETUP_V12(); printf("test_measurement_sig_verification...\n"); - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; - /* Generate ECC P-384 keypair for testing */ rc = wc_InitRng(&rng); TEST_ASSERT(rc == 0, "wc_InitRng failed"); @@ -642,7 +853,7 @@ static int test_measurement_sig_verification(void) pubDerSz); TEST_ASSERT(rc == 0, "EccPublicKeyDecode failed"); } - ctx->hasResponderPubKey = 1; + ctx->flags.hasResponderPubKey = 1; /* Build the response buffer (rspBase + signature) */ XMEMCPY(rspBuf, rspBase, sizeof(rspBase)); @@ -730,7 +941,7 @@ static int test_measurement_sig_verification(void) wc_ecc_free(&sigKey); wc_FreeRng(&rng); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } @@ -743,36 +954,31 @@ static int test_measurement_sig_verification(void) static int test_set_trusted_cas(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte fakeCa[] = {0x30, 0x82, 0x01, 0x00, 0xAA, 0xBB, 0xCC, 0xDD}; + TEST_CTX_SETUP(); printf("test_set_trusted_cas...\n"); - - wolfSPDM_Init(ctx); ASSERT_FAIL(wolfSPDM_SetTrustedCAs(NULL, fakeCa, sizeof(fakeCa))); ASSERT_FAIL(wolfSPDM_SetTrustedCAs(ctx, NULL, sizeof(fakeCa))); ASSERT_FAIL(wolfSPDM_SetTrustedCAs(ctx, fakeCa, 0)); ASSERT_SUCCESS(wolfSPDM_SetTrustedCAs(ctx, fakeCa, sizeof(fakeCa))); - ASSERT_EQ(ctx->hasTrustedCAs, 1, "hasTrustedCAs not set"); + ASSERT_EQ(ctx->flags.hasTrustedCAs, 1, "hasTrustedCAs not set"); ASSERT_EQ(ctx->trustedCAsSz, sizeof(fakeCa), "Size mismatch"); ASSERT_EQ(memcmp(ctx->trustedCAs, fakeCa, sizeof(fakeCa)), 0, "Data mismatch"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_validate_cert_chain_no_cas(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; + TEST_CTX_SETUP(); printf("test_validate_cert_chain_no_cas...\n"); - wolfSPDM_Init(ctx); ASSERT_EQ(wolfSPDM_ValidateCertChain(ctx), WOLFSPDM_E_CERT_PARSE, "Should fail without trusted CAs"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } @@ -784,17 +990,13 @@ static int test_validate_cert_chain_no_cas(void) static int test_build_challenge(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte buf[64]; byte zeros[32]; word32 bufSz; + TEST_CTX_SETUP_V12(); printf("test_build_challenge...\n"); - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; - bufSz = sizeof(buf); ASSERT_SUCCESS(wolfSPDM_BuildChallenge(ctx, buf, &bufSz, 0, SPDM_MEAS_SUMMARY_HASH_NONE)); ASSERT_EQ(bufSz, 36, "CHALLENGE should be 36 bytes"); @@ -809,27 +1011,33 @@ static int test_build_challenge(void) ASSERT_SUCCESS(wolfSPDM_BuildChallenge(ctx, buf, &bufSz, 3, SPDM_MEAS_SUMMARY_HASH_ALL)); ASSERT_EQ(buf[2], 0x03, "SlotID should be 3"); + /* SPDM 1.3+ adds an 8-byte RequesterContext at the end. */ + ctx->spdmVersion = SPDM_VERSION_13; + bufSz = sizeof(buf); + ASSERT_SUCCESS(wolfSPDM_BuildChallenge(ctx, buf, &bufSz, 0, SPDM_MEAS_SUMMARY_HASH_NONE)); + ASSERT_EQ(bufSz, 44, "1.3 CHALLENGE should be 36 + 8 = 44 bytes"); + ASSERT_EQ(memcmp(ctx->challengeReqCtx, &buf[36], 8), 0, + "ReqCtx should follow nonce"); + /* Buffer too small */ + ctx->spdmVersion = SPDM_VERSION_12; bufSz = 10; ASSERT_EQ(wolfSPDM_BuildChallenge(ctx, buf, &bufSz, 0, SPDM_MEAS_SUMMARY_HASH_NONE), WOLFSPDM_E_BUFFER_SMALL, "Should fail on small buffer"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_parse_challenge_auth(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; /* Fake CHALLENGE_AUTH: hdr(4) + CertHash(48) + Nonce(32) + OpaqueLen(2) + Sig(96) = 182 */ byte rsp[182]; word32 sigOffset = 0; + TEST_CTX_SETUP_V12(); printf("test_parse_challenge_auth...\n"); - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; ctx->challengeMeasHashType = SPDM_MEAS_SUMMARY_HASH_NONE; XMEMSET(rsp, 0, sizeof(rsp)); @@ -854,7 +1062,51 @@ static int test_parse_challenge_auth(void) ASSERT_EQ(wolfSPDM_ParseChallengeAuth(ctx, rsp, sizeof(rsp), &sigOffset), WOLFSPDM_E_CHALLENGE, "Hash mismatch should fail"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_parse_challenge_auth_reqctx_echo(void) +{ + /* SPDM 1.3+: CHALLENGE_AUTH carries an 8-byte echo of the + * RequesterContext from the request. wolfSPDM_ParseChallengeAuth + * must compare it against ctx->challengeReqCtx and reject mismatches. */ + byte rsp[190]; /* 1.3 layout: hdr(4) + cert(48) + nonce(32) + opaqueLen(2) + reqCtx(8) + sig(96) */ + word32 sigOffset = 0; + int i; + TEST_CTX_SETUP(); + + printf("test_parse_challenge_auth_reqctx_echo...\n"); + + ctx->spdmVersion = SPDM_VERSION_13; + ctx->challengeMeasHashType = SPDM_MEAS_SUMMARY_HASH_NONE; + + XMEMSET(rsp, 0, sizeof(rsp)); + rsp[0] = SPDM_VERSION_13; + rsp[1] = SPDM_CHALLENGE_AUTH; + XMEMSET(&rsp[4], 0xAA, WOLFSPDM_HASH_SIZE); /* CertHash */ + XMEMCPY(ctx->certChainHash, &rsp[4], WOLFSPDM_HASH_SIZE); + XMEMSET(&rsp[52], 0xBB, 32); /* Nonce */ + /* OpaqueLen at offset 84..85 = 0 (already memset) */ + /* RequesterContext echo at offset 86..93 */ + for (i = 0; i < 8; i++) { + rsp[86 + i] = (byte)(0xE0 + i); + ctx->challengeReqCtx[i] = (byte)(0xE0 + i); + } + /* Signature at offset 94..189 */ + XMEMSET(&rsp[94], 0xCC, WOLFSPDM_ECC_SIG_SIZE); + + ASSERT_SUCCESS( + wolfSPDM_ParseChallengeAuth(ctx, rsp, sizeof(rsp), &sigOffset)); + ASSERT_EQ(sigOffset, 94, "Signature offset should be 94 (1.3+)"); + + /* Flip a single echo byte; parser must reject. */ + rsp[86] ^= 0x01; + ASSERT_EQ( + wolfSPDM_ParseChallengeAuth(ctx, rsp, sizeof(rsp), &sigOffset), + WOLFSPDM_E_CHALLENGE, "Tampered ReqCtx echo must be refused"); + + TEST_CTX_FREE(); TEST_PASS(); } @@ -866,15 +1118,11 @@ static int test_parse_challenge_auth(void) static int test_build_heartbeat(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte buf[16]; word32 bufSz; + TEST_CTX_SETUP_V12(); printf("test_build_heartbeat...\n"); - - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; bufSz = sizeof(buf); ASSERT_SUCCESS(wolfSPDM_BuildHeartbeat(ctx, buf, &bufSz)); ASSERT_EQ(bufSz, 4, "HEARTBEAT should be 4 bytes"); @@ -883,38 +1131,32 @@ static int test_build_heartbeat(void) bufSz = 2; ASSERT_EQ(wolfSPDM_BuildHeartbeat(ctx, buf, &bufSz), WOLFSPDM_E_BUFFER_SMALL, "Should fail on small buffer"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_parse_heartbeat_ack(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte ack[] = {0x12, SPDM_HEARTBEAT_ACK, 0x00, 0x00}; byte err[] = {0x12, SPDM_ERROR, 0x01, 0x00}; + TEST_CTX_SETUP(); printf("test_parse_heartbeat_ack...\n"); - - wolfSPDM_Init(ctx); ASSERT_SUCCESS(wolfSPDM_ParseHeartbeatAck(ctx, ack, sizeof(ack))); ASSERT_EQ(wolfSPDM_ParseHeartbeatAck(ctx, err, sizeof(err)), WOLFSPDM_E_PEER_ERROR, "Error should return PEER_ERROR"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_heartbeat_state_check(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; + TEST_CTX_SETUP(); printf("test_heartbeat_state_check...\n"); - - wolfSPDM_Init(ctx); ASSERT_EQ(wolfSPDM_Heartbeat(ctx), WOLFSPDM_E_NOT_CONNECTED, "Heartbeat should fail when not connected"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } @@ -924,16 +1166,12 @@ static int test_heartbeat_state_check(void) static int test_build_key_update(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte buf[16]; word32 bufSz; byte tag = 0; + TEST_CTX_SETUP_V12(); printf("test_build_key_update...\n"); - - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; bufSz = sizeof(buf); ASSERT_SUCCESS(wolfSPDM_BuildKeyUpdate(ctx, buf, &bufSz, SPDM_KEY_UPDATE_OP_UPDATE_ALL_KEYS, &tag)); ASSERT_EQ(bufSz, 4, "KEY_UPDATE should be 4 bytes"); @@ -945,40 +1183,33 @@ static int test_build_key_update(void) ASSERT_EQ(wolfSPDM_BuildKeyUpdate(ctx, buf, &bufSz, SPDM_KEY_UPDATE_OP_UPDATE_KEY, &tag), WOLFSPDM_E_BUFFER_SMALL, "Should fail on small buffer"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_parse_key_update_ack(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte ack[] = {0x12, SPDM_KEY_UPDATE_ACK, 0x02, 0x42}; + TEST_CTX_SETUP(); printf("test_parse_key_update_ack...\n"); - - wolfSPDM_Init(ctx); ASSERT_SUCCESS(wolfSPDM_ParseKeyUpdateAck(ctx, ack, sizeof(ack), SPDM_KEY_UPDATE_OP_UPDATE_ALL_KEYS, 0x42)); ASSERT_EQ(wolfSPDM_ParseKeyUpdateAck(ctx, ack, sizeof(ack), SPDM_KEY_UPDATE_OP_UPDATE_ALL_KEYS, 0xFF), WOLFSPDM_E_KEY_UPDATE, "Mismatched tag should fail"); ASSERT_EQ(wolfSPDM_ParseKeyUpdateAck(ctx, ack, sizeof(ack), SPDM_KEY_UPDATE_OP_UPDATE_KEY, 0x42), WOLFSPDM_E_KEY_UPDATE, "Mismatched op should fail"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_derive_updated_keys(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; byte origReqKey[WOLFSPDM_AEAD_KEY_SIZE]; byte origRspKey[WOLFSPDM_AEAD_KEY_SIZE]; + TEST_CTX_SETUP_V12(); printf("test_derive_updated_keys...\n"); - - wolfSPDM_Init(ctx); - ctx->spdmVersion = SPDM_VERSION_12; XMEMSET(ctx->reqAppSecret, 0x5A, WOLFSPDM_HASH_SIZE); XMEMSET(ctx->rspAppSecret, 0xA5, WOLFSPDM_HASH_SIZE); XMEMSET(ctx->reqDataKey, 0x11, WOLFSPDM_AEAD_KEY_SIZE); @@ -998,21 +1229,459 @@ static int test_derive_updated_keys(void) ASSERT_NE(memcmp(ctx->reqDataKey, origReqKey, WOLFSPDM_AEAD_KEY_SIZE), 0, "Req key should change"); ASSERT_EQ(memcmp(ctx->rspDataKey, origRspKey, WOLFSPDM_AEAD_KEY_SIZE), 0, "Rsp key should NOT change"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } static int test_key_update_state_check(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; + TEST_CTX_SETUP(); printf("test_key_update_state_check...\n"); - - wolfSPDM_Init(ctx); ASSERT_EQ(wolfSPDM_KeyUpdate(ctx, 1), WOLFSPDM_E_NOT_CONNECTED, "KeyUpdate should fail when not connected"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); + TEST_PASS(); +} + +/* ========================================================================== */ +/* Multi-Version Tests */ +/* ========================================================================== */ + +static int test_kdf_version_prefix(void) +{ + byte secret[48]; + byte context[48]; + byte out12[32], out13[32], out14[32]; + + printf("test_kdf_version_prefix...\n"); + + memset(secret, 0x5A, sizeof(secret)); + memset(context, 0x00, sizeof(context)); + + ASSERT_SUCCESS(wolfSPDM_HkdfExpandLabel(SPDM_VERSION_12, secret, + sizeof(secret), SPDM_LABEL_KEY, context, sizeof(context), + out12, sizeof(out12))); + ASSERT_SUCCESS(wolfSPDM_HkdfExpandLabel(SPDM_VERSION_13, secret, + sizeof(secret), SPDM_LABEL_KEY, context, sizeof(context), + out13, sizeof(out13))); + ASSERT_SUCCESS(wolfSPDM_HkdfExpandLabel(SPDM_VERSION_14, secret, + sizeof(secret), SPDM_LABEL_KEY, context, sizeof(context), + out14, sizeof(out14))); + + /* All three outputs should differ due to different BinConcat prefixes */ + ASSERT_NE(memcmp(out12, out13, sizeof(out12)), 0, + "1.2 and 1.3 outputs should differ"); + ASSERT_NE(memcmp(out13, out14, sizeof(out13)), 0, + "1.3 and 1.4 outputs should differ"); + ASSERT_NE(memcmp(out12, out14, sizeof(out12)), 0, + "1.2 and 1.4 outputs should differ"); + + TEST_PASS(); +} + +static int test_hmac_mismatch_negative(void) +{ + byte finishedKeyA[WOLFSPDM_HASH_SIZE]; + byte finishedKeyB[WOLFSPDM_HASH_SIZE]; + byte thHash[WOLFSPDM_HASH_SIZE]; + byte verifyA[WOLFSPDM_HASH_SIZE]; + byte verifyB[WOLFSPDM_HASH_SIZE]; + + printf("test_hmac_mismatch_negative...\n"); + + memset(finishedKeyA, 0xAB, sizeof(finishedKeyA)); + memset(finishedKeyB, 0xAC, sizeof(finishedKeyB)); /* Differs by 1 bit */ + memset(thHash, 0xCD, sizeof(thHash)); + + ASSERT_SUCCESS(wolfSPDM_ComputeVerifyData(finishedKeyA, thHash, verifyA)); + ASSERT_SUCCESS(wolfSPDM_ComputeVerifyData(finishedKeyB, thHash, verifyB)); + + /* Single-bit change in key must produce different verify data */ + ASSERT_NE(memcmp(verifyA, verifyB, WOLFSPDM_HASH_SIZE), 0, + "Different keys should produce different verify data"); + + TEST_PASS(); +} + +static int test_transcript_overflow(void) +{ + byte chunk[256]; + word32 i, needed; + TEST_CTX_SETUP(); + + printf("test_transcript_overflow...\n"); + + memset(chunk, 0x42, sizeof(chunk)); + + /* Fill transcript to capacity */ + needed = WOLFSPDM_MAX_TRANSCRIPT / sizeof(chunk); + for (i = 0; i < needed; i++) { + ASSERT_SUCCESS(wolfSPDM_TranscriptAdd(ctx, chunk, sizeof(chunk))); + } + ASSERT_EQ(ctx->transcriptLen, (word32)(needed * sizeof(chunk)), + "Transcript should be full"); + + /* Next add should fail with BUFFER_SMALL */ + ASSERT_EQ(wolfSPDM_TranscriptAdd(ctx, chunk, sizeof(chunk)), + WOLFSPDM_E_BUFFER_SMALL, "Overflow should return BUFFER_SMALL"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +#ifndef NO_WOLFSPDM_MEAS + +/* Mock I/O callback that returns a fixed SPDM_ERROR response. */ +static int error_io_cb(WOLFSPDM_CTX* ctx, + const byte* txBuf, word32 txSz, + byte* rxBuf, word32* rxSz, void* userCtx) +{ + (void)ctx; (void)txBuf; (void)txSz; (void)userCtx; + if (*rxSz < 4) return -1; + rxBuf[0] = SPDM_VERSION_12; + rxBuf[1] = SPDM_ERROR; + rxBuf[2] = SPDM_ERROR_INVALID_REQUEST; /* param1 = error code */ + rxBuf[3] = 0x00; /* param2 */ + *rxSz = 4; + return 0; +} + +/* Captures the txBuf the wolfSPDM_SecuredExchange layer hands us, so a + * test can prove the encrypt path ran (txBuf will start with the + * sessionId, not an SPDM version byte). Returns "session terminated" + * via a peer error so the caller doesn't try to parse a bogus response. */ +typedef struct { byte first; int hit; } SECURED_PROBE; +static int secured_probe_io_cb(WOLFSPDM_CTX* ctx, + const byte* txBuf, word32 txSz, + byte* rxBuf, word32* rxSz, void* userCtx) +{ + SECURED_PROBE* p = (SECURED_PROBE*)userCtx; + (void)ctx; + if (p != NULL && txSz > 0 && txBuf != NULL) { + p->first = txBuf[0]; + p->hit = 1; + } + /* Respond with a 4-byte SPDM_ERROR so the caller surfaces PEER_ERROR. */ + if (*rxSz < 4) return -1; + rxBuf[0] = SPDM_VERSION_12; + rxBuf[1] = SPDM_ERROR; + rxBuf[2] = SPDM_ERROR_INVALID_REQUEST; + rxBuf[3] = 0x00; + *rxSz = 4; + return 0; +} + +static int test_get_measurements_uses_secured_when_measured(void) +{ + /* After the first GetMeasurements completes, ctx->state advances to + * WOLFSPDM_STATE_MEASURED. The dispatch must keep using the encrypted + * (SecuredExchange) path on subsequent calls rather than falling back + * to cleartext. We assert this by capturing the first txBuf byte: + * - plain SPDM: version byte (0x10-0x1F) + * - secured: low byte of sessionId (we plant 0xAB) */ + SECURED_PROBE probe = { 0, 0 }; + int i; + TEST_CTX_SETUP_V12(); + + printf("test_get_measurements_uses_secured_when_measured...\n"); + + ctx->state = WOLFSPDM_STATE_MEASURED; + ctx->sessionId = 0xCAFEBAAB; /* LSB = 0xAB, distinct from 0x10..0x1F */ + for (i = 0; i < WOLFSPDM_AEAD_KEY_SIZE; i++) { + ctx->reqDataKey[i] = (byte)(i + 7); + ctx->rspDataKey[i] = (byte)(i + 7); + } + for (i = 0; i < WOLFSPDM_AEAD_IV_SIZE; i++) { + ctx->reqDataIv[i] = (byte)(0x60 + i); + ctx->rspDataIv[i] = (byte)(0x60 + i); + } + ASSERT_SUCCESS(wolfSPDM_SetIO(ctx, secured_probe_io_cb, &probe)); + + /* Drive GetMeasurements. The mocked I/O returns SPDM_ERROR, which is + * fine - we only care that the secured path was taken. */ + (void)wolfSPDM_GetMeasurements(ctx, SPDM_MEAS_OPERATION_ALL, 0); + ASSERT_EQ(probe.hit, 1, "I/O callback should have been invoked"); + ASSERT_EQ(probe.first, 0xAB, + "STATE_MEASURED dispatch must use SecuredExchange (sessionId byte)"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_get_measurements_peer_error(void) +{ + TEST_CTX_SETUP_V12(); + + printf("test_get_measurements_peer_error...\n"); + + /* Drive GetMeasurements through a callback that returns SPDM_ERROR - + * exercises the new wolfSPDM_CheckError branch before ParseMeasurements. */ + ctx->state = WOLFSPDM_STATE_ALGO; + ASSERT_SUCCESS(wolfSPDM_SetIO(ctx, error_io_cb, NULL)); + + ASSERT_EQ( + wolfSPDM_GetMeasurements(ctx, SPDM_MEAS_OPERATION_ALL, 0), + WOLFSPDM_E_PEER_ERROR, + "SPDM_ERROR response should surface as PEER_ERROR"); + /* Side-effect: the responder's error code must be reachable via + * the public accessor so callers can branch on BUSY vs UNSUPPORTED. */ + ASSERT_EQ(wolfSPDM_GetLastPeerError(ctx), SPDM_ERROR_INVALID_REQUEST, + "Peer error code should be captured for retrieval"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_parse_measurements_13_requester_context(void) +{ + /* Exercise the SPDM 1.3+ branch in ParseMeasurements that requires + * the 8-byte RequesterContext echo between OpaqueData and Signature. + * + * Layout for SIGNED 1.3+: hdr(4) + NumBlocks(1) + RecLen(3) + Record(N) + * + Nonce(32) + OpaqueLen(2) + RequesterContext(8) + * + Signature(96). + * We use a 0-block record (8 hdr) so the parser jumps straight to the + * post-record tail. */ + byte rsp[8 + 32 + 2 + 8 + WOLFSPDM_ECC_SIG_SIZE]; + byte rspShort[8 + 32 + 2 + 4]; /* truncated: ReqCtx missing */ + TEST_CTX_SETUP(); + + printf("test_parse_measurements_13_requester_context...\n"); + + ctx->spdmVersion = SPDM_VERSION_13; + + /* Valid response: 0 blocks, RecLen=0, Nonce(32), OpaqueLen=0, ReqCtx(8), Sig(96). */ + XMEMSET(rsp, 0, sizeof(rsp)); + rsp[0] = SPDM_VERSION_13; + rsp[1] = SPDM_MEASUREMENTS; + /* NumBlocks=0 at buf[4]; RecLen=0 at buf[5..7]; nothing else needs writing. */ + ASSERT_SUCCESS(wolfSPDM_ParseMeasurements(ctx, rsp, sizeof(rsp))); + ASSERT_EQ(ctx->measSignatureSize, WOLFSPDM_ECC_SIG_SIZE, + "Signature should be captured"); + + /* Missing RequesterContext (only 4 bytes after OpaqueLen) - must fail. */ + XMEMSET(rspShort, 0, sizeof(rspShort)); + rspShort[0] = SPDM_VERSION_13; + rspShort[1] = SPDM_MEASUREMENTS; + ASSERT_EQ(wolfSPDM_ParseMeasurements(ctx, rspShort, sizeof(rspShort)), + WOLFSPDM_E_MEASUREMENT, + "1.3+ response missing RequesterContext must be refused"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_parse_measurements_negative(void) +{ + byte truncated[] = {0x12, 0x60, 0x00, 0x00, 0x01}; + byte wrongCode[] = {0x12, 0xFF, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00}; + TEST_CTX_SETUP(); + + printf("test_parse_measurements_negative...\n"); + + /* Truncated buffer */ + ASSERT_FAIL(wolfSPDM_ParseMeasurements(ctx, truncated, sizeof(truncated))); + + /* Wrong response code */ + ASSERT_FAIL(wolfSPDM_ParseMeasurements(ctx, wrongCode, sizeof(wrongCode))); + + /* NULL inputs */ + ASSERT_FAIL(wolfSPDM_ParseMeasurements(NULL, truncated, sizeof(truncated))); + ASSERT_FAIL(wolfSPDM_ParseMeasurements(ctx, NULL, sizeof(truncated))); + + /* Zero length */ + ASSERT_FAIL(wolfSPDM_ParseMeasurements(ctx, truncated, 0)); + + TEST_CTX_FREE(); + TEST_PASS(); +} +#endif /* !NO_WOLFSPDM_MEAS */ + +static int test_version_fallback(void) +{ + /* Fake VERSION response with versions 1.0, 1.1, 1.2, 1.3 */ + byte rsp[] = { + 0x10, SPDM_VERSION, 0x00, 0x00, /* header */ + 0x04, 0x00, /* entryCount = 4 */ + 0x00, 0x10, /* 1.0 */ + 0x00, 0x11, /* 1.1 */ + 0x00, 0x12, /* 1.2 */ + 0x00, 0x13 /* 1.3 */ + }; + TEST_CTX_SETUP(); + + printf("test_version_fallback...\n"); + + /* With no maxVersion set, should select 1.3 (highest mutual) */ + ASSERT_SUCCESS(wolfSPDM_ParseVersion(ctx, rsp, sizeof(rsp))); + ASSERT_EQ(ctx->spdmVersion, SPDM_VERSION_13, + "Should select 1.3 as highest mutual"); + + /* Reset state and set maxVersion to 1.2 */ + ctx->state = WOLFSPDM_STATE_INIT; + ctx->spdmVersion = 0; + ctx->maxVersion = SPDM_VERSION_12; + ASSERT_SUCCESS(wolfSPDM_ParseVersion(ctx, rsp, sizeof(rsp))); + ASSERT_EQ(ctx->spdmVersion, SPDM_VERSION_12, + "Should fall back to 1.2 with maxVersion cap"); + + /* A responder advertising 1.4 should be selected when we allow it. */ + { + byte rsp14[] = { + 0x10, SPDM_VERSION, 0x00, 0x00, + 0x05, 0x00, + 0x00, 0x10, + 0x00, 0x11, + 0x00, 0x12, + 0x00, 0x13, + 0x00, 0x14 + }; + ctx->state = WOLFSPDM_STATE_INIT; + ctx->spdmVersion = 0; + ctx->maxVersion = 0; /* compile-time default = 1.4 */ + ASSERT_SUCCESS(wolfSPDM_ParseVersion(ctx, rsp14, sizeof(rsp14))); + ASSERT_EQ(ctx->spdmVersion, SPDM_VERSION_14, + "Should select 1.4 when offered and allowed"); + } + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_set_max_version(void) +{ + TEST_CTX_SETUP(); + + printf("test_set_max_version...\n"); + + /* Valid versions */ + ASSERT_SUCCESS(wolfSPDM_SetMaxVersion(ctx, SPDM_VERSION_12)); + ASSERT_EQ(ctx->maxVersion, SPDM_VERSION_12, "maxVersion should be 0x12"); + ASSERT_SUCCESS(wolfSPDM_SetMaxVersion(ctx, SPDM_VERSION_14)); + ASSERT_EQ(ctx->maxVersion, SPDM_VERSION_14, "maxVersion should be 0x14"); + + /* Reset to default */ + ASSERT_SUCCESS(wolfSPDM_SetMaxVersion(ctx, 0)); + ASSERT_EQ(ctx->maxVersion, 0, "maxVersion should be 0 (default)"); + + /* Invalid: too low */ + ASSERT_FAIL(wolfSPDM_SetMaxVersion(ctx, 0x11)); + /* Invalid: too high */ + ASSERT_FAIL(wolfSPDM_SetMaxVersion(ctx, 0x15)); + /* NULL ctx */ + ASSERT_FAIL(wolfSPDM_SetMaxVersion(NULL, SPDM_VERSION_12)); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +/* ========================================================================== */ +/* Sequence Number Wrap Tests */ +/* ========================================================================== */ + +static int test_sequence_number_mismatch(void) +{ + /* DSP0277 Sec. 11 mandates strict monotonic sequence numbers on the + * receive side. Plant a record whose wire seqNum doesn't match the + * locally expected counter and confirm DecryptInternal refuses it. */ + byte plain[] = "hello-spdm"; + byte enc[256]; + byte dec[256]; + word32 encSz = sizeof(enc); + word32 decSz = sizeof(dec); + int i; + TEST_CTX_SETUP_V12(); + + printf("test_sequence_number_mismatch...\n"); + + ctx->state = WOLFSPDM_STATE_CONNECTED; + ctx->sessionId = 0xCAFEBABE; + for (i = 0; i < WOLFSPDM_AEAD_KEY_SIZE; i++) { + ctx->reqDataKey[i] = (byte)(i + 1); + ctx->rspDataKey[i] = (byte)(i + 1); + } + for (i = 0; i < WOLFSPDM_AEAD_IV_SIZE; i++) { + ctx->reqDataIv[i] = (byte)(0x40 + i); + ctx->rspDataIv[i] = (byte)(0x40 + i); + } + + /* Encrypt at req seq = 5 (wire seqNum encoded as 5). */ + ctx->reqSeqNum = 5; + ASSERT_SUCCESS(wolfSPDM_EncryptInternal(ctx, plain, sizeof(plain), + enc, &encSz)); + + /* Decrypt-side expects seq = 7, not 5 - must reject. */ + ctx->rspSeqNum = 7; + ASSERT_EQ(wolfSPDM_DecryptInternal(ctx, enc, encSz, dec, &decSz), + WOLFSPDM_E_SEQUENCE, "seqNum mismatch must return SEQUENCE error"); + + /* Sanity: when seq matches, decrypt succeeds. */ + ctx->rspSeqNum = 5; + decSz = sizeof(dec); + ASSERT_SUCCESS(wolfSPDM_DecryptInternal(ctx, enc, encSz, dec, &decSz)); + ASSERT_EQ(memcmp(dec, plain, sizeof(plain)), 0, "Decrypt payload mismatch"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_sequence_number_wrap(void) +{ + byte plain[] = "hello-spdm"; + byte enc[256]; + word32 encSz = sizeof(enc); + int i; + TEST_CTX_SETUP_V12(); + + printf("test_sequence_number_wrap...\n"); + + /* Zero the encrypt buffer so a future change that reads before the + * seq-check fails would be visible rather than reading stack garbage. */ + XMEMSET(enc, 0, sizeof(enc)); + + /* Set up a fake session: loopback (req keys == rsp keys). */ + ctx->state = WOLFSPDM_STATE_CONNECTED; + ctx->sessionId = 0xDEADBEEF; + for (i = 0; i < WOLFSPDM_AEAD_KEY_SIZE; i++) { + ctx->reqDataKey[i] = (byte)i; + ctx->rspDataKey[i] = (byte)i; + } + for (i = 0; i < WOLFSPDM_AEAD_IV_SIZE; i++) { + ctx->reqDataIv[i] = (byte)(0x20 + i); + ctx->rspDataIv[i] = (byte)(0x20 + i); + } + + /* DSP0277 Sec. 11.3: wire seqNum is 16-bit and shall not wrap. wolfSPDM_BuildIV + * mixes only the low 16 bits into the AES-GCM IV, so any wrap would reuse + * an IV under the same key. Encrypt/decrypt must refuse to proceed past + * the 16-bit boundary; caller is expected to wolfSPDM_KeyUpdate first. */ + + /* Counter planted just past the 16-bit wire boundary: encrypt must + * refuse rather than reuse an IV. */ + ctx->reqSeqNum = 0x10000; + encSz = sizeof(enc); + ASSERT_EQ(wolfSPDM_EncryptInternal(ctx, plain, sizeof(plain), enc, &encSz), + WOLFSPDM_E_SEQUENCE, "Encrypt past seq=0xFFFF must be refused"); + + /* Decrypt-side cap mirrors the encrypt side. Use a separate output + * buffer so a future change that writes before checking seqNum would + * be caught instead of silently corrupting the input. */ + ctx->rspSeqNum = 0x10000; + { + byte dec[256]; + word32 decSz = sizeof(dec); + ASSERT_EQ(wolfSPDM_DecryptInternal(ctx, enc, sizeof(enc), dec, &decSz), + WOLFSPDM_E_SEQUENCE, "Decrypt past seq=0xFFFF must be refused"); + } + + /* A counter just inside the limit still works. */ + ctx->reqSeqNum = 0xFFFF; + encSz = sizeof(enc); + ASSERT_SUCCESS(wolfSPDM_EncryptInternal(ctx, plain, sizeof(plain), + enc, &encSz)); + + TEST_CTX_FREE(); TEST_PASS(); } @@ -1022,24 +1691,31 @@ static int test_key_update_state_check(void) static int test_session_state(void) { - WOLFSPDM_CTX ctxBuf; - WOLFSPDM_CTX* ctx = &ctxBuf; + TEST_CTX_SETUP(); printf("test_session_state...\n"); - - wolfSPDM_Init(ctx); ASSERT_EQ(wolfSPDM_IsConnected(ctx), 0, "Should not be connected"); ASSERT_EQ(wolfSPDM_GetSessionId(ctx), 0, "SessionId should be 0"); + /* Mid-handshake (KEY_EXCHANGE done, FINISH pending): IsConnected is + * still 0 but sessionId IS available so the I/O callback can tag + * the encrypted FINISH record. */ + ctx->state = WOLFSPDM_STATE_KEY_EX; + ctx->sessionId = 0x12345678; + ASSERT_EQ(wolfSPDM_IsConnected(ctx), 0, + "IsConnected should remain 0 mid-handshake"); + ASSERT_EQ(wolfSPDM_GetSessionId(ctx), (word32)0x12345678, + "GetSessionId must return value before STATE_CONNECTED"); + /* Simulate connected state */ ctx->state = WOLFSPDM_STATE_CONNECTED; ctx->sessionId = 0xAABBCCDD; ctx->spdmVersion = SPDM_VERSION_12; ASSERT_EQ(wolfSPDM_IsConnected(ctx), 1, "Should be connected"); ASSERT_EQ(wolfSPDM_GetSessionId(ctx), (word32)0xAABBCCDD, "SessionId wrong"); - ASSERT_EQ(wolfSPDM_GetVersion_Negotiated(ctx), SPDM_VERSION_12, "Version wrong"); + ASSERT_EQ(wolfSPDM_GetNegotiatedVersion(ctx), SPDM_VERSION_12, "Version wrong"); - wolfSPDM_Free(ctx); + TEST_CTX_FREE(); TEST_PASS(); } @@ -1078,8 +1754,15 @@ int main(void) test_build_get_version(); test_build_get_capabilities(); test_build_negotiate_algorithms(); + test_parse_algorithms_set_b_enforcement(); test_build_get_digests(); test_build_get_certificate(); + test_build_key_exchange_opaque_data(); + test_build_finish_opaque_length_14(); + test_parse_finish_rsp_14_opaque_length(); + test_key_exchange_requires_cert(); + test_parse_key_exchange_rsp_too_short(); + test_parse_key_exchange_rsp_mutual_auth_refused(); test_build_end_session(); /* Error tests */ @@ -1104,6 +1787,7 @@ int main(void) #ifndef NO_WOLFSPDM_CHALLENGE test_build_challenge(); test_parse_challenge_auth(); + test_parse_challenge_auth_reqctx_echo(); #endif /* Heartbeat tests */ @@ -1117,6 +1801,21 @@ int main(void) test_derive_updated_keys(); test_key_update_state_check(); + /* Multi-version tests */ + test_kdf_version_prefix(); + test_hmac_mismatch_negative(); + test_transcript_overflow(); +#ifndef NO_WOLFSPDM_MEAS + test_parse_measurements_negative(); + test_parse_measurements_13_requester_context(); + test_get_measurements_peer_error(); + test_get_measurements_uses_secured_when_measured(); +#endif + test_version_fallback(); + test_set_max_version(); + test_sequence_number_wrap(); + test_sequence_number_mismatch(); + /* Session state tests */ test_session_state(); diff --git a/wolfspdm/spdm.h b/wolfspdm/spdm.h index 1577afb..aad5b64 100644 --- a/wolfspdm/spdm.h +++ b/wolfspdm/spdm.h @@ -22,7 +22,7 @@ #ifndef WOLFSPDM_SPDM_H #define WOLFSPDM_SPDM_H -/* Include build options (WOLFSPDM_DYNAMIC_MEMORY, WOLFSPDM_NUVOTON, etc.) +/* Include build options (WOLFSPDM_DYNAMIC_MEMORY, etc.) * Generated from config.h during build; installed alongside this header. */ #ifndef HAVE_CONFIG_H #include @@ -31,7 +31,7 @@ #include #include -/* Feature detection macros — external projects (e.g. wolfTPM) can check these +/* Feature detection macros - external projects (e.g. wolfTPM) can check these * to conditionally compile against optional wolfSPDM APIs. */ #ifndef NO_WOLFSPDM_MEAS #define WOLFSPDM_HAS_MEASUREMENTS @@ -46,25 +46,10 @@ extern "C" { #endif -/* --- Protocol Mode Selection --- - * - * wolfSPDM supports two protocol modes: - * - * WOLFSPDM_MODE_STANDARD (default): - * Standard SPDM 1.2 protocol per DMTF DSP0274/DSP0277. - * Flow: GET_VERSION -> GET_CAPABILITIES -> NEGOTIATE_ALGORITHMS -> - * GET_DIGESTS -> GET_CERTIFICATE -> KEY_EXCHANGE -> FINISH - * Use with: libspdm emulator, standard SPDM responders - * - * WOLFSPDM_MODE_NUVOTON (requires --enable-nuvoton): - * Nuvoton TPM-specific protocol with TCG binding headers. - * Flow: GET_VERSION -> GET_PUB_KEY -> KEY_EXCHANGE -> GIVE_PUB_KEY -> FINISH - * Use with: Nuvoton NPCT75x TPMs (FW 7.2+) */ - -typedef enum { - WOLFSPDM_MODE_STANDARD = 0, /* Standard SPDM 1.2 (default) */ - WOLFSPDM_MODE_NUVOTON = 1, /* Nuvoton TCG binding + vendor commands */ -} WOLFSPDM_MODE; +/* wolfSPDM implements the standard SPDM 1.2+ protocol per DMTF DSP0274/DSP0277. + * Flow: GET_VERSION -> GET_CAPABILITIES -> NEGOTIATE_ALGORITHMS -> + * GET_DIGESTS -> GET_CERTIFICATE -> KEY_EXCHANGE -> FINISH + * Use with: libspdm emulator, standard SPDM responders */ /* --- wolfSPDM Overview --- * @@ -75,6 +60,7 @@ typedef enum { * - Requester-only (initiator) implementation * - Algorithm Set B fixed: P-384/SHA-384/AES-256-GCM * - Full transcript tracking for proper TH1/TH2 computation + * - Supports SPDM 1.2, 1.3, and 1.4 * - Compatible with libspdm emulator for testing * - No external dependencies beyond wolfCrypt * @@ -113,11 +99,6 @@ typedef enum { struct WOLFSPDM_CTX; typedef struct WOLFSPDM_CTX WOLFSPDM_CTX; -/* Include Nuvoton support if enabled (must be after WOLFSPDM_CTX forward declaration) */ -#ifdef WOLFSPDM_NUVOTON - #include -#endif - /* --- I/O Callback --- * * The I/O callback is called by wolfSPDM to send and receive raw SPDM @@ -158,7 +139,7 @@ typedef int (*WOLFSPDM_IO_CB)( * @param ctx The wolfSPDM context. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_Init(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_Init(WOLFSPDM_CTX* ctx); #ifdef WOLFSPDM_DYNAMIC_MEMORY /** @@ -168,7 +149,7 @@ int wolfSPDM_Init(WOLFSPDM_CTX* ctx); * * @return Pointer to new context, or NULL on failure. */ -WOLFSPDM_CTX* wolfSPDM_New(void); +WOLFSPDM_API WOLFSPDM_CTX* wolfSPDM_New(void); #endif /** @@ -178,7 +159,7 @@ WOLFSPDM_CTX* wolfSPDM_New(void); * * @param ctx The wolfSPDM context to free. */ -void wolfSPDM_Free(WOLFSPDM_CTX* ctx); +WOLFSPDM_API void wolfSPDM_Free(WOLFSPDM_CTX* ctx); /** * Get the size of the WOLFSPDM_CTX structure. @@ -186,7 +167,7 @@ void wolfSPDM_Free(WOLFSPDM_CTX* ctx); * * @return Size in bytes. */ -int wolfSPDM_GetCtxSize(void); +WOLFSPDM_API int wolfSPDM_GetCtxSize(void); /** * Initialize a statically-allocated context with size check. @@ -196,7 +177,7 @@ int wolfSPDM_GetCtxSize(void); * @param size Size of the provided buffer. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_InitStatic(WOLFSPDM_CTX* ctx, int size); +WOLFSPDM_API int wolfSPDM_InitStatic(WOLFSPDM_CTX* ctx, int size); /* --- Configuration --- */ @@ -208,54 +189,19 @@ int wolfSPDM_InitStatic(WOLFSPDM_CTX* ctx, int size); * @param userCtx User context pointer passed to callback. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_SetIO(WOLFSPDM_CTX* ctx, WOLFSPDM_IO_CB ioCb, void* userCtx); +WOLFSPDM_API int wolfSPDM_SetIO(WOLFSPDM_CTX* ctx, WOLFSPDM_IO_CB ioCb, void* userCtx); /** - * Set the protocol mode (standard SPDM or Nuvoton-specific). + * Set the maximum SPDM version to negotiate. + * Caps the version selected during GET_VERSION exchange. * Must be called before wolfSPDM_Connect(). * - * @param ctx The wolfSPDM context. - * @param mode WOLFSPDM_MODE_STANDARD or WOLFSPDM_MODE_NUVOTON. - * @return WOLFSPDM_SUCCESS or negative error code. - * Returns WOLFSPDM_E_INVALID_ARG if NUVOTON mode requested - * but wolfSPDM was not built with --enable-nuvoton. - */ -int wolfSPDM_SetMode(WOLFSPDM_CTX* ctx, WOLFSPDM_MODE mode); - -/** - * Get the current protocol mode. - * - * @param ctx The wolfSPDM context. - * @return Current mode (WOLFSPDM_MODE_STANDARD or WOLFSPDM_MODE_NUVOTON). - */ -WOLFSPDM_MODE wolfSPDM_GetMode(WOLFSPDM_CTX* ctx); - -/** - * Set the responder's public key for certificate-less operation. - * Used when the responder doesn't send a certificate chain (e.g., Nuvoton TPM). - * - * @param ctx The wolfSPDM context. - * @param pubKey Raw public key bytes (96 bytes for P-384: X||Y). - * @param pubKeySz Size of public key (must be 96 for P-384). - * @return WOLFSPDM_SUCCESS or negative error code. - */ -int wolfSPDM_SetResponderPubKey(WOLFSPDM_CTX* ctx, - const byte* pubKey, word32 pubKeySz); - -/** - * Set the requester's key pair for mutual authentication. - * Optional - only needed if responder requires mutual auth. - * - * @param ctx The wolfSPDM context. - * @param privKey Raw private key bytes (48 bytes for P-384). - * @param privKeySz Size of private key. - * @param pubKey Raw public key bytes (96 bytes for P-384: X||Y). - * @param pubKeySz Size of public key. + * @param ctx The wolfSPDM context. + * @param maxVersion Maximum version (e.g., SPDM_VERSION_12, SPDM_VERSION_14). + * Must be in range 0x12-0x14. Use 0 to reset to compile-time default. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_SetRequesterKeyPair(WOLFSPDM_CTX* ctx, - const byte* privKey, word32 privKeySz, - const byte* pubKey, word32 pubKeySz); +WOLFSPDM_API int wolfSPDM_SetMaxVersion(WOLFSPDM_CTX* ctx, byte maxVersion); /* --- Session Establishment --- */ @@ -270,7 +216,7 @@ int wolfSPDM_SetRequesterKeyPair(WOLFSPDM_CTX* ctx, * @param ctx The wolfSPDM context. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_Connect(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_Connect(WOLFSPDM_CTX* ctx); /** * Check if an SPDM session is established. @@ -278,7 +224,7 @@ int wolfSPDM_Connect(WOLFSPDM_CTX* ctx); * @param ctx The wolfSPDM context. * @return 1 if connected, 0 if not. */ -int wolfSPDM_IsConnected(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_IsConnected(WOLFSPDM_CTX* ctx); /** * End the SPDM session gracefully. @@ -286,7 +232,7 @@ int wolfSPDM_IsConnected(WOLFSPDM_CTX* ctx); * @param ctx The wolfSPDM context. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_Disconnect(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_Disconnect(WOLFSPDM_CTX* ctx); /* --- Individual Handshake Steps (for fine-grained control) --- */ @@ -297,7 +243,7 @@ int wolfSPDM_Disconnect(WOLFSPDM_CTX* ctx); * @param ctx The wolfSPDM context. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_GetVersion(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_GetVersion(WOLFSPDM_CTX* ctx); /** * Send GET_CAPABILITIES and receive CAPABILITIES response. @@ -306,7 +252,7 @@ int wolfSPDM_GetVersion(WOLFSPDM_CTX* ctx); * @param ctx The wolfSPDM context. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_GetCapabilities(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_GetCapabilities(WOLFSPDM_CTX* ctx); /** * Send NEGOTIATE_ALGORITHMS and receive ALGORITHMS response. @@ -315,7 +261,7 @@ int wolfSPDM_GetCapabilities(WOLFSPDM_CTX* ctx); * @param ctx The wolfSPDM context. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_NegotiateAlgorithms(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_NegotiateAlgorithms(WOLFSPDM_CTX* ctx); /** * Send GET_DIGESTS and receive DIGESTS response. @@ -323,7 +269,7 @@ int wolfSPDM_NegotiateAlgorithms(WOLFSPDM_CTX* ctx); * @param ctx The wolfSPDM context. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_GetDigests(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_GetDigests(WOLFSPDM_CTX* ctx); /** * Send GET_CERTIFICATE and receive full certificate chain. @@ -333,7 +279,7 @@ int wolfSPDM_GetDigests(WOLFSPDM_CTX* ctx); * @param slotId Certificate slot (0-7, typically 0). * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_GetCertificate(WOLFSPDM_CTX* ctx, int slotId); +WOLFSPDM_API int wolfSPDM_GetCertificate(WOLFSPDM_CTX* ctx, int slotId); /** * Send KEY_EXCHANGE and receive KEY_EXCHANGE_RSP. @@ -342,7 +288,7 @@ int wolfSPDM_GetCertificate(WOLFSPDM_CTX* ctx, int slotId); * @param ctx The wolfSPDM context. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_KeyExchange(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_KeyExchange(WOLFSPDM_CTX* ctx); /** * Send FINISH and receive FINISH_RSP (encrypted). @@ -351,7 +297,7 @@ int wolfSPDM_KeyExchange(WOLFSPDM_CTX* ctx); * @param ctx The wolfSPDM context. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_Finish(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_Finish(WOLFSPDM_CTX* ctx); /* --- Secured Messaging --- */ @@ -366,7 +312,7 @@ int wolfSPDM_Finish(WOLFSPDM_CTX* ctx); * @param encSz [in] Size of enc buffer, [out] Actual encrypted size. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_EncryptMessage(WOLFSPDM_CTX* ctx, +WOLFSPDM_API int wolfSPDM_EncryptMessage(WOLFSPDM_CTX* ctx, const byte* plain, word32 plainSz, byte* enc, word32* encSz); @@ -380,7 +326,7 @@ int wolfSPDM_EncryptMessage(WOLFSPDM_CTX* ctx, * @param plainSz [in] Size of plain buffer, [out] Actual decrypted size. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_DecryptMessage(WOLFSPDM_CTX* ctx, +WOLFSPDM_API int wolfSPDM_DecryptMessage(WOLFSPDM_CTX* ctx, const byte* enc, word32 encSz, byte* plain, word32* plainSz); #endif /* !WOLFSPDM_LEAN */ @@ -396,7 +342,7 @@ int wolfSPDM_DecryptMessage(WOLFSPDM_CTX* ctx, * @param rspSz [in] Size of response buffer, [out] Actual response size. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_SecuredExchange(WOLFSPDM_CTX* ctx, +WOLFSPDM_API int wolfSPDM_SecuredExchange(WOLFSPDM_CTX* ctx, const byte* cmdPlain, word32 cmdSz, byte* rspPlain, word32* rspSz); @@ -429,7 +375,7 @@ int wolfSPDM_SecuredExchange(WOLFSPDM_CTX* ctx, * @return WOLFSPDM_SUCCESS (verified), WOLFSPDM_E_MEAS_NOT_VERIFIED (unsigned), * WOLFSPDM_E_MEAS_SIG_FAIL (sig invalid), or negative error code. */ -int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, +WOLFSPDM_API int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, int requestSignature); /** @@ -438,7 +384,7 @@ int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, * @param ctx The wolfSPDM context. * @return Number of measurement blocks, or 0 if none. */ -int wolfSPDM_GetMeasurementCount(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_GetMeasurementCount(WOLFSPDM_CTX* ctx); /** * Get a specific measurement block by index. @@ -451,7 +397,7 @@ int wolfSPDM_GetMeasurementCount(WOLFSPDM_CTX* ctx); * @param valueSz [in] Size of value buffer, [out] Actual value size. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_GetMeasurementBlock(WOLFSPDM_CTX* ctx, int blockIdx, +WOLFSPDM_API int wolfSPDM_GetMeasurementBlock(WOLFSPDM_CTX* ctx, int blockIdx, byte* measIndex, byte* measType, byte* value, word32* valueSz); #endif /* !NO_WOLFSPDM_MEAS */ @@ -471,7 +417,7 @@ int wolfSPDM_GetMeasurementBlock(WOLFSPDM_CTX* ctx, int blockIdx, * @param dataSz Size of data. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_SendData(WOLFSPDM_CTX* ctx, const byte* data, word32 dataSz); +WOLFSPDM_API int wolfSPDM_SendData(WOLFSPDM_CTX* ctx, const byte* data, word32 dataSz); /** * Receive application data over an established SPDM session. @@ -481,7 +427,7 @@ int wolfSPDM_SendData(WOLFSPDM_CTX* ctx, const byte* data, word32 dataSz); * @param dataSz [in] Size of buffer, [out] Actual data size. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_ReceiveData(WOLFSPDM_CTX* ctx, byte* data, word32* dataSz); +WOLFSPDM_API int wolfSPDM_ReceiveData(WOLFSPDM_CTX* ctx, byte* data, word32* dataSz); #endif /* !WOLFSPDM_LEAN */ /* --- Session Information --- */ @@ -489,10 +435,20 @@ int wolfSPDM_ReceiveData(WOLFSPDM_CTX* ctx, byte* data, word32* dataSz); /** * Get the current session ID. * + * The session ID is allocated by KEY_EXCHANGE_RSP and remains valid for + * the rest of the handshake (FINISH) and the application phase. This + * returns the value as soon as KEY_EXCHANGE_RSP sets it, not only after + * wolfSPDM_IsConnected() goes true - I/O callbacks need it between + * KEY_EXCHANGE and FINISH to distinguish secured records. + * + * NOTE: a non-zero return does NOT imply the session is established. Use + * wolfSPDM_IsConnected() to test for completion of the handshake. + * * @param ctx The wolfSPDM context. - * @return Session ID (combined reqSessionId | rspSessionId << 16), or 0 if not connected. + * @return Session ID (combined reqSessionId | rspSessionId << 16), or 0 + * before KEY_EXCHANGE_RSP has run, or after the handshake errored. */ -word32 wolfSPDM_GetSessionId(WOLFSPDM_CTX* ctx); +WOLFSPDM_API word32 wolfSPDM_GetSessionId(WOLFSPDM_CTX* ctx); /** * Get negotiated SPDM version. @@ -500,39 +456,42 @@ word32 wolfSPDM_GetSessionId(WOLFSPDM_CTX* ctx); * @param ctx The wolfSPDM context. * @return Version (e.g., 0x12 for SPDM 1.2), or 0 if not negotiated. */ -byte wolfSPDM_GetVersion_Negotiated(WOLFSPDM_CTX* ctx); +WOLFSPDM_API byte wolfSPDM_GetNegotiatedVersion(WOLFSPDM_CTX* ctx); -#ifdef WOLFSPDM_NUVOTON /** - * Get the connection handle (Nuvoton TCG binding). + * Get the responder's last SPDM_ERROR code (Param1). * - * @param ctx The wolfSPDM context. - * @return Connection handle value. - */ -word32 wolfSPDM_GetConnectionHandle(WOLFSPDM_CTX* ctx); - -/** - * Get the FIPS indicator (Nuvoton TCG binding). + * Set by APIs that receive an SPDM_ERROR response from the responder + * (e.g. wolfSPDM_GetMeasurements returning WOLFSPDM_E_PEER_ERROR). + * Callers can branch on this code to back off on BUSY, abort on + * UNSUPPORTED_REQUEST, retry on REQUEST_RESYNCH, etc. * * @param ctx The wolfSPDM context. - * @return FIPS indicator value. + * @return Last responder error code, or 0 if none has been received. */ -word16 wolfSPDM_GetFipsIndicator(WOLFSPDM_CTX* ctx); -#endif +WOLFSPDM_API byte wolfSPDM_GetLastPeerError(WOLFSPDM_CTX* ctx); + +/* Backwards-compat for the original spelling. Exported as a real symbol + * so binaries previously linked against the old name keep loading. New + * code should use wolfSPDM_GetNegotiatedVersion directly. */ +WOLFSPDM_API byte wolfSPDM_GetVersion_Negotiated(WOLFSPDM_CTX* ctx); /* --- Certificate Chain Validation --- */ /** - * Load trusted root CA certificates for certificate chain validation. - * When set, wolfSPDM_Connect() will validate the responder's certificate - * chain against these CAs. Without this, only the public key is extracted. + * Load the trusted root CA certificate for certificate chain validation. + * When set, wolfSPDM_Connect() / wolfSPDM_Challenge() will validate the + * responder's certificate chain by comparing SHA-384 of the supplied + * cert against the chain's RootHash. Only a single CA cert is supported + * - the buffer is hashed as one DER blob, not parsed as a list. + * Without this, only the public key is extracted (no chain anchor). * * @param ctx The wolfSPDM context. - * @param derCerts DER-encoded CA certificate(s) (concatenated if multiple). + * @param derCerts DER-encoded CA certificate (single cert, not a chain). * @param derCertsSz Size of DER certificate data. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_SetTrustedCAs(WOLFSPDM_CTX* ctx, const byte* derCerts, +WOLFSPDM_API int wolfSPDM_SetTrustedCAs(WOLFSPDM_CTX* ctx, const byte* derCerts, word32 derCertsSz); #ifndef NO_WOLFSPDM_CHALLENGE @@ -552,7 +511,7 @@ int wolfSPDM_SetTrustedCAs(WOLFSPDM_CTX* ctx, const byte* derCerts, * SPDM_MEAS_SUMMARY_HASH_ALL (0xFF). * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_Challenge(WOLFSPDM_CTX* ctx, int slotId, byte measHashType); +WOLFSPDM_API int wolfSPDM_Challenge(WOLFSPDM_CTX* ctx, int slotId, byte measHashType); #endif /* !NO_WOLFSPDM_CHALLENGE */ /* --- Session Keep-Alive --- */ @@ -565,7 +524,7 @@ int wolfSPDM_Challenge(WOLFSPDM_CTX* ctx, int slotId, byte measHashType); * @param ctx The wolfSPDM context. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_Heartbeat(WOLFSPDM_CTX* ctx); +WOLFSPDM_API int wolfSPDM_Heartbeat(WOLFSPDM_CTX* ctx); /* --- Key Update (Session Key Rotation) --- */ @@ -579,7 +538,7 @@ int wolfSPDM_Heartbeat(WOLFSPDM_CTX* ctx); * 1 = rotate both requester and responder keys. * @return WOLFSPDM_SUCCESS or negative error code. */ -int wolfSPDM_KeyUpdate(WOLFSPDM_CTX* ctx, int updateAll); +WOLFSPDM_API int wolfSPDM_KeyUpdate(WOLFSPDM_CTX* ctx, int updateAll); /* --- Debug/Utility --- */ @@ -589,7 +548,7 @@ int wolfSPDM_KeyUpdate(WOLFSPDM_CTX* ctx, int updateAll); * @param ctx The wolfSPDM context. * @param enable Non-zero to enable, 0 to disable. */ -void wolfSPDM_SetDebug(WOLFSPDM_CTX* ctx, int enable); +WOLFSPDM_API void wolfSPDM_SetDebug(WOLFSPDM_CTX* ctx, int enable); #ifdef __cplusplus } diff --git a/wolfspdm/spdm_error.h b/wolfspdm/spdm_error.h index 9ddf173..02ca78b 100644 --- a/wolfspdm/spdm_error.h +++ b/wolfspdm/spdm_error.h @@ -22,6 +22,9 @@ #ifndef WOLFSPDM_ERROR_H #define WOLFSPDM_ERROR_H +/* Pull in WOLFSPDM_API visibility macro so this header is self-contained. */ +#include + #ifdef __cplusplus extern "C" { #endif @@ -58,7 +61,7 @@ enum WOLFSPDM_ERROR { }; /* Get human-readable error string */ -const char* wolfSPDM_GetErrorString(int error); +WOLFSPDM_API const char* wolfSPDM_GetErrorString(int error); #ifdef __cplusplus } diff --git a/wolfspdm/spdm_nuvoton.h b/wolfspdm/spdm_nuvoton.h deleted file mode 100644 index a1da94c..0000000 --- a/wolfspdm/spdm_nuvoton.h +++ /dev/null @@ -1,319 +0,0 @@ -/* spdm_nuvoton.h - * - * Copyright (C) 2006-2025 wolfSSL Inc. - * - * This file is part of wolfSPDM. - * - * wolfSPDM 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. - * - * wolfSPDM 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 - */ - -/* Nuvoton TPM SPDM Support - * - * This header provides Nuvoton-specific SPDM functionality: - * - TCG SPDM Binding message framing (per TCG SPDM Binding Spec v1.0) - * - Nuvoton vendor-defined commands (GET_PUBK, GIVE_PUB, GET_STS_, SPDMONLY) - * - Nuvoton SPDM handshake flow (differs from standard SPDM) - * - * The Nuvoton NPCT75x TPM uses a simplified SPDM flow: - * GET_VERSION -> GET_PUB_KEY -> KEY_EXCHANGE -> GIVE_PUB_KEY -> FINISH - * - * Notable differences from standard SPDM: - * - No GET_CAPABILITIES or NEGOTIATE_ALGORITHMS (Algorithm Set B is fixed) - * - Uses vendor-defined commands for identity key exchange - * - TCG binding headers wrap all SPDM messages - * - * Reference: Nuvoton SPDM Guidance Rev 1.11 - */ - -#ifndef WOLFSPDM_NUVOTON_H -#define WOLFSPDM_NUVOTON_H - -/* Note: This header is included from spdm.h after WOLFSPDM_CTX forward declaration. - * DO NOT include spdm.h here to avoid circular dependency. - * Include spdm_types.h for basic types only. */ -#include - -#ifdef WOLFSPDM_NUVOTON - -#ifdef __cplusplus -extern "C" { -#endif - -/* --- TCG SPDM Binding Constants (per TCG SPDM Binding Spec v1.0) --- */ - -/* Message Tags */ -#define WOLFSPDM_TCG_TAG_CLEAR 0x8101 /* Clear (unencrypted) message */ -#define WOLFSPDM_TCG_TAG_SECURED 0x8201 /* Secured (encrypted) message */ - -/* Header Sizes */ -#define WOLFSPDM_TCG_HEADER_SIZE 16 /* TCG binding header size */ -#define WOLFSPDM_TCG_SECURED_HDR_SIZE 14 /* Session record header (4+8+2) */ - -/* FIPS Service Indicator */ -#define WOLFSPDM_FIPS_NON_FIPS 0x00 -#define WOLFSPDM_FIPS_APPROVED 0x01 - -/* --- Nuvoton Vendor-Defined Command Codes --- */ - -/* 8-byte ASCII vendor codes for SPDM VENDOR_DEFINED messages */ -#define WOLFSPDM_VDCODE_LEN 8 - -#define WOLFSPDM_VDCODE_TPM2_CMD "TPM2_CMD" /* TPM command over SPDM */ -#define WOLFSPDM_VDCODE_GET_PUBK "GET_PUBK" /* Get TPM's SPDM-Identity key */ -#define WOLFSPDM_VDCODE_GIVE_PUB "GIVE_PUB" /* Give host's SPDM-Identity key */ -#define WOLFSPDM_VDCODE_GET_STS "GET_STS_" /* Get SPDM status */ -#define WOLFSPDM_VDCODE_SPDMONLY "SPDMONLY" /* Lock/unlock SPDM-only mode */ - -/* SPDMONLY command parameters */ -#define WOLFSPDM_SPDMONLY_LOCK 0x01 -#define WOLFSPDM_SPDMONLY_UNLOCK 0x00 - -/* --- TCG Binding Header Structures --- */ - -/* Clear message header (tag 0x8101) - * Layout: tag(2/BE) + size(4/BE) + connectionHandle(4/BE) + - * fipsIndicator(2/BE) + reserved(4) = 16 bytes */ -typedef struct WOLFSPDM_TCG_CLEAR_HDR { - word16 tag; /* WOLFSPDM_TCG_TAG_CLEAR (0x8101) */ - word32 size; /* Total message size including header */ - word32 connectionHandle; /* Connection handle (0 for single connection) */ - word16 fipsIndicator; /* FIPS service indicator */ - word32 reserved; /* Must be 0 */ -} WOLFSPDM_TCG_CLEAR_HDR; - -/* Secured message header (tag 0x8201) - * Layout: tag(2/BE) + size(4/BE) + connectionHandle(4/BE) + - * fipsIndicator(2/BE) + reserved(4) = 16 bytes - * Followed by SPDM secured record (per DSP0277, all LE): - * sessionId(4/LE) + seqNum(8/LE) + length(2/LE) + encData + MAC(16) */ -typedef struct WOLFSPDM_TCG_SECURED_HDR { - word16 tag; /* WOLFSPDM_TCG_TAG_SECURED (0x8201) */ - word32 size; /* Total message size including header */ - word32 connectionHandle; /* Connection handle */ - word16 fipsIndicator; /* FIPS service indicator */ - word32 reserved; /* Must be 0 */ -} WOLFSPDM_TCG_SECURED_HDR; - -/* --- Nuvoton SPDM Status --- */ - -typedef struct WOLFSPDM_NUVOTON_STATUS { - int spdmEnabled; /* SPDM is enabled on the TPM */ - int sessionActive; /* An SPDM session is currently active */ - int spdmOnlyLocked; /* SPDM-only mode is locked */ - word32 fwVersion; /* TPM firmware version */ - byte specVersionMajor; /* SPDM spec version major (0 for 1.x) */ - byte specVersionMinor; /* SPDM spec version minor (1=1.1, 3=1.3) */ -} WOLFSPDM_NUVOTON_STATUS; - -/* --- TCG Binding Message Framing Functions --- */ - -/** - * Build a TCG SPDM clear message (tag 0x8101). - * Wraps an SPDM payload in the TCG binding header format. - * - * @param ctx wolfSPDM context - * @param spdmPayload SPDM message payload - * @param spdmPayloadSz Size of SPDM payload - * @param outBuf Output buffer for framed message - * @param outBufSz Size of output buffer - * @return Total message size on success, negative on error - */ -int wolfSPDM_BuildTcgClearMessage( - WOLFSPDM_CTX* ctx, - const byte* spdmPayload, word32 spdmPayloadSz, - byte* outBuf, word32 outBufSz); - -/** - * Parse a TCG SPDM clear message (tag 0x8101). - * Extracts the SPDM payload from the TCG binding header. - * - * @param inBuf Input buffer containing framed message - * @param inBufSz Size of input buffer - * @param spdmPayload Output buffer for SPDM payload - * @param spdmPayloadSz [in] Size of output buffer, [out] Actual payload size - * @param hdr Optional: receives parsed header fields - * @return Payload size on success, negative on error - */ -int wolfSPDM_ParseTcgClearMessage( - const byte* inBuf, word32 inBufSz, - byte* spdmPayload, word32* spdmPayloadSz, - WOLFSPDM_TCG_CLEAR_HDR* hdr); - -/** - * Build a TCG SPDM secured message (tag 0x8201). - * Wraps encrypted payload with session ID, sequence number, and MAC. - * - * @param ctx wolfSPDM context - * @param encPayload Encrypted payload (ciphertext) - * @param encPayloadSz Size of encrypted payload - * @param mac Authentication tag (AES-GCM tag) - * @param macSz Size of MAC (must be 16) - * @param outBuf Output buffer for framed message - * @param outBufSz Size of output buffer - * @return Total message size on success, negative on error - */ -int wolfSPDM_BuildTcgSecuredMessage( - WOLFSPDM_CTX* ctx, - const byte* encPayload, word32 encPayloadSz, - const byte* mac, word32 macSz, - byte* outBuf, word32 outBufSz); - -/** - * Parse a TCG SPDM secured message (tag 0x8201). - * Extracts session ID, sequence number, encrypted payload, and MAC. - * - * @param inBuf Input buffer containing framed message - * @param inBufSz Size of input buffer - * @param sessionId Receives session ID - * @param seqNum Receives sequence number - * @param encPayload Output buffer for encrypted payload - * @param encPayloadSz [in] Size of output buffer, [out] Actual payload size - * @param mac Output buffer for MAC - * @param macSz [in] Size of MAC buffer, [out] Actual MAC size - * @param hdr Optional: receives parsed header fields - * @return Payload size on success, negative on error - */ -int wolfSPDM_ParseTcgSecuredMessage( - const byte* inBuf, word32 inBufSz, - word32* sessionId, word64* seqNum, - byte* encPayload, word32* encPayloadSz, - byte* mac, word32* macSz, - WOLFSPDM_TCG_SECURED_HDR* hdr); - -/* --- Vendor-Defined Message Helpers --- */ - -/** - * Build an SPDM VENDOR_DEFINED_REQUEST message. - * - * @param vdCode 8-byte ASCII vendor code (e.g., "GET_PUBK") - * @param payload Vendor-specific payload (may be NULL) - * @param payloadSz Size of payload - * @param outBuf Output buffer for message - * @param outBufSz Size of output buffer - * @return Message size on success, negative on error - */ -int wolfSPDM_BuildVendorDefined( - const char* vdCode, - const byte* payload, word32 payloadSz, - byte* outBuf, word32 outBufSz); - -/** - * Parse an SPDM VENDOR_DEFINED_RESPONSE message. - * - * @param inBuf Input buffer containing message - * @param inBufSz Size of input buffer - * @param vdCode Receives 8-byte vendor code (buffer must be 9+ bytes) - * @param payload Output buffer for payload - * @param payloadSz [in] Size of output buffer, [out] Actual payload size - * @return Payload size on success, negative on error - */ -int wolfSPDM_ParseVendorDefined( - const byte* inBuf, word32 inBufSz, - char* vdCode, - byte* payload, word32* payloadSz); - -/* --- Nuvoton-Specific SPDM Functions --- */ - -/** - * Get the TPM's SPDM-Identity public key (GET_PUBK vendor command). - * This is sent as a clear (unencrypted) SPDM message before key exchange. - * - * @param ctx wolfSPDM context - * @param pubKey Output buffer for public key (raw X||Y, 96 bytes for P-384) - * @param pubKeySz [in] Size of buffer, [out] Actual key size - * @return WOLFSPDM_SUCCESS or negative error code - */ -int wolfSPDM_Nuvoton_GetPubKey( - WOLFSPDM_CTX* ctx, - byte* pubKey, word32* pubKeySz); - -/** - * Give the host's SPDM-Identity public key to the TPM (GIVE_PUB vendor command). - * This is sent as a secured (encrypted) message after key exchange. - * - * @param ctx wolfSPDM context - * @param pubKey Host's public key (TPMT_PUBLIC format, ~120 bytes) - * @param pubKeySz Size of public key - * @return WOLFSPDM_SUCCESS or negative error code - */ -int wolfSPDM_Nuvoton_GivePubKey( - WOLFSPDM_CTX* ctx, - const byte* pubKey, word32 pubKeySz); - -/** - * Get SPDM status from the TPM (GET_STS_ vendor command). - * Can be called before or after session establishment. - * - * @param ctx wolfSPDM context - * @param status Receives SPDM status information - * @return WOLFSPDM_SUCCESS or negative error code - */ -int wolfSPDM_Nuvoton_GetStatus( - WOLFSPDM_CTX* ctx, - WOLFSPDM_NUVOTON_STATUS* status); - -/** - * Lock or unlock SPDM-only mode (SPDMONLY vendor command). - * When locked, the TPM only accepts commands over SPDM. - * - * @param ctx wolfSPDM context - * @param lock WOLFSPDM_SPDMONLY_LOCK (1) or WOLFSPDM_SPDMONLY_UNLOCK (0) - * @return WOLFSPDM_SUCCESS or negative error code - */ -int wolfSPDM_Nuvoton_SetOnlyMode( - WOLFSPDM_CTX* ctx, - int lock); - -/** - * Set the requester's SPDM-Identity public key in TPMT_PUBLIC format. - * Required for GIVE_PUB step in Nuvoton handshake. - * - * @param ctx wolfSPDM context - * @param tpmtPub Public key in TPMT_PUBLIC format (~120 bytes for P-384) - * @param tpmtPubSz Size of TPMT_PUBLIC - * @return WOLFSPDM_SUCCESS or negative error code - */ -int wolfSPDM_SetRequesterKeyTPMT(WOLFSPDM_CTX* ctx, - const byte* tpmtPub, word32 tpmtPubSz); - -/** - * Perform Nuvoton-specific SPDM connection. - * Uses the Nuvoton handshake flow: - * GET_VERSION -> GET_PUB_KEY -> KEY_EXCHANGE -> GIVE_PUB_KEY -> FINISH - * - * This is called internally by wolfSPDM_Connect() when mode is NUVOTON. - * - * @param ctx wolfSPDM context - * @return WOLFSPDM_SUCCESS or negative error code - */ -int wolfSPDM_ConnectNuvoton(WOLFSPDM_CTX* ctx); - -/* --- Nuvoton Context Fields --- */ - -/* These fields are added to WOLFSPDM_CTX when WOLFSPDM_NUVOTON is defined */ - -/* Connection handle for TCG binding (usually 0) */ -#define WOLFSPDM_NUVOTON_CONN_HANDLE_DEFAULT 0 - -/* FIPS indicator for TCG binding */ -#define WOLFSPDM_NUVOTON_FIPS_DEFAULT WOLFSPDM_FIPS_NON_FIPS - -#ifdef __cplusplus -} -#endif - -#endif /* WOLFSPDM_NUVOTON */ - -#endif /* WOLFSPDM_NUVOTON_H */ diff --git a/wolfspdm/spdm_types.h b/wolfspdm/spdm_types.h index 043056b..69f474c 100644 --- a/wolfspdm/spdm_types.h +++ b/wolfspdm/spdm_types.h @@ -28,6 +28,16 @@ #endif #include +/* Visibility: when built as part of wolfTPM, use WOLFTPM_API for export */ +#ifdef BUILDING_WOLFTPM + #include + #define WOLFSPDM_API WOLFTPM_API +#else + #ifndef WOLFSPDM_API + #define WOLFSPDM_API + #endif +#endif + #ifdef __cplusplus extern "C" { #endif @@ -44,6 +54,7 @@ extern "C" { #define SPDM_VERSION_11 0x11 /* SPDM 1.1 */ #define SPDM_VERSION_12 0x12 /* SPDM 1.2 */ #define SPDM_VERSION_13 0x13 /* SPDM 1.3 */ +#define SPDM_VERSION_14 0x14 /* SPDM 1.4 */ /* SPDM Message Header Size */ #define SPDM_HEADER_SIZE 4 /* Version + Code + Param1 + Param2 */ @@ -128,8 +139,8 @@ extern "C" { /* Algorithm Set B Fixed Parameters */ #define WOLFSPDM_HASH_SIZE 48 /* SHA-384 output size */ #define WOLFSPDM_ECC_KEY_SIZE 48 /* P-384 coordinate size */ -#define WOLFSPDM_ECC_POINT_SIZE 96 /* P-384 X||Y uncompressed */ -#define WOLFSPDM_ECC_SIG_SIZE 96 /* ECDSA P-384 r||s */ +#define WOLFSPDM_ECC_POINT_SIZE (2 * WOLFSPDM_ECC_KEY_SIZE) /* P-384 X||Y */ +#define WOLFSPDM_ECC_SIG_SIZE (2 * WOLFSPDM_ECC_KEY_SIZE) /* ECDSA r||s */ #define WOLFSPDM_AEAD_KEY_SIZE 32 /* AES-256 key size */ #define WOLFSPDM_AEAD_IV_SIZE 12 /* AES-GCM IV size */ #define WOLFSPDM_AEAD_TAG_SIZE 16 /* AES-GCM tag size */ From 5b83c6f1c76a0e32022aaaea819abc354ab7d560 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 13 May 2026 18:05:47 +0100 Subject: [PATCH 2/7] Fix CI failures: htons conversion, cppcheck style, CodeQL path taint - examples/spdm_demo.c: cast int port to uint16_t for htons (-Wconversion). Sanitize SPDM_EMU_PATH via realpath() + length/control-char/traversal checks before using it in fopen() (CodeQL "uncontrolled data in path expression" High). - src/spdm_msg.c: move loop counter 'i' into inner scope where it is used (cppcheck style: variableScope). - test/test_spdm.c: same htons cast fix; size context buffer via WOLFSPDM_CTX_STATIC_SIZE so it tracks wolfSSL config-dependent struct growth (previously hard-coded 16384 was too small under the CI's wolfSSL build, leaving the legacy smoke test failing on startup). - test/unit_test.c: drop redundant encSz initializer (cppcheck style: redundantInitialization); value is reassigned before first read. All 53 unit tests pass; -Wall -Wextra -Wpedantic -Werror -Wconversion -Wshadow build clean. --- examples/spdm_demo.c | 46 +++++++++++++++++++++++++++++++++++++++++--- src/spdm_msg.c | 2 +- test/test_spdm.c | 9 +++++---- test/unit_test.c | 2 +- 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/examples/spdm_demo.c b/examples/spdm_demo.c index f35f833..7818679 100644 --- a/examples/spdm_demo.c +++ b/examples/spdm_demo.c @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #ifdef __linux__ @@ -138,7 +140,7 @@ static int tcp_connect(const char* host, int port) memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; - addr.sin_port = htons(port); + addr.sin_port = htons((uint16_t)port); if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { close(sockFd); return -1; @@ -218,20 +220,58 @@ static byte parse_version(const char* s) return 0; } +/* Sanitize SPDM_EMU_PATH before joining a fixed suffix and handing it to + * fopen(): reject NULL, oversized, traversal-bearing, or non-printable + * input, then canonicalize with realpath() so the value used by fopen is + * a resolved filesystem path, not raw env data. This is a demo, but + * CodeQL flags concatenated env-vars in path expressions and the fix is + * also defensive against a malicious shell environment. */ +static int sanitize_emu_path(const char* emuPath, char* outReal, size_t outSz) +{ + size_t len, i; + char* resolved; + + if (emuPath == NULL) return -1; + len = strlen(emuPath); + if (len == 0 || len > PATH_MAX - 32) return -1; + for (i = 0; i < len; i++) { + unsigned char c = (unsigned char)emuPath[i]; + if (c < 0x20 || c == 0x7F) return -1; /* no control chars */ + } + if (strstr(emuPath, "..") != NULL) return -1; /* no traversal */ + + resolved = realpath(emuPath, NULL); + if (resolved == NULL) return -1; + if (strlen(resolved) >= outSz) { free(resolved); return -1; } + memcpy(outReal, resolved, strlen(resolved) + 1); + free(resolved); + return 0; +} + static int load_trusted_ca(WOLFSPDM_CTX* ctx) { const char* emuPath = getenv("SPDM_EMU_PATH"); - char path[512]; + char realEmu[PATH_MAX]; + char path[PATH_MAX]; byte* der; word32 derSz; int rc; + int n; if (emuPath == NULL) { fprintf(stderr, "ERROR: SPDM_EMU_PATH not set; cannot locate " "ca.cert.der for --challenge\n"); return -1; } - snprintf(path, sizeof(path), "%s/ecp384/ca.cert.der", emuPath); + if (sanitize_emu_path(emuPath, realEmu, sizeof(realEmu)) != 0) { + 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 (n < 0 || (size_t)n >= sizeof(path)) { + fprintf(stderr, "ERROR: certificate path too long\n"); + return -1; + } der = load_der(path, &derSz); if (der == NULL) { diff --git a/src/spdm_msg.c b/src/spdm_msg.c index b237bff..dc40ef9 100644 --- a/src/spdm_msg.c +++ b/src/spdm_msg.c @@ -383,7 +383,6 @@ int wolfSPDM_ParseVersion(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) { word16 entryCount; word16 maxEntries; - word32 i; byte highestVersion = 0; /* No version found yet */ SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 6); @@ -408,6 +407,7 @@ int wolfSPDM_ParseVersion(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) { byte maxVer = (ctx->maxVersion != 0) ? ctx->maxVersion : WOLFSPDM_MAX_SPDM_VERSION; + word32 i; for (i = 0; i < entryCount; i++) { /* Each entry is 2 bytes; high byte (offset +1) is Major.Minor */ byte ver = buf[6 + i * 2 + 1]; diff --git a/test/test_spdm.c b/test/test_spdm.c index bb8e203..d247139 100644 --- a/test/test_spdm.c +++ b/test/test_spdm.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #ifdef __linux__ @@ -116,7 +117,7 @@ static int tcp_connect(const char* host, int port) memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; - addr.sin_port = htons(port); + addr.sin_port = htons((uint16_t)port); if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { close(sockFd); return -1; @@ -139,9 +140,9 @@ static void tcp_disconnect(void) } } -/* Static context buffer - sized via wolfSPDM_GetCtxSize() at runtime, - * but we need a compile-time upper bound. 16KB is generous. */ -#define CTX_BUF_SIZE 16384 +/* Static context buffer sized by the public header so wolfSSL configs that + * grow internal struct sizes (e.g. sp-math/ecc variants in CI) still fit. */ +#define CTX_BUF_SIZE WOLFSPDM_CTX_STATIC_SIZE static byte g_ctxBuf[CTX_BUF_SIZE]; int main(int argc, char* argv[]) diff --git a/test/unit_test.c b/test/unit_test.c index 4edff6b..7db7b09 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -1630,7 +1630,7 @@ static int test_sequence_number_wrap(void) { byte plain[] = "hello-spdm"; byte enc[256]; - word32 encSz = sizeof(enc); + word32 encSz; int i; TEST_CTX_SETUP_V12(); From 50f7c6afa3f93fa0139c3c9947fb14c729b1624e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 13 May 2026 18:18:11 +0100 Subject: [PATCH 3/7] Fix test_spdm secured-record detection + Copilot review items test/test_spdm.c was the failing spdm-emu integration step: a static isSecured flag on TCP_CTX was never flipped to 1, so FINISH (which IS encrypted but runs before WOLFSPDM_STATE_CONNECTED) was sent with MCTP type 0x05 (plain) instead of 0x06 (secured). The emu rejected with ERROR 0x01. Replaced with the dynamic is_secured_spdm() pattern from examples/spdm_demo.c, which matches the first 4 tx bytes against the live session ID. Verified end-to-end against the local DMTF spdm-emu. PR review fixes: - src/spdm_secured.c: do not call wc_AesFree on an uninitialized aes when wc_AesInit fails. Bail with explicit cleanup before the exit label in both Encrypt/Decrypt paths. - examples/spdm_demo.c tcp_io_callback: loop send()/recv() (handling EINTR/short returns) via new send_all/recv_all helpers; previous code treated any short send as a fatal error. - examples/spdm_demo.c sanitize_emu_path: switch from realpath(p, NULL) (GNU extension) to POSIX realpath(p, buf[PATH_MAX]). - examples/spdm_test.sh: gate the port-in-use check on ss/netstat/lsof availability with a clear warning when none are present. - src/spdm_msg.c: replace AlgStruct magic numbers (AlgType 2/3/5 and bits 0x0010/0x0002/0x0001) with named constants SPDM_ALG_TYPE_DHE/ AEAD/KEY_SCHEDULE and SPDM_DHE_ALGO_SECP384R1/AEAD_ALGO_AES_256_GCM/ KEY_SCHEDULE_SPDM. Added the AlgType defines to wolfspdm/spdm_types.h. - docs/ATTESTATION.md: overview said SPDM 1.2 only; updated to 1.2-1.4 with a version-specific note about RequesterContext. All 53 unit tests still pass; strict build (-Wall -Wextra -Wpedantic -Werror -Wconversion -Wshadow) clean. --- docs/ATTESTATION.md | 16 ++++++++--- examples/spdm_demo.c | 66 +++++++++++++++++++++++++++++++++---------- examples/spdm_test.sh | 27 ++++++++++++++---- src/spdm_msg.c | 12 ++++---- src/spdm_secured.c | 19 +++++++++---- test/test_spdm.c | 25 ++++++++++++---- wolfspdm/spdm_types.h | 6 ++++ 7 files changed, 129 insertions(+), 42 deletions(-) diff --git a/docs/ATTESTATION.md b/docs/ATTESTATION.md index a8aa05e..de6fe12 100644 --- a/docs/ATTESTATION.md +++ b/docs/ATTESTATION.md @@ -2,10 +2,18 @@ ## Overview -wolfSPDM supports SPDM 1.2 device attestation via GET_MEASUREMENTS with -optional cryptographic signature verification. This enables a requester to -retrieve firmware/hardware measurement blocks from any SPDM-capable device -and verify their authenticity. +wolfSPDM supports SPDM 1.2, 1.3, and 1.4 device attestation via +GET_MEASUREMENTS with optional cryptographic signature verification. This +enables a requester to retrieve firmware/hardware measurement blocks from +any SPDM-capable device and verify their authenticity. + +Version-specific notes: +- **SPDM 1.3 and 1.4** add a 32-byte RequesterContext field in + GET_MEASUREMENTS requests, which wolfSPDM populates automatically and + echo-verifies in the response. +- The 18-test integration matrix exercises both signed and unsigned + GET_MEASUREMENTS against the DMTF spdm-emu under each negotiated + version (1.2 / 1.3 / 1.4). The same protocol is used by: - **GPUs** - NVIDIA uses SPDM over MCTP for GPU attestation diff --git a/examples/spdm_demo.c b/examples/spdm_demo.c index 7818679..7ca00e9 100644 --- a/examples/spdm_demo.c +++ b/examples/spdm_demo.c @@ -60,6 +60,44 @@ static int is_secured_spdm(WOLFSPDM_CTX* ctx, const byte* buf, word32 sz) return b0 == sid; } +/* send_all / recv_all: loop until the full count is transferred or a hard + * error occurs. TCP send/recv may return short on a busy / interrupted + * socket; MSG_WAITALL handles most recv cases but is advisory only, and + * send() must always be looped. */ +static int send_all(int fd, const void* buf, size_t len) +{ + const byte* p = (const byte*)buf; + size_t left = len; + while (left > 0) { + ssize_t n = send(fd, p, left, 0); + if (n < 0) { + if (errno == EINTR) continue; + return -1; + } + if (n == 0) return -1; + p += (size_t)n; + left -= (size_t)n; + } + return 0; +} + +static int recv_all(int fd, void* buf, size_t len) +{ + byte* p = (byte*)buf; + size_t left = len; + while (left > 0) { + ssize_t n = recv(fd, p, left, 0); + if (n < 0) { + if (errno == EINTR) continue; + return -1; + } + if (n == 0) return -1; /* peer closed */ + p += (size_t)n; + left -= (size_t)n; + } + return 0; +} + /* MCTP transport I/O callback for spdm-emu (--trans MCTP, the default) */ static int tcp_io_callback(WOLFSPDM_CTX* ctx, const byte* txBuf, word32 txSz, @@ -69,7 +107,6 @@ static int tcp_io_callback(WOLFSPDM_CTX* ctx, TCP_CTX* tcpCtx = (TCP_CTX*)userCtx; byte sendBuf[4096]; byte recvHdr[12]; - ssize_t sent, recvd; word32 payloadSz, respSize; if (tcpCtx == NULL || tcpCtx->sockFd < 0) { @@ -97,13 +134,13 @@ static int tcp_io_callback(WOLFSPDM_CTX* ctx, memcpy(sendBuf + 13, txBuf, txSz); } - sent = send(tcpCtx->sockFd, sendBuf, 12 + payloadSz, 0); - if (sent != (ssize_t)(12 + payloadSz)) { + if (send_all(tcpCtx->sockFd, sendBuf, (size_t)(12 + payloadSz)) != 0) { return -1; } - recvd = recv(tcpCtx->sockFd, recvHdr, 12, MSG_WAITALL); - if (recvd != 12) return -1; + if (recv_all(tcpCtx->sockFd, recvHdr, sizeof(recvHdr)) != 0) { + return -1; + } respSize = ((word32)recvHdr[8] << 24) | ((word32)recvHdr[9] << 16) | ((word32)recvHdr[10] << 8) | (word32)recvHdr[11]; @@ -115,14 +152,12 @@ static int tcp_io_callback(WOLFSPDM_CTX* ctx, /* Skip MCTP header byte */ { byte mctpHdr; - recvd = recv(tcpCtx->sockFd, &mctpHdr, 1, MSG_WAITALL); - if (recvd != 1) return -1; + if (recv_all(tcpCtx->sockFd, &mctpHdr, 1) != 0) return -1; } *rxSz = respSize - 1; if (*rxSz > 0) { - recvd = recv(tcpCtx->sockFd, rxBuf, *rxSz, MSG_WAITALL); - if (recvd != (ssize_t)*rxSz) return -1; + if (recv_all(tcpCtx->sockFd, rxBuf, (size_t)*rxSz) != 0) return -1; } return 0; } @@ -229,7 +264,7 @@ static byte parse_version(const char* s) static int sanitize_emu_path(const char* emuPath, char* outReal, size_t outSz) { size_t len, i; - char* resolved; + char resolved[PATH_MAX]; if (emuPath == NULL) return -1; len = strlen(emuPath); @@ -240,11 +275,12 @@ static int sanitize_emu_path(const char* emuPath, char* outReal, size_t outSz) } if (strstr(emuPath, "..") != NULL) return -1; /* no traversal */ - resolved = realpath(emuPath, NULL); - if (resolved == NULL) return -1; - if (strlen(resolved) >= outSz) { free(resolved); return -1; } - memcpy(outReal, resolved, strlen(resolved) + 1); - free(resolved); + /* POSIX realpath(path, resolved): resolved must point to a buffer of + * PATH_MAX bytes. (Avoid the GNU realpath(path, NULL) extension.) */ + if (realpath(emuPath, resolved) == NULL) return -1; + len = strlen(resolved); + if (len >= outSz) return -1; + memcpy(outReal, resolved, len + 1); return 0; } diff --git a/examples/spdm_test.sh b/examples/spdm_test.sh index 99551db..4008777 100755 --- a/examples/spdm_test.sh +++ b/examples/spdm_test.sh @@ -110,11 +110,28 @@ start_emu() { fi # If port 2323 is still occupied, it isn't ours - surface that clearly - # rather than kicking the unrelated holder off the port. - if ss -tlnp 2>/dev/null | grep -q ":2323 "; then - echo -e " ${RED}ERROR: Port 2323 already in use by another process${NC}" - ss -tlnp 2>/dev/null | grep ":2323 " - return 1 + # rather than kicking the unrelated holder off the port. Try ss, then + # netstat, then lsof - skip the check (with a warning) if none exist. + if command -v ss >/dev/null 2>&1; then + if ss -tlnp 2>/dev/null | grep -q ":2323 "; then + echo -e " ${RED}ERROR: Port 2323 already in use by another process${NC}" + ss -tlnp 2>/dev/null | grep ":2323 " + return 1 + fi + elif command -v netstat >/dev/null 2>&1; then + if netstat -tlnp 2>/dev/null | grep -q ":2323 "; then + echo -e " ${RED}ERROR: Port 2323 already in use by another process${NC}" + netstat -tlnp 2>/dev/null | grep ":2323 " + return 1 + fi + elif command -v lsof >/dev/null 2>&1; then + if lsof -iTCP:2323 -sTCP:LISTEN >/dev/null 2>&1; then + echo -e " ${RED}ERROR: Port 2323 already in use by another process${NC}" + lsof -iTCP:2323 -sTCP:LISTEN + return 1 + fi + else + echo -e " ${YELLOW}WARNING: ss/netstat/lsof unavailable - skipping port-in-use check${NC}" fi # Verify cert/key files exist in EMU_DIR (spdm-emu uses lowercase 'ecp384') diff --git a/src/spdm_msg.c b/src/spdm_msg.c index dc40ef9..2a7f03d 100644 --- a/src/spdm_msg.c +++ b/src/spdm_msg.c @@ -509,24 +509,24 @@ int wolfSPDM_ParseAlgorithms(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) * (= 2 in current spec). Use the LOW nibble for extLen. */ word32 extLen = ((word32)(algCount & 0x0F)) * 4; switch (algType) { - case 2: /* DHE - selected, single bit */ - if (algSel != 0x0010) { /* SECP_384_R1 = bit 4 */ + case SPDM_ALG_TYPE_DHE: + if (algSel != SPDM_DHE_ALGO_SECP384R1) { wolfSPDM_DebugPrint(ctx, "ALGORITHMS: DHE not SECP_384_R1 (0x%04x)\n", algSel); return WOLFSPDM_E_ALGO_MISMATCH; } dheOk = 1; break; - case 3: /* AEAD - selected, single bit */ - if (algSel != 0x0002) { /* AES_256_GCM = bit 1 */ + case SPDM_ALG_TYPE_AEAD: + if (algSel != SPDM_AEAD_ALGO_AES_256_GCM) { wolfSPDM_DebugPrint(ctx, "ALGORITHMS: AEAD not AES_256_GCM (0x%04x)\n", algSel); return WOLFSPDM_E_ALGO_MISMATCH; } aeadOk = 1; break; - case 5: /* KeySchedule - selected, single bit */ - if (algSel != 0x0001) { /* SPDM = bit 0 */ + case SPDM_ALG_TYPE_KEY_SCHEDULE: + if (algSel != SPDM_KEY_SCHEDULE_SPDM) { wolfSPDM_DebugPrint(ctx, "ALGORITHMS: KeySchedule not SPDM (0x%04x)\n", algSel); return WOLFSPDM_E_ALGO_MISMATCH; diff --git a/src/spdm_secured.c b/src/spdm_secured.c index 873ccae..c4b616a 100644 --- a/src/spdm_secured.c +++ b/src/spdm_secured.c @@ -95,8 +95,11 @@ int wolfSPDM_EncryptInternal(WOLFSPDM_CTX* ctx, rc = wc_AesInit(&aes, NULL, INVALID_DEVID); if (rc != 0) { - rc = WOLFSPDM_E_CRYPTO_FAIL; - goto exit; + /* wc_AesInit failed: do NOT touch aes (don't call wc_AesFree on + * an uninitialized object). Caller-side cleanup below is guarded + * by aesInit. */ + wc_ForceZero(plainBuf, sizeof(plainBuf)); + return WOLFSPDM_E_CRYPTO_FAIL; } rc = wc_AesGcmSetKey(&aes, ctx->reqDataKey, WOLFSPDM_AEAD_KEY_SIZE); if (rc != 0) { @@ -122,10 +125,11 @@ int wolfSPDM_EncryptInternal(WOLFSPDM_CTX* ctx, rc = WOLFSPDM_SUCCESS; exit: + /* aes was initialized (we jumped here past the init check); safe to free. */ + wc_AesFree(&aes); /* Wipe the plaintext buffer so the outgoing payload doesn't linger * on the stack frame after this call returns. */ wc_ForceZero(plainBuf, sizeof(plainBuf)); - wc_AesFree(&aes); return rc; } @@ -199,8 +203,10 @@ int wolfSPDM_DecryptInternal(WOLFSPDM_CTX* ctx, rc = wc_AesInit(&aes, NULL, INVALID_DEVID); if (rc != 0) { - rc = WOLFSPDM_E_CRYPTO_FAIL; - goto exit; + /* wc_AesInit failed: aes is not safe to wc_AesFree. Wipe stack + * decrypted buffer and bail without touching aes. */ + wc_ForceZero(decrypted, sizeof(decrypted)); + return WOLFSPDM_E_CRYPTO_FAIL; } rc = wc_AesGcmSetKey(&aes, ctx->rspDataKey, WOLFSPDM_AEAD_KEY_SIZE); if (rc != 0) { @@ -255,10 +261,11 @@ int wolfSPDM_DecryptInternal(WOLFSPDM_CTX* ctx, rc = WOLFSPDM_SUCCESS; exit: + /* aes was initialized (we jumped here past the init check); safe to free. */ + wc_AesFree(&aes); /* Wipe the decrypted plaintext so secured-channel payloads don't * linger on the stack frame after this call returns. */ wc_ForceZero(decrypted, sizeof(decrypted)); - wc_AesFree(&aes); return rc; } diff --git a/test/test_spdm.c b/test/test_spdm.c index d247139..3f2742f 100644 --- a/test/test_spdm.c +++ b/test/test_spdm.c @@ -26,10 +26,24 @@ #ifdef HAS_SOCKET typedef struct { int sockFd; - int isSecured; } TCP_CTX; -static TCP_CTX g_tcpCtx = { -1, 0 }; +static TCP_CTX g_tcpCtx = { -1 }; + +/* Tell secured (post-KEY_EXCHANGE_RSP) records from plaintext handshake by + * matching the leading 4 bytes against the live session ID, the same way + * examples/spdm_demo.c does it. Using a static flag here would miss FINISH + * (encrypted, but before WOLFSPDM_STATE_CONNECTED). */ +static int is_secured_spdm(WOLFSPDM_CTX* ctx, const byte* buf, word32 sz) +{ + word32 sid, b0; + if (sz < 4) return 0; + sid = wolfSPDM_GetSessionId(ctx); + if (sid == 0) return 0; + b0 = (word32)buf[0] | ((word32)buf[1] << 8) | + ((word32)buf[2] << 16) | ((word32)buf[3] << 24); + return b0 == sid; +} /* MCTP transport I/O callback for libspdm emulator */ static int tcp_io_callback(WOLFSPDM_CTX* ctx, @@ -43,8 +57,6 @@ static int tcp_io_callback(WOLFSPDM_CTX* ctx, ssize_t sent, recvd; word32 payloadSz, respSize; - (void)ctx; - if (tcpCtx == NULL || tcpCtx->sockFd < 0) { return -1; } @@ -64,8 +76,9 @@ static int tcp_io_callback(WOLFSPDM_CTX* ctx, sendBuf[10] = (byte)(payloadSz >> 8); sendBuf[11] = (byte)(payloadSz & 0xFF); - /* MCTP header */ - sendBuf[12] = tcpCtx->isSecured ? 0x06 : 0x05; + /* MCTP message type: secured (0x06) once the tx buffer starts with the + * live session ID, otherwise plaintext SPDM (0x05). */ + sendBuf[12] = is_secured_spdm(ctx, txBuf, txSz) ? 0x06 : 0x05; if (txSz > 0) { memcpy(sendBuf + 13, txBuf, txSz); diff --git a/wolfspdm/spdm_types.h b/wolfspdm/spdm_types.h index 69f474c..191c7bf 100644 --- a/wolfspdm/spdm_types.h +++ b/wolfspdm/spdm_types.h @@ -136,6 +136,12 @@ extern "C" { /* Key Schedule (SPDM 1.2) */ #define SPDM_KEY_SCHEDULE_SPDM 0x0001 /* Standard SPDM key schedule */ +/* ALGORITHMS AlgStruct AlgType values (DSP0274 Sec. 10.4 Table 16) */ +#define SPDM_ALG_TYPE_DHE 2 +#define SPDM_ALG_TYPE_AEAD 3 +#define SPDM_ALG_TYPE_REQ_BASE_ASYM 4 +#define SPDM_ALG_TYPE_KEY_SCHEDULE 5 + /* Algorithm Set B Fixed Parameters */ #define WOLFSPDM_HASH_SIZE 48 /* SHA-384 output size */ #define WOLFSPDM_ECC_KEY_SIZE 48 /* P-384 coordinate size */ From 084bd5ddc653a7c4009df78d7448b06ea27c7ade Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 13 May 2026 22:14:47 +0100 Subject: [PATCH 4/7] Protocol-compliance and API hardening across requester Parser updates per DSP0274 / DSP0277: - ParseCapabilities now extracts CTExponent, DataTransferSize, and MaxSPDMmsgSize. GetCertificate clamps the per-fragment Length to the responder's negotiated DataTransferSize. - ParseDigests stores Param1 SlotMask; ConnectStandard picks the lowest populated slot rather than always using slot 0. - ParseCertificate, ParseChallengeAuth, ParseMeasurements all check the echoed SlotID. ParseCertificate uses the shared parse-or-error helper so short ERROR responses route to PEER_ERROR. - ParseAlgorithms validates the Length field and the MeasurementSpecificationSel / OtherParamsSel echoes. - Certificate fetch loop has an iteration cap and a forward-progress guard so a peer cannot stall the requester. State and lifecycle: - Connect requires either a configured trust anchor or an explicit wolfSPDM_AllowUntrustedCerts opt-in. - New wolfSPDM_SetRequesterSessionId; Init draws a random non-reserved ReqSessionID by default. - GetMeasurements requires the encrypted channel and returns SUCCESS on the no-signature retrieval path. - Public encrypt / decrypt / secured-exchange entry points share a single state guard that requires the application-data phase. - KeyUpdate snapshots the keying material and rolls it back if the ACK fails to decrypt. - Disconnect and the Connect reset path call a shared key-wipe helper so derived material does not survive into the next attempt. Cleanup of intermediate digests, MACs, and stack key material on every exit path of ParseKeyExchangeRsp, BuildFinish, Finish, BuildSignedHash, HkdfExpandLabel, DeriveAppDataKeys, VerifyMeasurementSig, and VerifyChallengeAuthSig. Defense-in-depth bound checks on EncryptInternal plainSz, DecryptInternal cipherLen, and the HkdfExpandLabel assembly buffer. DecryptInternal also enforces strict DSP0277 Length equality so trailing bytes past the declared record boundary are rejected. Wire-format cleanups: - Default requester capabilities drop the responder-only CERT_CAP and CHAL_CAP bits. - ECDH shared-secret zero-pad walks the full curve size in a fixed loop so the memory-access pattern is independent of the X-coordinate's leading-zero count. - Removed the unused ctx->th2 field. New unit tests cover the constant-time MAC compare, IV byte positions, sessionId-mismatch decrypt path, the empty-payload encrypt / decrypt boundary, ParseVersion entryCount=0 and all-below-min, ParseAlgorithms numAlgs=0, and the full CAPABILITIES field extraction. Suite is 60/60 locally; integration matrix is 18/18 against the DMTF spdm-emu across SPDM 1.2, 1.3, and 1.4. --- README.md | 27 ----- examples/spdm_demo.c | 12 +- src/spdm_context.c | 146 ++++++++++++++++++++---- src/spdm_crypto.c | 28 ++++- src/spdm_internal.h | 7 +- src/spdm_kdf.c | 22 +++- src/spdm_msg.c | 215 ++++++++++++++++++++++++++--------- src/spdm_secured.c | 58 +++++++++- src/spdm_session.c | 164 ++++++++++++++++++++++----- src/spdm_transcript.c | 1 - test/unit_test.c | 252 ++++++++++++++++++++++++++++++++++++++++-- wolfspdm/spdm.h | 25 +++++ wolfspdm/spdm_types.h | 6 +- 13 files changed, 805 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index 1ad2133..e36de1b 100644 --- a/README.md +++ b/README.md @@ -101,33 +101,6 @@ export SPDM_EMU_PATH=../spdm-emu/build/bin The driver starts/stops `spdm_responder_emu` per test and runs six scenarios — Session, Signed Measurements, Unsigned Measurements, Challenge, Heartbeat, Key Update — across SPDM 1.2, 1.3, and 1.4 (18 tests total). -## API Reference - -| Function | Description | -|---|---| -| `wolfSPDM_InitStatic()` | Initialize context in caller-provided buffer (static mode) | -| `wolfSPDM_New()` | Allocate and initialize context on heap (dynamic mode) | -| `wolfSPDM_Init()` | Initialize a pre-allocated context | -| `wolfSPDM_Free()` | Free context (releases resources; frees heap only if dynamic) | -| `wolfSPDM_GetCtxSize()` | Return `sizeof(WOLFSPDM_CTX)` at runtime | -| `wolfSPDM_SetIO()` | Set transport I/O callback | -| `wolfSPDM_SetDebug()` | Enable/disable debug output | -| `wolfSPDM_SetMaxVersion()` | Runtime cap on the highest SPDM version to negotiate | -| `wolfSPDM_Connect()` | Full SPDM handshake (`GET_VERSION` -> `FINISH`) | -| `wolfSPDM_IsConnected()` | Check session status | -| `wolfSPDM_Disconnect()` | End session | -| `wolfSPDM_SecuredExchange()` | Combined encrypted send/receive | -| `wolfSPDM_SendData()` / `wolfSPDM_ReceiveData()` | Application data over an established session | -| `wolfSPDM_SetTrustedCAs()` | Load the trusted root CA certificate for chain validation | -| `wolfSPDM_GetMeasurements()` | Retrieve device measurements with optional signature verification | -| `wolfSPDM_GetMeasurementCount()` / `wolfSPDM_GetMeasurementBlock()` | Access individual measurement block data | -| `wolfSPDM_Challenge()` | Sessionless device attestation via `CHALLENGE` / `CHALLENGE_AUTH` | -| `wolfSPDM_Heartbeat()` | Session keep-alive (`HEARTBEAT` / `HEARTBEAT_ACK`) | -| `wolfSPDM_KeyUpdate()` | Rotate session encryption keys (`KEY_UPDATE` / `KEY_UPDATE_ACK`) | -| `wolfSPDM_GetSessionId()` | Combined req/rsp session ID; available from `KEY_EXCHANGE_RSP` onward so I/O callbacks can distinguish the encrypted `FINISH` record from plaintext handshake messages | -| `wolfSPDM_GetNegotiatedVersion()` | Negotiated SPDM version (e.g. 0x12, 0x13, 0x14). The old spelling `wolfSPDM_GetVersion_Negotiated` is kept as an exported ABI-compat alias | -| `wolfSPDM_GetLastPeerError()` | Last `SPDM_ERROR` code received from the responder, for retry/backoff logic | - ## CI / Testing Runs on every push and PR: diff --git a/examples/spdm_demo.c b/examples/spdm_demo.c index 7ca00e9..5e74b05 100644 --- a/examples/spdm_demo.c +++ b/examples/spdm_demo.c @@ -273,10 +273,12 @@ static int sanitize_emu_path(const char* emuPath, char* outReal, size_t outSz) unsigned char c = (unsigned char)emuPath[i]; if (c < 0x20 || c == 0x7F) return -1; /* no control chars */ } - if (strstr(emuPath, "..") != NULL) return -1; /* no traversal */ /* POSIX realpath(path, resolved): resolved must point to a buffer of - * PATH_MAX bytes. (Avoid the GNU realpath(path, NULL) extension.) */ + * PATH_MAX bytes. (Avoid the GNU realpath(path, NULL) extension.) + * realpath() canonicalizes any ../ segments so the resolved path is + * the actual filesystem location used by fopen(), which is what + * CodeQL's "uncontrolled data in path expression" rule asks for. */ if (realpath(emuPath, resolved) == NULL) return -1; len = strlen(resolved); if (len >= outSz) return -1; @@ -498,6 +500,12 @@ int main(int argc, char* argv[]) wolfSPDM_SetIO(ctx, tcp_io_callback, &g_tcpCtx); + /* 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 + * deployments should call wolfSPDM_SetTrustedCAs instead. */ + wolfSPDM_AllowUntrustedCerts(ctx, 1); + if (maxVer != 0) { rc = wolfSPDM_SetMaxVersion(ctx, maxVer); if (rc != WOLFSPDM_SUCCESS) { diff --git a/src/spdm_context.c b/src/spdm_context.c index 19b18c2..fe8019b 100644 --- a/src/spdm_context.c +++ b/src/spdm_context.c @@ -25,15 +25,41 @@ /* --- Context Management --- */ +/* Wipe every long-lived session-key field. Used by Disconnect, ConnectStandard + * reset, and anywhere derived material from a prior session must not leak + * into the next. */ +static void wolfSPDM_WipeSessionKeys(WOLFSPDM_CTX* ctx) +{ + wc_ForceZero(ctx->sharedSecret, sizeof(ctx->sharedSecret)); + wc_ForceZero(ctx->handshakeSecret, sizeof(ctx->handshakeSecret)); + wc_ForceZero(ctx->reqHsSecret, sizeof(ctx->reqHsSecret)); + wc_ForceZero(ctx->rspHsSecret, sizeof(ctx->rspHsSecret)); + wc_ForceZero(ctx->reqFinishedKey, sizeof(ctx->reqFinishedKey)); + wc_ForceZero(ctx->rspFinishedKey, sizeof(ctx->rspFinishedKey)); + wc_ForceZero(ctx->reqDataKey, sizeof(ctx->reqDataKey)); + wc_ForceZero(ctx->rspDataKey, sizeof(ctx->rspDataKey)); + wc_ForceZero(ctx->reqDataIv, sizeof(ctx->reqDataIv)); + wc_ForceZero(ctx->rspDataIv, sizeof(ctx->rspDataIv)); + wc_ForceZero(ctx->reqAppSecret, sizeof(ctx->reqAppSecret)); + wc_ForceZero(ctx->rspAppSecret, sizeof(ctx->rspAppSecret)); + wc_ForceZero(ctx->th1, sizeof(ctx->th1)); + ctx->sharedSecretSz = 0; +} + int wolfSPDM_Init(WOLFSPDM_CTX* ctx) { int rc; + word16 sid; if (ctx == NULL) { return WOLFSPDM_E_INVALID_ARG; } - /* Clean slate - do NOT read any fields before this (could be garbage) */ + /* Clean slate - do NOT read any fields before this (could be garbage). + * Callers must wolfSPDM_Free before re-initializing an existing ctx; + * skipping that step leaks the wolfCrypt RNG/ECC/SHA states opened by + * the prior Init. We cannot reliably detect that from inside Init + * (the flag byte is itself part of the garbage we are about to wipe). */ XMEMSET(ctx, 0, sizeof(WOLFSPDM_CTX)); ctx->state = WOLFSPDM_STATE_INIT; @@ -47,8 +73,16 @@ int wolfSPDM_Init(WOLFSPDM_CTX* ctx) /* Set default requester capabilities */ ctx->reqCaps = WOLFSPDM_DEFAULT_REQ_CAPS; - /* Set default session ID (0x0001 is valid; 0x0000/0xFFFF are reserved) */ - ctx->reqSessionId = 0x0001; + /* Pick a random, non-reserved ReqSessionID (DSP0277 reserves 0x0000 and + * 0xFFFF). Callers needing determinism can override via + * wolfSPDM_SetRequesterSessionId. */ + do { + if (wc_RNG_GenerateBlock(&ctx->rng, (byte*)&sid, sizeof(sid)) != 0) { + sid = 0x0001; /* RNG failed; fall back to legacy default */ + break; + } + } while (sid == 0x0000 || sid == 0xFFFF); + ctx->reqSessionId = sid; ctx->flags.initialized = 1; /* isDynamic remains 0 - only wolfSPDM_New sets it */ @@ -204,6 +238,28 @@ byte wolfSPDM_GetVersion_Negotiated(WOLFSPDM_CTX* ctx) return wolfSPDM_GetNegotiatedVersion(ctx); } +int wolfSPDM_SetRequesterSessionId(WOLFSPDM_CTX* ctx, word16 reqSessionId) +{ + if (ctx == NULL) { + return WOLFSPDM_E_INVALID_ARG; + } + /* DSP0277: 0x0000 and 0xFFFF are reserved and shall not appear on wire. */ + if (reqSessionId == 0x0000 || reqSessionId == 0xFFFF) { + return WOLFSPDM_E_INVALID_ARG; + } + ctx->reqSessionId = reqSessionId; + return WOLFSPDM_SUCCESS; +} + +int wolfSPDM_AllowUntrustedCerts(WOLFSPDM_CTX* ctx, int allow) +{ + if (ctx == NULL) { + return WOLFSPDM_E_INVALID_ARG; + } + ctx->flags.allowUntrustedCert = allow ? 1 : 0; + return WOLFSPDM_SUCCESS; +} + int wolfSPDM_SetMaxVersion(WOLFSPDM_CTX* ctx, byte maxVersion) { if (ctx == NULL) { @@ -277,15 +333,20 @@ static int wolfSPDM_ConnectStandard(WOLFSPDM_CTX* ctx) wc_ecc_free(&ctx->responderPubKey); ctx->flags.hasResponderPubKey = 0; } + /* Wipe derived key material from any prior session before starting a + * fresh handshake. If this new handshake fails before + * wolfSPDM_DeriveHandshakeKeys overwrites the fields, the prior + * session's secrets must not linger in the context. */ + wolfSPDM_WipeSessionKeys(ctx); ctx->state = WOLFSPDM_STATE_INIT; ctx->sessionId = 0; - /* Re-pick a non-reserved reqSessionId; DSP0277 reserves 0x0000 and - * 0xFFFF, so use the same default Init picked. */ - ctx->reqSessionId = 0x0001; + /* Preserve caller-set reqSessionId from Init / SetRequesterSessionId. */ ctx->rspSessionId = 0; ctx->reqSeqNum = 0; ctx->rspSeqNum = 0; ctx->lastPeerErrorCode = 0; + ctx->slotMask = 0; + ctx->currentSlotId = 0; #ifndef NO_WOLFSPDM_MEAS /* Drop stale measurement state from a prior connect so reconnect- * without-disconnect doesn't surface old blocks. */ @@ -303,16 +364,42 @@ static int wolfSPDM_ConnectStandard(WOLFSPDM_CTX* ctx) wolfSPDM_NegotiateAlgorithms(ctx)); SPDM_CONNECT_STEP(ctx, "Step 4: GET_DIGESTS\n", wolfSPDM_GetDigests(ctx)); - SPDM_CONNECT_STEP(ctx, "Step 5: GET_CERTIFICATE\n", - wolfSPDM_GetCertificate(ctx, 0)); + + /* DSP0274 Sec. 10.5: pick the lowest-numbered slot the responder said + * is populated (DIGESTS Param1 SlotMask). Fall back to slot 0 if the + * responder did not report a mask, matching the prior behavior. */ + { + int slot = 0; + if (ctx->slotMask != 0) { + int i; + for (i = 0; i < 8; i++) { + if (ctx->slotMask & (1 << i)) { + slot = i; + break; + } + } + } + SPDM_CONNECT_STEP(ctx, "Step 5: GET_CERTIFICATE\n", + wolfSPDM_GetCertificate(ctx, slot)); + } /* Validate certificate chain if trusted CAs are loaded. GetCertificate * already guarantees flags.hasResponderPubKey is set on success (returns - * an error otherwise), so we only need to gate on the CA-bundle. */ + * an error otherwise), so we only need to gate on the CA-bundle. Fail + * closed by default: refuse to derive session keys against an + * unauthenticated responder unless the caller has explicitly opted + * into untrusted operation via wolfSPDM_AllowUntrustedCerts. */ if (ctx->flags.hasTrustedCAs) { SPDM_CONNECT_STEP(ctx, "Validating certificate chain\n", wolfSPDM_ValidateCertChain(ctx)); } + else if (!ctx->flags.allowUntrustedCert) { + wolfSPDM_DebugPrint(ctx, + "Refusing handshake: no trust anchor configured; call " + "wolfSPDM_SetTrustedCAs or wolfSPDM_AllowUntrustedCerts\n"); + ctx->state = WOLFSPDM_STATE_ERROR; + return WOLFSPDM_E_CERT_FAIL; + } else { wolfSPDM_DebugPrint(ctx, "Warning: No trusted CAs loaded - chain not validated\n"); @@ -349,30 +436,30 @@ int wolfSPDM_Connect(WOLFSPDM_CTX* ctx) int wolfSPDM_Disconnect(WOLFSPDM_CTX* ctx) { - int rc; + int rc = WOLFSPDM_SUCCESS; byte txBuf[8]; byte rxBuf[16]; /* END_SESSION_ACK: 4 bytes */ word32 txSz, rxSz; + int sendEndSession; if (ctx == NULL) { return WOLFSPDM_E_INVALID_ARG; } - if (ctx->state != WOLFSPDM_STATE_CONNECTED) { - return WOLFSPDM_E_NOT_CONNECTED; - } + /* Only send END_SESSION when we actually have a connected secured + * channel. For partial-handshake failures (state below CONNECTED) we + * still want to wipe locally derived material on the way out. */ + sendEndSession = (ctx->state == WOLFSPDM_STATE_CONNECTED); - /* Build END_SESSION */ - txSz = sizeof(txBuf); - rc = wolfSPDM_BuildEndSession(ctx, txBuf, &txSz); - if (rc != WOLFSPDM_SUCCESS) { - return rc; + if (sendEndSession) { + txSz = sizeof(txBuf); + rc = wolfSPDM_BuildEndSession(ctx, txBuf, &txSz); + if (rc == WOLFSPDM_SUCCESS) { + rxSz = sizeof(rxBuf); + rc = wolfSPDM_SecuredExchange(ctx, txBuf, txSz, rxBuf, &rxSz); + } } - /* Send as secured message */ - rxSz = sizeof(rxBuf); - rc = wolfSPDM_SecuredExchange(ctx, txBuf, txSz, rxBuf, &rxSz); - /* Reset state regardless of result. Free the cached responder public * key so the next Connect re-extracts it from the (potentially new) * responder's certificate chain - otherwise KEY_EXCHANGE_RSP signature @@ -381,11 +468,18 @@ int wolfSPDM_Disconnect(WOLFSPDM_CTX* ctx) wc_ecc_free(&ctx->responderPubKey); ctx->flags.hasResponderPubKey = 0; } + /* Wipe every long-lived session secret so disconnected contexts cannot + * be recovered for the duration before wolfSPDM_Free or a fresh + * Connect overwrites them. */ + wolfSPDM_WipeSessionKeys(ctx); ctx->state = WOLFSPDM_STATE_INIT; ctx->sessionId = 0; + ctx->rspSessionId = 0; ctx->reqSeqNum = 0; ctx->rspSeqNum = 0; ctx->lastPeerErrorCode = 0; + ctx->slotMask = 0; + ctx->currentSlotId = 0; #ifndef NO_WOLFSPDM_MEAS /* Drop stale measurement state so callers can't accidentally read * blocks from the previous session after a reconnect. */ @@ -394,7 +488,13 @@ int wolfSPDM_Disconnect(WOLFSPDM_CTX* ctx) ctx->flags.hasMeasurements = 0; #endif - return (rc == WOLFSPDM_SUCCESS) ? WOLFSPDM_SUCCESS : rc; + /* If we never had a session, the caller did not request a real + * Disconnect; surface that distinction without masking it as a + * successful teardown. */ + if (!sendEndSession) { + return WOLFSPDM_E_NOT_CONNECTED; + } + return rc; } /* --- I/O Helper --- */ diff --git a/src/spdm_crypto.c b/src/spdm_crypto.c index e6c96e3..e169b66 100644 --- a/src/spdm_crypto.c +++ b/src/spdm_crypto.c @@ -184,13 +184,29 @@ int wolfSPDM_ComputeSharedSecret(WOLFSPDM_CTX* ctx, goto cleanup; } - /* Zero-pad if needed (P-384 should always return 48 bytes, but just in case) */ - rc = wolfSPDM_LeftPadToSize(ctx->sharedSecret, ctx->sharedSecretSz, - WOLFSPDM_ECC_KEY_SIZE); - if (rc != WOLFSPDM_SUCCESS) { - goto cleanup; + /* Zero-pad the X-coordinate to the full curve size in a way that does + * not branch on the secret's leading-zero count: always touch every + * byte of a scratch buffer so the memory-access pattern is independent + * of how many high-order zero bytes wolfCrypt stripped. The underlying + * wc_ecc_shared_secret length is itself a function of the secret X + * coordinate; this routine just keeps the wolfSPDM-level work uniform. */ + { + byte scratch[WOLFSPDM_ECC_KEY_SIZE]; + word32 retSz = ctx->sharedSecretSz; + word32 pad; + word32 i; + if (retSz > WOLFSPDM_ECC_KEY_SIZE) { + rc = WOLFSPDM_E_CRYPTO_FAIL; + goto cleanup; + } + pad = WOLFSPDM_ECC_KEY_SIZE - retSz; + for (i = 0; i < WOLFSPDM_ECC_KEY_SIZE; i++) { + scratch[i] = (i < pad) ? (byte)0 : ctx->sharedSecret[i - pad]; + } + XMEMCPY(ctx->sharedSecret, scratch, WOLFSPDM_ECC_KEY_SIZE); + wc_ForceZero(scratch, sizeof(scratch)); + ctx->sharedSecretSz = WOLFSPDM_ECC_KEY_SIZE; } - ctx->sharedSecretSz = WOLFSPDM_ECC_KEY_SIZE; wolfSPDM_DebugPrint(ctx, "ECDH shared secret computed (%u bytes)\n", ctx->sharedSecretSz); diff --git a/src/spdm_internal.h b/src/spdm_internal.h index f9bda43..36e081a 100644 --- a/src/spdm_internal.h +++ b/src/spdm_internal.h @@ -122,6 +122,7 @@ struct WOLFSPDM_CTX { unsigned int hasResponderPubKey : 1; unsigned int hasTrustedCAs : 1; unsigned int m1m2HashInit : 1; + unsigned int allowUntrustedCert : 1; /* Explicit opt-in for missing trust anchor */ } flags; /* I/O callback */ @@ -137,6 +138,11 @@ struct WOLFSPDM_CTX { byte lastPeerErrorCode; /* Last SPDM_ERROR Param1 from responder (0 = none) */ word32 rspCaps; /* Responder capabilities */ word32 reqCaps; /* Our (requester) capabilities */ + byte ctExponent; /* DSP0274 Table 12: CT = 2^CTExponent us */ + word32 dataTransferSize; /* SPDM 1.2+ max per-fragment payload */ + word32 maxSpdmMsgSize; /* SPDM 1.2+ max full SPDM message size */ + byte slotMask; /* DIGESTS Param1: bit i = slot i populated */ + byte currentSlotId; /* Slot the most recent GET_CERTIFICATE used */ /* Ephemeral ECDHE key (generated for KEY_EXCHANGE) */ ecc_key ephemeralKey; @@ -157,7 +163,6 @@ struct WOLFSPDM_CTX { /* Computed hashes */ byte certChainHash[WOLFSPDM_HASH_SIZE]; /* Ct = Hash(cert_chain) */ byte th1[WOLFSPDM_HASH_SIZE]; /* TH1 after KEY_EXCHANGE_RSP */ - byte th2[WOLFSPDM_HASH_SIZE]; /* TH2 after FINISH */ /* Derived keys */ byte handshakeSecret[WOLFSPDM_HASH_SIZE]; diff --git a/src/spdm_kdf.c b/src/spdm_kdf.c index 378582d..bef4c8e 100644 --- a/src/spdm_kdf.c +++ b/src/spdm_kdf.c @@ -45,12 +45,23 @@ int wolfSPDM_HkdfExpandLabel(byte spdmVersion, const byte* secret, word32 secret byte info[128]; word32 infoLen = 0; const char* prefix; + word32 labelLen; int rc; if (secret == NULL || label == NULL || out == NULL) { return WOLFSPDM_E_INVALID_ARG; } + /* Defense-in-depth bound check: 2 (outLen) + 8 (version prefix) + + * strlen(label) + contextSz must fit into info[128]. Reject before any + * XMEMCPY rather than relying on every caller to stay within bounds. */ + labelLen = (word32)XSTRLEN(label); + if (labelLen > sizeof(info) || + contextSz > sizeof(info) || + 2 + SPDM_BIN_CONCAT_PREFIX_LEN + labelLen + contextSz > sizeof(info)) { + return WOLFSPDM_E_BUFFER_SMALL; + } + /* Select version-specific prefix */ if (spdmVersion >= 0x14) { prefix = SPDM_BIN_CONCAT_PREFIX_14; /* "spdm1.4 " */ @@ -67,8 +78,8 @@ int wolfSPDM_HkdfExpandLabel(byte spdmVersion, const byte* secret, word32 secret XMEMCPY(info + infoLen, prefix, SPDM_BIN_CONCAT_PREFIX_LEN); infoLen += SPDM_BIN_CONCAT_PREFIX_LEN; - XMEMCPY(info + infoLen, label, XSTRLEN(label)); - infoLen += (word32)XSTRLEN(label); + XMEMCPY(info + infoLen, label, labelLen); + infoLen += labelLen; if (context != NULL && contextSz > 0) { XMEMCPY(info + infoLen, context, contextSz); @@ -77,6 +88,11 @@ int wolfSPDM_HkdfExpandLabel(byte spdmVersion, const byte* secret, word32 secret rc = wc_HKDF_Expand(WC_SHA384, secret, secretSz, info, infoLen, out, outSz); + /* info embeds the context bytes (TH1/TH2 transcript hash for handshake + * derivations). Wipe before returning so the assembled label does not + * linger on the stack. */ + wc_ForceZero(info, sizeof(info)); + return (rc == 0) ? WOLFSPDM_SUCCESS : WOLFSPDM_E_CRYPTO_FAIL; } @@ -259,6 +275,8 @@ int wolfSPDM_DeriveAppDataKeys(WOLFSPDM_CTX* ctx) wc_ForceZero(masterSecret, sizeof(masterSecret)); wc_ForceZero(reqAppSecret, sizeof(reqAppSecret)); wc_ForceZero(rspAppSecret, sizeof(rspAppSecret)); + wc_ForceZero(th2Hash, sizeof(th2Hash)); + wc_ForceZero(zeroIkm, sizeof(zeroIkm)); return rc; } diff --git a/src/spdm_msg.c b/src/spdm_msg.c index 2a7f03d..83e48d7 100644 --- a/src/spdm_msg.c +++ b/src/spdm_msg.c @@ -252,9 +252,11 @@ static int wolfSPDM_BuildSignedHash(byte spdmVersion, /* Hash M */ rc = wolfSPDM_Sha384Hash(outputDigest, signMsg, signMsgLen, NULL, 0, NULL, 0); - if (rc != WOLFSPDM_SUCCESS) return rc; - - return WOLFSPDM_SUCCESS; + /* signMsg embeds the inputDigest (a transcript-state hash). Wipe it + * before returning so the assembled signing input does not linger on + * the stack frame. */ + wc_ForceZero(signMsg, sizeof(signMsg)); + return rc; } /* Verify an SPDM ECDSA signature (raw r||s format) against a digest @@ -327,22 +329,20 @@ int wolfSPDM_BuildFinish(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) /* Add FINISH header (+ OpaqueLength for 1.4) to transcript for TH2 */ rc = wolfSPDM_TranscriptAdd(ctx, buf, offset); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } /* TH2 = Hash(transcript with FINISH header) */ rc = wolfSPDM_TranscriptHash(ctx, th2Hash); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } - XMEMCPY(ctx->th2, th2Hash, WOLFSPDM_HASH_SIZE); - /* RequesterVerifyData = HMAC(reqFinishedKey, TH2) where TH2 is the * transcript hash through the FINISH header. */ rc = wolfSPDM_ComputeVerifyData(ctx->reqFinishedKey, th2Hash, verifyData); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } XMEMCPY(&buf[offset], verifyData, WOLFSPDM_HASH_SIZE); @@ -351,11 +351,19 @@ int wolfSPDM_BuildFinish(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) /* Add RequesterVerifyData to transcript for TH2_final (app data key derivation) */ rc = wolfSPDM_TranscriptAdd(ctx, verifyData, WOLFSPDM_HASH_SIZE); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } *bufSz = offset; - return WOLFSPDM_SUCCESS; + rc = WOLFSPDM_SUCCESS; + +cleanup: + /* th2Hash is the FINISH transcript-state digest; verifyData is the + * requester FINISH MAC keyed with reqFinishedKey. Both must not linger + * on the stack frame after this call returns. */ + wc_ForceZero(th2Hash, sizeof(th2Hash)); + wc_ForceZero(verifyData, sizeof(verifyData)); + return rc; } int wolfSPDM_BuildEndSession(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) @@ -438,7 +446,19 @@ int wolfSPDM_ParseCapabilities(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 12); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_CAPABILITIES, WOLFSPDM_E_CAPS_MISMATCH); + /* DSP0274 Table 12: CAPABILITIES response layout + * buf[4]: CTExponent + * buf[8-11]: Flags (rspCaps) + * buf[12-15]: DataTransferSize (SPDM 1.2+) + * buf[16-19]: MaxSPDMmsgSize (SPDM 1.2+) + * The 1.2+ fields are populated when the response carries them; pre-1.2 + * leaves them as 0 and downstream paths fall back to fixed defaults. */ + ctx->ctExponent = buf[4]; ctx->rspCaps = SPDM_Get32LE(&buf[8]); + if (ctx->spdmVersion >= SPDM_VERSION_12 && bufSz >= 20) { + ctx->dataTransferSize = SPDM_Get32LE(&buf[12]); + ctx->maxSpdmMsgSize = SPDM_Get32LE(&buf[16]); + } ctx->state = WOLFSPDM_STATE_CAPS; wolfSPDM_DebugPrint(ctx, "Responder caps: 0x%08x\n", ctx->rspCaps); @@ -449,10 +469,39 @@ int wolfSPDM_ParseAlgorithms(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) { word32 baseAsymAlgo; word32 baseHashAlgo; + word16 declaredLen; SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 36); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_ALGORITHMS, WOLFSPDM_E_ALGO_MISMATCH); + /* DSP0274 Table 18: Length (offset 4-5, LE) is the entire response size + * including header. Reject responses whose declared length does not + * match the received buffer. */ + declaredLen = SPDM_Get16LE(&buf[4]); + if (declaredLen != bufSz) { + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: declared Length %u != bufSz %u\n", + declaredLen, bufSz); + return WOLFSPDM_E_ALGO_MISMATCH; + } + + /* DSP0274 Table 18 offsets 6-7: MeasurementSpecificationSel and + * OtherParamsSel. We advertise DMTF (0x01) and OpaqueDataFormat1 (0x02) + * in NEGOTIATE_ALGORITHMS; reject responders that select a different + * bit or zero. */ + if (buf[6] != 0x01) { + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: MeasurementSpecificationSel != DMTF (0x%02x)\n", + buf[6]); + return WOLFSPDM_E_ALGO_MISMATCH; + } + if (ctx->spdmVersion >= SPDM_VERSION_12 && buf[7] != 0x02) { + wolfSPDM_DebugPrint(ctx, + "ALGORITHMS: OtherParamsSel != OpaqueDataFormat1 (0x%02x)\n", + buf[7]); + return WOLFSPDM_E_ALGO_MISMATCH; + } + /* Validate negotiated algorithms match Algorithm Set B. * ALGORITHMS response layout (DSP0274 Table 18): * Offset 8-11: MeasurementHashAlgo (4 LE) @@ -557,6 +606,10 @@ int wolfSPDM_ParseDigests(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 4); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_DIGESTS, WOLFSPDM_E_CERT_FAIL); + /* DSP0274 Sec. 10.5: Param1 = SlotMask, bit i = slot i populated. + * Stash it so GetCertificate can pick a populated slot rather than + * blindly requesting slot 0. */ + ctx->slotMask = buf[2]; ctx->state = WOLFSPDM_STATE_DIGESTS; return WOLFSPDM_SUCCESS; } @@ -564,19 +617,42 @@ int wolfSPDM_ParseDigests(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) int wolfSPDM_ParseCertificate(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz, word16* portionLen, word16* remainderLen) { - if (ctx == NULL || buf == NULL || bufSz < 8 || - portionLen == NULL || remainderLen == NULL) { + int rc; + + if (portionLen == NULL || remainderLen == NULL) { return WOLFSPDM_E_INVALID_ARG; } + /* Use the shared parse-or-error helper so a 4-byte SPDM_ERROR is allowed + * to fall through to SPDM_CHECK_RESPONSE, which surfaces it as + * WOLFSPDM_E_PEER_ERROR and stashes the responder error code. */ + SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 8); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_CERTIFICATE, WOLFSPDM_E_CERT_FAIL); + /* DSP0274 Sec. 10.6: Param1[3:0] echoes the SlotID the requester asked + * for. A responder returning a different slot's chain could trick us + * into validating the wrong identity. */ + if ((buf[2] & 0x0F) != (ctx->currentSlotId & 0x0F)) { + wolfSPDM_DebugPrint(ctx, + "CERTIFICATE: SlotID echo mismatch (got %u, expected %u)\n", + buf[2] & 0x0F, ctx->currentSlotId & 0x0F); + return WOLFSPDM_E_CERT_FAIL; + } + *portionLen = SPDM_Get16LE(&buf[4]); *remainderLen = SPDM_Get16LE(&buf[6]); - /* Add certificate chain data (starting at offset 8) */ - if (*portionLen > 0 && bufSz >= (word32)(8 + *portionLen)) { - wolfSPDM_CertChainAdd(ctx, buf + 8, *portionLen); + /* Reject truncated chunks - returning success here would let GetCertificate + * advance offset by a portionLen that was never actually delivered, and + * eventually advance state with a partial chain. */ + if (*portionLen > 0) { + if (bufSz < (word32)(8 + *portionLen)) { + return WOLFSPDM_E_BUFFER_SMALL; + } + rc = wolfSPDM_CertChainAdd(ctx, buf + 8, *portionLen); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } } if (*remainderLen == 0) { @@ -642,7 +718,7 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS /* Add KEY_EXCHANGE_RSP partial (without sig/verify) to transcript */ rc = wolfSPDM_TranscriptAdd(ctx, buf, keRspPartialLen); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } /* Verify responder signature per DSP0274 Sec 14: signature is over @@ -655,73 +731,87 @@ int wolfSPDM_ParseKeyExchangeRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufS byte signedDigest[WOLFSPDM_HASH_SIZE]; rc = wolfSPDM_TranscriptHash(ctx, th1Partial); - if (rc != WOLFSPDM_SUCCESS) { - return rc; + if (rc == WOLFSPDM_SUCCESS) { + rc = wolfSPDM_BuildSignedHash(ctx->spdmVersion, + sigCtx, (word32)(sizeof(sigCtx) - 1), + th1Partial, signedDigest); } - rc = wolfSPDM_BuildSignedHash(ctx->spdmVersion, - sigCtx, (word32)(sizeof(sigCtx) - 1), - th1Partial, signedDigest); - if (rc != WOLFSPDM_SUCCESS) { - return rc; + if (rc == WOLFSPDM_SUCCESS) { + rc = wolfSPDM_VerifyEccSig(ctx, signature, WOLFSPDM_ECC_SIG_SIZE, + signedDigest, WOLFSPDM_HASH_SIZE); + if (rc != WOLFSPDM_SUCCESS) { + wolfSPDM_DebugPrint(ctx, + "KEY_EXCHANGE_RSP signature verification failed (rc=%d)\n", + rc); + } + else { + wolfSPDM_DebugPrint(ctx, + "KEY_EXCHANGE_RSP signature verified\n"); + } } - rc = wolfSPDM_VerifyEccSig(ctx, signature, WOLFSPDM_ECC_SIG_SIZE, - signedDigest, WOLFSPDM_HASH_SIZE); + /* Wipe partial-TH1 and assembled-digest before they go out of scope. + * These are intermediate handshake material; do not let them linger + * on the stack frame after this call returns. */ + wc_ForceZero(th1Partial, sizeof(th1Partial)); + wc_ForceZero(signedDigest, sizeof(signedDigest)); if (rc != WOLFSPDM_SUCCESS) { - wolfSPDM_DebugPrint(ctx, - "KEY_EXCHANGE_RSP signature verification failed (rc=%d)\n", rc); - /* Preserve the distinction wolfSPDM_VerifyEccSig draws between - * BAD_SIGNATURE (peer-level violation) and CRYPTO_FAIL - * (transient wolfCrypt issue). */ - return rc; + goto cleanup; } - wolfSPDM_DebugPrint(ctx, "KEY_EXCHANGE_RSP signature verified\n"); } /* Add signature to transcript (TH1 includes signature) */ rc = wolfSPDM_TranscriptAdd(ctx, signature, WOLFSPDM_ECC_SIG_SIZE); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } /* Compute ECDH shared secret */ rc = wolfSPDM_ComputeSharedSecret(ctx, peerPubKeyX, peerPubKeyY); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } /* Compute TH1 = Hash(transcript including signature) */ rc = wolfSPDM_TranscriptHash(ctx, ctx->th1); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } /* Derive all session keys */ rc = wolfSPDM_DeriveHandshakeKeys(ctx, ctx->th1); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } /* Verify ResponderVerifyData = HMAC(rspFinishedKey, TH1) */ rc = wolfSPDM_ComputeVerifyData(ctx->rspFinishedKey, ctx->th1, expectedHmac); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } /* Constant-time compare to avoid leaking HMAC bytes via timing. */ if (wolfSPDM_ConstCompare(expectedHmac, rspVerifyData, WOLFSPDM_HASH_SIZE) != 0) { wolfSPDM_DebugPrint(ctx, "ResponderVerifyData MISMATCH\n"); - return WOLFSPDM_E_BAD_HMAC; + rc = WOLFSPDM_E_BAD_HMAC; + goto cleanup; } wolfSPDM_DebugPrint(ctx, "ResponderVerifyData VERIFIED OK\n"); /* Add ResponderVerifyData to transcript (per SPDM spec, always included) */ rc = wolfSPDM_TranscriptAdd(ctx, rspVerifyData, WOLFSPDM_HASH_SIZE); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } ctx->state = WOLFSPDM_STATE_KEY_EX; - return WOLFSPDM_SUCCESS; + rc = WOLFSPDM_SUCCESS; + +cleanup: + /* expectedHmac is derived from rspFinishedKey; wipe regardless of path. */ + wc_ForceZero(expectedHmac, sizeof(expectedHmac)); + wc_ForceZero(peerPubKeyX, sizeof(peerPubKeyX)); + wc_ForceZero(peerPubKeyY, sizeof(peerPubKeyY)); + return rc; } int wolfSPDM_ParseFinishRsp(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) @@ -848,6 +938,15 @@ int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 8); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_MEASUREMENTS, WOLFSPDM_E_MEASUREMENT); + /* DSP0274: Param2[3:0] echoes the SlotID the requester sent. wolfSPDM + * always issues GET_MEASUREMENTS with slot 0, so reject any other + * slot value. */ + if ((buf[3] & 0x0F) != 0) { + wolfSPDM_DebugPrint(ctx, + "MEASUREMENTS: SlotID echo mismatch (%u)\n", buf[3] & 0x0F); + return WOLFSPDM_E_MEASUREMENT; + } + numBlocks = buf[4]; /* MeasurementRecordLength: 3 bytes LE at offset 5..7 */ recordLen = (word32)buf[5] | ((word32)buf[6] << 8) | ((word32)buf[7] << 16); @@ -1057,14 +1156,16 @@ int wolfSPDM_VerifyMeasurementSig(WOLFSPDM_CTX* ctx, ctx->transcript, ctx->vcaLen, reqMsg, reqMsgSz, rspBuf, sigOffset); - if (rc != WOLFSPDM_SUCCESS) return rc; - - return wolfSPDM_VerifySignedDigest(ctx, - "responder-measurements signing", 30, digest, - rspBuf + sigOffset, WOLFSPDM_ECC_SIG_SIZE, - "Measurement signature VERIFIED", - "Measurement signature INVALID", - WOLFSPDM_E_MEAS_SIG_FAIL); + if (rc == WOLFSPDM_SUCCESS) { + rc = wolfSPDM_VerifySignedDigest(ctx, + "responder-measurements signing", 30, digest, + rspBuf + sigOffset, WOLFSPDM_ECC_SIG_SIZE, + "Measurement signature VERIFIED", + "Measurement signature INVALID", + WOLFSPDM_E_MEAS_SIG_FAIL); + } + wc_ForceZero(digest, sizeof(digest)); + return rc; } #endif /* !NO_WOLFSPDM_MEAS_VERIFY */ @@ -1318,6 +1419,15 @@ int wolfSPDM_ParseChallengeAuth(WOLFSPDM_CTX* ctx, const byte* buf, SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_CHALLENGE_AUTH, WOLFSPDM_E_CHALLENGE); + /* DSP0274 Sec. 10.8: Param1[3:0] echoes the requested SlotID. wolfSPDM + * currently always issues CHALLENGE for slot 0; reject a responder that + * authenticates a different slot. */ + if ((buf[2] & 0x0F) != 0) { + wolfSPDM_DebugPrint(ctx, + "CHALLENGE_AUTH: SlotID echo mismatch (%u)\n", buf[2] & 0x0F); + return WOLFSPDM_E_CHALLENGE; + } + offset = 4; /* CertChainHash (H bytes, 48 for SHA-384) */ @@ -1435,14 +1545,19 @@ int wolfSPDM_VerifyChallengeAuthSig(WOLFSPDM_CTX* ctx, /* Finalize M1/M2 hash */ rc = wc_Sha384Final(&ctx->m1m2Hash, digest); ctx->flags.m1m2HashInit = 0; /* Hash consumed regardless */ - if (rc != 0) return WOLFSPDM_E_CRYPTO_FAIL; + if (rc != 0) { + wc_ForceZero(digest, sizeof(digest)); + return WOLFSPDM_E_CRYPTO_FAIL; + } - return wolfSPDM_VerifySignedDigest(ctx, + rc = wolfSPDM_VerifySignedDigest(ctx, "responder-challenge_auth signing", 32, digest, rspBuf + sigOffset, WOLFSPDM_ECC_SIG_SIZE, "CHALLENGE_AUTH signature VERIFIED", "CHALLENGE_AUTH signature INVALID", WOLFSPDM_E_CHALLENGE); + wc_ForceZero(digest, sizeof(digest)); + return rc; } #endif /* !NO_WOLFSPDM_CHALLENGE */ diff --git a/src/spdm_secured.c b/src/spdm_secured.c index c4b616a..4bba4d9 100644 --- a/src/spdm_secured.c +++ b/src/spdm_secured.c @@ -51,6 +51,14 @@ int wolfSPDM_EncryptInternal(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_INVALID_ARG; } + /* Defense-in-depth: the public wolfSPDM_EncryptMessage wrapper exposes + * this function with caller-supplied plainSz. plainBuf holds + * AppDataLen(2) + MCTPheader(1) + plaintext, so bound plainSz against + * the buffer size minus those 3 prefix bytes. */ + if (plainSz > sizeof(plainBuf) - 3) { + return WOLFSPDM_E_BUFFER_SMALL; + } + /* DSP0277 Sec. 11.3: the sequence number shall not wrap. The wire field is * 16-bit and wolfSPDM_BuildIV mixes only the low 16 bits into the AES-GCM * IV, so a wrap would reuse an IV under the same key. Refuse to encrypt @@ -191,8 +199,23 @@ int wolfSPDM_DecryptInternal(WOLFSPDM_CTX* ctx, if (rspLen < WOLFSPDM_AEAD_TAG_SIZE || encSz < (word32)(hdrSz + rspLen)) { return WOLFSPDM_E_BUFFER_SMALL; } + /* DSP0277: the Length field SHALL equal the exact length of the + * encrypted payload. Reject over-received records with extra + * unauthenticated trailing bytes. */ + if (encSz != (word32)(hdrSz + rspLen)) { + wolfSPDM_DebugPrint(ctx, + "Secured msg: encSz %u != hdrSz+rspLen %u\n", + encSz, (unsigned)(hdrSz + rspLen)); + return WOLFSPDM_E_BUFFER_SMALL; + } cipherLen = (word16)(rspLen - WOLFSPDM_AEAD_TAG_SIZE); + /* Defense-in-depth: the decrypted[] stack buffer is the upper bound on + * what wc_AesGcmDecrypt may write. Reject anything larger before the + * AEAD call so a wire-supplied rspLen cannot overflow the buffer. */ + if (cipherLen > sizeof(decrypted)) { + return WOLFSPDM_E_BUFFER_SMALL; + } ciphertext = enc + hdrSz; tag = enc + hdrSz + cipherLen; @@ -269,6 +292,27 @@ int wolfSPDM_DecryptInternal(WOLFSPDM_CTX* ctx, return rc; } +/* Public wrappers must only operate after FINISH has installed the + * application-phase AEAD keys. Allow STATE_FINISH (DeriveAppDataKeys has + * just run) through STATE_MEASURED, but reject STATE_KEY_EX (handshake + * keys still in place) and below. */ +static int wolfSPDM_AppPhaseStateOk(const WOLFSPDM_CTX* ctx) +{ + if (ctx == NULL) { + return 0; + } + if (ctx->state == WOLFSPDM_STATE_FINISH || + ctx->state == WOLFSPDM_STATE_CONNECTED) { + return 1; + } +#ifndef NO_WOLFSPDM_MEAS + if (ctx->state == WOLFSPDM_STATE_MEASURED) { + return 1; + } +#endif + return 0; +} + #ifndef WOLFSPDM_LEAN int wolfSPDM_EncryptMessage(WOLFSPDM_CTX* ctx, const byte* plain, word32 plainSz, @@ -278,9 +322,7 @@ int wolfSPDM_EncryptMessage(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_INVALID_ARG; } - if (ctx->state != WOLFSPDM_STATE_CONNECTED && - ctx->state != WOLFSPDM_STATE_KEY_EX && - ctx->state != WOLFSPDM_STATE_FINISH) { + if (!wolfSPDM_AppPhaseStateOk(ctx)) { return WOLFSPDM_E_NOT_CONNECTED; } @@ -295,9 +337,7 @@ int wolfSPDM_DecryptMessage(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_INVALID_ARG; } - if (ctx->state != WOLFSPDM_STATE_CONNECTED && - ctx->state != WOLFSPDM_STATE_KEY_EX && - ctx->state != WOLFSPDM_STATE_FINISH) { + if (!wolfSPDM_AppPhaseStateOk(ctx)) { return WOLFSPDM_E_NOT_CONNECTED; } @@ -319,6 +359,12 @@ int wolfSPDM_SecuredExchange(WOLFSPDM_CTX* ctx, return WOLFSPDM_E_INVALID_ARG; } + /* Match the EncryptMessage/DecryptMessage state guard so callers can't + * encrypt application data with handshake or zeroed keys. */ + if (!wolfSPDM_AppPhaseStateOk(ctx)) { + return WOLFSPDM_E_NOT_CONNECTED; + } + rc = wolfSPDM_EncryptInternal(ctx, cmdPlain, cmdSz, encBuf, &encSz); if (rc != WOLFSPDM_SUCCESS) { return rc; diff --git a/src/spdm_session.c b/src/spdm_session.c index 7810e2e..285ce08 100644 --- a/src/spdm_session.c +++ b/src/spdm_session.c @@ -173,11 +173,33 @@ int wolfSPDM_GetCertificate(WOLFSPDM_CTX* ctx, int slotId) word16 offset = 0; word16 portionLen; word16 remainderLen = 1; + word16 chunkLen; + word32 iterations = 0; + /* (WOLFSPDM_MAX_CERT_CHAIN / 1) + slack: every progressing chunk delivers + * at least 1 byte, so the chain itself bounds the loop. The extra slack + * absorbs any responder that returns smaller-than-requested chunks. */ + const word32 maxIterations = WOLFSPDM_MAX_CERT_CHAIN + 16; int rc; + /* DSP0274 Sec. 10.3: per-fragment Length must not exceed the responder's + * negotiated DataTransferSize. Our chunk buffer also caps at 1024. */ + chunkLen = 1024; + if (ctx->dataTransferSize != 0 && ctx->dataTransferSize < chunkLen) { + chunkLen = (word16)ctx->dataTransferSize; + } + + ctx->currentSlotId = (byte)(slotId & 0x0F); + while (remainderLen > 0) { + if (++iterations > maxIterations) { + wolfSPDM_DebugPrint(ctx, + "GET_CERTIFICATE: iteration cap reached; aborting\n"); + return WOLFSPDM_E_CERT_FAIL; + } + txSz = sizeof(txBuf); - rc = wolfSPDM_BuildGetCertificate(ctx, txBuf, &txSz, slotId, offset, 1024); + rc = wolfSPDM_BuildGetCertificate(ctx, txBuf, &txSz, slotId, offset, + chunkLen); if (rc != WOLFSPDM_SUCCESS) { return rc; } @@ -201,6 +223,16 @@ int wolfSPDM_GetCertificate(WOLFSPDM_CTX* ctx, int slotId) return rc; } + /* Forward-progress guard: a responder reporting portionLen=0 with + * remainderLen>0 is non-compliant and would spin this loop. Per + * DSP0274 each non-final chunk shall deliver some data. */ + if (portionLen == 0 && remainderLen > 0) { + wolfSPDM_DebugPrint(ctx, + "GET_CERTIFICATE: responder returned portionLen=0 with " + "remainder=%u\n", remainderLen); + return WOLFSPDM_E_CERT_FAIL; + } + offset += portionLen; wolfSPDM_DebugPrint(ctx, "Certificate: offset=%u, portion=%u, remainder=%u\n", offset, portionLen, remainderLen); @@ -255,12 +287,22 @@ int wolfSPDM_KeyExchange(WOLFSPDM_CTX* ctx) return WOLFSPDM_E_BAD_STATE; } + /* DSP0274 Sec. 10.13.5: HANDSHAKE_IN_THE_CLEAR is only entered when + * the requester also opts in (KEY_EXCHANGE Param1 bit set). wolfSPDM + * never opts in, so the encrypted FINISH_RSP path always applies and + * a responder merely advertising HANDSHAKE_IN_THE_CLEAR is fine. The + * separate ResponderVerifyData-in-the-clear parsing path is therefore + * intentionally unimplemented. */ + rc = wolfSPDM_BuildKeyExchange(ctx, txBuf, &txSz); if (rc != WOLFSPDM_SUCCESS) { return rc; } - wolfSPDM_TranscriptAdd(ctx, txBuf, txSz); + rc = wolfSPDM_TranscriptAdd(ctx, txBuf, txSz); + if (rc != WOLFSPDM_SUCCESS) { + return rc; + } rc = wolfSPDM_SendReceive(ctx, txBuf, txSz, rxBuf, &rxSz); if (rc != WOLFSPDM_SUCCESS) { @@ -292,20 +334,20 @@ int wolfSPDM_Finish(WOLFSPDM_CTX* ctx) rc = wolfSPDM_BuildFinish(ctx, finishBuf, &finishSz); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } /* FINISH must be sent encrypted (HANDSHAKE_IN_THE_CLEAR not negotiated) */ rc = wolfSPDM_EncryptInternal(ctx, finishBuf, finishSz, encBuf, &encSz); if (rc != WOLFSPDM_SUCCESS) { wolfSPDM_DebugPrint(ctx, "FINISH encrypt failed: %d\n", rc); - return rc; + goto cleanup; } rc = wolfSPDM_SendReceive(ctx, encBuf, encSz, rxBuf, &rxSz); if (rc != WOLFSPDM_SUCCESS) { wolfSPDM_DebugPrint(ctx, "FINISH SendReceive failed: %d\n", rc); - return rc; + goto cleanup; } /* Classify the response: an encrypted record's first 4 bytes are the @@ -319,31 +361,41 @@ int wolfSPDM_Finish(WOLFSPDM_CTX* ctx) ctx->lastPeerErrorCode = rxBuf[2]; wolfSPDM_DebugPrint(ctx, "FINISH: peer returned SPDM ERROR 0x%02x\n", rxBuf[2]); - return WOLFSPDM_E_PEER_ERROR; + rc = WOLFSPDM_E_PEER_ERROR; + goto cleanup; } wolfSPDM_DebugPrint(ctx, "FINISH: unexpected response code 0x%02x\n", rxBuf[1]); - return WOLFSPDM_E_PEER_ERROR; + rc = WOLFSPDM_E_PEER_ERROR; + goto cleanup; } rc = wolfSPDM_DecryptInternal(ctx, rxBuf, rxSz, decBuf, &decSz); if (rc != WOLFSPDM_SUCCESS) { wolfSPDM_DebugPrint(ctx, "FINISH decrypt failed: %d\n", rc); - return rc; + goto cleanup; } rc = wolfSPDM_ParseFinishRsp(ctx, decBuf, decSz); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto cleanup; } /* Derive application data keys (transition from handshake to app phase) */ rc = wolfSPDM_DeriveAppDataKeys(ctx); if (rc != WOLFSPDM_SUCCESS) { wolfSPDM_DebugPrint(ctx, "App data key derivation failed: %d\n", rc); - return rc; + goto cleanup; } - return WOLFSPDM_SUCCESS; + rc = WOLFSPDM_SUCCESS; + +cleanup: + /* finishBuf holds the requester VerifyData MAC; decBuf holds decrypted + * FINISH_RSP including the responder VerifyData MAC. Wipe both so the + * FINISH-stage authentication material does not linger on the stack. */ + wc_ForceZero(finishBuf, sizeof(finishBuf)); + wc_ForceZero(decBuf, sizeof(decBuf)); + return rc; } /* --- Measurements (Device Attestation) --- */ @@ -363,9 +415,15 @@ int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, return WOLFSPDM_E_INVALID_ARG; } - /* Must be at least past algorithm negotiation */ - if (ctx->state < WOLFSPDM_STATE_ALGO) { - return WOLFSPDM_E_BAD_STATE; + /* Refuse to fetch measurements over an unencrypted channel: device + * attestation content is sensitive, and the public API is intended for + * post-session use. Allow STATE_FINISH (intermediate, but session keys + * have been derived), CONNECTED, and MEASURED. */ + if (ctx->state < WOLFSPDM_STATE_FINISH) { + wolfSPDM_DebugPrint(ctx, + "GET_MEASUREMENTS: refusing in state %d (need >= FINISH)\n", + ctx->state); + return WOLFSPDM_E_NOT_CONNECTED; } /* Build GET_MEASUREMENTS request */ @@ -383,15 +441,9 @@ int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, } #endif - /* Send/receive: use secured exchange once FINISH has installed session - * keys (STATE_FINISH and beyond - includes both end-of-Connect and - * step-by-step callers), else cleartext. */ - if (ctx->state >= WOLFSPDM_STATE_FINISH) { - rc = wolfSPDM_SecuredExchange(ctx, txBuf, txSz, rxBuf, &rxSz); - } - else { - rc = wolfSPDM_SendReceive(ctx, txBuf, txSz, rxBuf, &rxSz); - } + /* Send over the secured channel; the state guard above already ensures + * session keys are installed. */ + rc = wolfSPDM_SecuredExchange(ctx, txBuf, txSz, rxBuf, &rxSz); if (rc != WOLFSPDM_SUCCESS) { wolfSPDM_DebugPrint(ctx, "GET_MEASUREMENTS exchange failed: %d\n", rc); return rc; @@ -442,9 +494,15 @@ int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, (void)requestSignature; #endif /* !NO_WOLFSPDM_MEAS_VERIFY */ - /* No signature requested or verification not compiled in */ + /* DSP0274: when the caller did not request a signature, treat the + * retrieval as success. Reserve WOLFSPDM_E_MEAS_NOT_VERIFIED for the + * case where verification was requested but cannot be performed (no + * responder public key, or build compiled without verify support). */ ctx->state = WOLFSPDM_STATE_MEASURED; - return WOLFSPDM_E_MEAS_NOT_VERIFIED; + if (requestSignature) { + return WOLFSPDM_E_MEAS_NOT_VERIFIED; + } + return WOLFSPDM_SUCCESS; } #endif /* !NO_WOLFSPDM_MEAS */ @@ -601,18 +659,38 @@ int wolfSPDM_KeyUpdate(WOLFSPDM_CTX* ctx, int updateAll) byte rawRxBuf[64]; word32 encSz = sizeof(encBuf); word32 rawRxSz = sizeof(rawRxBuf); + /* Snapshot the request-side keying material so a failed ACK decrypt + * can roll the session back to the pre-update state instead of + * leaving requester and responder permanently desynchronised. The + * responder side is only mutated when updateAll is set, so the rsp + * snapshot is only relevant in that branch. */ + byte savedReqDataKey[WOLFSPDM_AEAD_KEY_SIZE]; + byte savedReqDataIv[WOLFSPDM_AEAD_IV_SIZE]; + byte savedReqAppSecret[WOLFSPDM_HASH_SIZE]; + byte savedRspDataKey[WOLFSPDM_AEAD_KEY_SIZE]; + byte savedRspDataIv[WOLFSPDM_AEAD_IV_SIZE]; + byte savedRspAppSecret[WOLFSPDM_HASH_SIZE]; + word64 savedReqSeqNum = ctx->reqSeqNum; + word64 savedRspSeqNum = ctx->rspSeqNum; + + XMEMCPY(savedReqDataKey, ctx->reqDataKey, sizeof(savedReqDataKey)); + XMEMCPY(savedReqDataIv, ctx->reqDataIv, sizeof(savedReqDataIv)); + XMEMCPY(savedReqAppSecret, ctx->reqAppSecret, sizeof(savedReqAppSecret)); + XMEMCPY(savedRspDataKey, ctx->rspDataKey, sizeof(savedRspDataKey)); + XMEMCPY(savedRspDataIv, ctx->rspDataIv, sizeof(savedRspDataIv)); + XMEMCPY(savedRspAppSecret, ctx->rspAppSecret, sizeof(savedRspAppSecret)); /* Encrypt with current req key */ rc = wolfSPDM_EncryptInternal(ctx, txBuf, txSz, encBuf, &encSz); if (rc != WOLFSPDM_SUCCESS) { - return rc; + goto kupd_cleanup; } /* Send and receive raw (don't decrypt yet) */ rc = wolfSPDM_SendReceive(ctx, encBuf, encSz, rawRxBuf, &rawRxSz); if (rc != WOLFSPDM_SUCCESS) { wolfSPDM_DebugPrint(ctx, "KEY_UPDATE: SendReceive failed: %d\n", rc); - return rc; + goto kupd_cleanup; } /* Step 2: Derive new keys BEFORE decrypting ACK. @@ -621,7 +699,7 @@ int wolfSPDM_KeyUpdate(WOLFSPDM_CTX* ctx, int updateAll) rc = wolfSPDM_DeriveUpdatedKeys(ctx, updateAll); if (rc != WOLFSPDM_SUCCESS) { wolfSPDM_DebugPrint(ctx, "KEY_UPDATE: DeriveUpdatedKeys failed: %d\n", rc); - return rc; + goto kupd_cleanup; } /* Per DSP0277 Sec 11: reset only the seqNum for directions whose * keys actually rotated. updateAll=0 (UpdateKey) only rotates the @@ -632,11 +710,37 @@ int wolfSPDM_KeyUpdate(WOLFSPDM_CTX* ctx, int updateAll) ctx->rspSeqNum = 0; } - /* Decrypt ACK with new rsp key */ + /* Decrypt ACK with new rsp key. If this fails, roll the session + * back to the pre-update keys / seqNums - otherwise a single failed + * ACK leaves the requester and responder permanently desynchronised + * (DoS). */ rxSz = sizeof(rxBuf); rc = wolfSPDM_DecryptInternal(ctx, rawRxBuf, rawRxSz, rxBuf, &rxSz); if (rc != WOLFSPDM_SUCCESS) { - wolfSPDM_DebugPrint(ctx, "KEY_UPDATE: ACK decrypt failed: %d\n", rc); + wolfSPDM_DebugPrint(ctx, + "KEY_UPDATE: ACK decrypt failed (%d); rolling keys back\n", rc); + XMEMCPY(ctx->reqDataKey, savedReqDataKey, sizeof(savedReqDataKey)); + XMEMCPY(ctx->reqDataIv, savedReqDataIv, sizeof(savedReqDataIv)); + XMEMCPY(ctx->reqAppSecret, savedReqAppSecret, + sizeof(savedReqAppSecret)); + if (updateAll) { + XMEMCPY(ctx->rspDataKey, savedRspDataKey, sizeof(savedRspDataKey)); + XMEMCPY(ctx->rspDataIv, savedRspDataIv, sizeof(savedRspDataIv)); + XMEMCPY(ctx->rspAppSecret, savedRspAppSecret, + sizeof(savedRspAppSecret)); + } + ctx->reqSeqNum = savedReqSeqNum; + ctx->rspSeqNum = savedRspSeqNum; + } + + kupd_cleanup: + wc_ForceZero(savedReqDataKey, sizeof(savedReqDataKey)); + wc_ForceZero(savedReqDataIv, sizeof(savedReqDataIv)); + wc_ForceZero(savedReqAppSecret, sizeof(savedReqAppSecret)); + wc_ForceZero(savedRspDataKey, sizeof(savedRspDataKey)); + wc_ForceZero(savedRspDataIv, sizeof(savedRspDataIv)); + wc_ForceZero(savedRspAppSecret, sizeof(savedRspAppSecret)); + if (rc != WOLFSPDM_SUCCESS) { return rc; } } diff --git a/src/spdm_transcript.c b/src/spdm_transcript.c index 893e5d9..1aae728 100644 --- a/src/spdm_transcript.c +++ b/src/spdm_transcript.c @@ -41,7 +41,6 @@ void wolfSPDM_TranscriptReset(WOLFSPDM_CTX* ctx) XMEMSET(ctx->certChainHash, 0, sizeof(ctx->certChainHash)); XMEMSET(ctx->th1, 0, sizeof(ctx->th1)); - XMEMSET(ctx->th2, 0, sizeof(ctx->th2)); } int wolfSPDM_TranscriptAdd(WOLFSPDM_CTX* ctx, const byte* data, word32 len) diff --git a/test/unit_test.c b/test/unit_test.c index 7db7b09..89d02f0 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -345,6 +345,10 @@ static int test_parse_algorithms_set_b_enforcement(void) rsp[0] = SPDM_VERSION_12; rsp[1] = SPDM_ALGORITHMS; rsp[2] = 4; /* Param1 = AlgStructCount */ + /* Length field (offset 4-5, LE) must match received bufSz (52). */ + SPDM_Set16LE(&rsp[4], 52); + rsp[6] = 0x01; /* MeasurementSpecificationSel = DMTF */ + rsp[7] = 0x02; /* OtherParamsSel = OpaqueDataFormat1 */ /* BaseAsymSel = ECDSA_P384 (bit 7) at offset 12 */ rsp[12] = SPDM_ASYM_ALGO_ECDSA_P384 & 0xFF; rsp[13] = (SPDM_ASYM_ALGO_ECDSA_P384 >> 8) & 0xFF; @@ -1415,19 +1419,16 @@ static int test_get_measurements_peer_error(void) printf("test_get_measurements_peer_error...\n"); - /* Drive GetMeasurements through a callback that returns SPDM_ERROR - - * exercises the new wolfSPDM_CheckError branch before ParseMeasurements. */ + /* GET_MEASUREMENTS now requires >= STATE_FINISH (post-handshake) so it + * cannot be issued in the clear. Setting state to ALGO must therefore + * be refused with NOT_CONNECTED rather than reaching the I/O callback. */ ctx->state = WOLFSPDM_STATE_ALGO; ASSERT_SUCCESS(wolfSPDM_SetIO(ctx, error_io_cb, NULL)); ASSERT_EQ( wolfSPDM_GetMeasurements(ctx, SPDM_MEAS_OPERATION_ALL, 0), - WOLFSPDM_E_PEER_ERROR, - "SPDM_ERROR response should surface as PEER_ERROR"); - /* Side-effect: the responder's error code must be reachable via - * the public accessor so callers can branch on BUSY vs UNSUPPORTED. */ - ASSERT_EQ(wolfSPDM_GetLastPeerError(ctx), SPDM_ERROR_INVALID_REQUEST, - "Peer error code should be captured for retrieval"); + WOLFSPDM_E_NOT_CONNECTED, + "GET_MEASUREMENTS pre-FINISH must be refused"); TEST_CTX_FREE(); TEST_PASS(); @@ -1719,6 +1720,230 @@ static int test_session_state(void) TEST_PASS(); } +static int test_const_compare(void) +{ + /* wolfSPDM_ConstCompare must return non-zero for any single-byte + * difference and 0 only for equal buffers. Catches the |= -> &= + * accumulator mutation that the HMAC compare relies on. */ + byte a[16], b[16]; + int i; + + printf("test_const_compare...\n"); + + for (i = 0; i < 16; i++) { a[i] = (byte)i; b[i] = (byte)i; } + ASSERT_EQ(wolfSPDM_ConstCompare(a, b, 16), 0, + "Equal buffers must compare equal"); + + /* Differ only at index 0 */ + b[0] ^= 0xFF; + ASSERT_NE(wolfSPDM_ConstCompare(a, b, 16), 0, "Diff at index 0"); + b[0] = a[0]; + + /* Differ only at the last index */ + b[15] ^= 0x01; + ASSERT_NE(wolfSPDM_ConstCompare(a, b, 16), 0, "Diff at last index"); + b[15] = a[15]; + + /* Asymmetric pair {0x01, 0x00} vs {0x00, 0x01} - a |=-to-&= mutation + * would falsely report equal here. */ + a[0] = 0x01; a[1] = 0x00; + b[0] = 0x00; b[1] = 0x01; + ASSERT_NE(wolfSPDM_ConstCompare(a, b, 2), 0, + "Asymmetric pair must compare unequal"); + + g_testsPassed++; + return 0; +} + +static int test_build_iv_byte_positions(void) +{ + /* wolfSPDM_BuildIV XORs the low 2 bytes of seqNum into iv[0]/iv[1]. + * Pin byte positions so swap/offset mutations are caught. */ + byte baseIv[WOLFSPDM_AEAD_IV_SIZE]; + byte iv[WOLFSPDM_AEAD_IV_SIZE]; + word32 i; + + printf("test_build_iv_byte_positions...\n"); + + XMEMSET(baseIv, 0, sizeof(baseIv)); + wolfSPDM_BuildIV(iv, baseIv, (word64)0x1234); + + ASSERT_EQ(iv[0], (byte)0x34, "iv[0] must hold low byte of seqNum"); + ASSERT_EQ(iv[1], (byte)0x12, "iv[1] must hold high byte of seqNum"); + for (i = 2; i < WOLFSPDM_AEAD_IV_SIZE; i++) { + ASSERT_EQ(iv[i], (byte)0, "iv past byte 1 must not be touched"); + } + g_testsPassed++; + return 0; +} + +static int test_decrypt_session_id_mismatch(void) +{ + /* Encrypt at one sessionId, then change ctx->sessionId and assert + * decrypt refuses with WOLFSPDM_E_SESSION_INVALID. */ + byte plain[] = "x"; + byte enc[256]; + byte dec[256]; + word32 encSz = sizeof(enc); + word32 decSz = sizeof(dec); + int i; + TEST_CTX_SETUP_V12(); + + printf("test_decrypt_session_id_mismatch...\n"); + + ctx->state = WOLFSPDM_STATE_CONNECTED; + ctx->sessionId = 0xAAAAAAAA; + for (i = 0; i < WOLFSPDM_AEAD_KEY_SIZE; i++) { + ctx->reqDataKey[i] = (byte)i; + ctx->rspDataKey[i] = (byte)i; + } + for (i = 0; i < WOLFSPDM_AEAD_IV_SIZE; i++) { + ctx->reqDataIv[i] = (byte)(0x20 + i); + ctx->rspDataIv[i] = (byte)(0x20 + i); + } + ASSERT_SUCCESS(wolfSPDM_EncryptInternal(ctx, plain, sizeof(plain), + enc, &encSz)); + + ctx->sessionId = 0xBBBBBBBB; + ASSERT_EQ(wolfSPDM_DecryptInternal(ctx, enc, encSz, dec, &decSz), + WOLFSPDM_E_SESSION_INVALID, + "Decrypt with mismatched sessionId must refuse"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_encrypt_decrypt_empty_payload(void) +{ + /* DSP0277 boundary: plainSz=0 should round-trip cleanly through the + * MCTP path. */ + byte plain[1]; + byte enc[64]; + byte dec[64]; + word32 encSz = sizeof(enc); + word32 decSz = sizeof(dec); + int i; + TEST_CTX_SETUP_V12(); + + printf("test_encrypt_decrypt_empty_payload...\n"); + + ctx->state = WOLFSPDM_STATE_CONNECTED; + ctx->sessionId = 0x12345678; + for (i = 0; i < WOLFSPDM_AEAD_KEY_SIZE; i++) { + ctx->reqDataKey[i] = (byte)(0x40 + i); + ctx->rspDataKey[i] = (byte)(0x40 + i); + } + for (i = 0; i < WOLFSPDM_AEAD_IV_SIZE; i++) { + ctx->reqDataIv[i] = (byte)(0x80 + i); + ctx->rspDataIv[i] = (byte)(0x80 + i); + } + + ASSERT_SUCCESS(wolfSPDM_EncryptInternal(ctx, plain, 0, enc, &encSz)); + ASSERT_SUCCESS(wolfSPDM_DecryptInternal(ctx, enc, encSz, dec, &decSz)); + ASSERT_EQ(decSz, (word32)0, "Decrypted size must be 0 for empty payload"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_parse_version_edge_cases(void) +{ + /* (a) entryCount = 0 -> MISMATCH + * (b) all entries below WOLFSPDM_MIN_SPDM_VERSION -> MISMATCH */ + byte rsp[64]; + TEST_CTX_SETUP(); + + printf("test_parse_version_edge_cases...\n"); + + /* (a) Zero entries */ + XMEMSET(rsp, 0, sizeof(rsp)); + rsp[0] = SPDM_VERSION_12; + rsp[1] = SPDM_VERSION; + SPDM_Set16LE(&rsp[4], 0); + ASSERT_EQ(wolfSPDM_ParseVersion(ctx, rsp, 6), + WOLFSPDM_E_VERSION_MISMATCH, "entryCount=0 must fail"); + + /* (b) Two entries, both below the 1.2 minimum (1.0 and 1.1) */ + XMEMSET(rsp, 0, sizeof(rsp)); + rsp[0] = SPDM_VERSION_12; + rsp[1] = SPDM_VERSION; + SPDM_Set16LE(&rsp[4], 2); + rsp[7] = 0x10; + rsp[9] = 0x11; + ASSERT_EQ(wolfSPDM_ParseVersion(ctx, rsp, 10), + WOLFSPDM_E_VERSION_MISMATCH, "All-below-min must fail"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_parse_algorithms_zero_numalgs(void) +{ + /* numAlgs=0 must trip the !dheOk || !aeadOk || !ksOk guard. */ + byte rsp[64]; + TEST_CTX_SETUP_V12(); + + printf("test_parse_algorithms_zero_numalgs...\n"); + + XMEMSET(rsp, 0, sizeof(rsp)); + rsp[0] = SPDM_VERSION_12; + rsp[1] = SPDM_ALGORITHMS; + rsp[2] = 0; /* numAlgs = 0 */ + SPDM_Set16LE(&rsp[4], 36); + rsp[6] = 0x01; + rsp[7] = 0x02; + rsp[12] = SPDM_ASYM_ALGO_ECDSA_P384 & 0xFF; + rsp[13] = (SPDM_ASYM_ALGO_ECDSA_P384 >> 8) & 0xFF; + rsp[16] = SPDM_HASH_ALGO_SHA_384; + + ASSERT_EQ(wolfSPDM_ParseAlgorithms(ctx, rsp, 36), + WOLFSPDM_E_ALGO_MISMATCH, "numAlgs=0 must fail Set-B"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + +static int test_parse_capabilities_full(void) +{ + /* CAPABILITIES response per DSP0274 Table 12: + * 0: SPDMVersion + * 1: RequestResponseCode (0x61 = CAPABILITIES) + * 2: Param1 3: Param2 + * 4: CTExponent + * 5-7: Reserved + * 8-11: Flags (rspCaps) + * 12-15: DataTransferSize (SPDM 1.2+) + * 16-19: MaxSPDMmsgSize (SPDM 1.2+) + * + * Parser must populate ctExponent, rspCaps, dataTransferSize, and + * maxSpdmMsgSize so the requester can honor the responder's negotiated + * limits for subsequent fragmented commands (e.g. GET_CERTIFICATE). */ + byte msg[20]; + TEST_CTX_SETUP_V12(); + + printf("test_parse_capabilities_full...\n"); + + XMEMSET(msg, 0, sizeof(msg)); + msg[0] = SPDM_VERSION_12; + msg[1] = SPDM_CAPABILITIES; + msg[4] = 0x0A; /* CTExponent = 10 */ + SPDM_Set32LE(&msg[8], 0x00012345); /* Flags / rspCaps */ + SPDM_Set32LE(&msg[12], 0x00001000); /* DataTransferSize = 4096 */ + SPDM_Set32LE(&msg[16], 0x00010000); /* MaxSPDMmsgSize = 64 KiB */ + + ASSERT_SUCCESS(wolfSPDM_ParseCapabilities(ctx, msg, sizeof(msg))); + + ASSERT_EQ(ctx->rspCaps, (word32)0x00012345, "rspCaps not parsed"); + ASSERT_EQ(ctx->ctExponent, (byte)0x0A, "CTExponent not parsed"); + ASSERT_EQ(ctx->dataTransferSize, (word32)0x00001000, + "DataTransferSize not parsed"); + ASSERT_EQ(ctx->maxSpdmMsgSize, (word32)0x00010000, + "MaxSPDMmsgSize not parsed"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + /* ========================================================================== */ /* Main */ /* ========================================================================== */ @@ -1819,6 +2044,17 @@ int main(void) /* Session state tests */ test_session_state(); + /* CAPABILITIES full-field parsing */ + test_parse_capabilities_full(); + + /* Crypto / wire primitives */ + test_const_compare(); + test_build_iv_byte_positions(); + test_decrypt_session_id_mismatch(); + test_encrypt_decrypt_empty_payload(); + test_parse_version_edge_cases(); + test_parse_algorithms_zero_numalgs(); + printf("\n===========================================\n"); printf("Results: %d passed, %d failed\n", g_testsPassed, g_testsFailed); printf("===========================================\n"); diff --git a/wolfspdm/spdm.h b/wolfspdm/spdm.h index aad5b64..b883602 100644 --- a/wolfspdm/spdm.h +++ b/wolfspdm/spdm.h @@ -203,6 +203,31 @@ WOLFSPDM_API int wolfSPDM_SetIO(WOLFSPDM_CTX* ctx, WOLFSPDM_IO_CB ioCb, void* us */ WOLFSPDM_API int wolfSPDM_SetMaxVersion(WOLFSPDM_CTX* ctx, byte maxVersion); +/** + * Pin the requester session ID used during KEY_EXCHANGE. + * Default behavior is to draw a random non-reserved value during Connect(). + * Use this only for deterministic test setups; the reserved values 0x0000 + * and 0xFFFF are rejected. + * + * @param ctx The wolfSPDM context. + * @param reqSessionId Caller-chosen ReqSessionID (1..0xFFFE). + * @return WOLFSPDM_SUCCESS or negative error code. + */ +WOLFSPDM_API int wolfSPDM_SetRequesterSessionId(WOLFSPDM_CTX* ctx, + word16 reqSessionId); + +/** + * Opt in to operating without a configured trust anchor. + * Without this call (and without wolfSPDM_SetTrustedCAs), wolfSPDM_Connect + * refuses to complete the handshake against an unauthenticated responder + * certificate chain. Intended for emulator / development use only. + * + * @param ctx The wolfSPDM context. + * @param allow Non-zero to permit handshakes without a trust anchor. + * @return WOLFSPDM_SUCCESS or negative error code. + */ +WOLFSPDM_API int wolfSPDM_AllowUntrustedCerts(WOLFSPDM_CTX* ctx, int allow); + /* --- Session Establishment --- */ /** diff --git a/wolfspdm/spdm_types.h b/wolfspdm/spdm_types.h index 191c7bf..dffc2ec 100644 --- a/wolfspdm/spdm_types.h +++ b/wolfspdm/spdm_types.h @@ -173,8 +173,10 @@ extern "C" { #define SPDM_CAP_PUB_KEY_ID_CAP 0x00010000 /* Public key ID */ /* Default requester capabilities for Algorithm Set B session */ -#define WOLFSPDM_DEFAULT_REQ_CAPS (SPDM_CAP_CERT_CAP | SPDM_CAP_CHAL_CAP | \ - SPDM_CAP_ENCRYPT_CAP | SPDM_CAP_MAC_CAP | \ +/* DSP0274 Table 11: CERT_CAP and CHAL_CAP are responder-only bits. + * wolfSPDM is a pure requester and never serves certs or challenges, so + * those bits are intentionally absent from the default. */ +#define WOLFSPDM_DEFAULT_REQ_CAPS (SPDM_CAP_ENCRYPT_CAP | SPDM_CAP_MAC_CAP | \ SPDM_CAP_KEY_EX_CAP | SPDM_CAP_HBEAT_CAP | \ SPDM_CAP_KEY_UPD_CAP) From bd9011c8fe08955446e0b0f59912805efd4908ec Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 13 May 2026 22:22:09 +0100 Subject: [PATCH 5/7] Tighten cppcheck scope and opt the legacy smoke test in ParseCertificate now declares the CertChainAdd return value inside the block that uses it, so cppcheck no longer flags the variable scope. test/test_spdm calls wolfSPDM_AllowUntrustedCerts since the standalone smoke test drives spdm-emu with self-signed test certs and does not configure a trust anchor. --- src/spdm_msg.c | 3 +-- test/test_spdm.c | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/spdm_msg.c b/src/spdm_msg.c index 83e48d7..f6b8e84 100644 --- a/src/spdm_msg.c +++ b/src/spdm_msg.c @@ -617,8 +617,6 @@ int wolfSPDM_ParseDigests(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) int wolfSPDM_ParseCertificate(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz, word16* portionLen, word16* remainderLen) { - int rc; - if (portionLen == NULL || remainderLen == NULL) { return WOLFSPDM_E_INVALID_ARG; } @@ -646,6 +644,7 @@ int wolfSPDM_ParseCertificate(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz, * advance offset by a portionLen that was never actually delivered, and * eventually advance state with a partial chain. */ if (*portionLen > 0) { + int rc; if (bufSz < (word32)(8 + *portionLen)) { return WOLFSPDM_E_BUFFER_SMALL; } diff --git a/test/test_spdm.c b/test/test_spdm.c index 3f2742f..de96732 100644 --- a/test/test_spdm.c +++ b/test/test_spdm.c @@ -192,6 +192,11 @@ int main(int argc, char* argv[]) wolfSPDM_SetDebug(ctx, 1); wolfSPDM_SetIO(ctx, tcp_io_callback, &g_tcpCtx); + /* Smoke test runs against spdm-emu with self-signed test certs; the + * library now refuses handshakes without a trust anchor unless the + * caller explicitly opts out. */ + wolfSPDM_AllowUntrustedCerts(ctx, 1); + printf("\nEstablishing SPDM session...\n\n"); rc = wolfSPDM_Connect(ctx); From 3626c23c274e3e568970b39860d128198c86c6bc Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 14 May 2026 19:42:47 +0100 Subject: [PATCH 6/7] wolfspdm: Address skoll review and harden tests --- src/spdm_internal.h | 1 + src/spdm_msg.c | 38 ++++++++++------ src/spdm_session.c | 7 +++ test/unit_test.c | 103 ++++++++++++++++++++++++++++++++++++++++++++ wolfspdm/spdm.h | 18 +++++--- 5 files changed, 146 insertions(+), 21 deletions(-) diff --git a/src/spdm_internal.h b/src/spdm_internal.h index 36e081a..96c4c1b 100644 --- a/src/spdm_internal.h +++ b/src/spdm_internal.h @@ -214,6 +214,7 @@ struct WOLFSPDM_CTX { byte challengeNonce[32]; /* Saved nonce from CHALLENGE request */ byte challengeReqCtx[8]; /* RequesterContext sent (1.3+) */ byte challengeMeasHashType; /* MeasurementSummaryHashType from req */ + byte challengeSlotId; /* SlotID sent (for echo verification) */ /* Running M1/M2 hash for CHALLENGE_AUTH signature verification. * Per DSP0274, M1/M2 = A || B || C where: diff --git a/src/spdm_msg.c b/src/spdm_msg.c index f6b8e84..de881c8 100644 --- a/src/spdm_msg.c +++ b/src/spdm_msg.c @@ -154,7 +154,11 @@ int wolfSPDM_BuildKeyExchange(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz) buf[offset++] = ctx->spdmVersion; buf[offset++] = SPDM_KEY_EXCHANGE; buf[offset++] = 0x00; /* MeasurementSummaryHashType = None */ - buf[offset++] = 0x00; /* SlotID = 0 (certificate slot 0) */ + /* SlotIDParam: authenticate the slot selected during GET_CERTIFICATE. + * Hard-coding 0 would break responders whose DIGESTS SlotMask omits + * slot 0 (the requester would then KEY_EXCHANGE against a different + * or empty slot than the one whose chain it just fetched). */ + buf[offset++] = (byte)(ctx->currentSlotId & 0x0F); /* ReqSessionID (2 LE) */ buf[offset++] = (byte)(ctx->reqSessionId & 0xFF); @@ -904,8 +908,11 @@ int wolfSPDM_BuildGetMeasurements(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz, XMEMCPY(ctx->measNonce, &buf[offset], 32); offset += 32; - /* SlotIDParam (1 byte) - slot 0 */ - buf[offset++] = 0x00; + /* SlotIDParam: the slot whose certificate authenticates the + * measurement signature. Must match the slot chosen during + * GET_CERTIFICATE (ctx->currentSlotId) so the responder signs + * with the key whose chain we hold. */ + buf[offset++] = (byte)(ctx->currentSlotId & 0x0F); } /* DSP0274 v1.3.0 Table 50 / v1.4.0 Table 49: RequesterContext (8 bytes) @@ -937,12 +944,13 @@ int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz) SPDM_CHECK_PARSE_OR_ERROR_ARGS(ctx, buf, bufSz, 8); SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_MEASUREMENTS, WOLFSPDM_E_MEASUREMENT); - /* DSP0274: Param2[3:0] echoes the SlotID the requester sent. wolfSPDM - * always issues GET_MEASUREMENTS with slot 0, so reject any other - * slot value. */ - if ((buf[3] & 0x0F) != 0) { + /* DSP0274: Param2[3:0] echoes the SlotID the requester sent. The + * requester picks ctx->currentSlotId for signed requests, so reject + * any other slot value. */ + if ((buf[3] & 0x0F) != (ctx->currentSlotId & 0x0F)) { wolfSPDM_DebugPrint(ctx, - "MEASUREMENTS: SlotID echo mismatch (%u)\n", buf[3] & 0x0F); + "MEASUREMENTS: SlotID echo mismatch (got %u expected %u)\n", + buf[3] & 0x0F, ctx->currentSlotId & 0x0F); return WOLFSPDM_E_MEASUREMENT; } @@ -1373,8 +1381,9 @@ int wolfSPDM_BuildChallenge(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz, buf[offset++] = (byte)(slotId & 0x0F); buf[offset++] = measHashType; - /* Save measHashType for ParseChallengeAuth */ + /* Save measHashType + slotId for ParseChallengeAuth echo check */ ctx->challengeMeasHashType = measHashType; + ctx->challengeSlotId = (byte)(slotId & 0x0F); /* Nonce (32 bytes random) */ rc = wolfSPDM_GetRandom(ctx, &buf[offset], 32); @@ -1418,12 +1427,13 @@ int wolfSPDM_ParseChallengeAuth(WOLFSPDM_CTX* ctx, const byte* buf, SPDM_CHECK_RESPONSE(ctx, buf, bufSz, SPDM_CHALLENGE_AUTH, WOLFSPDM_E_CHALLENGE); - /* DSP0274 Sec. 10.8: Param1[3:0] echoes the requested SlotID. wolfSPDM - * currently always issues CHALLENGE for slot 0; reject a responder that - * authenticates a different slot. */ - if ((buf[2] & 0x0F) != 0) { + /* DSP0274 Sec. 10.8: Param1[3:0] echoes the requested SlotID. + * BuildChallenge saved the slot it sent in ctx->challengeSlotId; + * reject a responder that authenticates a different slot. */ + if ((buf[2] & 0x0F) != (ctx->challengeSlotId & 0x0F)) { wolfSPDM_DebugPrint(ctx, - "CHALLENGE_AUTH: SlotID echo mismatch (%u)\n", buf[2] & 0x0F); + "CHALLENGE_AUTH: SlotID echo mismatch (got %u expected %u)\n", + buf[2] & 0x0F, ctx->challengeSlotId & 0x0F); return WOLFSPDM_E_CHALLENGE; } diff --git a/src/spdm_session.c b/src/spdm_session.c index 285ce08..72a69f3 100644 --- a/src/spdm_session.c +++ b/src/spdm_session.c @@ -38,6 +38,13 @@ static int wolfSPDM_ExchangeMsg(WOLFSPDM_CTX* ctx, word32 transcriptSnapshot; int rc; + /* Not every adapter (e.g. BuildGetVersion's) validates ctx, so guard + * here to keep wolfSPDM_GetVersion(NULL) and similar paths from + * dereferencing a NULL context. */ + if (ctx == NULL) { + return WOLFSPDM_E_INVALID_ARG; + } + rc = buildFn(ctx, txBuf, &txSz); if (rc != WOLFSPDM_SUCCESS) return rc; diff --git a/test/unit_test.c b/test/unit_test.c index 89d02f0..8433a68 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -299,6 +299,21 @@ static int test_build_get_version(void) TEST_PASS(); } +static int test_get_version_null_ctx(void) +{ + /* wolfSPDM_GetVersion routes through the shared ExchangeMsg helper, + * which dereferences ctx for transcript snapshotting. The + * BuildGetVersion adapter ignores ctx, so without an explicit guard + * a NULL caller would crash inside the helper. Match the rest of + * the public API and return WOLFSPDM_E_INVALID_ARG instead. */ + printf("test_get_version_null_ctx...\n"); + + ASSERT_EQ(wolfSPDM_GetVersion(NULL), WOLFSPDM_E_INVALID_ARG, + "GetVersion(NULL) must return INVALID_ARG, not crash"); + + TEST_PASS(); +} + static int test_build_get_capabilities(void) { byte buf[32]; @@ -549,6 +564,28 @@ static int test_build_key_exchange_opaque_data(void) TEST_PASS(); } +static int test_build_key_exchange_slot(void) +{ + /* When ConnectStandard selects a non-zero cert slot (DIGESTS SlotMask), + * KEY_EXCHANGE must authenticate that slot. Hard-coding 0 would ask + * the responder to sign with a different (possibly empty) slot's + * key than the chain the requester just fetched. */ + byte buf[256]; + word32 bufSz; + TEST_CTX_SETUP_V12(); + + printf("test_build_key_exchange_slot...\n"); + + ctx->currentSlotId = 2; + bufSz = sizeof(buf); + ASSERT_SUCCESS(wolfSPDM_BuildKeyExchange(ctx, buf, &bufSz)); + /* Header: ver, code, measSummary, slotIDParam */ + ASSERT_EQ(buf[3] & 0x0F, 2, "SlotIDParam should echo currentSlotId"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + static int test_key_exchange_requires_cert(void) { /* wolfSPDM_KeyExchange must refuse to proceed without an extracted @@ -704,6 +741,31 @@ static int test_build_get_measurements(void) TEST_PASS(); } +static int test_build_get_measurements_slot(void) +{ + /* DSP0274: signed GET_MEASUREMENTS carries the SlotID whose + * certificate authenticates the response. When ConnectStandard picks + * a non-zero slot (lowest populated bit in DIGESTS SlotMask), the + * build path must echo that selection rather than hard-coding 0. */ + byte buf[64]; + word32 bufSz; + TEST_CTX_SETUP_V12(); + + printf("test_build_get_measurements_slot...\n"); + + ctx->currentSlotId = 3; + bufSz = sizeof(buf); + ASSERT_SUCCESS(wolfSPDM_BuildGetMeasurements(ctx, buf, &bufSz, + SPDM_MEAS_OPERATION_ALL, 1)); + /* Layout (1.2 signed): hdr(4) + nonce(32) + slot(1) = 37 bytes. + * SlotIDParam follows the 32-byte nonce, so buf[36]. */ + ASSERT_EQ(bufSz, 37, "1.2 signed length wrong"); + ASSERT_EQ(buf[36] & 0x0F, 3, "SlotIDParam should echo currentSlotId"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + static int test_measurement_accessors(void) { byte measIdx, measType; @@ -1070,6 +1132,43 @@ static int test_parse_challenge_auth(void) TEST_PASS(); } +static int test_parse_challenge_auth_slot_echo(void) +{ + /* DSP0274 Sec. 10.8: Param1[3:0] echoes the requested SlotID. Public + * API accepts slots 0..7, so a CHALLENGE for slot 3 must accept a + * matching CHALLENGE_AUTH and reject any other slot value. */ + byte rsp[182]; + word32 sigOffset = 0; + TEST_CTX_SETUP_V12(); + + printf("test_parse_challenge_auth_slot_echo...\n"); + + ctx->challengeMeasHashType = SPDM_MEAS_SUMMARY_HASH_NONE; + ctx->challengeSlotId = 3; /* What BuildChallenge(...,3,...) would set */ + + XMEMSET(rsp, 0, sizeof(rsp)); + rsp[0] = SPDM_VERSION_12; + rsp[1] = SPDM_CHALLENGE_AUTH; + rsp[2] = 0x03; /* Param1 echoes slot 3 */ + XMEMSET(&rsp[4], 0xAA, WOLFSPDM_HASH_SIZE); + XMEMCPY(ctx->certChainHash, &rsp[4], WOLFSPDM_HASH_SIZE); + XMEMSET(&rsp[52], 0xBB, 32); + XMEMSET(&rsp[86], 0xCC, WOLFSPDM_ECC_SIG_SIZE); + + /* Matching echo must be accepted. */ + ASSERT_SUCCESS( + wolfSPDM_ParseChallengeAuth(ctx, rsp, sizeof(rsp), &sigOffset)); + + /* Wrong echo (slot 0 when slot 3 was requested) must be refused. */ + rsp[2] = 0x00; + ASSERT_EQ( + wolfSPDM_ParseChallengeAuth(ctx, rsp, sizeof(rsp), &sigOffset), + WOLFSPDM_E_CHALLENGE, "SlotID echo mismatch must be rejected"); + + TEST_CTX_FREE(); + TEST_PASS(); +} + static int test_parse_challenge_auth_reqctx_echo(void) { /* SPDM 1.3+: CHALLENGE_AUTH carries an 8-byte echo of the @@ -1977,12 +2076,14 @@ int main(void) /* Message builder tests */ test_build_get_version(); + test_get_version_null_ctx(); test_build_get_capabilities(); test_build_negotiate_algorithms(); test_parse_algorithms_set_b_enforcement(); test_build_get_digests(); test_build_get_certificate(); test_build_key_exchange_opaque_data(); + test_build_key_exchange_slot(); test_build_finish_opaque_length_14(); test_parse_finish_rsp_14_opaque_length(); test_key_exchange_requires_cert(); @@ -1997,6 +2098,7 @@ int main(void) /* Measurement tests */ #ifndef NO_WOLFSPDM_MEAS test_build_get_measurements(); + test_build_get_measurements_slot(); test_measurement_accessors(); test_parse_measurements(); #ifndef NO_WOLFSPDM_MEAS_VERIFY @@ -2012,6 +2114,7 @@ int main(void) #ifndef NO_WOLFSPDM_CHALLENGE test_build_challenge(); test_parse_challenge_auth(); + test_parse_challenge_auth_slot_echo(); test_parse_challenge_auth_reqctx_echo(); #endif diff --git a/wolfspdm/spdm.h b/wolfspdm/spdm.h index b883602..0ca5198 100644 --- a/wolfspdm/spdm.h +++ b/wolfspdm/spdm.h @@ -381,13 +381,14 @@ WOLFSPDM_API int wolfSPDM_SecuredExchange(WOLFSPDM_CTX* ctx, * Returns WOLFSPDM_E_MEAS_SIG_FAIL if the signature is invalid. * * When requestSignature=0: - * Retrieves measurements WITHOUT a signature. - * Returns WOLFSPDM_E_MEAS_NOT_VERIFIED. Measurements are informational - * only and should not be used for security-critical decisions. + * Retrieves measurements WITHOUT a signature. Returns WOLFSPDM_SUCCESS on + * retrieval; the call is treated as informational only and the blocks + * must not be used for security-critical decisions. * * If compiled with NO_WOLFSPDM_MEAS_VERIFY, signature verification is - * disabled and returns WOLFSPDM_E_MEAS_NOT_VERIFIED regardless of - * requestSignature (signature bytes are still captured in the context). + * unavailable. In that build a signed request (requestSignature=1) returns + * WOLFSPDM_E_MEAS_NOT_VERIFIED (the signature bytes are still captured in + * the context); unsigned requests still return WOLFSPDM_SUCCESS. * * Contexts are NOT thread-safe; do not call from multiple threads. */ @@ -397,8 +398,11 @@ WOLFSPDM_API int wolfSPDM_SecuredExchange(WOLFSPDM_CTX* ctx, * @param ctx The wolfSPDM context. * @param measOperation SPDM_MEAS_OPERATION_ALL (0xFF) or specific index. * @param requestSignature 1 to request signed measurements, 0 for unsigned. - * @return WOLFSPDM_SUCCESS (verified), WOLFSPDM_E_MEAS_NOT_VERIFIED (unsigned), - * WOLFSPDM_E_MEAS_SIG_FAIL (sig invalid), or negative error code. + * @return WOLFSPDM_SUCCESS on retrieval (unsigned) or successful signature + * verification (signed). WOLFSPDM_E_MEAS_SIG_FAIL when a signed + * response fails verification. WOLFSPDM_E_MEAS_NOT_VERIFIED only + * when a signed request is made in a build without verification + * support. Other negative error codes on protocol/transport errors. */ WOLFSPDM_API int wolfSPDM_GetMeasurements(WOLFSPDM_CTX* ctx, byte measOperation, int requestSignature); From d542ce40424804df4f77100c8be535b249967771 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Thu, 14 May 2026 23:52:04 +0100 Subject: [PATCH 7/7] Pin wolfSSL >= v5.8.0 and add version matrix + nightly CI - configure.ac errors out on LIBWOLFSSL_VERSION_HEX < 0x05008000 so the floor is documented at build time rather than via runtime breakage. - spdm_internal.h ships a wc_ForceZero stub when LIBWOLFSSL_VERSION_HEX < 0x05008004 since the public symbol was added in v5.8.4; older releases only expose a WOLFSSL_LOCAL ForceZero which is not linkable. - wolfssl-versions.yml resolves the latest -stable tag dynamically and builds/unit-tests both static and --enable-dynamic-mem against v5.8.0-stable, that latest -stable, and master. - nightly.yml dispatches a nightly-trigger repository_dispatch event at 02:17 UTC; every existing workflow now listens for it so upstream wolfSSL drift surfaces within ~24h on master. Verified locally: 64/64 unit + 18/18 spdm-emu integration tests on both v5.8.0-stable and the installed v5.9.0. --- .github/workflows/build-test.yml | 2 + .github/workflows/codeql.yml | 2 + .github/workflows/codespell.yml | 2 + .github/workflows/compiler-warnings.yml | 2 + .github/workflows/memory-check.yml | 2 + .github/workflows/multi-compiler.yml | 2 + .github/workflows/nightly.yml | 40 ++++++++ .github/workflows/spdm-emu-test.yml | 2 + .github/workflows/static-analysis.yml | 2 + .github/workflows/wolfssl-versions.yml | 131 ++++++++++++++++++++++++ configure.ac | 16 +++ src/spdm_internal.h | 12 +++ 12 files changed, 215 insertions(+) create mode 100644 .github/workflows/nightly.yml create mode 100644 .github/workflows/wolfssl-versions.yml diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 945deea..2a41b87 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -5,6 +5,8 @@ on: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] + repository_dispatch: + types: [nightly-trigger] jobs: build: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 79c8373..5562a16 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -7,6 +7,8 @@ on: branches: [ '*' ] schedule: - cron: '0 6 * * 1' + repository_dispatch: + types: [nightly-trigger] jobs: analyze: diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 9a00668..05e1348 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -8,6 +8,8 @@ on: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] + repository_dispatch: + types: [nightly-trigger] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/compiler-warnings.yml b/.github/workflows/compiler-warnings.yml index 619ed1c..27ea8e7 100644 --- a/.github/workflows/compiler-warnings.yml +++ b/.github/workflows/compiler-warnings.yml @@ -5,6 +5,8 @@ on: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] + repository_dispatch: + types: [nightly-trigger] jobs: gcc-strict: diff --git a/.github/workflows/memory-check.yml b/.github/workflows/memory-check.yml index 430453c..59cdc14 100644 --- a/.github/workflows/memory-check.yml +++ b/.github/workflows/memory-check.yml @@ -5,6 +5,8 @@ on: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] + repository_dispatch: + types: [nightly-trigger] jobs: valgrind: diff --git a/.github/workflows/multi-compiler.yml b/.github/workflows/multi-compiler.yml index a4b7e8c..f569c8e 100644 --- a/.github/workflows/multi-compiler.yml +++ b/.github/workflows/multi-compiler.yml @@ -5,6 +5,8 @@ on: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] + repository_dispatch: + types: [nightly-trigger] concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000..9db0def --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,40 @@ +name: Nightly CI + +# Daily fan-out to every wolfSPDM workflow that listens for the +# 'nightly-trigger' repository_dispatch event. Catches upstream wolfSSL +# drift (e.g. header renames, API breaks) within ~24h. +# +# Workflows opt in by adding the listener to their `on:` block: +# repository_dispatch: +# types: [nightly-trigger] +# +# repository_dispatch is API-only — there is no UI button to trigger +# these workflows by hand. Maintainers cannot accidentally fire them. +# Workflows that should never be batch-triggered simply don't add the +# listener. + +on: + schedule: + # 02:00 UTC daily (offset from typical top-of-hour congestion). + - cron: '17 2 * * *' + +permissions: + contents: write + +jobs: + fan-out: + name: Dispatch nightly-trigger to all listening workflows + runs-on: ubuntu-latest + if: github.repository == 'aidangarske/wolfSPDM' + + steps: + - name: Send repository_dispatch event + uses: actions/github-script@v7 + with: + script: | + await github.rest.repos.createDispatchEvent({ + owner: context.repo.owner, + repo: context.repo.repo, + event_type: 'nightly-trigger', + }); + core.info('Dispatched nightly-trigger; all listening workflows will fire.'); diff --git a/.github/workflows/spdm-emu-test.yml b/.github/workflows/spdm-emu-test.yml index 079cafb..67e7746 100644 --- a/.github/workflows/spdm-emu-test.yml +++ b/.github/workflows/spdm-emu-test.yml @@ -5,6 +5,8 @@ on: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] + repository_dispatch: + types: [nightly-trigger] jobs: spdm-emu-test: diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 50ab404..f37b61e 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -5,6 +5,8 @@ on: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] + repository_dispatch: + types: [nightly-trigger] jobs: cppcheck: diff --git a/.github/workflows/wolfssl-versions.yml b/.github/workflows/wolfssl-versions.yml new file mode 100644 index 0000000..8224db8 --- /dev/null +++ b/.github/workflows/wolfssl-versions.yml @@ -0,0 +1,131 @@ +name: wolfSSL Version Matrix + +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: + # Resolve the latest -stable wolfSSL tag at run time so we don't have to + # bump this workflow every release. Floor (v5.8.0) and master are fixed: + # v5.8.0 matches the LIBWOLFSSL_VERSION_HEX gate in configure.ac, and + # master surfaces upstream drift on the nightly run. + discover-versions: + name: Resolve wolfSSL version matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + latest-stable: ${{ steps.set-matrix.outputs.latest-stable }} + steps: + - name: Resolve latest -stable wolfSSL tag + id: set-matrix + run: | + set -euo pipefail + # List remote v*-stable tags, version-sort, take the highest. + # Equivalent to `git tag -l 'v*-stable' | sort -V | tail -1` in a + # local clone, but avoids cloning just to read tag names. + LATEST=$(git ls-remote --tags --refs https://github.com/wolfSSL/wolfssl.git 'v*-stable' \ + | awk -F/ '{print $NF}' | sort -V | tail -n 1) + if [ -z "${LATEST:-}" ]; then + echo "::error::Could not resolve latest wolfSSL -stable tag from remote" + exit 1 + fi + echo "Latest stable wolfSSL: $LATEST" + echo "latest-stable=$LATEST" >> "$GITHUB_OUTPUT" + MATRIX=$(jq -nc --arg latest "$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":""} + ] + }') + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" + + build-test: + name: wolfSSL ${{ matrix.wolfssl-version }} + needs: discover-versions + runs-on: ubuntu-latest + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.discover-versions.outputs.matrix) }} + + steps: + - name: Checkout wolfSPDM + uses: actions/checkout@v4 + + - name: Install build deps + run: | + sudo apt-get update + sudo apt-get install -y autoconf automake libtool + + - name: Cache wolfSSL ${{ matrix.wolfssl-version }} + if: matrix.wolfssl-version != 'master' + id: cache-wolfssl + uses: actions/cache@v4 + with: + path: ~/wolfssl-install + key: ${{ matrix.cache-key }} + + - name: Build wolfSSL ${{ matrix.wolfssl-version }} + if: matrix.wolfssl-version == 'master' || steps.cache-wolfssl.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 --branch ${{ matrix.wolfssl-ref }} \ + https://github.com/wolfSSL/wolfssl.git + cd wolfssl + ./autogen.sh + ./configure --enable-wolftpm --enable-ecc --enable-sha384 \ + --enable-aesgcm --enable-hkdf --enable-sp \ + --prefix=$HOME/wolfssl-install + make -j"$(nproc)" + make install + + - name: wolfSSL version info + run: | + grep LIBWOLFSSL_VERSION_STRING $HOME/wolfssl-install/include/wolfssl/version.h + grep LIBWOLFSSL_VERSION_HEX $HOME/wolfssl-install/include/wolfssl/version.h + + - name: Build wolfSPDM + run: | + ./autogen.sh + ./configure --with-wolfssl=$HOME/wolfssl-install + make -j"$(nproc)" + + - name: Run unit tests + run: make check + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/src/.libs:${{ env.HOME }}/wolfssl-install/lib + + - name: Build with --enable-dynamic-mem + run: | + make distclean || true + ./autogen.sh + ./configure --with-wolfssl=$HOME/wolfssl-install --enable-dynamic-mem + make -j"$(nproc)" + + - name: Run unit tests (dynamic-mem) + run: make check + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/src/.libs:${{ env.HOME }}/wolfssl-install/lib + + - name: Upload failure logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: wolfssl-versions-${{ matrix.wolfssl-version }}-logs + path: | + config.log + test/*.log + test-suite.log + retention-days: 5 diff --git a/configure.ac b/configure.ac index 66ab506..621a559 100644 --- a/configure.ac +++ b/configure.ac @@ -41,6 +41,22 @@ AC_CHECK_LIB([wolfssl], [wc_InitRng], [], AC_CHECK_HEADER([wolfssl/wolfcrypt/ecc.h], [], [AC_MSG_ERROR([wolfSSL headers not found. Use --with-wolfssl=PATH])]) +# wolfSSL minimum version: v5.8.0-stable. This is the oldest release the CI +# matrix in .github/workflows/wolfssl-versions.yml exercises end-to-end; older +# releases are not validated and may be missing API/curve fixes wolfSPDM relies +# on (ECDHE P-384, HKDF-SHA384, AES-256-GCM record protection). +AC_MSG_CHECKING([wolfSSL version >= v5.8.0]) +AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + #if !defined(LIBWOLFSSL_VERSION_HEX) || LIBWOLFSSL_VERSION_HEX < 0x05008000 + #error "wolfSSL < v5.8.0" + #endif + int main(void) { return 0; } +]])], +[AC_MSG_RESULT([yes])], +[AC_MSG_RESULT([no]) + AC_MSG_ERROR([wolfSPDM requires wolfSSL >= v5.8.0-stable. Please upgrade wolfSSL.])]) + # Debug mode AC_ARG_ENABLE([debug], [AS_HELP_STRING([--enable-debug], [Enable debug output])], diff --git a/src/spdm_internal.h b/src/spdm_internal.h index 96c4c1b..38a3172 100644 --- a/src/spdm_internal.h +++ b/src/spdm_internal.h @@ -38,6 +38,7 @@ #include /* wolfCrypt includes */ +#include #include #include #include @@ -47,6 +48,17 @@ #include #include +#if defined(LIBWOLFSSL_VERSION_HEX) && LIBWOLFSSL_VERSION_HEX < 0x05008004 +/* wc_ForceZero added in wolfSSL v5.8.4; provide a stub for older releases. */ +static WC_INLINE void wc_ForceZero(void* mem, word32 len) +{ + volatile byte* z = (volatile byte*)mem; + while (len--) { + *z++ = 0; + } +} +#endif + /* Constant-time byte comparison: returns 0 iff a==b for the full length. * Used for MAC/HMAC equality so we don't leak match position via timing. */ static WC_INLINE int wolfSPDM_ConstCompare(const byte* a, const byte* b,