Add fwtpm SPDM support#510
Open
aidangarske wants to merge 19 commits into
Open
Conversation
TCG mode auto-on with --enable-spdm + (fwtpm|nuvoton|nations). PSK mode auto-on with --enable-spdm + --enable-nations. fwtpm + spdm requires at least one of TCG or PSK. --enable-tcg / --enable-psk alone (without --enable-spdm) is rejected. Adds BUILD_SPDM_TCG / BUILD_SPDM_PSK / BUILD_FWTPM_SPDM AM_CONDITIONALs.
spdm_tcg.c now builds under BUILD_SPDM_TCG (was: always under BUILD_SPDM). spdm_psk.c now builds under BUILD_SPDM_PSK (was: BUILD_NATIONS — PSK isn't Nations-specific). spdm_responder.c added under BUILD_FWTPM_SPDM.
wolfSPDM_Resp* API (Init/Free/SetMode/SetPSK/SetIdentityKey/SetTpmCallback/ HandleMessage/Reset) wrapping the existing WOLFSPDM_CTX. HandleMessage enforces TCG-tag framing as the bus-snooping defence and returns WOLFSPDM_E_NOT_IMPL for per-message dispatch (filled in next commit). Adds WOLFSPDM_E_NOT_AVAILABLE / E_FRAMING / E_NOT_IMPL with matching strings. Fixes pre-existing unused-arg warning in wolfSPDM_SetMode that fires when SPDM is built without a vendor flag.
fwtpm_main parses --spdm-tcg, --spdm-psk, --no-spdm, --spdm-psk-hex, allocates a WOLFSPDM_RESP_CTX, configures the requested mode, installs the FWTPM_ProcessCommand dispatcher, and frees on shutdown. fwtpm_io's HandleCommandConnection routes inbound bytes through the responder when spdmMode != OFF and drops the connection with TPM_RC_BAD_TAG if the first 2 bytes are not a TCG SPDM tag (0x8101 or 0x8201) — this is the bus-snooping defence the responder exists to provide. Per-message dispatch returns NOT_IMPL until the handlers land.
Clear-message dispatcher in wolfSPDM_RespHandleMessage strips the TCG header, dispatches by SPDM request code, runs the per-message handler, adds both sides to the transcript, and wraps the response in a TCG-clear header. Pins the negotiated version to SPDM 1.3, advertises Algorithm Set B (P-384 / SHA-384 / AES-256-GCM), and sets PSK_CAP iff PSK mode is enabled. KEY_EXCHANGE / PSK_EXCHANGE / FINISH / secured-envelope / vendor-defined commands still TODO — unsupported codes return SPDM_ERROR_UNSUPPORTED_REQUEST.
PSK_EXCHANGE_RSP picks rspSessionId, derives handshake keys from PSK via the existing wolfSPDM_DeriveHandshakeKeysPsk, signs verifyData with rspFinishedKey. PSK_FINISH_RSP verifies the requester's HMAC over TH2, derives app keys via wolfSPDM_DeriveAppDataKeys, transitions to CONNECTED. Secured envelope decrypt/encrypt reuses wolfSPDM_EncryptInternal / wolfSPDM_DecryptInternal with a swap-call-swap of the req/rsp key directions (the existing helpers hard-code 'use reqDataKey for encrypt', which is requester-correct but inverted for the responder). VENDOR_DEFINED dispatch routes 'TPM2_CMD' to the registered TPM callback (FWTPM_ProcessCommand in fwtpm_server) and replies with the TPM response, plus GET_PUBK / GIVE_PUB / GET_STS_ stubs for the TCG identity-key dance. END_SESSION acks and resets the session. Sets ctx->mode = NATIONS_PSK or NUVOTON in RespSetMode so the shared encrypt/decrypt path uses the 14-byte TCG-binding AAD format instead of the MCTP 8-byte default.
VERSION response: entry count belongs at offset 4 (LE) per how the requester parses it; the prior layout put it at offset 7 and the requester read 0 entries → version mismatch. PSK_FINISH_RSP: app-key derivation moved out of the handler to a post-encrypt step. The handler used to derive app keys before the response was encrypted, so the responder wrote ciphertext with app keys but the requester decrypted with handshake keys. Unit tests: test_responder_init_free, _setmode_rejects_both_off, _no_plaintext_bypass (regression for the bus-snooping defence), _psk_roundtrip (full handshake + tunneled VENDOR_DEFINED TPM2_CMD via wolfSPDM_TCG_VendorCmdSecured, asserts the TPM stub fires exactly once). Also fixes a latent issue in test_tcg_underflow: wolfSPDM_SetMode is vendor-gated and silently fails in vendor-neutral builds, leaving ctx->mode=0 and skipping the TCG framing path the test was meant to exercise. Falls back to setting ctx->mode directly when no vendor flag is enabled.
Drops the self-hosted Raspberry Pi runners (wolftpm-nuvoton, wolftpm-nations) and the GPIO reset / spi_cs / health-check machinery. The new workflow exercises the same SPDM protocol coverage by running the responder unit tests, which drive the requester and the responder back-to-back in-process across all the configurations the prior hardware path tested (PSK, TCG, TCG+PSK, --enable-nuvoton, --enable-nations). The plaintext-bypass regression (raw TPM2 frames rejected when SPDM mode is on) runs in every matrix entry.
Before this change, fwtpm_server's SPDM responder only accepted raw TCG-framed bytes on the socket — useful for a direct-TCP test tool, but incompatible with wolfTPM's normal SWTPM client which wraps every payload in MSSIM (TPM_SEND_COMMAND + locality + size + bytes). The MSSIM bytes arriving first looked like 0x0000 to the responder and got rejected as a plaintext-bypass attempt. After this change DispatchAndRespond inspects the *payload* (post-MSSIM unwrap): if it starts with a TCG tag and SPDM mode is on, the bytes go through the SPDM responder and the response is sent back wrapped in the same MSSIM (or swtpm raw) framing the request used. The bus-snooping defence still fires — a raw TPM2 frame inside MSSIM in SPDM mode is rejected with TPM_RC_BAD_TAG. The early routing branch on raw TCG bytes (HandleSpdmConnection) is preserved for direct-TCP test tools that don't go through wolfTPM's SWTPM stack.
Adds wolfTPM2_SPDM_SwtpmIoCb so SPDM traffic rides inside MSSIM TPM_SEND_COMMAND envelopes over the existing swtpm TCP socket. Paired with the fwtpm_server MSSIM-unwrap-then-dispatch change, this lets spdm_ctrl talk SPDM to fwtpm_server using the same transport stack as the normal swtpm client. wolfTPM2_SPDM_SetTisIO auto-picks TIS (hardware) vs SWTPM at compile time so callers don't need to know which build path is active.
…SABLED spdm_fwtpm_test.sh spawns fwtpm_server in PSK mode, points spdm_ctrl at the swtpm port, and runs the same PSK connect / status sequence that spdm_test.sh runs against real silicon — minus the vendor NV provisioning steps (PSK_SET / PSK_CLEAR / IDENTITY_KEY_*) which are Nations-only NV writes, not generic SPDM. When a plaintext TPM2 frame arrives in SPDM mode the responder now returns TPM_RC_DISABLED instead of TPM_RC_BAD_TAG. This matches real Nuvoton / Nations silicon's SPDM-only behavior so wolfTPM2_Init handles it gracefully (the existing tpm2_wrap.c TPM_RC_DISABLED branch sets spdmOnlyDetected=1 and continues). CI: the new script runs on the spdm-nations and spdm-tcg-psk matrix entries.
Three issues found running spdm_ctrl --psk against fwtpm_server: 1. Plaintext TPM commands were rejected unconditionally in SPDM mode. Real silicon accepts plaintext until SPDMONLY is locked at runtime, and wolfTPM2_Init's auto-connect needs to traverse the TCG flow which isn't implemented yet in the responder. Loosened the inspection to the payload-tag level: TCG-tagged payloads go to the SPDM responder, TPM2-tagged payloads go to FWTPM_ProcessCommand. SPDMONLY locking stays as a future runtime-controlled state. 2. Responder state (transcript, session keys) carried over between sessions. Added a wolfSPDM_TranscriptReset + wolfSPDM_RespReset at GET_VERSION dispatch, mirroring the requester's wolfSPDM_TranscriptReset at the start of wolfSPDM_Connect. 3. ctx->psk gets wiped by wolfSPDM_DeriveHandshakeKeysPsk after each derivation, so the second PSK_EXCHANGE failed with BAD_STATE. Added a persistent pskStore to WOLFSPDM_RESP_CTX that survives between sessions; PSK_EXCHANGE reloads ctx->psk from it. Also removes the orphan HandleSpdmConnection (early TCG-tag routing that the new MSSIM-unwrap dispatcher replaces) and fixes spdm_fwtpm_test.sh to use space-separated CLI args. End-to-end verified: PSK connect, repeat connect, status query — all pass against fwtpm_server in --spdm-psk mode via spdm_ctrl --psk through the swtpm transport.
Responder implements the full TCG cert handshake the Nuvoton/Nations
silicon supports: generates a P-384 identity key at fwtpm_server startup,
exposes the public half via GET_PUBK, signs TH1 with the private half in
KEY_EXCHANGE_RSP, completes FINISH with the requester's HMAC verified
against TH2. Mutual auth (MutAuth=1) optional via GIVE_PUB.
SPDMONLY ("lock") is now a real runtime state: the secured-message
SPDMONLY vendor command toggles wolfSPDM_RespIsLocked, and fwtpm_io's
DispatchAndRespond rejects plaintext TPM frames with TPM_RC_DISABLED
once locked. Matches real-silicon "SPDM-only mode" semantics so
wolfTPM2_Init's auto-connect path engages.
Two fixes found along the way (both also benefit real-silicon paths):
- wolfSPDM_GenerateEphemeralKey now calls wc_ecc_set_rng so
wc_ecc_shared_secret doesn't fail with MISSING_RNG_E in builds that
enable ECC hardening.
- Responder dispatcher skips VENDOR_DEFINED transcript adds (the
requester's wolfSPDM_TCG_VendorCmdClear doesn't add them either) and
contributes Ct = SHA-384(rspPubKey) instead, matching ConnectTCG.
spdm_test.sh: replaces standalone spdm_fwtpm_test.sh — fwtpm-tcg and
fwtpm-psk modes integrated into the existing vendor mode flow. The
script spawns fwtpm_server in the right mode, points spdm_ctrl at the
swtpm port, and runs the same commands the vendor modes run against
silicon.
PSK_SET / PSK_CLR vendor commands now do real work in the responder: PSK_SET parses the 112-byte payload (PSK(64) + SHA-384(ClearAuth)(48)), stores both, and sets pskProvisioned. PSK_CLR verifies the 32-byte ClearAuth by hashing it and comparing to the stored digest; on match, wipes the PSK store and clears the flag. Mismatch returns BAD_HMAC. GET_STS_ now reports the actual SPDM-only-lock and PSK-provisioned state bits, matching the TCG GET_STATUS_RSP format (SpecMajorVer=0, SpecMinorVer=4, PSKSet, SPDMOnly). fwtpm_io: TPM_CC_GetCapability is exempted from the SPDMONLY lock — real Nuvoton/Nations silicon lets read-only capability queries through even when locked (the wolfTPM2 client probes vendor IDs during init before establishing SPDM). spdm_test.sh: fwtpm-tcg runs the full 6-step Nuvoton sequence; fwtpm-psk runs the 10-step Nations-PSK sequence (the 2 identity-key TPM2 vendor NV-writes are Nations-only and skipped). All implemented tests pass against fwtpm with the same commands the script runs against silicon.
1. RespHandleVendorDefined had a 256-byte payload buffer (WOLFSPDM_VENDOR_BUF_SZ), too small for tunneled TPM2_CMD commands — CreatePrimary runs ~370 bytes. wolfSPDM_ParseVendorDefined returned BUFFER_SMALL, the responder bailed, fwtpm fell back to plaintext TPM_RC_FAILURE. Buffer now sized to WOLFSPDM_MAX_MSG_SIZE. 2. fwtpmSpdmTpmDispatch was treating any non-zero TPM_RC from FWTPM_ProcessCommand as an I/O failure (responder returned E_IO_FAIL). But a TPM error code is a valid TPM response — the requester must see it. The dispatcher now synthesizes a 10-byte TPM_ST_NO_SESSIONS header with the actual error code when FWTPM_ProcessCommand doesn't write a proper response itself, and always returns 0 at the I/O layer so the SPDM stack encrypts and sends the error back to the client.
- TCG transport constants + wolfTPM2_SPDM_SetTisIO out of WOLFTPM_SPDM_TCG (used by PSK builds too) - spdm_tcg.c GET_CAPABILITIES/NEGOTIATE_ALGORITHMS no longer ifdef'd on WOLFSPDM_NATIONS — runtime mode!=NUVOTON check suffices - New wolfTPM2_SpdmConnectPsk under WOLFTPM_SPDM_PSK (spec-pure DSP0274 handshake). Old wolfTPM2_SpdmConnectNationsPsk kept as alias macro. - Responder GET_STS/SPDMONLY gated on NUVOTON||NATIONS (shared format); PSK_SET_/PSK_CLR_ gated on NATIONS only (their NSING format). - spdm_ctrl CLI rewritten with per-vendor blocks + match flag — drops the nested ifdef chain. - configure.ac: error on --enable-nuvoton --disable-tcg, --enable-nations --disable-tcg, --enable-nations --disable-psk. - CI: spdm-test.yml runs 8 build-only configs + 2 e2e (fwtpm-tcg/psk). hw-spdm-test.yml restored from master alongside it — real silicon coverage stays.
empty-brace-scan flagged the SPDM-only scope block as a coding-rule violation. Move firstTag/isSpdmFrame/outSz/cc declarations to the top of the function under #ifdef WOLFTPM_SPDM_RESPONDER and use a 'dispatched' flag instead of else-fallthrough to the default branch.
aidangarske
added a commit
to aidangarske/wolfTPM
that referenced
this pull request
May 23, 2026
- fwtpm_main: identity-key gen zero+left-pads ECC export buffers (wc_ecc may trim leading zeros), use wc_ForceZero on tmp scratch. - spdm_responder: move static 4KB working buffers (plain/respPlain, payload/respPayload) into WOLFSPDM_RESP_CTX so concurrent contexts don't share working memory. - spdm_responder: hoist 'derivedAppKeys' out of mid-block declaration (C89 violation on stricter toolchains). - fwtpm_io: WOLFSPDM_E_FRAMING return now drops the TCP connection per the responder API contract, instead of swallowing it as TPM_RC_FAILURE. - fwtpm_io: trim SPDMONLY-lock comment to match actual allowlist (GetCapability only). - configure.ac: reject --enable-psk --disable-tcg (PSK uses TCG framing). - spdm-test.yml: drop spdm-psk-only build entry — now intentionally rejected by configure.
- fwtpm_main: pad ECC export buffers; wc_ForceZero scratch - spdm_responder: per-ctx working buffers, not file static - spdm_responder: hoist derivedAppKeys decl - fwtpm_io: drop conn on E_FRAMING per API contract - fwtpm_io: trim SPDMONLY comment to match code - configure: reject --enable-psk --disable-tcg - ci: drop now-invalid spdm-psk-only matrix entry
- HIGH-1: identity-key gen fails closed on ECC export size overflow - M-2: wc_ForceZero pskBuf / idPriv after handoff to responder - M-3: soften bus-snooping banner to match actual lock-state behavior - M-4: WOLFSPDM_MODE_NATIONS_PSK now gated on WOLFTPM_SPDM_PSK - M-5: bump WOLFSPDM_RESP_CTX_STATIC_SIZE + compile-time assert - M-6: TPM callback cap reserves VENDOR_DEFINED_RSP wrapper overhead - M-7: --vendor=nuvoton|nations runtime switch for dual-vendor builds - L-8: PSK_SET uses sizeof(pskStore) not literal 64 - L-9: track keyInit so wc_ecc_free skips uninit ecc_key - L-10: SwtpmIoCb lower-bounds rspSz against TPM2_HEADER_SIZE - L-11/I-14: drop unused locals; strengthen plaintext-bypass test to exercise the tag check path (>= 16 byte frame) - L-12: drop redundant 0xFF mask in fwtpmHexDecode - I-15: wolfSPDM_BuildSignedHash now WOLFTPM_LOCAL (was WOLFTPM_API) - docs: src/spdm/README.md and src/fwtpm/README.md updated with new flags, responder usage, vendor switch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SPDM 1.8.4 responder in
fwtpm_server(TCG cert + DSP0274 PSK) so theSPDM stack can be tested end-to-end without real silicon. New CI
spdm-test.ymlto replaces the self-hosted-Pigate for simulator coverage; existing
hw-spdm-test.ymlstays for realhardware.
Configure:
--enable-fwtpm --enable-spdm --enable-tcg --enable-psk.Vendor flags optional (
--enable-nuvoton,--enable-nationsforvendor wire-format adapters). Spec code gated on spec flags, vendor
adapters on vendor flags.
Test plan