Skip to content

Standalone SPDM 1.2/1.3/1.4 requester#6

Open
aidangarske wants to merge 6 commits into
masterfrom
cleanup-and-skoll-hardening
Open

Standalone SPDM 1.2/1.3/1.4 requester#6
aidangarske wants to merge 6 commits into
masterfrom
cleanup-and-skoll-hardening

Conversation

@aidangarske
Copy link
Copy Markdown
Owner

@aidangarske aidangarske commented May 13, 2026

Summary

Strips Nuvoton TPM mode (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 hardening

  • KEY_EXCHANGE_RSP responder signature verified before key derivation (was a MITM TODO)
  • ResponderVerifyData HMAC mismatch fatal; constant-time compare
  • Sequence-number wrap cap + explicit replay-mismatch check (DSP0277 Sec. 11.3)
  • Strict Algorithm Set B enforcement in ParseAlgorithms (P-384 / SHA-384 / AES-256-GCM / SECP_384_R1 / SPDM KeySchedule)
  • Mutual-auth requested by responder refused before sessionId commit
  • 1.3+ RequesterContext echo verified in CHALLENGE_AUTH
  • Inner MCTP-type byte validated post-decrypt
  • EncryptInternal / DecryptInternal refactored to goto exit with wc_ForceZero on plaintext stack buffers
  • wc_ForceZero of transient secrets in DeriveAppDataKeys / DeriveUpdatedKeys
  • 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 with WOLFSPDM_MIN/MAX_SPDM_VERSION compile-time caps and wolfSPDM_SetMaxVersion runtime clamp
  • 1.3+ RequesterContext on 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 version-byte heuristic)
  • ExchangeMsg snapshots transcriptLen and rolls back on failure

API changes

  • New: wolfSPDM_GetNegotiatedVersion (old wolfSPDM_GetVersion_Negotiated retained as exported ABI-compat alias)
  • New: wolfSPDM_SetMaxVersion, wolfSPDM_GetLastPeerError
  • Removed: wolfSPDM_SetRequesterKeyPair, wolfSPDM_SignHash, wolfSPDM_SetResponderPubKey, wolfSPDM_SetMode (Nuvoton mode is gone; these wrote ctx state nothing read)
  • wolfSPDM_GetSessionId returns id from STATE_KEY_EX onward so I/O callbacks can tag the encrypted FINISH record (non-zero return does NOT imply session established)
  • WOLFSPDM_API visibility shim (maps to WOLFTPM_API when built inside wolfTPM)
  • Bit-field packed flags struct shaves ~28 B from CTX

Tests + CI

  • 53 unit tests (was 27); covers KDF version prefix, transcript overflow, HMAC mismatch, Algorithm Set B enforcement, KEY_EXCHANGE_RSP preconditions and mutual-auth refusal, GET_MEASUREMENTS peer-error path with lastPeerErrorCode capture, ParseMeasurements 1.3+ RequesterContext, ParseFinishRsp 1.4 OpaqueLength, ParseChallengeAuth echo verification, sequence wrap rejection, sequence mismatch, GetSessionId mid-handshake, FINISH 1.4 OpaqueLength, KeyExchange requires cert, GetMeasurements uses SecuredExchange in STATE_MEASURED, BuildKeyExchange OpaqueData, ParseVersion 1.4
  • New examples/spdm_demo CLI: `--emu / --meas / --no-sig / --challenge / --heartbeat / --key-update / --ver`
  • examples/spdm_test.sh 18-test driver (6 scenarios x SPDM 1.2/1.3/1.4); cleanup tightened to only kill emulator instances we started
  • CI drops `--enable-nuvoton` from build-test, static-analysis, codeql, compiler-warnings, multi-compiler matrices
  • `spdm-emu-test.yml` rewritten end-to-end: builds wolfSSL + wolfSPDM + spdm-emu, runs the 18-test matrix across ubuntu-22.04 x64, ubuntu-24.04 x64, ubuntu-24.04-arm aarch64

Test plan

  • `make` clean (no `-Wall -Wextra` warnings)
  • `make check` -> 53/53 unit tests pass (static-mem and `--enable-dynamic-mem`)
  • `SPDM_EMU_PATH=... ./examples/spdm_test.sh` -> 18/18 integration tests pass
  • `skoll review --security --full` -> 0 Critical / 0 High / 0 Medium on the resulting tree
  • CI: `build-test`, `spdm-emu-test`, `codeql`, `compiler-warnings`, `static-analysis`, `multi-compiler` green

@aidangarske aidangarske changed the title Standalone SPDM 1.2/1.3/1.4 requester + skoll security hardening Standalone SPDM 1.2/1.3/1.4 requester May 13, 2026
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)
@aidangarske aidangarske force-pushed the cleanup-and-skoll-hardening branch from 53cd346 to f216e73 Compare May 13, 2026 16:56
Comment thread examples/spdm_demo.c Fixed
- 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.

This comment was marked as resolved.

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.
@aidangarske aidangarske added the enhancement New feature or request label May 13, 2026
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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants