Skip to content

Add fwtpm SPDM support#510

Open
aidangarske wants to merge 19 commits into
wolfSSL:masterfrom
aidangarske:spdm-ftpm
Open

Add fwtpm SPDM support#510
aidangarske wants to merge 19 commits into
wolfSSL:masterfrom
aidangarske:spdm-ftpm

Conversation

@aidangarske
Copy link
Copy Markdown
Member

@aidangarske aidangarske commented May 23, 2026

SPDM 1.8.4 responder in fwtpm_server (TCG cert + DSP0274 PSK) so the
SPDM stack can be tested end-to-end without real silicon. New CI
spdm-test.yml to replaces the self-hosted-Pi
gate for simulator coverage; existing hw-spdm-test.yml stays for real
hardware.

Configure: --enable-fwtpm --enable-spdm --enable-tcg --enable-psk.
Vendor flags optional (--enable-nuvoton, --enable-nations for
vendor wire-format adapters). Spec code gated on spec flags, vendor
adapters on vendor flags.

Test plan

  • spdm-test.yml: 8 build-only + 2 e2e (fwtpm-tcg, fwtpm-psk)
  • hw-spdm-test.yml still green on real Nuvoton + Nations
  • fwtpm-test.yml unchanged

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.
Copilot AI review requested due to automatic review settings May 23, 2026 04:50
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.

This comment was marked as resolved.

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
@aidangarske aidangarske marked this pull request as ready for review May 26, 2026 18:04
@aidangarske aidangarske requested a review from dgarske May 26, 2026 18:26
@dgarske dgarske self-assigned this May 27, 2026
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.

3 participants