Skip to content

Make wolfTPM's SPDM core the wolfSPDM stack, with the standard requester and PQC on top - #34

Open
aidangarske wants to merge 27 commits into
mainfrom
wolftpm-core
Open

aidangarske wants to merge 27 commits into
mainfrom
wolftpm-core

Conversation

@aidangarske

Copy link
Copy Markdown
Member

Summary

wolfSPDM and wolfTPM's src/spdm had diverged into two SPDM stacks. This branch makes wolfTPM's core, the one that is maintained, reviewed and validated on silicon, into wolfSPDM. The standard DMTF requester features from main are then ported onto it behind compile-time switches.

What you get:

  • A standalone requester for SPDM 1.2, 1.3 and 1.4 with:
    • certificates with real chain validation
    • measurements, challenge, heartbeat and key update
    • chunking, including CHUNK_SEND
    • application data
    • ML-DSA 44, 65 and 87, and ML-KEM 512, 768 and 1024
  • The TPM side, built only when enabled: the TCG binding, Nuvoton and Nations, PSK and the responder.
  • wolfTPM compatibility. WOLFSPDM_PROFILE_TPM builds exactly what wolfTPM needs. The context is about 9.5 KB and no standard-requester symbols are exported.

A later wolfTPM PR replaces its src/spdm with lib/wolfSPDM as a submodule.

Closes #10.

Layout

  • cdd1221 imports wolfTPM's core as of wolfTPM 0f639565. It is one mechanical commit, with renames only.

  • Each commit after that ports one main feature, or fixes something found in review.

  • Standalone builds turn every feature on. You can turn features off with:

    Kind Switches
    Feature WOLFSPDM_NO_CERT, NO_MEAS, NO_CHALLENGE, NO_HEARTBEAT, NO_KEY_UPDATE, NO_CHUNK, NO_APP_DATA, NO_MLDSA, NO_MLKEM, LEAN
    configure --disable-mldsa, --disable-mlkem, --disable-app-data
  • ML-DSA and ML-KEM follow wolfSSL's WOLFSSL_HAVE_MLDSA and WOLFSSL_HAVE_MLKEM.

PQC

  • Negotiation. At SPDM 1.4, NEGOTIATE_ALGORITHMS advertises PqcAsymAlgo and a KEM AlgStruct (type 7). The responder must pick exactly one of DHE and KEM, since SPDM has no hybrid key exchange.
  • ML-DSA signatures. The signature is pure ML-DSA over the combined SPDM prefix followed by the message hash, with the SPDM context string as the ML-DSA context.
  • ML-DSA certificate chains. Chains are verified link by link. Each link is either ECDSA-SHA384 or pure ML-DSA with an empty context.
  • ML-KEM key exchange. The ML-KEM encapsulation key replaces the ECDHE point in KEY_EXCHANGE. The ciphertext is decapsulated from KEY_EXCHANGE_RSP.
  • Large messages. A 6297-byte ML-DSA-87 KEY_EXCHANGE_RSP is reassembled with CHUNK_GET.
  • Oversized requests. A clear request larger than the responder's DataTransferSize, sent without chunking, is refused.
  • New API.
    • wolfSPDM_SetKeyExchangePref
    • wolfSPDM_SetRequesterSessionId
    • wolfSPDM_GetVersion_Negotiated
    • wolfSPDM_ValidateCertChain

API and ABI changes vs main

  • Error codes are renumbered from -16 on to follow wolfTPM.
    • wolfTPM already shipped these numbers in v4.2.0. wolfSPDM has never had a release.
    • Names are unchanged except for two, which are kept as aliases:
      • WOLFSPDM_E_MEAS_NOT_VERIFIED → E_MEASUREMENT
      • WOLFSPDM_E_MEAS_SIG_FAIL → E_BAD_SIGNATURE
    • WOLFSPDM_E_MEAS_NOT_VERIFIED still compiles but is never returned (-1000): it used to be a soft success, and aliasing it to a real error would let old rc != E_MEAS_NOT_VERIFIED checks accept malformed responses. WOLFSPDM_E_MEAS_SIG_FAIL stays as an alias of E_BAD_SIGNATURE.
    • Callers that compare against the names need no change. Callers that stored the raw numbers do.
    • libwolfspdm now sets -version-info 1:0:0, so the shared library soname marks the ABI change.
  • I/O callback contract.
    • rxSz is the buffer size on input and the received size on output, and must never exceed the buffer size.
    • SendData calls the callback send-only: rxBuf NULL and *rxSz 0.
    • ReceiveData calls it receive-only: txBuf NULL and txSz 0.
  • Application data. Each SendData / ReceiveData message starts with its own MCTP message type (for example 0x01 for PLDM, never 0x05). An SPDM ERROR received in place of data returns WOLFSPDM_E_PEER_ERROR.
  • GetMeasurementBlock. It accepts NULL output buffers and reports the size it needs with WOLFSPDM_E_BUFFER_SMALL.
  • Stricter session gates. wolfSPDM_KeyExchange refuses to send without a trusted responder key (ValidateCertChain or SetResponderPubKey first). wolfSPDM_Finish runs only in the KEY_EX state. wolfSPDM_SecuredExchange accepts handshake-state requests only for the TCG binding (GIVE_PUB).
  • SPDM 1.4 FINISH_RSP OpaqueData is accepted up to 256 bytes.
  • Configuration for consumers. Public headers load the generated wolfspdm/options.h unless the library itself is being built (BUILDING_WOLFSPDM), so an application with its own config.h still sees the right feature switches.
  • Debug output exists only in WOLFSPDM_DEBUG builds; wolfSPDM_SetDebug does nothing otherwise.
  • Legacy switches. NO_WOLFSPDM_MEAS and NO_WOLFSPDM_CHALLENGE still work; they map to WOLFSPDM_NO_MEAS and WOLFSPDM_NO_CHALLENGE.
  • Context size.
    • WOLFSPDM_CTX_STATIC_SIZE depends on the build:

      Build WOLFSPDM_CTX_STATIC_SIZE
      classical 32 KB
      with ML-KEM 40 KB
      with ML-DSA 72 KB
    • Measured sizeof on arm64 is about 19 KB, 24 KB and 59 KB respectively.

