Skip to content

Latest commit

 

History

History
607 lines (466 loc) · 23.7 KB

README.md

File metadata and controls

607 lines (466 loc) · 23.7 KB

wolfTPM SPDM

wolfTPM includes built-in SPDM support for Nuvoton NPCT75x and Nations NS350 TPMs using wolfSSL/wolfCrypt. Both vendors support identity key mode (ECDHE P-384) for session establishment. The Nations NS350 additionally supports PSK (pre-shared key) mode. Once a session is established, all TPM commands and responses are encrypted with AES-256-GCM over the existing SPI/I2C bus. Identity key mode requires the responder's P-384 public key from a trusted provisioning source.

The SPDM code lives in the wolfSPDM library, included as the lib/wolfSPDM submodule and compiled into libwolftpm in its TPM profile. Because SPDM is a submodule, any wolfTPM checkout that will use SPDM must be cloned with --recursive (see Building step 1). Without the submodule, ./configure --enable-spdm stops with: --enable-spdm needs the wolfSPDM submodule: run git submodule update --init lib/wolfSPDM.

Building inside wolfTPM selects wolfSPDM's TPM profile automatically. A TPM speaks only the TCG SPDM Binding, so this profile compiles out the DMTF standard requester, measurements, challenge, chunking, heartbeat, key update, and the MCTP application-data API. You do not pass --disable-mctp here; --enable-spdm is enough. See The wolfTPM SPDM profile. The same wolfSPDM tree also builds standalone as a DMTF SPDM 1.2-1.4 requester with PQC and full MCTP for spdm-emu testing.

Quick Start

Get the source

# SPDM lives in the lib/wolfSPDM submodule, so clone wolfTPM recursively
git clone --recursive https://github.com/wolfSSL/wolfTPM.git
git clone https://github.com/wolfSSL/wolfssl.git   # sibling checkout
cd wolfTPM

Already cloned without --recursive? Run git submodule update --init lib/wolfSPDM once inside the checkout.

Nuvoton NPCT75x

# Build wolfSSL (in the sibling checkout, then return here)
cd ../wolfssl && ./autogen.sh && \
./configure --enable-wolftpm --enable-ecc --enable-sha384 --enable-aesgcm --enable-hkdf --enable-sp && \
make && sudo make install && sudo ldconfig && cd -

# Build wolfTPM (submodule already present from the recursive clone)
./autogen.sh && ./configure --enable-spdm --enable-nuvoton && make

# Enable SPDM (one-time), reset, connect
./examples/spdm/spdm_ctrl --enable
gpioset gpiochip0 4=0 && sleep 0.1 && gpioset gpiochip0 4=1 && sleep 2
./examples/spdm/spdm_ctrl --responder-pubkey <trusted_p384_x_y_hex> --connect

See Building and Nuvoton NPCT75x Details for full instructions.

Nations NS350

# Build wolfSSL (in the sibling checkout, then return here)
cd ../wolfssl && ./autogen.sh && \
./configure --enable-wolftpm --enable-ecc --enable-sha384 --enable-aesgcm --enable-hkdf --enable-sp && \
make && sudo make install && sudo ldconfig && cd -

# Build wolfTPM (submodule already present from the recursive clone)
./autogen.sh && ./configure --enable-spdm --enable-nations && make

# Connect (identity key is factory default)
./examples/spdm/spdm_ctrl --responder-pubkey <trusted_p384_x_y_hex> --connect

See Building and Nations NS350 Details for full instructions.

How It Works

SPDM (Security Protocol and Data Model) establishes an authenticated encrypted channel over the existing SPI/I2C bus. The implementation uses Algorithm Set B: ECDH P-384 / SHA-384 / AES-256-GCM. Two session establishment modes are supported.

Protocol Flow: Identity Key Mode (Nuvoton + Nations)

