Skip to content

Commit 6d38ef5

Browse files
committed
Add measurements, challenge, heartbeat, key update, and cert chain validation
Implement core SPDM 1.2 protocol features for production device attestation: - GET_MEASUREMENTS with signature verification (L1/L2 transcript, DSP0274) - CHALLENGE/CHALLENGE_AUTH sessionless attestation with signature verification - Certificate chain validation against trusted root CAs via wolfSPDM_SetTrustedCAs() - HEARTBEAT/HEARTBEAT_ACK session keep-alive - KEY_UPDATE/KEY_UPDATE_ACK session key rotation (DSP0277 traffic upd label) - Application data send/receive over encrypted sessions - Fix IV XOR position for MCTP to leftmost bytes per DSP0277 All 6 spdm-emu integration tests pass. Bumps WOLFSPDM_CTX_STATIC_SIZE to 32KB.
1 parent e390fc1 commit 6d38ef5

11 files changed

Lines changed: 2829 additions & 65 deletions

File tree

README.md

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ Lightweight SPDM 1.2+ requester-only stack implementation using wolfSSL/wolfCryp
66

77
- SPDM 1.2 requester implementation
88
- Algorithm Set B (FIPS 140-3 Level 3): ECDSA/ECDHE P-384, SHA-384, AES-256-GCM, HKDF-SHA384
9-
- **Zero-malloc by default** — fully static memory, ideal for constrained/embedded environments
9+
- **Zero-malloc by default** — fully static memory (~32 KB context), ideal for constrained/embedded environments
1010
- Optional `--enable-dynamic-mem` for heap-allocated contexts (useful for small-stack platforms)
11+
- Session establishment with full key exchange and encrypted messaging
12+
- Device attestation via signed/unsigned measurements (GET_MEASUREMENTS)
13+
- Sessionless attestation via CHALLENGE/CHALLENGE_AUTH with signature verification
14+
- Certificate chain validation against trusted root CAs
15+
- Session keep-alive via HEARTBEAT/HEARTBEAT_ACK
16+
- Session key rotation via KEY_UPDATE/KEY_UPDATE_ACK (DSP0277)
1117
- Hardware SPDM via wolfTPM + Nuvoton TPM
1218
- Full transcript tracking for TH1/TH2 computation
1319
- Compatible with DMTF spdm-emu for interoperability testing
@@ -53,7 +59,7 @@ make
5359
### Memory Modes
5460

5561
**Static (default):** Zero heap allocation. The caller provides a buffer
56-
(`WOLFSPDM_CTX_STATIC_SIZE` bytes, ~22 KB) and wolfSPDM operates entirely
62+
(`WOLFSPDM_CTX_STATIC_SIZE` bytes, ~32 KB) and wolfSPDM operates entirely
5763
within it. This is ideal for embedded and constrained environments where
5864
malloc is unavailable or undesirable.
5965

@@ -68,7 +74,7 @@ wolfSPDM_Free(ctx);
6874
```
6975
7076
**Dynamic (`--enable-dynamic-mem`):** Context is heap-allocated via
71-
`wolfSPDM_New()`. Useful on platforms with small stacks where a ~22 KB
77+
`wolfSPDM_New()`. Useful on platforms with small stacks where a ~32 KB
7278
local variable is impractical.
7379
7480
```c
@@ -98,17 +104,15 @@ cd wolfTPM
98104
./configure --enable-spdm --enable-swtpm --with-wolfspdm=path/to/wolfspdm
99105
make
100106

101-
# Terminal 1: Start responder with Algorithm Set B
102-
cd spdm-emu
103-
./bin/spdm_responder_emu --ver 1.2 \
104-
--hash SHA_384 --asym ECDSA_P384 \
105-
--dhe SECP_384_R1 --aead AES_256_GCM
106-
107-
# Terminal 2: Run wolfTPM example
107+
# Run emulator tests (starts/stops emulator automatically)
108108
cd wolfTPM
109-
./examples/spdm/spdm_demo --emu
109+
./examples/spdm/spdm_test.sh --emu
110110
```
111111

112+
The test script automatically finds `spdm_responder_emu` in `../spdm-emu/build/bin/`,
113+
starts it for each test, and runs session establishment, signed measurements,
114+
unsigned measurements, challenge authentication, heartbeat, and key update.
115+
112116
## Testing with Nuvoton NPCT75x
113117

114118
```bash
@@ -122,8 +126,8 @@ cd wolfTPM
122126
./configure --enable-spdm --enable-nuvoton --with-wolfspdm=path/to/wolfspdm
123127
make
124128

