From fa616ac090edacef74b788ac148d3e52ecbfe2c6 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:13:11 -0700 Subject: [PATCH 01/22] =?UTF-8?q?F-4157=20=E2=80=94=20Fix=20native=5Ftest?= =?UTF-8?q?=20hash=20check=20using=20&&=20instead=20of=20||?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/native/native_test.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/native/native_test.c b/examples/native/native_test.c index 79eec0e9..41a43df0 100644 --- a/examples/native/native_test.c +++ b/examples/native/native_test.c @@ -963,7 +963,7 @@ int TPM2_Native_TestArgs(void* userCtx, int argc, char *argv[]) TPM2_GetRCString(rc)); goto exit; } - if (cmdOut.seqComp.result.size != TPM_SHA256_DIGEST_SIZE && + if (cmdOut.seqComp.result.size != TPM_SHA256_DIGEST_SIZE || XMEMCMP(cmdOut.seqComp.result.buffer, hashTestDig, TPM_SHA256_DIGEST_SIZE) != 0) { printf("Hash SHA256 test failed, result not as expected!\n"); From 5477be2125d44f849683bcae601e238da04c8e84 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:13:37 -0700 Subject: [PATCH 02/22] =?UTF-8?q?F-4622=20=E2=80=94=20Pass=20full=20hex=20?= =?UTF-8?q?length=20to=20hexToByte=20in=20policy.c=20-digest=3D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/pcr/policy.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pcr/policy.c b/examples/pcr/policy.c index ad2600c7..acc72cd2 100644 --- a/examples/pcr/policy.c +++ b/examples/pcr/policy.c @@ -109,7 +109,7 @@ int TPM2_PCR_Policy_Test(void* userCtx, int argc, char *argv[]) usage(); return 0; } - hexRet = hexToByte(digestStr, digest, digestLen / 2); + hexRet = hexToByte(digestStr, digest, digestLen); if (hexRet < 0) { printf("Invalid hex digest string\n"); usage(); From bcb3f579790a46fd9b14445cd1a33706640ec438 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:13:58 -0700 Subject: [PATCH 03/22] =?UTF-8?q?F-4623=20=E2=80=94=20Reject=20st33=5Ffw?= =?UTF-8?q?=5Fupdate=20blob=5Flen=20>=202048=20before=20XMEMCPY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/firmware/st33_fw_update.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/firmware/st33_fw_update.c b/examples/firmware/st33_fw_update.c index 55a53c5e..2120df3d 100644 --- a/examples/firmware/st33_fw_update.c +++ b/examples/firmware/st33_fw_update.c @@ -97,6 +97,13 @@ static int TPM2_ST33_SendFirmwareData(fw_info_t* fwinfo) blob_len = ((uint32_t)blob_header[1] << 8) | blob_header[2]; blob_total = blob_len + 3; + if (blob_len > 2048) { + printf("Error: Blob length %u exceeds maximum 2048 at offset %u\n", + blob_len, offset); + rc = BUFFER_E; + break; + } + if (offset + blob_total > fwinfo->firmware_bufSz) { printf("Error: Incomplete blob at offset %u\n", offset); rc = BUFFER_E; From 6c1b690914deef70e4f9b1b5930d5a338b79b8c5 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:15:36 -0700 Subject: [PATCH 04/22] =?UTF-8?q?F-4743=20=E2=80=94=20Guard=20mod=5Flen=20?= =?UTF-8?q?in=20TPM2=5FASN=5FDecodeRsaPubKey=20to=20prevent=20underflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tpm2_asn.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/tpm2_asn.c b/src/tpm2_asn.c index 1abe5aa9..6e4ec27b 100644 --- a/src/tpm2_asn.c +++ b/src/tpm2_asn.c @@ -317,6 +317,15 @@ int TPM2_ASN_DecodeRsaPubKey(uint8_t* input, int inputSz, if (rc == 0) { rc = TPM2_ASN_DecodeTag(input, inputSz, &idx, &mod_len, TPM2_ASN_INTEGER); } + if (rc == 0) { + /* Validate mod_len and idx before accessing input buffer. Without + * this guard a length-0 INTEGER followed by no bytes would let + * mod_len underflow from 0 to -1 below, bypassing the size check + * via signed comparison and passing SIZE_MAX to XMEMCPY. */ + if (mod_len <= 0 || idx >= inputSz) { + rc = -1; + } + } if (rc == 0) { if (input[idx] == 0x00) { idx++; From 17e4cbc5f6c26baf94928b94ae7e4bec8e430347 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:16:00 -0700 Subject: [PATCH 05/22] =?UTF-8?q?F-4674=20=E2=80=94=20Reject=20NV=20journa?= =?UTF-8?q?l=20nvPublic.dataSize=20>=20FWTPM=5FMAX=5FNV=5FDATA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_nv.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index 66314a0f..359d850f 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -410,6 +410,13 @@ static int FwNvUnmarshalNvPublic(const byte* buf, word32* pos, word32 maxSz, if (rc == 0) { rc = FwNvUnmarshalU16(buf, pos, maxSz, &nvPub->dataSize); } + /* nv->data[] is sized to FWTPM_MAX_NV_DATA; rejecting an inflated + * dataSize here prevents the later NV_Write bounds check from + * comparing offset+size against an attacker-chosen ceiling and + * overrunning the fixed destination buffer. */ + if (rc == 0 && nvPub->dataSize > FWTPM_MAX_NV_DATA) { + rc = TPM_RC_FAILURE; + } return rc; } From c1f703294131b3f14f02c5fb85ee64dd96797629 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:17:49 -0700 Subject: [PATCH 06/22] =?UTF-8?q?F-4377=20=E2=80=94=20Reject=20NULL-hierar?= =?UTF-8?q?chy=20ticket=20in=20FwCmd=5FPolicyAuthorize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 7 ++++++ tests/fwtpm_unit_tests.c | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 0a81c5ac..5b5629f2 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -8792,6 +8792,13 @@ static TPM_RC FwCmd_PolicyAuthorize(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0 && ticketTag != TPM_ST_VERIFIED) { rc = TPM_RC_TICKET; } + /* Per TPM 2.0 Part 3 Sec.23.16, a TPMT_TK_VERIFIED with + * hierarchy == TPM_RH_NULL is invalid input. Reject before + * HMAC verification to prevent a NULL-hierarchy proofValue + * being fed into FwComputeTicketHmac. */ + if (rc == 0 && ticketHier == TPM_RH_NULL) { + rc = TPM_RC_HIERARCHY; + } /* Verify ticket HMAC per TPM 2.0 Part 3 Section 23.16: * 1. Compute aHash = H(approvedPolicy || policyRef) * 2. Ticket from VerifySignature is HMAC(proofValue, aHash || keyName) diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 2175009c..280ea4c2 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -6776,6 +6776,56 @@ static void test_fwtpm_policy_pcr(void) FWTPM_Cleanup(&ctx); fwtpm_pass("PolicyPCR:", 0); } + +/* Per TPM 2.0 Part 3 Sec.23.16, a TPMT_TK_VERIFIED with hierarchy == + * TPM_RH_NULL is invalid input and must be rejected with + * TPM_RC_HIERARCHY. Without this guard the handler proceeds into HMAC + * verification with a NULL-hierarchy proofValue, which an attacker can + * pair with a zero-digest ticket to fabricate PolicyAuthorize extensions. */ +static void test_fwtpm_policy_authorize_null_hierarchy_rejected(void) +{ + FWTPM_CTX ctx; + UINT32 sessH; + int pos, rspSize; + byte fakeKeyName[34]; + byte fakeDigest[32]; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + sessH = StartSessionHelper(&ctx, TPM_SE_POLICY); + AssertIntNE(sessH, 0); + + PutU16BE(fakeKeyName, TPM_ALG_SHA256); + memset(fakeKeyName + 2, 0xCD, 32); + memset(fakeDigest, 0xEF, sizeof(fakeDigest)); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_PolicyAuthorize); pos += 4; + PutU32BE(gCmd + pos, sessH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU16BE(gCmd + pos, 32); pos += 2; + memset(gCmd + pos, 0, 32); pos += 32; /* approvedPolicy */ + PutU16BE(gCmd + pos, 0); pos += 2; /* policyRef */ + PutU16BE(gCmd + pos, sizeof(fakeKeyName)); pos += 2; + memcpy(gCmd + pos, fakeKeyName, sizeof(fakeKeyName)); + pos += sizeof(fakeKeyName); + /* TPMT_TK_VERIFIED: tag | hierarchy=NULL | digest(32 garbage bytes) */ + PutU16BE(gCmd + pos, TPM_ST_VERIFIED); pos += 2; + PutU32BE(gCmd + pos, TPM_RH_NULL); pos += 4; + PutU16BE(gCmd + pos, sizeof(fakeDigest)); pos += 2; + memcpy(gCmd + pos, fakeDigest, sizeof(fakeDigest)); + pos += sizeof(fakeDigest); + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_HIERARCHY); + + FlushHandle(&ctx, sessH); + FWTPM_Cleanup(&ctx); + fwtpm_pass("PolicyAuthorize NULL hierarchy (HIERARCHY):", 0); +} #endif /* !FWTPM_NO_POLICY */ /* ================================================================== */ @@ -7963,6 +8013,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_policy_command_code(); test_fwtpm_policy_locality(); test_fwtpm_policy_pcr(); + test_fwtpm_policy_authorize_null_hierarchy_rejected(); #endif /* NV operations */ From 75216a3db076c5f4c1ee11c12b436f4de7fdb306 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:19:31 -0700 Subject: [PATCH 07/22] =?UTF-8?q?F-4744=20=E2=80=94=20Reject=20zero-digest?= =?UTF-8?q?=20creation=20ticket=20in=20FwCmd=5FCertifyCreation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 9 ++++++++- tests/fwtpm_unit_tests.c | 9 ++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 5b5629f2..cb39d02f 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -12006,7 +12006,14 @@ static TPM_RC FwCmd_CertifyCreation(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0 && tag != TPM_ST_CREATION) { rc = TPM_RC_TICKET; } - if (rc == 0 && tickDSz > 0) { + /* A zero-length ticket digest cannot bind the creationHash to the + * object name. Without this guard, the attestation embeds the + * caller-supplied creationHash verbatim with no cryptographic + * proof of provenance. */ + if (rc == 0 && tickDSz == 0) { + rc = TPM_RC_TICKET; + } + if (rc == 0) { byte ticketData[TPM_MAX_DIGEST_SIZE + sizeof(TPM2B_NAME)]; int ticketDataSz = 0; byte expectedHmac[TPM_MAX_DIGEST_SIZE]; diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 280ea4c2..8ece8270 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7075,8 +7075,11 @@ static void test_fwtpm_certify_creation_ecdaa_scheme(void) /* Build TPM2_CertifyCreation with ECDAA inScheme. Use the same key as * both signHandle and objectHandle to avoid additional setup. The - * ticket carries hier=TPM_RH_NULL with a zero digest so HMAC - * validation is skipped (only the tag is checked). */ + * ticket carries hier=TPM_RH_NULL with a zero digest, which must be + * rejected with TPM_RC_TICKET — reaching that reject still proves + * the ECDAA inScheme parser consumed the extra UINT16 count, since + * a wrong-shape scheme parse would have failed earlier with a + * different code. */ pos = 0; PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; PutU32BE(gCmd + pos, 0); pos += 4; @@ -7099,7 +7102,7 @@ static void test_fwtpm_certify_creation_ecdaa_scheme(void) PutU32BE(gCmd + 2, (UINT32)pos); rspSize = 0; FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); - AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_TICKET); FlushHandle(&ctx, keyH); FWTPM_Cleanup(&ctx); From a8d7c84bed5198bcedd21865af2cd94651fba913 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:27:00 -0700 Subject: [PATCH 08/22] =?UTF-8?q?F-4745=20=E2=80=94=20Look=20up=20ctx->pcr?= =?UTF-8?q?Auth=20in=20FwLookupEntityAuth=20for=20PCR=20handles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 8 ++++ tests/fwtpm_unit_tests.c | 81 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index cb39d02f..8effcc40 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -416,6 +416,14 @@ static void FwLookupEntityAuth(FWTPM_CTX* ctx, TPM_HANDLE handle, } } #endif /* !FWTPM_NO_NV */ + else if (handle >= PCR_FIRST && handle <= PCR_LAST) { + /* PCR handles carry per-index authValue set by PCR_SetAuthValue. + * Without this case the password verifier resolves them to + * authSz=0 and any empty-password caller passes the compare. */ + int pcrIdx = (int)(handle - PCR_FIRST); + *authVal = ctx->pcrAuth[pcrIdx].buffer; + *authValSz = (int)ctx->pcrAuth[pcrIdx].size; + } else { FWTPM_Object* objEnt = FwFindObject(ctx, handle); if (objEnt != NULL) { diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 8ece8270..84ead7aa 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -792,6 +792,86 @@ static void test_fwtpm_pcr_extend_and_read(void) fwtpm_pass("PCR_Extend + Read(16):", 0); } +/* Per TPM 2.0 Part 3 Sec.22.3, PCR_Extend takes Auth Role USER on the + * PCR handle. When PCR_SetAuthValue has installed a non-empty + * authValue, a subsequent password session with hmacSize=0 must be + * rejected — the password lookup must consult ctx->pcrAuth[idx] not + * fall through to an empty default. */ +static void test_fwtpm_pcr_extend_empty_pw_rejected_after_setauth(void) +{ + FWTPM_CTX ctx; + int rc, rspSize, cmdSz; + static const byte pcrAuth[] = "pcr-auth-secret-32-bytes-aaaaaaa"; + const int pcrAuthSz = (int)sizeof(pcrAuth) - 1; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + /* PCR_SetAuthValue(PCR 16, authValue). Auth area is empty-PW + * (PCR 16 starts with no authValue). */ + cmdSz = 0; + PutU16BE(gCmd + cmdSz, TPM_ST_SESSIONS); cmdSz += 2; + PutU32BE(gCmd + cmdSz, 0); cmdSz += 4; + PutU32BE(gCmd + cmdSz, TPM_CC_PCR_SetAuthValue); cmdSz += 4; + PutU32BE(gCmd + cmdSz, 16); cmdSz += 4; + PutU32BE(gCmd + cmdSz, 9); cmdSz += 4; + PutU32BE(gCmd + cmdSz, TPM_RS_PW); cmdSz += 4; + PutU16BE(gCmd + cmdSz, 0); cmdSz += 2; + gCmd[cmdSz++] = 0; + PutU16BE(gCmd + cmdSz, 0); cmdSz += 2; + PutU16BE(gCmd + cmdSz, (UINT16)pcrAuthSz); cmdSz += 2; + memcpy(gCmd + cmdSz, pcrAuth, pcrAuthSz); + cmdSz += pcrAuthSz; + PutU32BE(gCmd + 2, (UINT32)cmdSz); + rspSize = 0; + rc = FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(rc, TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + /* PCR_Extend(PCR 16) with empty password — must be rejected. */ + cmdSz = 0; + PutU16BE(gCmd + cmdSz, TPM_ST_SESSIONS); cmdSz += 2; + PutU32BE(gCmd + cmdSz, 0); cmdSz += 4; + PutU32BE(gCmd + cmdSz, TPM_CC_PCR_Extend); cmdSz += 4; + PutU32BE(gCmd + cmdSz, 16); cmdSz += 4; + PutU32BE(gCmd + cmdSz, 9); cmdSz += 4; + PutU32BE(gCmd + cmdSz, TPM_RS_PW); cmdSz += 4; + PutU16BE(gCmd + cmdSz, 0); cmdSz += 2; + gCmd[cmdSz++] = 0; + PutU16BE(gCmd + cmdSz, 0); cmdSz += 2; + PutU32BE(gCmd + cmdSz, 1); cmdSz += 4; + PutU16BE(gCmd + cmdSz, TPM_ALG_SHA256); cmdSz += 2; + memset(gCmd + cmdSz, 0x42, 32); cmdSz += 32; + PutU32BE(gCmd + 2, (UINT32)cmdSz); + rspSize = 0; + rc = FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(rc, TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_AUTH_FAIL); + + /* Restore PCR 16 to empty auth so the NV journal doesn't contaminate + * later tests that share FWTPM_NV_FILE. */ + cmdSz = 0; + PutU16BE(gCmd + cmdSz, TPM_ST_SESSIONS); cmdSz += 2; + PutU32BE(gCmd + cmdSz, 0); cmdSz += 4; + PutU32BE(gCmd + cmdSz, TPM_CC_PCR_SetAuthValue); cmdSz += 4; + PutU32BE(gCmd + cmdSz, 16); cmdSz += 4; + PutU32BE(gCmd + cmdSz, 9 + pcrAuthSz); cmdSz += 4; + PutU32BE(gCmd + cmdSz, TPM_RS_PW); cmdSz += 4; + PutU16BE(gCmd + cmdSz, 0); cmdSz += 2; + gCmd[cmdSz++] = 0; + PutU16BE(gCmd + cmdSz, (UINT16)pcrAuthSz); cmdSz += 2; + memcpy(gCmd + cmdSz, pcrAuth, pcrAuthSz); cmdSz += pcrAuthSz; + PutU16BE(gCmd + cmdSz, 0); cmdSz += 2; /* new auth.size = 0 */ + PutU32BE(gCmd + 2, (UINT32)cmdSz); + rspSize = 0; + rc = FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(rc, TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + FWTPM_Cleanup(&ctx); + fwtpm_pass("PCR_Extend empty-pw after SetAuth (AUTH_FAIL):", 0); +} + /* ================================================================== */ /* 7. ReadClock */ /* ================================================================== */ @@ -7887,6 +7967,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) /* PCR operations */ test_fwtpm_pcr_read(); test_fwtpm_pcr_extend_and_read(); + test_fwtpm_pcr_extend_empty_pw_rejected_after_setauth(); test_fwtpm_pcr_reset(); test_fwtpm_pcr_event(); From 566faa55c0d396b69798ab0065b186de53b1194f Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:28:13 -0700 Subject: [PATCH 09/22] =?UTF-8?q?F-4746=20=E2=80=94=20Reject=20out-of-rang?= =?UTF-8?q?e=20persistentHandle=20in=20FwCmd=5FEvictControl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 10 ++++++++++ tests/fwtpm_unit_tests.c | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 8effcc40..db27ef34 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -3675,6 +3675,16 @@ static TPM_RC FwCmd_EvictControl(FWTPM_CTX* ctx, TPM2_Packet* cmd, TPM2_Packet_ParseU32(cmd, &persistentHandle); } + /* Per TPM 2.0 Part 2 Sec.7.4, persistent handles MUST fall in the + * 0x81000000..0x81FFFFFF range. Storing an out-of-range value would + * let a later FwFindObject lookup mistakenly resolve to a transient + * slot and serve attacker-controlled key material. */ + if (rc == 0 && + (persistentHandle < PERSISTENT_FIRST || + persistentHandle > PERSISTENT_LAST)) { + rc = TPM_RC_VALUE; + } + /* Validate auth handle: owner or platform required by spec, * endorsement also accepted for EH-created objects */ if (rc == 0 && authHandle != TPM_RH_OWNER && diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 84ead7aa..09c66b95 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7735,6 +7735,44 @@ static void test_fwtpm_evict_control(void) fwtpm_pass("EvictControl (persist/remove):", 0); } +/* Per TPM 2.0 Part 2 Sec.7.4, persistent handles must fall in + * 0x81000000..0x81FFFFFF. A persistentHandle outside this range must + * be rejected so an attacker cannot plant a persistent record at a + * transient-range handle that FwFindObject would later resolve. */ +static void test_fwtpm_evict_control_bad_persistent_handle_rejected(void) +{ + FWTPM_CTX ctx; + int pos, rspSize; + UINT32 keyH; + UINT32 badPersH = 0x80000010; /* transient range */ + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + +#ifdef HAVE_ECC + keyH = CreatePrimaryHelper(&ctx, TPM_ALG_ECC); +#else + keyH = CreatePrimaryHelper(&ctx, TPM_ALG_RSA); +#endif + AssertIntNE(keyH, 0); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_EvictControl); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + PutU32BE(gCmd + pos, keyH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU32BE(gCmd + pos, badPersH); pos += 4; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_VALUE); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + fwtpm_pass("EvictControl bad persistentHandle (VALUE):", 0); +} + static void test_fwtpm_clock_set(void) { FWTPM_CTX ctx; @@ -8068,6 +8106,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) #endif test_fwtpm_read_public(); test_fwtpm_evict_control(); + test_fwtpm_evict_control_bad_persistent_handle_rejected(); test_fwtpm_context_save(); /* Crypto */ From f1117a609f104bb122bba03cfafb163892ea2c7f Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:29:15 -0700 Subject: [PATCH 10/22] =?UTF-8?q?F-4747=20=E2=80=94=20Reject=20persistent?= =?UTF-8?q?=20objectHandle=20in=20EvictControl=20make-persistent=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 14 ++++++-- tests/fwtpm_unit_tests.c | 72 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index db27ef34..8166ebdb 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -3719,10 +3719,20 @@ static TPM_RC FwCmd_EvictControl(FWTPM_CTX* ctx, TPM2_Packet* cmd, } /* objectHandle is transient -> make persistent */ else if (rc == 0) { - obj = FwFindObject(ctx, objectHandle); - if (obj == NULL) { + /* Per TPM 2.0 Part 3 Sec.28, objectHandle for the make-persistent + * form MUST be a loaded transient. Accepting a persistent handle + * here lets a caller clone an existing persistent record into a + * new slot, exhaust the persistent table, and serve attacker- + * controlled key material at a fresh handle. */ + if ((objectHandle & 0xFF000000) != 0x80000000) { rc = TPM_RC_HANDLE; } + if (rc == 0) { + obj = FwFindObject(ctx, objectHandle); + if (obj == NULL) { + rc = TPM_RC_HANDLE; + } + } /* Check if persistent handle already in use */ if (rc == 0) { diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 09c66b95..42478f40 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7773,6 +7773,77 @@ static void test_fwtpm_evict_control_bad_persistent_handle_rejected(void) fwtpm_pass("EvictControl bad persistentHandle (VALUE):", 0); } +/* Per TPM 2.0 Part 3 Sec.28, the make-persistent form of EvictControl + * requires objectHandle to be a loaded transient. Passing a persistent + * objectHandle that differs from persistentHandle would otherwise let + * a caller clone a persistent record into a fresh persistent slot. */ +static void test_fwtpm_evict_control_persistent_object_rejected(void) +{ + FWTPM_CTX ctx; + int pos, rspSize; + UINT32 keyH; + UINT32 persH = 0x81000002; + UINT32 cloneH = 0x81000003; + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + +#ifdef HAVE_ECC + keyH = CreatePrimaryHelper(&ctx, TPM_ALG_ECC); +#else + keyH = CreatePrimaryHelper(&ctx, TPM_ALG_RSA); +#endif + AssertIntNE(keyH, 0); + + /* First make the transient persistent at persH. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_EvictControl); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + PutU32BE(gCmd + pos, keyH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU32BE(gCmd + pos, persH); pos += 4; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + FlushHandle(&ctx, keyH); + + /* Now attempt to clone the persistent record into a new persistent + * slot by passing the persistent handle as objectHandle (which is + * not the same as persistentHandle, so the evict branch is not + * taken). Must be rejected with TPM_RC_HANDLE. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_EvictControl); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + PutU32BE(gCmd + pos, persH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU32BE(gCmd + pos, cloneH); pos += 4; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_HANDLE); + + /* Clean up persH so the NV journal doesn't leak. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_EvictControl); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + PutU32BE(gCmd + pos, persH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU32BE(gCmd + pos, persH); pos += 4; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + FWTPM_Cleanup(&ctx); + fwtpm_pass("EvictControl persistent object reject (HANDLE):", 0); +} + static void test_fwtpm_clock_set(void) { FWTPM_CTX ctx; @@ -8107,6 +8178,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_read_public(); test_fwtpm_evict_control(); test_fwtpm_evict_control_bad_persistent_handle_rejected(); + test_fwtpm_evict_control_persistent_object_rejected(); test_fwtpm_context_save(); /* Crypto */ From b3ca0273ea1288fca84714a956c8c36488f05d9e Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:30:18 -0700 Subject: [PATCH 11/22] =?UTF-8?q?F-4750=20=E2=80=94=20Enforce=20authPolicy?= =?UTF-8?q?.size=20matches=20hashAlg=20in=20FwCmd=5FSetPrimaryPolicy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 8 ++++++++ tests/fwtpm_unit_tests.c | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 8166ebdb..91fa2d3b 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -3594,6 +3594,14 @@ static TPM_RC FwCmd_SetPrimaryPolicy(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (policySz > 0 && TPM2_GetHashDigestSize(hashAlg) <= 0) { rc = TPM_RC_HASH; } + /* Per TPM 2.0 Part 3 Sec.23.1, authPolicy.size must equal the + * digest size of hashAlg. A mismatched length installs a policy + * whose size never matches any legitimate session policyDigest, + * permanently locking the hierarchy out of policy-based access. */ + if (rc == 0 && policySz > 0 && + (int)policySz != TPM2_GetHashDigestSize(hashAlg)) { + rc = TPM_RC_SIZE; + } if (policySz == 0) { hashAlg = TPM_ALG_NULL; } diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 42478f40..3ef5b10d 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7479,6 +7479,40 @@ static void test_fwtpm_hierarchy_change_auth(void) fwtpm_pass("HierarchyChangeAuth:", 0); } +/* Per TPM 2.0 Part 3 Sec.23.1, authPolicy.size in SetPrimaryPolicy must + * equal the digest size of hashAlg. A mismatched length installs a + * policy whose size never matches any legitimate session policyDigest, + * permanently denying policy-based access to the hierarchy. */ +static void test_fwtpm_set_primary_policy_bad_size_rejected(void) +{ + FWTPM_CTX ctx; + int pos, rspSize; + byte badPolicy[64]; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + memset(badPolicy, 0xAB, sizeof(badPolicy)); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_SetPrimaryPolicy); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + /* authPolicy: 64 bytes, but hashAlg = SHA256 (expects 32) */ + PutU16BE(gCmd + pos, sizeof(badPolicy)); pos += 2; + memcpy(gCmd + pos, badPolicy, sizeof(badPolicy)); + pos += sizeof(badPolicy); + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SIZE); + + FWTPM_Cleanup(&ctx); + fwtpm_pass("SetPrimaryPolicy size mismatch (SIZE):", 0); +} + static void test_fwtpm_clear(void) { FWTPM_CTX ctx; @@ -8195,6 +8229,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) /* Auth */ test_fwtpm_hierarchy_change_auth(); + test_fwtpm_set_primary_policy_bad_size_rejected(); #ifndef FWTPM_NO_DA test_fwtpm_da_parameters_and_reset(); #endif From f27c3b17c37365cf499b566a33247cf9b688ac06 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:30:47 -0700 Subject: [PATCH 12/22] =?UTF-8?q?F-4749=20=E2=80=94=20Reject=20NV=20authPo?= =?UTF-8?q?licy.size=20that=20does=20not=20match=20nameAlg=20digest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_nv.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index 359d850f..78e09e75 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -407,6 +407,15 @@ static int FwNvUnmarshalNvPublic(const byte* buf, word32* pos, word32 maxSz, if (rc == 0) { rc = FwNvUnmarshalDigest(buf, pos, maxSz, &nvPub->authPolicy); } + /* Per TPM 2.0 Part 3 Sec.31.3, authPolicy.size must equal the digest + * size of nameAlg. A mismatched length installs a policy whose size + * never matches any legitimate session policyDigest, permanently + * denying policy-based access to this NV index. */ + if (rc == 0 && nvPub->authPolicy.size > 0 && + (int)nvPub->authPolicy.size != + TPM2_GetHashDigestSize(nvPub->nameAlg)) { + rc = TPM_RC_FAILURE; + } if (rc == 0) { rc = FwNvUnmarshalU16(buf, pos, maxSz, &nvPub->dataSize); } From eb8723634dc6518321b019f34f3470413a6db747 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:31:27 -0700 Subject: [PATCH 13/22] =?UTF-8?q?F-4748=20=E2=80=94=20Discard=20NV=20hiera?= =?UTF-8?q?rchy=20policy=20with=20size=20!=3D=20alg=20digest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_nv.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index 78e09e75..2d63bf8d 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -848,6 +848,16 @@ static int FwNvProcessEntry(FWTPM_CTX* ctx, UINT16 tag, FwNvUnmarshalU32(value, &vPos, vMax, &hier); FwNvUnmarshalU16(value, &vPos, vMax, &alg); FwNvUnmarshalDigest(value, &vPos, vMax, &policy); + /* Per TPM 2.0 Part 3 Sec.23.1, policy.size must equal the + * digest size of alg. Discard a journal entry where the + * sizes diverge so it cannot lock the hierarchy out by + * forcing every legitimate policy session to fail the + * size check at policyDigest enforcement time. */ + if (policy.size > 0 && + (int)policy.size != TPM2_GetHashDigestSize(alg)) { + XMEMSET(&policy, 0, sizeof(policy)); + break; + } switch (hier) { case TPM_RH_OWNER: XMEMCPY(&ctx->ownerPolicy, &policy, From 2bd2f8951c848b0cd548f45b78ddf26c921532c7 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:32:19 -0700 Subject: [PATCH 14/22] =?UTF-8?q?F-4376=20=E2=80=94=20Reject=20policy=20se?= =?UTF-8?q?ssion=20with=20empty=20HMAC=20when=20entity=20authPolicy=20is?= =?UTF-8?q?=20empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 91fa2d3b..29fd17c1 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -15484,6 +15484,22 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx, return TPM_RC_SUCCESS; } } + else if (authPolicy != NULL && authPolicy->size == 0 && + cmdAuths[pj].cmdHmacSize == 0) { + /* Per TPM 2.0 Part 1 Sec.19.7, a policy session can only + * authorize an entity whose authPolicy is non-empty. + * When the entity has no authPolicy AND the session + * supplied no HMAC, every downstream auth check would + * be skipped — reject up front. */ + #ifdef DEBUG_WOLFTPM + printf("fwTPM: Policy session empty-HMAC rejected for " + "handle 0x%x without authPolicy (CC=0x%x)\n", + entityH, cmdCode); + #endif + *rspSize = FwBuildErrorResponse(rspBuf, + TPM_ST_NO_SESSIONS, TPM_RC_POLICY_FAIL); + return TPM_RC_SUCCESS; + } } } From f23e79415e97169d57ca5cb5dfda936c56fbf6b1 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:32:57 -0700 Subject: [PATCH 15/22] =?UTF-8?q?F-4379=20=E2=80=94=20Check=20mp=5Fdiv=20r?= =?UTF-8?q?emainder=20in=20FwCmd=5FLoadExternal=20RSA=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 29fd17c1..0bb30438 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -4590,10 +4590,22 @@ static TPM_RC FwCmd_LoadExternal(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_FAILURE; } - /* p = n / q */ + /* p = n / q, capturing the remainder so we can reject a caller- + * supplied q that does not evenly divide n. Without this check + * FwRsaComputeCRT would succeed on a mathematically inconsistent + * key whose CRT components do not match the public modulus, + * mirroring the existing guard in FwReconstructRsaPrivateKey. */ if (rc == 0) { - rc = mp_div(&rsaKey->n, &rsaKey->q, &rsaKey->p, NULL); - if (rc != 0) { + mp_int rem; + rc = mp_init(&rem); + if (rc == 0) { + rc = mp_div(&rsaKey->n, &rsaKey->q, &rsaKey->p, &rem); + if (rc == 0 && !mp_iszero(&rem)) { + rc = TPM_RC_BINDING; + } + mp_forcezero(&rem); + } + if (rc != 0 && rc != TPM_RC_BINDING) { rc = TPM_RC_FAILURE; } } From 6914245badfbd24483299d99845e30511f992d88 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:34:46 -0700 Subject: [PATCH 16/22] =?UTF-8?q?F-4378=20=E2=80=94=20Enforce=20per-PCR=20?= =?UTF-8?q?locality=20table=20in=20FwCmd=5FPCR=5FReset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 19 +++++++++++++++++++ tests/fwtpm_unit_tests.c | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 0bb30438..f3ee916a 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -1739,11 +1739,30 @@ static TPM_RC FwCmd_PCR_Reset(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, if (rc == 0) { pcrIndex = pcrHandle - PCR_FIRST; + /* PCR 0-15 (SRTM) are not user-resettable per TPM 2.0 Part 2 + * Table 3-8; they reset only via TPM2_Startup(CLEAR). */ if (pcrIndex < 16) { rc = TPM_RC_LOCALITY; } } + /* Per TCG PC Client TPM Profile Table 5, PCR_Reset locality rules + * for indices 16..23 are: + * 16, 23 — any locality + * 17 — locality 4 only (DRTM MLE) + * 18..22 — locality 3 or 4 (DRTM ACM/OS) + * Without this check any caller at locality 0 can wipe DRTM PCRs + * and defeat attestation policies sealed to them. */ + if (rc == 0) { + if (pcrIndex == 17 && ctx->activeLocality != 4) { + rc = TPM_RC_LOCALITY; + } + else if (pcrIndex >= 18 && pcrIndex <= 22 && + ctx->activeLocality != 3 && ctx->activeLocality != 4) { + rc = TPM_RC_LOCALITY; + } + } + if (rc == 0 && cmdTag == TPM_ST_SESSIONS) { rc = FwSkipAuthArea(cmd, cmdSize); } diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 3ef5b10d..39451891 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7415,6 +7415,43 @@ static void test_fwtpm_pcr_reset(void) fwtpm_pass("PCR_Reset(16):", 0); } +/* Per TCG PC Client TPM Profile Table 5, PCR 17 (DRTM MLE) may only be + * reset from locality 4. The default test locality is 0, so the reset + * must be rejected with TPM_RC_LOCALITY. Same expectation for PCR 22 + * which requires locality 3 or 4. */ +static void test_fwtpm_pcr_reset_locality_enforced(void) +{ + FWTPM_CTX ctx; + int pos, rspSize; + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_PCR_Reset); pos += 4; + PutU32BE(gCmd + pos, 17); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_LOCALITY); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_PCR_Reset); pos += 4; + PutU32BE(gCmd + pos, 22); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_LOCALITY); + + FWTPM_Cleanup(&ctx); + fwtpm_pass("PCR_Reset locality enforced (LOCALITY):", 0); +} + static void test_fwtpm_pcr_event(void) { FWTPM_CTX ctx; @@ -8112,6 +8149,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_pcr_extend_and_read(); test_fwtpm_pcr_extend_empty_pw_rejected_after_setauth(); test_fwtpm_pcr_reset(); + test_fwtpm_pcr_reset_locality_enforced(); test_fwtpm_pcr_event(); /* Clock */ From 2ac2f41d1a6bf7dbd7799b54f4e675157d4fe2ac Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:35:27 -0700 Subject: [PATCH 17/22] =?UTF-8?q?F-4160=20=E2=80=94=20Return=20BUFFER=5FE?= =?UTF-8?q?=20for=20oversized=20auth=20in=20wolfTPM2=5FCreateKeySeal=5Fex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tpm2_wrap.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index f36817ee..659c47e0 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -9048,7 +9048,7 @@ int wolfTPM2_CreateKeySeal_ex(WOLFTPM2_DEV* dev, WOLFTPM2_KEYBLOB* keyBlob, if (auth) { TPM2B_AUTH* pAuth = &createIn.inSensitive.sensitive.userAuth; if (authSz > (int)sizeof(pAuth->buffer)) { - authSz = (int)sizeof(pAuth->buffer); /* truncate */ + return BUFFER_E; } pAuth->size = authSz; XMEMCPY(pAuth->buffer, auth, authSz); From b33569e03c928503883bdb2818930973c357f006 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 12:36:09 -0700 Subject: [PATCH 18/22] =?UTF-8?q?F-4680=20=E2=80=94=20Reject=20wire=20resp?= =?UTF-8?q?Sz=20exceeding=20physical=20buffer=20in=20TPM2=5FPacket=5FParse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tpm2_packet.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tpm2_packet.c b/src/tpm2_packet.c index cb2d4b01..df016816 100644 --- a/src/tpm2_packet.c +++ b/src/tpm2_packet.c @@ -1588,6 +1588,14 @@ TPM_RC TPM2_Packet_Parse(TPM_RC rc, TPM2_Packet* packet) TPM2_Packet_ParseU16(packet, NULL); /* tag */ TPM2_Packet_ParseU32(packet, &respSz); /* response size */ TPM2_Packet_ParseU32(packet, &tmpRc); /* response code */ + /* Reject a wire respSz that exceeds the physical buffer size + * captured in packet->size at entry. Without this guard a + * malicious or MITM responder could inflate respSz and cause + * downstream parsers (bounded only by packet->size) to read + * past the physical allocation. */ + if (respSz > (UINT32)packet->size) { + return TPM_RC_SIZE; + } packet->size = respSz; rc = tmpRc; } From 48eaf5cfe64033b83da376630c7d0a86a4e936e8 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 13:39:44 -0700 Subject: [PATCH 19/22] =?UTF-8?q?F-4745=20=E2=80=94=20Fix=20type-limits=20?= =?UTF-8?q?warning=20on=20unsigned=20handle=20>=3D=20PCR=5FFIRST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index f3ee916a..d3c771c2 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -416,10 +416,12 @@ static void FwLookupEntityAuth(FWTPM_CTX* ctx, TPM_HANDLE handle, } } #endif /* !FWTPM_NO_NV */ - else if (handle >= PCR_FIRST && handle <= PCR_LAST) { + else if (handle <= PCR_LAST) { /* PCR handles carry per-index authValue set by PCR_SetAuthValue. * Without this case the password verifier resolves them to - * authSz=0 and any empty-password caller passes the compare. */ + * authSz=0 and any empty-password caller passes the compare. + * PCR_FIRST is 0, so the lower bound is implicit in the + * unsigned type. */ int pcrIdx = (int)(handle - PCR_FIRST); *authVal = ctx->pcrAuth[pcrIdx].buffer; *authValSz = (int)ctx->pcrAuth[pcrIdx].size; From a53c28be005f789ff7bfaee0b6a4c7ca9d53c3cd Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 13:40:33 -0700 Subject: [PATCH 20/22] =?UTF-8?q?F-4377=20=E2=80=94=20Revert=20NULL-hierar?= =?UTF-8?q?chy=20reject;=20NULL=20tickets=20are=20spec-compliant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 7 ------ tests/fwtpm_unit_tests.c | 50 --------------------------------------- 2 files changed, 57 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index d3c771c2..cef28320 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -8861,13 +8861,6 @@ static TPM_RC FwCmd_PolicyAuthorize(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0 && ticketTag != TPM_ST_VERIFIED) { rc = TPM_RC_TICKET; } - /* Per TPM 2.0 Part 3 Sec.23.16, a TPMT_TK_VERIFIED with - * hierarchy == TPM_RH_NULL is invalid input. Reject before - * HMAC verification to prevent a NULL-hierarchy proofValue - * being fed into FwComputeTicketHmac. */ - if (rc == 0 && ticketHier == TPM_RH_NULL) { - rc = TPM_RC_HIERARCHY; - } /* Verify ticket HMAC per TPM 2.0 Part 3 Section 23.16: * 1. Compute aHash = H(approvedPolicy || policyRef) * 2. Ticket from VerifySignature is HMAC(proofValue, aHash || keyName) diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 39451891..6072eccf 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -6857,55 +6857,6 @@ static void test_fwtpm_policy_pcr(void) fwtpm_pass("PolicyPCR:", 0); } -/* Per TPM 2.0 Part 3 Sec.23.16, a TPMT_TK_VERIFIED with hierarchy == - * TPM_RH_NULL is invalid input and must be rejected with - * TPM_RC_HIERARCHY. Without this guard the handler proceeds into HMAC - * verification with a NULL-hierarchy proofValue, which an attacker can - * pair with a zero-digest ticket to fabricate PolicyAuthorize extensions. */ -static void test_fwtpm_policy_authorize_null_hierarchy_rejected(void) -{ - FWTPM_CTX ctx; - UINT32 sessH; - int pos, rspSize; - byte fakeKeyName[34]; - byte fakeDigest[32]; - - memset(&ctx, 0, sizeof(ctx)); - AssertIntEQ(fwtpm_test_startup(&ctx), 0); - sessH = StartSessionHelper(&ctx, TPM_SE_POLICY); - AssertIntNE(sessH, 0); - - PutU16BE(fakeKeyName, TPM_ALG_SHA256); - memset(fakeKeyName + 2, 0xCD, 32); - memset(fakeDigest, 0xEF, sizeof(fakeDigest)); - - pos = 0; - PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; - PutU32BE(gCmd + pos, 0); pos += 4; - PutU32BE(gCmd + pos, TPM_CC_PolicyAuthorize); pos += 4; - PutU32BE(gCmd + pos, sessH); pos += 4; - pos = AppendPwAuth(gCmd, pos, NULL, 0); - PutU16BE(gCmd + pos, 32); pos += 2; - memset(gCmd + pos, 0, 32); pos += 32; /* approvedPolicy */ - PutU16BE(gCmd + pos, 0); pos += 2; /* policyRef */ - PutU16BE(gCmd + pos, sizeof(fakeKeyName)); pos += 2; - memcpy(gCmd + pos, fakeKeyName, sizeof(fakeKeyName)); - pos += sizeof(fakeKeyName); - /* TPMT_TK_VERIFIED: tag | hierarchy=NULL | digest(32 garbage bytes) */ - PutU16BE(gCmd + pos, TPM_ST_VERIFIED); pos += 2; - PutU32BE(gCmd + pos, TPM_RH_NULL); pos += 4; - PutU16BE(gCmd + pos, sizeof(fakeDigest)); pos += 2; - memcpy(gCmd + pos, fakeDigest, sizeof(fakeDigest)); - pos += sizeof(fakeDigest); - PutU32BE(gCmd + 2, (UINT32)pos); - rspSize = 0; - FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); - AssertIntEQ(GetRspRC(gRsp), TPM_RC_HIERARCHY); - - FlushHandle(&ctx, sessH); - FWTPM_Cleanup(&ctx); - fwtpm_pass("PolicyAuthorize NULL hierarchy (HIERARCHY):", 0); -} #endif /* !FWTPM_NO_POLICY */ /* ================================================================== */ @@ -8281,7 +8232,6 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_policy_command_code(); test_fwtpm_policy_locality(); test_fwtpm_policy_pcr(); - test_fwtpm_policy_authorize_null_hierarchy_rejected(); #endif /* NV operations */ From 1cf5b9bfc12789b9545c50e37f48ff02c20d0760 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 13:40:56 -0700 Subject: [PATCH 21/22] =?UTF-8?q?F-4379=20=E2=80=94=20Call=20mp=5Fclear=20?= =?UTF-8?q?on=20rem=20after=20mp=5Fforcezero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fwtpm/fwtpm_command.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index cef28320..fd9d6af1 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -4625,6 +4625,7 @@ static TPM_RC FwCmd_LoadExternal(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_BINDING; } mp_forcezero(&rem); + mp_clear(&rem); } if (rc != 0 && rc != TPM_RC_BINDING) { rc = TPM_RC_FAILURE; From 90866dd3f47b0f65e1307b165431d3d2b2f3dce1 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 26 May 2026 13:41:17 -0700 Subject: [PATCH 22/22] =?UTF-8?q?F-4622=20=E2=80=94=20Reject=20odd-length?= =?UTF-8?q?=20-digest=3D=20argument=20before=20hexToByte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/pcr/policy.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/pcr/policy.c b/examples/pcr/policy.c index acc72cd2..d79d0cb1 100644 --- a/examples/pcr/policy.c +++ b/examples/pcr/policy.c @@ -104,7 +104,8 @@ int TPM2_PCR_Policy_Test(void* userCtx, int argc, char *argv[]) else { digestLen = (word32)XSTRLEN(digestStr); } - if (digestLen > sizeof(digest)*2) { + if (digestLen == 0 || (digestLen % 2) != 0 || + digestLen > sizeof(digest)*2) { printf("Invalid digest! Must be 16 or 32 bytes of hex like 01020304050607080910111213141516\n"); usage(); return 0;