Host                                TPM (Nuvoton NPCT75x / Nations NS350)
  |                                   |
  |--- GET_VERSION ------------------>|  (negotiate SPDM version)
  |<-- VERSION -----------------------|
  |                                   |
  |--- GET_PUB_KEY ------------------>|  (get TPM's P-384 identity key)
  |<-- PUB_KEY_RSP -------------------|
  |                                   |
  |--- KEY_EXCHANGE ----------------->|  (ECDHE P-384 key agreement)
  |<-- KEY_EXCHANGE_RSP --------------|  (+ HMAC proof of shared secret)
  |                                   |
  |    --- Handshake keys derived --- |
  |                                   |
  |=== GIVE_PUB_KEY =================>|  (encrypted: host's P-384 key)
  |<== GIVE_PUB_KEY_RSP ==============|
  |                                   |
  |=== FINISH =======================>|  (encrypted: signature + HMAC)
  |<== FINISH_RSP ====================|
  |                                   |
  |    --- App data keys derived ---  |
  |                                   |
  |=== TPM2_CMD (AES-256-GCM) =======>|  (every command encrypted)
  |<== TPM2_RSP (AES-256-GCM) ========|

The handshake uses ECDH P-384 for key agreement and HMAC-SHA384 for authentication. After the handshake, all TPM commands are wrapped in SPDM VENDOR_DEFINED_REQUEST("TPM2_CMD") messages and encrypted with AES-256-GCM. A sequence number increments with each message to prevent replay attacks.

Protocol Flow: PSK Mode (Nations Only)

PSK mode replaces the ECDHE key exchange with a symmetric pre-shared key. The same AES-256-GCM encryption is used for data transport.

Host                                TPM (Nations NS350)
  |                                   |
  |--- GET_VERSION ------------------>| (negotiate SPDM version)
  |<-- VERSION -----------------------|
  |                                   |
  |--- GET_CAPABILITIES ------------->| (capability exchange)
  |<-- CAPABILITIES ------------------|
  |                                   |
  |--- NEGOTIATE_ALGORITHMS --------->| (Algorithm Set B: P-384/SHA-384)
  |<-- ALGORITHMS --------------------|
  |                                   |
  |--- PSK_EXCHANGE ----------------->| (session key from PSK)
  |<-- PSK_EXCHANGE_RSP --------------| (+ HMAC proof)
  |                                   |
  |    --- Handshake keys derived --- | (Salt_0 = 0xFF * H for PSK mode)
  |                                   |
  |=== PSK_FINISH ===================>| (encrypted: requester HMAC)
  |<== PSK_FINISH_RSP ================|
  |                                   |
  |    --- App data keys derived ---  |
  |                                   |
  |=== TPM2_CMD (AES-256-GCM) =======>| (every command encrypted)
  |<== TPM2_RSP (AES-256-GCM) ========|

PSK and identity key modes are mutually exclusive on the NS350. The identity key is provisioned by factory default; it must be unset before PSK can be used. See PSK Lifecycle (Nations).

SPDM-Only Mode (Encrypted Bus Enforcement)

SPDM-only mode forces all TPM commands through the encrypted SPDM channel. Both vendors support this. The typical lifecycle:

1. Enable SPDM        (one-time, persists across resets)
2. Connect            (handshake, derives session keys)
3. Lock SPDM-only     (TPM rejects all cleartext commands)
4. Reset              (TPM enters SPDM-only enforcement)
5. Initialize with the trusted key or PSK and run commands (all encrypted)
6. Unlock             (connect + unlock in one session)
7. Reset              (TPM back to normal cleartext mode)

After an application supplies the responder key through wolfTPM2_InitWithSpdmKey(), wolfTPM authenticates the responder and establishes the encrypted session regardless of the cleartext startup result. See How Auto-SPDM Works for details.

Reset method differs by vendor:

  • Nuvoton: GPIO reset: gpioset gpiochip0 4=0 && sleep 0.1 && gpioset gpiochip0 4=1 && sleep 2
  • Nations: Full power cycle required (GPIO 4 is not wired to TPM_RST on NS350 daughter boards)

Building

1. Clone with the wolfSPDM submodule

SPDM is built from the lib/wolfSPDM submodule, so clone wolfTPM recursively:

git clone --recursive https://github.com/wolfSSL/wolfTPM.git
git clone https://github.com/wolfSSL/wolfssl.git   # sibling checkout for wolfSSL
cd wolfTPM

Already cloned wolfTPM without --recursive? Pull the submodule in:

git submodule update --init lib/wolfSPDM

2. wolfSSL

cd ../wolfssl
./autogen.sh
./configure --enable-wolftpm --enable-ecc --enable-sha384 \
    --enable-aesgcm --enable-hkdf --enable-sp
make
sudo make install && sudo ldconfig
cd -   # back to the wolfTPM checkout

Both Nuvoton and Nations use the same wolfSSL flags above.

3. wolfTPM

./autogen.sh
./configure --enable-spdm --enable-nuvoton   # Nuvoton
# or
./configure --enable-spdm --enable-nations    # Nations
make

The wolfTPM SPDM profile

--enable-spdm defines WOLFTPM_SPDM, which auto-selects wolfSPDM's WOLFSPDM_PROFILE_TPM. A TPM speaks only the TCG SPDM Binding, so this profile is a lean, TCG-focused build. Compiled out automatically:

  • the DMTF standard requester: GET_CAPABILITIES, NEGOTIATE_ALGORITHMS, GET_DIGESTS, GET_CERTIFICATE, and certificate-chain validation
  • measurements, challenge, and chunking (they ride the cert flow)
  • heartbeat and key update
  • the MCTP application-data API (secured messages use the TCG 16-byte pad)

There is no --disable-mctp option on wolfTPM's configure, and you should not try to add one. The lean profile is automatic with --enable-spdm, and wolfSPDM's downstream CI asserts the standard-requester symbols above are absent from libwolftpm.

The profile does not define WOLFSPDM_NO_MCTP, so the MCTP secured-message framing stays compiled, though a TCG-only TPM never exercises it. Building wolfSPDM standalone with --disable-mctp (which requires --enable-tcg) additionally strips that path for a pure-TCG requester. That flag belongs to wolfSPDM's own configure, never wolfTPM's.

Configure Options

Option Description
--enable-spdm Enable SPDM support (required)
--enable-tcg TCG SPDM Binding spec handshake (auto when fwtpm/nuvoton/nations on)
--enable-psk DSP0274 PSK handshake (auto with --enable-nations; requires --enable-tcg)
--enable-fwtpm Build fwtpm_server with the SPDM responder (no silicon needed)
--enable-nuvoton Enable Nuvoton TPM hardware support (auto-enables --enable-tcg)
--enable-nations Enable Nations NS350 hardware support (auto-enables --enable-tcg --enable-psk)
--enable-debug Debug output with verbose SPDM tracing
--enable-smallstack Heap-allocated SPDM context (default: static ~32 KB)

Incompatibility errors enforced by configure:

  • --enable-nuvoton --disable-tcg (Nuvoton uses TCG SPDM Binding)
  • --enable-nations --disable-tcg or --enable-nations --disable-psk
  • --enable-psk --disable-tcg (PSK rides on TCG framing)

fwtpm SPDM Responder (no silicon required)

fwtpm_server ships an SPDM 1.3 responder so the full TCG + PSK stack can be exercised in CI without real hardware:

./src/fwtpm/fwtpm_server --spdm-tcg          # TCG cert handshake
./src/fwtpm/fwtpm_server --spdm-psk \
    --spdm-psk-hex dbc2192291d807742441b963f6712841...   # PSK handshake

Test it end-to-end:

./examples/spdm/spdm_test.sh ./examples/spdm/spdm_ctrl fwtpm-tcg
./examples/spdm/spdm_test.sh ./examples/spdm/spdm_ctrl fwtpm-psk

Vendor selection in dual-vendor builds

When both --enable-nuvoton and --enable-nations are compiled in, the spdm_ctrl CLI selects which vendor adapter to use via an optional runtime flag:

./examples/spdm/spdm_ctrl --vendor=nuvoton \
    --responder-pubkey <trusted_p384_x_y_hex> --connect
./examples/spdm/spdm_ctrl --vendor=nations \
    --responder-pubkey <trusted_p384_x_y_hex> --connect

Single-vendor builds accept only the adapter compiled into the binary and reject an unavailable --vendor= value.

Usage

One-Time Setup

Nuvoton

# Enable SPDM on the TPM (persists across resets)
./examples/spdm/spdm_ctrl --enable

# GPIO reset
gpioset gpiochip0 4=0 && sleep 0.1 && gpioset gpiochip0 4=1 && sleep 2

# Verify SPDM is enabled
./examples/spdm/spdm_ctrl --status

Nations

Identity key mode is the factory default, no setup required. If previously unset, restore with:

./examples/spdm/spdm_ctrl --identity-key-set

Establishing a Session

Identity Key Mode (Both Vendors)

# Establish SPDM session (VERSION → GET_PUBK → KEY_EXCHANGE → GIVE_PUB → FINISH)
./examples/spdm/spdm_ctrl \
    --responder-pubkey <trusted_p384_x_y_hex> --connect

# Query SPDM status
./examples/spdm/spdm_ctrl --status

--responder-pubkey takes the trusted raw P-384 X||Y point as 192 hex characters. Obtain it from device provisioning records or another authenticated manufacturer channel. --get-pubkey is unauthenticated discovery and must not be used by itself to establish trust.

PSK Mode (Nations)

Requires PSK to be provisioned first. See PSK Lifecycle (Nations).

# Establish PSK session (VERSION → CAPS → ALGO → PSK_EXCHANGE → PSK_FINISH)
./examples/spdm/spdm_ctrl --psk <psk_hex_128chars>

Lock/Unlock SPDM-Only Mode

Lock requires an active SPDM session. After locking, a reset is required for enforcement to take effect.

Nuvoton (identity key):

./examples/spdm/spdm_ctrl \
    --responder-pubkey <trusted_p384_x_y_hex> --connect --lock
gpioset gpiochip0 4=0 && sleep 0.1 && gpioset gpiochip0 4=1 && sleep 2

# Unlock
./examples/spdm/spdm_ctrl \
    --responder-pubkey <trusted_p384_x_y_hex> --connect --unlock
gpioset gpiochip0 4=0 && sleep 0.1 && gpioset gpiochip0 4=1 && sleep 2

Nations (identity key):

./examples/spdm/spdm_ctrl \
    --responder-pubkey <trusted_p384_x_y_hex> --connect --lock
# Power cycle required (unplug and re-plug Raspberry Pi)

./examples/spdm/spdm_ctrl \
    --responder-pubkey <trusted_p384_x_y_hex> --connect --unlock
# Power cycle again

Nations (PSK mode):

./examples/spdm/spdm_ctrl --psk <hex> --lock
# Power cycle required

./examples/spdm/spdm_ctrl --psk <hex> --unlock
# Power cycle again

PSK Lifecycle (Nations)

PSK and identity key modes are mutually exclusive on the NS350. The identity key is provisioned by default; it must be unset before PSK can be used.

# 1. Unset identity key (enables PSK mode)
./examples/spdm/spdm_ctrl --identity-key-unset

# 2. Provision PSK (64-byte PSK + 32-byte ClearAuth)
#    The demo computes SHA-384(ClearAuth) and sends PSK(64)+Digest(48) = 112 bytes
./examples/spdm/spdm_ctrl --psk-set <psk_hex_128chars> <clearauth_hex_64chars>

# 3. Establish PSK session
./examples/spdm/spdm_ctrl --psk <psk_hex_128chars>

# 4. Clear PSK (sends raw 32-byte ClearAuth; TPM verifies SHA-384 internally)
./examples/spdm/spdm_ctrl --psk-clear <clearauth_hex_64chars>

# 5. Restore identity key (factory default)
./examples/spdm/spdm_ctrl --identity-key-set

Important: The ClearAuth must be exactly 32 bytes. PSK_SET stores its SHA-384 digest (48 bytes). PSK_CLEAR sends the raw 32 bytes and the TPM computes SHA-384 to verify. Using the wrong size makes PSK_CLEAR impossible.

Running the Test Suite

# Nuvoton (identity key, includes GPIO resets between tests)
export SPDM_RESPONDER_PUBKEY=<trusted_p384_x_y_hex>
./examples/spdm/spdm_test.sh ./examples/spdm/spdm_ctrl nuvoton

# Nations (identity key, no GPIO resets)
./examples/spdm/spdm_test.sh ./examples/spdm/spdm_ctrl nations

# Nations (PSK, full lifecycle: provision -> connect -> clear -> restore)
./examples/spdm/spdm_test.sh ./examples/spdm/spdm_ctrl nations-psk

Identity-mode hardware tests require SPDM_RESPONDER_PUBKEY from trusted device provisioning records. The local fwtpm-tcg test instead reads the freshly generated public key from the owner-only server log created by the test harness; this local bootstrap is not a hardware provisioning mechanism.

TCG SPDM Vendor Commands

Both Nuvoton and Nations TPMs implement the TCG "TPM Communication over SPDM Secure Session" specification. These commands use 8-byte ASCII vendor codes in SPDM VENDOR_DEFINED_REQUEST messages with StandardID=0x0001 (TCG).

VdCode Command Vendor Description
GET_PUBK Get Public Key Both Get TPM's SPDM-Identity P-384 public key
GIVE_PUB Give Public Key Both Send host's P-384 public key to TPM
TPM2_CMD TPM Command Both Wrap TPM command in SPDM secured message
GET_STS_ Get Status Both Query SPDM status
SPDMONLY SPDM-Only Mode Both Lock/unlock SPDM-only enforcement
PSK_SET_ PSK Set Nations Provision pre-shared key (64-byte PSK + SHA-384 digest)
PSK_CLR_ PSK Clear Nations Clear provisioned PSK (requires ClearAuth)

Command Reference

All spdm_ctrl options in one table:

Option Vendor Description
--enable Nuvoton Enable SPDM via NTC2_PreConfig (one-time, persists)
--disable Nuvoton Disable SPDM via NTC2_PreConfig
--identity-key-set Nations Provision SPDM identity key (factory default)
--identity-key-unset Nations Un-provision identity key (required before PSK)
--vendor=nuvoton|nations Both Select the identity/vendor adapter explicitly
--get-pubkey Both Discover the TPM identity key without authenticating it
--responder-pubkey <hex> Both Pin a trusted raw P-384 X||Y responder key
--connect Both Establish identity key SPDM session
--caps Both Read TPM capabilities over the current transport
--status Both Query SPDM status
--lock Both Lock SPDM-only mode (requires active session)
--unlock Both Unlock SPDM-only mode (requires active session)
--psk <psk> Nations Establish PSK session (64-byte PSK)
--psk-set <psk> <clearauth> Nations Provision PSK (64-byte PSK, 32-byte ClearAuth)
--psk-clear <clearauth> Nations Clear PSK (32-byte ClearAuth)
--caps184 Nations Query TPM 184 vendor properties and SPDM session info
--tpm-clear Nations Send TPM2_Clear (platform auth)

Vendor-Specific Details

Nuvoton NPCT75x

Enable/Disable: SPDM is enabled via the NTC2_PreConfig vendor command (--enable / --disable). This persists across resets.

GPIO Reset: GPIO 4 is wired to TPM_RST on the Nuvoton daughter board. A GPIO reset clears stale SPDM state:

gpioset gpiochip0 4=0 && sleep 0.1 && gpioset gpiochip0 4=1 && sleep 2

Nations NS350

Mode Switching: Identity key and PSK modes are mutually exclusive. The identity key is provisioned by factory default. Use --identity-key-unset before provisioning PSK, and --identity-key-set to restore.

No GPIO Reset: GPIO 4 is NOT wired to TPM_RST on the NS350 daughter board. A full power cycle (unplug and re-plug the Raspberry Pi) is required to reset the TPM. sudo reboot is not sufficient as the 3.3V rail stays powered.

Capabilities Query: Use --caps184 to query TPM 184 vendor properties including SPDM session info.

Status Caveat: On some NS350 firmware versions, --status may report "Identity Key: not provisioned" even when the key is present. The --connect command is the definitive test: if the ECDHE handshake succeeds, the identity key is provisioned.

ClearAuth: Must be exactly 32 bytes. PSK_SET stores its SHA-384 digest (48 bytes). PSK_CLEAR sends the raw 32 bytes and the TPM computes SHA-384 to verify.

PSK Vendor Error Codes:

Code Name Description
0xA1 Vd_PSKAlreadySet PSK already provisioned (must PSK_CLEAR first)
0xA2 Vd_InternalFailure SPDM session layer internal error
0xA3 Vd_PSKNotSet No PSK provisioned
0xA5 Vd_AuthFail ClearAuth SHA-384 doesn't match stored digest

How Auto-SPDM Works

Call wolfTPM2_InitWithSpdmKey() with the trusted responder key for identity mode, or wolfTPM2_InitWithSpdmPsk() with the provisioned PSK for PSK mode. Both entry points recover a TPM that is already locked in SPDM-only mode. The identity-mode initialization sequence is:

  1. TPM2_Startup probes whether the TPM is already in SPDM-only mode
  2. The caller-provided responder key is installed as the trust anchor
  3. The discovered responder key is compared with that trusted key
  4. An SPDM session is always established (P-384 keygen + handshake)
  5. If the probe returned TPM_RC_DISABLED, TPM2_Startup is retried securely
  6. All subsequent commands go through the SPDM encrypted channel

In a dual-vendor build, wolfTPM2_InitWithSpdmKey() selects the identity adapter from TPM DID/VID. A transport that does not expose DID/VID must call wolfTPM2_InitWithSpdmKey_ex() with WOLFSPDM_MODE_NUVOTON or WOLFSPDM_MODE_NATIONS; automatic mode fails closed rather than guessing.

wolfTPM2_Init() without a credential fails closed if it detects any SPDM-only mode. For a normal-mode TPM that does not require an immediate secure channel, identity-mode applications may instead call wolfTPM2_SpdmInit(), wolfTPM2_SpdmSetResponderPubKey(), and then the vendor-specific connect function.

Both TPM2_SendCommand (non-auth commands) and TPM2_SendCommandAuth (auth-session commands like PCR operations, key creation, signing) are intercepted and routed through SPDM when a session is active.

Memory Modes

Static (default): Zero heap allocation. SPDM context uses ~32 KB of static memory, ideal for embedded environments.

Small stack (--enable-smallstack): Context is heap-allocated. Useful on platforms with small stacks.

wolfSPDM API

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_SetResponderPubKey() Pin the trusted responder P-384 key
wolfSPDM_SetDebug() Enable/disable debug output
wolfSPDM_Connect() Full SPDM handshake
wolfSPDM_IsConnected() Check session status
wolfSPDM_Disconnect() End session
wolfSPDM_SecuredExchange() Encrypt/send/receive/decrypt in one call

Troubleshooting

SPDM handshake fails after interrupted session

Nuvoton: GPIO 4 is wired to TPM_RST on the Nuvoton daughter board. A GPIO reset clears stale SPDM state:

gpioset gpiochip0 4=0 && sleep 0.1 && gpioset gpiochip0 4=1 && sleep 2

Nations NS350: GPIO 4 is NOT wired to TPM_RST on the NS350 daughter board. A full power cycle (unplug and re-plug the Raspberry Pi) is required to reset the TPM. sudo reboot is not sufficient as the 3.3V rail stays powered.

SPDM Error Codes

Code Name Description
0x01 InvalidRequest Message format incorrect
0x04 UnexpectedRequest Message out of sequence
0x05 DecryptError Decryption or MAC verification failed
0x06 UnsupportedRequest Request not supported or format rejected
0x41 VersionMismatch SPDM version mismatch

Standard SPDM Support

For standard SPDM protocol support including session establishment with the DMTF spdm-emu emulator, measurements, challenge authentication, heartbeat, and key update, see the wolfSPDM standalone library.

License

GPLv3. See COPYING file. Copyright (C) 2006-2026 wolfSSL Inc.