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 — no application code changes needed.
For standard SPDM protocol testing with the DMTF spdm-emu emulator, see the wolfSPDM standalone library.
# Build wolfSSL
pushd ../wolfssl && ./autogen.sh && \
./configure --enable-wolftpm --enable-ecc --enable-sha384 --enable-aesgcm --enable-hkdf --enable-sp && \
make && sudo make install && sudo ldconfig && popd
# Build wolfTPM
./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 --connectSee Building and Nuvoton NPCT75x Details for full instructions.
# Build wolfSSL
pushd ../wolfssl && ./autogen.sh && \
./configure --enable-wolftpm --enable-ecc --enable-sha384 --enable-aesgcm --enable-hkdf --enable-sp && \
make && sudo make install && sudo ldconfig && popd
# Build wolfTPM
./autogen.sh && ./configure --enable-spdm --enable-nations && make
# Connect (identity key is factory default)
./examples/spdm/spdm_ctrl --connectSee Building and Nations NS350 Details for full instructions.
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.
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.
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 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. Run any commands (each auto-establishes SPDM, all AES-256-GCM encrypted)
6. Unlock (connect + unlock in one session)
7. Reset (TPM back to normal cleartext mode)
Step 5 is fully automatic. When wolfTPM detects SPDM-only mode (TPM2_Startup
returns TPM_RC_DISABLED), it transparently establishes an SPDM session.
Existing applications like caps, wrap_test, and unit.test work without
modification — all commands are encrypted over the bus. 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)
pushd ../wolfssl
./autogen.sh
./configure --enable-wolftpm --enable-ecc --enable-sha384 \
--enable-aesgcm --enable-hkdf --enable-sp
make
sudo make install && sudo ldconfig
popdBoth Nuvoton and Nations use the same wolfSSL flags above.
./autogen.sh
./configure --enable-spdm --enable-nuvoton # Nuvoton
# or
./configure --enable-spdm --enable-nations # Nations
make| 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-tcgor--enable-nations --disable-psk--enable-psk --disable-tcg(PSK rides on TCG framing)
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 handshakeTest 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-pskWhen 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 --connect # default
./examples/spdm/spdm_ctrl --vendor=nations --connectSingle-vendor builds ignore --vendor=.
# 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 --statusIdentity key mode is the factory default — no setup required. If previously unset, restore with:
./examples/spdm/spdm_ctrl --identity-key-set# Establish SPDM session (VERSION → GET_PUBK → KEY_EXCHANGE → GIVE_PUB → FINISH)
./examples/spdm/spdm_ctrl --connect
# Query SPDM status
./examples/spdm/spdm_ctrl --statusNote: --get-pubkey retrieves the TPM's identity key as part of the full
handshake within --connect. It is not intended as a standalone command.
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 requires an active SPDM session. After locking, a reset is required for enforcement to take effect.
Nuvoton (identity key):
./examples/spdm/spdm_ctrl --connect --lock
gpioset gpiochip0 4=0 && sleep 0.1 && gpioset gpiochip0 4=1 && sleep 2
# TPM now requires SPDM — all commands auto-encrypted:
./examples/wrap/caps # auto-SPDM session, all AES-256-GCM
./tests/unit.test # full test suite over encrypted bus
# Unlock
./examples/spdm/spdm_ctrl --connect --unlock
gpioset gpiochip0 4=0 && sleep 0.1 && gpioset gpiochip0 4=1 && sleep 2Nations (identity key):
./examples/spdm/spdm_ctrl --connect --lock
# Power cycle required (unplug and re-plug Raspberry Pi)
./examples/spdm/spdm_ctrl --connect --unlock
# Power cycle againNations (PSK mode):
./examples/spdm/spdm_ctrl --psk <hex> --lock
# Power cycle required
./examples/spdm/spdm_ctrl --psk <hex> --unlock
# Power cycle againPSK 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-setImportant: 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.
# Nuvoton (identity key — includes GPIO resets between tests)
./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-pskBoth 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) |
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) |
--get-pubkey |
Both | Get TPM's SPDM-Identity P-384 public key (used within --connect) |
--connect |
Both | Establish identity key SPDM session |
--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) |
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 2Mode 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 |
When the TPM is in SPDM-only mode, wolfTPM2_Init() handles everything:
TPM2_Startupis sent in cleartext — TPM returnsTPM_RC_DISABLED- wolfTPM detects this and sets
spdmOnlyDetected - An SPDM session is automatically established (P-384 keygen + handshake)
TPM2_Startupis retried over the encrypted channel — succeeds- All subsequent commands go through the SPDM encrypted channel
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.
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.
| 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_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 |
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 2Nations 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.
| 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 |
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.
GPLv3 — see COPYING file. Copyright (C) 2006-2026 wolfSSL Inc.