125-
# Run test suite
126-
./examples/spdm/spdm_test.sh
129+
# Run Nuvoton test suite
130+
./examples/spdm/spdm_test.sh --nuvoton
127131
```
128132

129133
## API Reference
@@ -143,6 +147,15 @@ make
143147
| `wolfSPDM_EncryptMessage()` | Encrypt outgoing message |
144148
| `wolfSPDM_DecryptMessage()` | Decrypt incoming message |
145149
| `wolfSPDM_SecuredExchange()` | Combined send/receive |
150+
| `wolfSPDM_SetTrustedCAs()` | Load trusted root CA certificates for chain validation |
151+
| `wolfSPDM_GetMeasurements()` | Retrieve device measurements with optional signature verification |
152+
| `wolfSPDM_GetMeasurementCount()` | Get number of measurement blocks retrieved |
153+
| `wolfSPDM_GetMeasurementBlock()` | Access individual measurement block data |
154+
| `wolfSPDM_Challenge()` | Sessionless device attestation via CHALLENGE/CHALLENGE_AUTH |
155+
| `wolfSPDM_Heartbeat()` | Session keep-alive (HEARTBEAT/HEARTBEAT_ACK) |
156+
| `wolfSPDM_KeyUpdate()` | Rotate session encryption keys (KEY_UPDATE/KEY_UPDATE_ACK) |
157+
| `wolfSPDM_SendData()` | Send application data over established session |
158+
| `wolfSPDM_ReceiveData()` | Receive application data over established session |
146159

147160
## License
148161

src/spdm_context.c

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,19 @@ void wolfSPDM_Free(WOLFSPDM_CTX* ctx)
102102
wc_ecc_free(&ctx->ephemeralKey);
103103
}
104104

105+
/* Free responder public key (used for measurement/challenge verification) */
106+
if (ctx->hasResponderPubKey) {
107+
wc_ecc_free(&ctx->responderPubKey);
108+
}
109+
110+
#ifndef NO_WOLFSPDM_CHALLENGE
111+
/* Free M1/M2 challenge hash if still initialized */
112+
if (ctx->m1m2HashInit) {
113+
wc_Sha384Free(&ctx->m1m2Hash);
114+
ctx->m1m2HashInit = 0;
115+
}
116+
#endif
117+
105118
/* Zero entire struct (covers all sensitive key material) */
106119
wc_ForceZero(ctx, sizeof(WOLFSPDM_CTX));
107120

@@ -203,6 +216,24 @@ int wolfSPDM_SetRequesterKeyTPMT(WOLFSPDM_CTX* ctx,
203216
}
204217
#endif /* WOLFSPDM_NUVOTON */
205218

219+
int wolfSPDM_SetTrustedCAs(WOLFSPDM_CTX* ctx, const byte* derCerts,
220+
word32 derCertsSz)
221+
{
222+
if (ctx == NULL || derCerts == NULL || derCertsSz == 0) {
223+
return WOLFSPDM_E_INVALID_ARG;
224+
}
225+
226+
if (derCertsSz > WOLFSPDM_MAX_CERT_CHAIN) {
227+
return WOLFSPDM_E_BUFFER_SMALL;
228+
}
229+
230+
XMEMCPY(ctx->trustedCAs, derCerts, derCertsSz);
231+
ctx->trustedCAsSz = derCertsSz;
232+
ctx->hasTrustedCAs = 1;
233+
234+
return WOLFSPDM_SUCCESS;
235+
}
236+
206237
void wolfSPDM_SetDebug(WOLFSPDM_CTX* ctx, int enable)
207238
{
208239
if (ctx != NULL) {
@@ -339,6 +370,22 @@ static int wolfSPDM_ConnectStandard(WOLFSPDM_CTX* ctx)
339370
return rc;
340371
}
341372

373+
/* Validate certificate chain if trusted CAs are loaded.
374+
* Public key extraction now happens automatically in GetCertificate. */
375+
if (ctx->hasTrustedCAs) {
376+
rc = wolfSPDM_ValidateCertChain(ctx);
377+
if (rc != WOLFSPDM_SUCCESS) {
378+
wolfSPDM_DebugPrint(ctx,
379+
"Certificate chain validation failed (%d)\n", rc);
380+
ctx->state = WOLFSPDM_STATE_ERROR;
381+
return rc;
382+
}
383+
}
384+
else if (!ctx->hasResponderPubKey) {
385+
wolfSPDM_DebugPrint(ctx,
386+
"Warning: No trusted CAs loaded — chain not validated\n");
387+
}
388+
342389
/* Step 6: KEY_EXCHANGE / KEY_EXCHANGE_RSP */
343390
wolfSPDM_DebugPrint(ctx, "Step 6: KEY_EXCHANGE\n");
344391
rc = wolfSPDM_KeyExchange(ctx);
@@ -482,6 +529,58 @@ void wolfSPDM_DebugHex(WOLFSPDM_CTX* ctx, const char* label,
482529
fflush(stdout);
483530
}
484531

532+
/* ==========================================================================
533+
* Measurement Accessors
534+
* ========================================================================== */
535+
536+
#ifndef NO_WOLFSPDM_MEAS
537+
538+
int wolfSPDM_GetMeasurementCount(WOLFSPDM_CTX* ctx)
539+
{
540+
if (ctx == NULL || !ctx->hasMeasurements) {
541+
return 0;
542+
}
543+
return (int)ctx->measBlockCount;
544+
}
545+
546+
int wolfSPDM_GetMeasurementBlock(WOLFSPDM_CTX* ctx, int blockIdx,
547+
byte* measIndex, byte* measType, byte* value, word32* valueSz)
548+
{
549+
WOLFSPDM_MEAS_BLOCK* blk;
550+
551+
if (ctx == NULL || !ctx->hasMeasurements) {
552+
return WOLFSPDM_E_INVALID_ARG;
553+
}
554+
if (blockIdx < 0 || blockIdx >= (int)ctx->measBlockCount) {
555+
return WOLFSPDM_E_INVALID_ARG;
556+
}
557+
if (valueSz == NULL) {
558+
return WOLFSPDM_E_INVALID_ARG;
559+
}
560+
561+
blk = &ctx->measBlocks[blockIdx];
562+
563+
if (measIndex != NULL) {
564+
*measIndex = blk->index;
565+
}
566+
if (measType != NULL) {
567+
*measType = blk->dmtfType;
568+
}
569+
570+
if (value != NULL) {
571+
word32 copySize = blk->valueSize;
572+
if (copySize > *valueSz) {
573+
copySize = *valueSz;
574+
}
575+
XMEMCPY(value, blk->value, copySize);
576+
}
577+
*valueSz = blk->valueSize;
578+
579+
return WOLFSPDM_SUCCESS;
580+
}
581+
582+
#endif /* !NO_WOLFSPDM_MEAS */
583+
485584
/* ==========================================================================
486585
* Error String
487586
* ========================================================================== */
@@ -510,6 +609,12 @@ const char* wolfSPDM_GetErrorString(int error)
510609
case WOLFSPDM_E_ALGO_MISMATCH: return "Algorithm mismatch";
511610
case WOLFSPDM_E_SESSION_INVALID: return "Invalid session";
512611
case WOLFSPDM_E_KEY_EXCHANGE: return "Key exchange failed";
612+
case WOLFSPDM_E_MEASUREMENT: return "Measurement retrieval failed";
613+
case WOLFSPDM_E_MEAS_NOT_VERIFIED: return "Measurements not signature-verified";
614+
case WOLFSPDM_E_MEAS_SIG_FAIL: return "Measurement signature verification failed";
615+
case WOLFSPDM_E_CERT_PARSE: return "Failed to parse responder certificate";
616+
case WOLFSPDM_E_CHALLENGE: return "Challenge authentication failed";
617+
case WOLFSPDM_E_KEY_UPDATE: return "Key update failed";
513618
default: return "Unknown error";
514619
}
515620
}

src/spdm_internal.h

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include <wolfssl/wolfcrypt/kdf.h>
4646
#include <wolfssl/wolfcrypt/aes.h>
4747
#include <wolfssl/wolfcrypt/memory.h>
48+
#include <wolfssl/wolfcrypt/asn_public.h>
4849

4950
#ifdef __cplusplus
5051
extern "C" {
@@ -64,6 +65,23 @@ extern "C" {
6465
#define WOLFSPDM_STATE_FINISH 7 /* FINISH complete */
6566
#define WOLFSPDM_STATE_CONNECTED 8 /* Session established */
6667
#define WOLFSPDM_STATE_ERROR 9 /* Error state */
68+
#ifndef NO_WOLFSPDM_MEAS
69+
#define WOLFSPDM_STATE_MEASURED 10 /* Measurements retrieved */
70+
#endif
71+
72+
/* ==========================================================================
73+
* Measurement Block Structure
74+
* ========================================================================== */
75+
76+
#ifndef NO_WOLFSPDM_MEAS
77+
typedef struct WOLFSPDM_MEAS_BLOCK {
78+
byte index; /* SPDM measurement index (1-based) */
79+
byte measurementSpec; /* Measurement specification (1=DMTF) */
80+
byte dmtfType; /* DMTFSpecMeasurementValueType */
81+
word16 valueSize; /* Actual value size in bytes */
82+
byte value[WOLFSPDM_MAX_MEAS_VALUE_SIZE]; /* Measurement value (digest/raw) */
83+
} WOLFSPDM_MEAS_BLOCK;
84+
#endif /* !NO_WOLFSPDM_MEAS */
6785

6886
/* ==========================================================================
6987
* Internal Context Structure
@@ -117,6 +135,7 @@ struct WOLFSPDM_CTX {
117135
/* Transcript hash for TH1/TH2 computation */
118136
byte transcript[WOLFSPDM_MAX_TRANSCRIPT];
119137
word32 transcriptLen;
138+
word32 vcaLen; /* VCA transcript size (after ALGORITHMS, used by measurement sig) */
120139

121140
/* Certificate chain buffer for Ct computation */
122141
byte certChain[WOLFSPDM_MAX_CERT_CHAIN];
@@ -164,6 +183,52 @@ struct WOLFSPDM_CTX {
164183
/* Message buffers */
165184
byte sendBuf[WOLFSPDM_MAX_MSG_SIZE + WOLFSPDM_AEAD_TAG_SIZE];
166185
byte recvBuf[WOLFSPDM_MAX_MSG_SIZE + WOLFSPDM_AEAD_TAG_SIZE];
186+
187+
#ifndef NO_WOLFSPDM_MEAS
188+
/* Measurement data */
189+
WOLFSPDM_MEAS_BLOCK measBlocks[WOLFSPDM_MAX_MEAS_BLOCKS];
190+
word32 measBlockCount;
191+
byte measNonce[32]; /* Nonce for signed measurements */
192+
byte measSummaryHash[WOLFSPDM_HASH_SIZE]; /* Summary hash from response */
193+
byte measSignature[WOLFSPDM_ECC_SIG_SIZE]; /* Captured signature (96 bytes P-384) */
194+
word32 measSignatureSize; /* 0 if unsigned, 96 if signed */
195+
int hasMeasurements;
196+
197+
#ifndef NO_WOLFSPDM_MEAS_VERIFY
198+
/* Saved GET_MEASUREMENTS request for L1/L2 transcript */
199+
byte measReqMsg[48]; /* Saved request (max 37 bytes) */
200+
word32 measReqMsgSz;
201+
#endif /* !NO_WOLFSPDM_MEAS_VERIFY */
202+
#endif /* !NO_WOLFSPDM_MEAS */
203+
204+
/* Responder identity for signature verification (measurements + challenge) */
205+
ecc_key responderPubKey; /* Extracted from cert chain leaf */
206+
int hasResponderPubKey; /* 1 if key extracted successfully */
207+
208+
/* Certificate chain validation */
209+
byte trustedCAs[WOLFSPDM_MAX_CERT_CHAIN]; /* DER-encoded root CAs */
210+
word32 trustedCAsSz;
211+
int hasTrustedCAs; /* 1 if CAs loaded */
212+
213+
#ifndef NO_WOLFSPDM_CHALLENGE
214+
/* Challenge authentication */
215+
byte challengeNonce[32]; /* Saved nonce from CHALLENGE request */
216+
byte challengeMeasHashType; /* MeasurementSummaryHashType from req */
217+
218+
/* Running M1/M2 hash for CHALLENGE_AUTH signature verification.
219+
* Per DSP0274, M1/M2 = A || B || C where:
220+
* A = VCA (GET_VERSION..ALGORITHMS)
221+
* B = GET_DIGESTS + DIGESTS + GET_CERTIFICATE + CERTIFICATE (all chunks)
222+
* C = CHALLENGE + CHALLENGE_AUTH (before sig)
223+
* This hash accumulates A+B during NegAlgo/GetDigests/GetCertificate,
224+
* then C is added in VerifyChallengeAuthSig. */
225+
wc_Sha384 m1m2Hash;
226+
int m1m2HashInit; /* 1 if m1m2Hash is initialized */
227+
#endif
228+
229+
/* Key update state — app secrets for re-derivation */
230+
byte reqAppSecret[WOLFSPDM_HASH_SIZE]; /* 48 bytes */
231+
byte rspAppSecret[WOLFSPDM_HASH_SIZE]; /* 48 bytes */
167232
};
168233

169234
/* ==========================================================================
@@ -315,6 +380,81 @@ void wolfSPDM_DebugPrint(WOLFSPDM_CTX* ctx, const char* fmt, ...);
315380
void wolfSPDM_DebugHex(WOLFSPDM_CTX* ctx, const char* label,
316381
const byte* data, word32 len);
317382

383+
/* ==========================================================================
384+
* Internal Function Declarations - Measurements
385+
* ========================================================================== */
386+
387+
#ifndef NO_WOLFSPDM_MEAS
388+
/* Build GET_MEASUREMENTS request */
389+
int wolfSPDM_BuildGetMeasurements(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz,
390+
byte operation, byte requestSig);
391+
392+
/* Parse MEASUREMENTS response */
393+
int wolfSPDM_ParseMeasurements(WOLFSPDM_CTX* ctx, const byte* buf, word32 bufSz);
394+
395+
#ifndef NO_WOLFSPDM_MEAS_VERIFY
396+
/* Verify measurement signature (L1/L2 transcript) */
397+
int wolfSPDM_VerifyMeasurementSig(WOLFSPDM_CTX* ctx,
398+
const byte* rspBuf, word32 rspBufSz,
399+
const byte* reqMsg, word32 reqMsgSz);
400+
#endif /* !NO_WOLFSPDM_MEAS_VERIFY */
401+
#endif /* !NO_WOLFSPDM_MEAS */
402+
403+
/* ==========================================================================
404+
* Internal Function Declarations - Certificate Chain Validation
405+
* ========================================================================== */
406+
407+
/* Extract responder's public key from certificate chain leaf cert */
408+
int wolfSPDM_ExtractResponderPubKey(WOLFSPDM_CTX* ctx);
409+
410+
/* Validate certificate chain using trusted CAs and extract public key */
411+
int wolfSPDM_ValidateCertChain(WOLFSPDM_CTX* ctx);
412+
413+
/* ==========================================================================
414+
* Internal Function Declarations - Challenge
415+
* ========================================================================== */
416+
417+
#ifndef NO_WOLFSPDM_CHALLENGE
418+
/* Build CHALLENGE request */
419+
int wolfSPDM_BuildChallenge(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz,
420+
int slotId, byte measHashType);
421+
422+
/* Parse CHALLENGE_AUTH response */
423+
int wolfSPDM_ParseChallengeAuth(WOLFSPDM_CTX* ctx, const byte* buf,
424+
word32 bufSz, word32* sigOffset);
425+
426+
/* Verify CHALLENGE_AUTH signature */
427+
int wolfSPDM_VerifyChallengeAuthSig(WOLFSPDM_CTX* ctx,
428+
const byte* rspBuf, word32 rspBufSz,
429+
const byte* reqMsg, word32 reqMsgSz, word32 sigOffset);
430+
#endif /* !NO_WOLFSPDM_CHALLENGE */
431+
432+
/* ==========================================================================
433+
* Internal Function Declarations - Heartbeat
434+
* ========================================================================== */
435+
436+
/* Build HEARTBEAT request */
437+
int wolfSPDM_BuildHeartbeat(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz);
438+
439+
/* Parse HEARTBEAT_ACK response */
440+
int wolfSPDM_ParseHeartbeatAck(WOLFSPDM_CTX* ctx, const byte* buf,
441+
word32 bufSz);
442+
443+
/* ==========================================================================
444+
* Internal Function Declarations - Key Update
445+
* ========================================================================== */
446+
447+
/* Build KEY_UPDATE request */
448+
int wolfSPDM_BuildKeyUpdate(WOLFSPDM_CTX* ctx, byte* buf, word32* bufSz,
449+
byte operation, byte* tag);
450+
451+
/* Parse KEY_UPDATE_ACK response */
452+
int wolfSPDM_ParseKeyUpdateAck(WOLFSPDM_CTX* ctx, const byte* buf,
453+
word32 bufSz, byte operation, byte tag);
454+
455+
/* Derive updated keys from saved app secrets */
456+
int wolfSPDM_DeriveUpdatedKeys(WOLFSPDM_CTX* ctx, int updateAll);
457+
318458
#ifdef __cplusplus
319459
}
320460
#endif

0 commit comments

Comments
 (0)