From 18066a940290ebbff077fbbfc121fe903495e8ea Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 21 Aug 2026 15:59:27 -0700 Subject: [PATCH] F-9203 - Bind sequence tickets to raw messages --- src/fwtpm/fwtpm_command.c | 166 ++++++++++++++++++++++---------------- tests/fwtpm_unit_tests.c | 27 +++---- wolftpm/fwtpm/fwtpm.h | 4 + 3 files changed, 109 insertions(+), 88 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 1ab4bf1f..4b39a389 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -8511,13 +8511,9 @@ static TPM_RC FwCmd_SequenceUpdate(FWTPM_CTX* ctx, TPM2_Packet* cmd, dataBuf, take); signSeq->firstBytesSz += take; } - /* Hash-ML-DSA, RSA, ECC: feed bytes into the hash accumulator - * only — the verify-side ticket binds the computed digest - * (matches TPM2_VerifySignature pattern), which removes the - * msgBuf cap for arbitrarily long sequences. - * KEYEDHASH (HMAC): stream into hmacCtx similarly. - * Pure ML-DSA: no digest exists, so accumulate raw message - * bytes in msgBuf (capped at FWTPM_MAX_DATA_BUF). */ + /* Hash-ML-DSA, RSA, ECC: stream into the hash accumulator. + * KEYEDHASH (HMAC): stream into hmacCtx. Pure ML-DSA retains + * the raw message because its signature operates on it. */ if (signSeq->sigScheme == TPM_ALG_HASH_MLDSA || signSeq->sigScheme == TPM_ALG_RSA || signSeq->sigScheme == TPM_ALG_ECC) { @@ -8552,6 +8548,16 @@ static TPM_RC FwCmd_SequenceUpdate(FWTPM_CTX* ctx, TPM2_Packet* cmd, dataBuf, dataSize); signSeq->msgBufSz += dataSize; } + /* MESSAGE_VERIFIED authenticates the raw message, not the + * scheme's internal digest. Keep its HMAC streaming so verify + * sequences remain unbounded by FWTPM_MAX_DATA_BUF. */ + if (rc == 0 && signSeq->isVerifySeq && + signSeq->ticketHmacCtxInit && dataSize > 0) { + if (wc_HmacUpdate(&signSeq->ticketHmacCtx, + dataBuf, dataSize) != 0) { + rc = TPM_RC_FAILURE; + } + } } #ifndef FWTPM_NO_HASH_CMDS else @@ -15198,6 +15204,9 @@ static void FwFreeSignSeq(FWTPM_SignSeq* seq) if (seq->hmacCtxInit) { wc_HmacFree(&seq->hmacCtx); } + if (seq->ticketHmacCtxInit) { + wc_HmacFree(&seq->ticketHmacCtx); + } #endif XMEMSET(seq, 0, sizeof(*seq)); } @@ -15228,6 +15237,48 @@ static TPM_RC FwSignSeqInitHashCtx(FWTPM_SignSeq* seq, TPMI_ALG_HASH hashAlg) seq->hashCtxInit = 1; return TPM_RC_SUCCESS; } + +/* Start the HMAC for a MESSAGE_VERIFIED ticket. SequenceUpdate streams the + * raw message into this context and VerifySequenceComplete appends keyName. */ +static TPM_RC FwSignSeqInitTicketHmac(FWTPM_CTX* ctx, FWTPM_SignSeq* seq, + UINT32 hierarchy) +{ + byte proof[TPM_MAX_DIGEST_SIZE]; + byte tagBytes[2]; + int proofSz = TPM2_GetHashDigestSize(CONTEXT_INTEGRITY_HASH_ALG); + int rc = TPM_RC_SUCCESS; + + seq->ticketHierarchy = hierarchy; + if (hierarchy == TPM_RH_NULL) { + return TPM_RC_SUCCESS; + } + if (proofSz <= 0) { + return TPM_RC_HASH; + } + + rc = FwComputeProofValue(ctx, hierarchy, CONTEXT_INTEGRITY_HASH_ALG, + proof, proofSz); + if (rc == 0) { + rc = wc_HmacInit(&seq->ticketHmacCtx, NULL, INVALID_DEVID); + if (rc == 0) { + seq->ticketHmacCtxInit = 1; + } + } + if (rc == 0) { + rc = wc_HmacSetKey(&seq->ticketHmacCtx, + (int)FwGetWcHashType(CONTEXT_INTEGRITY_HASH_ALG), + proof, (word32)proofSz); + } + if (rc == 0) { + tagBytes[0] = (byte)(TPM_ST_MESSAGE_VERIFIED >> 8); + tagBytes[1] = (byte)TPM_ST_MESSAGE_VERIFIED; + rc = wc_HmacUpdate(&seq->ticketHmacCtx, + tagBytes, sizeof(tagBytes)); + } + + TPM2_ForceZero(proof, sizeof(proof)); + return (rc == 0) ? TPM_RC_SUCCESS : TPM_RC_FAILURE; +} #endif /* WOLFTPM_MLDSA */ #ifdef WOLFTPM_MLDSA_SIGN @@ -15513,10 +15564,9 @@ static TPM_RC FwCmd_VerifySequenceStart(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (obj->name.size == 0) FwComputeObjectName(obj); XMEMCPY(&seq->keyName, &obj->name, sizeof(seq->keyName)); seq->sigScheme = obj->pub.type; - /* Verify sequences always accept SequenceUpdate — the message has - * to accumulate somewhere since VerifySequenceComplete carries no - * buffer parameter (Part 3 Sec.20.3 Table 118). Hash-ML-DSA verify - * sequences accumulate into a hash ctx; Pure ML-DSA into msgBuf. */ + /* Verify sequences always accept SequenceUpdate. Signature checking + * uses hashCtx, hmacCtx, or msgBuf according to the scheme; the raw + * message also streams into ticketHmacCtx for the verified ticket. */ seq->oneShot = 0; if (obj->pub.type == TPM_ALG_HASH_MLDSA) { rc = FwSignSeqInitHashCtx(seq, @@ -15569,6 +15619,9 @@ static TPM_RC FwCmd_VerifySequenceStart(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } } + if (rc == 0) { + rc = FwSignSeqInitTicketHmac(ctx, seq, obj->hierarchy); + } if (rc == 0) { /* sequenceHandle is an output handle per Table 87 — emitted @@ -15945,23 +15998,16 @@ static TPM_RC FwCmd_VerifySequenceComplete(FWTPM_CTX* ctx, TPM2_Packet* cmd, FWTPM_Object* keyObj = NULL; UINT16 sigAlg = 0, sigHashAlg = 0, wireSize = 0; FWTPM_DECLARE_BUF(sigBuf, MAX_MLDSA_SIG_SIZE); - FWTPM_DECLARE_BUF(ticketData, FWTPM_MAX_DATA_BUF + sizeof(TPM2B_NAME)); + byte ticketHmac[TPM_MAX_DIGEST_SIZE]; int sigSz = 0; int paramSzPos, paramStart; UINT32 ticketHier = 0; - int ticketDataSz = 0; + int ticketHmacSz = 0; int sigStartPos = 0; TPMT_SIGNATURE classicalSig; - /* Snapshot the computed digest for hash-then-sign verify paths so the - * ticket builder can bind it (Part 2 Sec.10.6.5). Pure ML-DSA leaves - * verifiedDigestSz==0 and falls back to seq->msgBuf. */ - byte verifiedDigest[TPM_MAX_DIGEST_SIZE]; - int verifiedDigestSz = 0; - FWTPM_ALLOC_BUF(sigBuf, MAX_MLDSA_SIG_SIZE); - FWTPM_ALLOC_BUF(ticketData, FWTPM_MAX_DATA_BUF + sizeof(TPM2B_NAME)); XMEMSET(&classicalSig, 0, sizeof(classicalSig)); - XMEMSET(verifiedDigest, 0, sizeof(verifiedDigest)); + XMEMSET(ticketHmac, 0, sizeof(ticketHmac)); if (cmdSize < TPM2_HEADER_SIZE + 8) { rc = TPM_RC_COMMAND_SIZE; @@ -16001,7 +16047,8 @@ static TPM_RC FwCmd_VerifySequenceComplete(FWTPM_CTX* ctx, TPM2_Packet* cmd, * transient slot between Start and Complete by binding to keyName. */ if (rc == 0) { if (keyObj->name.size == 0) FwComputeObjectName(keyObj); - if (keyObj->name.size != seq->keyName.size || + if (keyObj->hierarchy != seq->ticketHierarchy || + keyObj->name.size != seq->keyName.size || XMEMCMP(keyObj->name.name, seq->keyName.name, keyObj->name.size) != 0) { rc = TPM_RC_SIGN_CONTEXT_KEY; @@ -16104,8 +16151,7 @@ static TPM_RC FwCmd_VerifySequenceComplete(FWTPM_CTX* ctx, TPM2_Packet* cmd, sigBuf, sigSz); } else if (sigAlg == TPM_ALG_HASH_MLDSA) { - /* Finalize accumulated hash, snapshot it for the ticket - * builder, then verify. */ + /* Finalize the accumulated hash, then verify. */ byte digestOut[TPM_MAX_DIGEST_SIZE]; int digestSz; enum wc_HashType wcHash = FwGetWcHashType(seq->hashAlg); @@ -16126,8 +16172,6 @@ static TPM_RC FwCmd_VerifySequenceComplete(FWTPM_CTX* ctx, TPM2_Packet* cmd, } if (rc == 0) { digestSz = TPM2_GetHashDigestSize(seq->hashAlg); - XMEMCPY(verifiedDigest, digestOut, digestSz); - verifiedDigestSz = digestSz; rc = FwVerifyMldsaHash( keyObj->pub.parameters.hash_mldsaDetail.parameterSet, &keyObj->pub.unique.mldsa, @@ -16159,10 +16203,6 @@ static TPM_RC FwCmd_VerifySequenceComplete(FWTPM_CTX* ctx, TPM2_Packet* cmd, (word32)digestSz) != 0) { rc = TPM_RC_SIGNATURE; } - else { - XMEMCPY(verifiedDigest, hmacOut, digestSz); - verifiedDigestSz = digestSz; - } } TPM2_ForceZero(hmacOut, sizeof(hmacOut)); } @@ -16202,8 +16242,6 @@ static TPM_RC FwCmd_VerifySequenceComplete(FWTPM_CTX* ctx, TPM2_Packet* cmd, } if (rc == 0) { digestSz = TPM2_GetHashDigestSize(seq->hashAlg); - XMEMCPY(verifiedDigest, digestOut, digestSz); - verifiedDigestSz = digestSz; rc = FwVerifySignatureCore(keyObj, digestOut, digestSz, &classicalSig); } @@ -16218,52 +16256,41 @@ static TPM_RC FwCmd_VerifySequenceComplete(FWTPM_CTX* ctx, TPM2_Packet* cmd, * Sec.10.6.5 Table 112 the ticket hierarchy is the hierarchy of * keyName, and Eq (5) requires the HMAC use that hierarchy's * proofValue. Pull the value captured at object load/create time. */ - ticketHier = keyObj->hierarchy; + ticketHier = seq->ticketHierarchy; if (keyObj->name.size == 0) { FwComputeObjectName(keyObj); } - /* Hash-then-sign verify (Hash-ML-DSA, RSA, ECC) binds the - * computed digest per the existing TPM2_VerifySignature pattern; - * Pure ML-DSA has no digest, so it binds the raw message accumulated - * in seq->msgBuf (capped at FWTPM_MAX_DATA_BUF). */ - if (verifiedDigestSz > 0) { - XMEMCPY(ticketData, verifiedDigest, (size_t)verifiedDigestSz); - ticketDataSz = verifiedDigestSz; - } - else if (seq->msgBufSz <= FWTPM_SIZEOF_BUF(ticketData, - FWTPM_MAX_DATA_BUF + sizeof(TPM2B_NAME))) { - XMEMCPY(ticketData, seq->msgBuf, seq->msgBufSz); - ticketDataSz = (int)seq->msgBufSz; - } - else { - rc = TPM_RC_FAILURE; - } - if (rc == 0 && - ticketDataSz + keyObj->name.size <= (int)FWTPM_SIZEOF_BUF( - ticketData, FWTPM_MAX_DATA_BUF + sizeof(TPM2B_NAME))) { - XMEMCPY(ticketData + ticketDataSz, - keyObj->name.name, keyObj->name.size); - ticketDataSz += keyObj->name.size; - } - else if (rc == 0) { - rc = TPM_RC_FAILURE; - } - /* Per Part 3 Sec.20.3.1 + Part 2 Sec.10.6.5 Table 111: every * successful TPM2_VerifySequenceComplete response SHALL carry * tag = TPM_ST_MESSAGE_VERIFIED regardless of signing scheme, * with TPMU_TK_VERIFIED_META = TPMS_EMPTY (no wire bytes). * Digest-verification tickets live on TPM2_VerifyDigestSignature, - * not here. */ - if (rc == 0) { - rc = FwAppendTicket(ctx, rsp, - TPM_ST_MESSAGE_VERIFIED, - ticketHier, - CONTEXT_INTEGRITY_HASH_ALG, - ticketData, ticketDataSz, - NULL, 0); + * not here. SequenceUpdate has already streamed the raw message into + * ticketHmacCtx; append keyName to complete Eq (5). */ + if (ticketHier == TPM_RH_NULL) { + TPM2_Packet_AppendU16(rsp, TPM_ST_MESSAGE_VERIFIED); + TPM2_Packet_AppendU32(rsp, TPM_RH_NULL); + TPM2_Packet_AppendU16(rsp, 0); + } + else if (!seq->ticketHmacCtxInit) { + rc = TPM_RC_FAILURE; + } + else { + if (wc_HmacUpdate(&seq->ticketHmacCtx, + keyObj->name.name, keyObj->name.size) != 0 || + wc_HmacFinal(&seq->ticketHmacCtx, ticketHmac) != 0) { + rc = TPM_RC_FAILURE; + } + if (rc == 0) { + ticketHmacSz = TPM2_GetHashDigestSize( + CONTEXT_INTEGRITY_HASH_ALG); + TPM2_Packet_AppendU16(rsp, TPM_ST_MESSAGE_VERIFIED); + TPM2_Packet_AppendU32(rsp, ticketHier); + TPM2_Packet_AppendU16(rsp, (UINT16)ticketHmacSz); + TPM2_Packet_AppendBytes(rsp, ticketHmac, ticketHmacSz); + } } FwRspParamsEnd(rsp, cmdTag, paramSzPos, paramStart); @@ -16277,8 +16304,7 @@ static TPM_RC FwCmd_VerifySequenceComplete(FWTPM_CTX* ctx, TPM2_Packet* cmd, FwFreeSignSeq(seq); } - TPM2_ForceZero(verifiedDigest, sizeof(verifiedDigest)); - FWTPM_FREE_BUF(ticketData); + TPM2_ForceZero(ticketHmac, sizeof(ticketHmac)); FWTPM_FREE_BUF(sigBuf); return rc; } diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 2d088b23..765cae8c 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -6104,13 +6104,13 @@ static void test_fwtpm_getcap_pqc_algorithm_attrs(void) fwtpm_pass("GetCap ALGS PQC signing/encrypting bits:", 1); } -/* Hash-ML-DSA verify ticket must bind the verified digest, not just +/* Hash-ML-DSA verify ticket must bind the verified message, not just * keyName. Pre-fix the ticket data was {keyName} for Hash-ML-DSA * because seq->msgBuf is never populated on that path (SequenceUpdate * routes the bytes into seq->hashCtx). Two distinct messages signed by * the same key produced byte-identical tickets, breaking * TPM2_PolicyAuthorize's chain of trust (Part 2 Sec.10.6.5 Eq (5)). */ -static void test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_binds_digest(void) +static void test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_changes(void) { FWTPM_CTX ctx; int rc, rspSize, pos, cmdSz; @@ -6328,7 +6328,7 @@ static void test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_binds_digest(void) FWTPM_Cleanup(&ctx); FWTPM_FREE_BUF(sigA); FWTPM_FREE_BUF(sigB); - fwtpm_pass("VerifySeqComplete Hash-MLDSA ticket binds digest:", 1); + fwtpm_pass("VerifySeqComplete Hash-MLDSA ticket changes with message:", 1); } /* Per Part 3 Sec.20.3.1 + Part 2 Sec.10.6.5 Table 111: every successful @@ -6485,8 +6485,6 @@ static void test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_binds_message(void) UINT32 signSeqHandle, verifySeqHandle; FWTPM_Object* keyObj; int oi; - wc_HashAlg msgHash; - byte msgDigest[WC_SHA256_DIGEST_SIZE]; FWTPM_ALLOC_BUF(sig, MAX_MLDSA_SIG_SIZE); memset(&ctx, 0, sizeof(ctx)); @@ -6597,10 +6595,8 @@ static void test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_binds_message(void) AssertIntEQ((int)hmacSz <= (int)sizeof(hmac), 1); memcpy(hmac, gRsp + pos, hmacSz); - /* Recompute expected HMAC over SHA-256(msg)||keyName. Hash-then-sign - * sequences bind the computed digest in the ticket (matches the - * TPM2_VerifySignature pattern; supports arbitrary-length sequences). - * FwFindObject is static-local so walk ctx.objects[] directly. */ + /* Recompute the expected HMAC over msg||keyName. FwFindObject is + * static-local, so walk ctx.objects[] directly. */ keyObj = NULL; for (oi = 0; oi < FWTPM_MAX_OBJECTS; oi++) { if (ctx.objects[oi].handle == keyHandle) { @@ -6612,13 +6608,8 @@ static void test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_binds_message(void) if (keyObj->name.size == 0) { FwComputeObjectName(keyObj); } - AssertIntEQ(wc_HashInit(&msgHash, WC_HASH_TYPE_SHA256), 0); - AssertIntEQ(wc_HashUpdate(&msgHash, WC_HASH_TYPE_SHA256, - msg, sizeof(msg) - 1), 0); - AssertIntEQ(wc_HashFinal(&msgHash, WC_HASH_TYPE_SHA256, msgDigest), 0); - wc_HashFree(&msgHash, WC_HASH_TYPE_SHA256); - memcpy(ticketData, msgDigest, sizeof(msgDigest)); - ticketDataSz = sizeof(msgDigest); + memcpy(ticketData, msg, sizeof(msg) - 1); + ticketDataSz = sizeof(msg) - 1; memcpy(ticketData + ticketDataSz, keyObj->name.name, keyObj->name.size); ticketDataSz += keyObj->name.size; @@ -6633,7 +6624,7 @@ static void test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_binds_message(void) FWTPM_Cleanup(&ctx); FWTPM_FREE_BUF(sig); - fwtpm_pass("VerifySeqComplete Hash-MLDSA ticket binds DIGEST:", 1); + fwtpm_pass("VerifySeqComplete Hash-MLDSA ticket binds MESSAGE:", 1); } /* Per Part 3 Sec.20.6.1 a restricted signing key MUST NOT sign a message @@ -12882,7 +12873,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_verifyseqcomplete_no_sessions_returns_auth_missing(); test_fwtpm_verifydigestsig_no_sign_attr_returns_key(); test_fwtpm_getcap_pqc_algorithm_attrs(); - test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_binds_digest(); + test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_changes(); test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_tag_digest(); test_fwtpm_verifyseqcomplete_hash_mldsa_ticket_binds_message(); test_fwtpm_signseqcomplete_hash_mldsa_genvalue_via_update_returns_value(); diff --git a/wolftpm/fwtpm/fwtpm.h b/wolftpm/fwtpm/fwtpm.h index 629c3a61..19fe750e 100644 --- a/wolftpm/fwtpm/fwtpm.h +++ b/wolftpm/fwtpm/fwtpm.h @@ -569,6 +569,10 @@ typedef struct FWTPM_SignSeq { /* HMAC accumulator for KEYEDHASH (HMAC) signing/verifying sequences. */ Hmac hmacCtx; int hmacCtxInit; /* 1 when hmacCtx is live */ + /* Incremental MESSAGE_VERIFIED ticket HMAC for verify sequences. */ + Hmac ticketHmacCtx; + int ticketHmacCtxInit; + UINT32 ticketHierarchy; #endif } FWTPM_SignSeq;