Security fixes found in review

These were found in security review of the branch. Several are also present in wolfTPM master today, and wolfTPM picks them up with the submodule switch.

Requester

Area Fix
Connect and Disconnect Wipe all session state, secrets, sequence numbers and the ephemeral key.
Secured exchange Refused before KEY_EXCHANGE completes.
Certificate chain The leaf key is installed only after the chain is trusted; a key from an earlier chain is dropped.
UpdateAllKeys The old keys are restored if the ACK authenticates under neither key set.
FINISH Refuses mutual auth when no requester key is set.
PSK Replacing a PSK wipes the whole buffer.
HKDF labels Bounds-checked field by field and wiped.
CAPABILITIES Rejected values are never committed; a bad reply cannot leave a tiny DataTransferSize behind. Reconnects drop the old limits and measurements.
PSK_EXCHANGE_RSP The session ID is set only after ResponderVerifyData verifies.
Constant-time compares One shared helper instead of six copies.
TCG transport The received size is checked against the buffer, and the scratch buffers are wiped.
Vendor commands A VdCode shorter than 8 characters is rejected.

Responder

Area Fix
KEY_EXCHANGE and PSK_EXCHANGE Each is accepted only in its own mode, and never over a live session.
Secured vendor commands before FINISH Only GIVE_PUB is allowed.
PSK_SET_ Cannot replace a configured PSK.
SPDMONLY The payload must be exactly one byte, LOCK or UNLOCK.
TPM callback The response length is checked.
Reset Frees the ephemeral key and wipes the application secrets and the transcript.
Plaintext scratch Decrypted TPM commands, responses and PSK payloads are wiped on every path, including errors.
VERSION The count moves to offset 5.

Known limitation, documented in spdm_responder.h: the responder does not authenticate the requester. GIVE_PUB is recorded but not verified.

Review findings not taken

Finding Why not
DIGESTS slot mask read from Param2 DSP0274 1.2 and 1.3 define Param2 as the provisioned slot mask.
Downstream symbol check expects no standard symbols wolfTPM builds always force WOLFSPDM_PROFILE_TPM.
TCG frames accept trailing bytes The frames are length-delimited and the AEAD covers the declared record. This behavior is unchanged from the silicon-validated wolfTPM core.
Error code renumbering Follows wolfTPM v4.2.0; wolfSPDM never shipped. Aliases were added.
test_spdm.c removed Superseded by spdm_demo and the spdm-emu CI.
Receive sequence advances on an authenticated but malformed record Correct behavior: the peer consumed that sequence number.
UpdateAllKeys rejects an ACK sealed under the old keys DSP0274 requires the ACK under the new keys. Key update passes against libspdm.
Global cppcheck constParameterCallback suppression Callback signatures are fixed by the public typedefs.
VdCode check should use strlen (and, on the next pass, should not) The check scans exactly the 8 fixed bytes: a short string is rejected without reading past a caller's non-terminated 8-byte array. The codes are the library's own constants.
SendData calls the I/O callback with a NULL receive buffer Deliberate and documented on WOLFSPDM_IO_CB; listed under API changes above.
Removed public macros in spdm_types.h (unused SPDM_CAP_*, measurement value types, socket constants) They were internal to the old standalone code; nothing in the new API takes them.
No in-process responder handshake test Covered end to end by the wolfTPM downstream fwTPM TCG and PSK legs.

Testing

CI. All workflows are green:

  • build and test
  • multiple compilers
  • compiler warnings
  • static analysis (cppcheck, scan-build)
  • CodeQL
  • valgrind
  • empty-brace scan
  • codespell
  • wolfSSL version matrix
  • spdm-emu classical and PQC
  • wolfTPM downstream

Local. A strict build matrix of 27 configurations passes, covering:

  • the full, lean, TCG-only and pure-TCG builds
  • no-cert, no-PQC, ML-DSA-only and ML-KEM-only
  • DataTransferSize 42 and 64
  • static and dynamic memory

Lab.

Check Result
wolfTPM master 0f63956 with this wolfSPDM (14 configs) all pass, 0 standard symbols exported
fwTPM TCG e2e 15/15
fwTPM PSK e2e 27/27
cppcheck 2.13 clean
GCC 14 strict (10 configs) pass
valgrind (4 builds, including PQC) 0 errors
PQC interop against spdm-emu with OpenSSL 3.5 (ML-DSA 44/65/87, ML-KEM 512/768/1024, full PQ with heartbeat, key update and app data) 31/31
classical spdm-emu (stock and OpenSSL emulators) 21/21 on each

Not tested here. The Nuvoton NPCT75x and Nations NS350 silicon bench runs before the wolfTPM switch-over PR, not before this PR.

Follow-ups tracked as issues

Copilot AI balanced review requested due to automatic review settings September 29, 2026 18:15

This comment was marked as low quality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Implement SPDM 1.4 Post-Quantum Cryptography (PQC) and SPDM 1.2 Message Chunking

2 participants