From eb359d33e1e767f49000bed44208c90d0a0041d1 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:08:41 -0700 Subject: [PATCH 01/50] F-5097 F-5103 F-5831 - Enforce decrypt attribute on ECDH commands --- src/fwtpm/fwtpm_command.c | 14 ++++++ tests/fwtpm_unit_tests.c | 89 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index ecb575e2..769bab9b 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -7576,6 +7576,11 @@ static TPM_RC FwCmd_ECDH_KeyGen(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_KEY; } } + /* Key agreement requires a decryption key (Part 3 Sec.14.3.3) */ + if (rc == 0) { + if (!(obj->pub.objectAttributes & TPMA_OBJECT_decrypt)) + rc = TPM_RC_ATTRIBUTES; + } if (rc == 0) { curveId = obj->pub.parameters.eccDetail.curveID; @@ -7713,6 +7718,11 @@ static TPM_RC FwCmd_ECDH_ZGen(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_KEY; } } + /* Key agreement requires a decryption key (Part 3 Sec.21.3) */ + if (rc == 0) { + if (!(obj->pub.objectAttributes & TPMA_OBJECT_decrypt)) + rc = TPM_RC_ATTRIBUTES; + } /* Skip auth area */ if (rc == 0 && cmdTag == TPM_ST_SESSIONS) { @@ -13038,6 +13048,10 @@ static TPM_RC FwCmd_ZGen_2Phase(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0 && keyA->pub.type != TPM_ALG_ECC) { rc = TPM_RC_KEY; } + /* Key agreement requires a decryption key (Part 3 Sec.14.7) */ + if (rc == 0 && !(keyA->pub.objectAttributes & TPMA_OBJECT_decrypt)) { + rc = TPM_RC_ATTRIBUTES; + } /* Parse inQsB (TPM2B_ECC_POINT) */ if (rc == 0) { diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index ae5c5c73..530450fb 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7310,6 +7310,94 @@ static void test_fwtpm_sign_ecdaa_scheme(void) FWTPM_Cleanup(&ctx); printf("Test fwTPM:\tSign(ECDAA scheme):\t\tPassed\n"); } + +/* ECDH key-agreement commands must reject a key without TPMA_OBJECT_decrypt + * per Part 3 Sec.14.3.3/14.7/21.3. A sign-only AIK would otherwise act as a + * CDH oracle over its private scalar. */ +static void test_fwtpm_ecdh_keygen_signkey_returns_attributes(void) +{ + FWTPM_CTX ctx; + int rspSize = 0; + UINT32 keyH; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + keyH = CreatePrimaryEccSignHelper(&ctx); + AssertIntNE(keyH, 0); + + BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 14, TPM_CC_ECDH_KeyGen); + PutU32BE(gCmd + 10, keyH); + FWTPM_ProcessCommand(&ctx, gCmd, 14, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_ATTRIBUTES); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tECDH_KeyGen(sign key) rejected:\tPassed\n"); +} + +static void test_fwtpm_ecdh_zgen_signkey_returns_attributes(void) +{ + FWTPM_CTX ctx; + int pos, rspSize = 0; + UINT32 keyH; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + keyH = CreatePrimaryEccSignHelper(&ctx); + AssertIntNE(keyH, 0); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_ECDH_ZGen); pos += 4; + PutU32BE(gCmd + pos, keyH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU16BE(gCmd + pos, 4); pos += 2; /* inPoint outer size */ + PutU16BE(gCmd + pos, 0); pos += 2; /* x.size */ + PutU16BE(gCmd + pos, 0); pos += 2; /* y.size */ + PutU32BE(gCmd + 2, (UINT32)pos); + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_ATTRIBUTES); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tECDH_ZGen(sign key) rejected:\tPassed\n"); +} + +static void test_fwtpm_zgen_2phase_signkey_returns_attributes(void) +{ + FWTPM_CTX ctx; + int pos, rspSize = 0; + UINT32 keyH; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + keyH = CreatePrimaryEccSignHelper(&ctx); + AssertIntNE(keyH, 0); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_ZGen_2Phase); pos += 4; + PutU32BE(gCmd + pos, keyH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU16BE(gCmd + pos, 4); pos += 2; PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* inQsB */ + PutU16BE(gCmd + pos, 4); pos += 2; PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* inQeB */ + PutU16BE(gCmd + pos, TPM_ALG_ECDH); pos += 2; /* inScheme */ + PutU16BE(gCmd + pos, 0); pos += 2; /* counter */ + PutU32BE(gCmd + 2, (UINT32)pos); + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_ATTRIBUTES); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tZGen_2Phase(sign key) rejected:\tPassed\n"); +} #endif /* HAVE_ECC */ /* NV_Certify with size=0 and offset=0 must emit TPMS_NV_DIGEST_CERTIFY_INFO @@ -8416,6 +8504,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) #endif test_fwtpm_read_public(); test_fwtpm_loadexternal_symcipher_bad_keysize_rejected(); + test_fwtpm_rewrap_null_newparent_rejected(); test_fwtpm_evict_control(); test_fwtpm_evict_control_bad_persistent_handle_rejected(); test_fwtpm_evict_control_persistent_object_rejected(); From d7beca244412a76418c5a15606dfb7387e201100 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:08:42 -0700 Subject: [PATCH 02/50] F-5120 - Reject TPM_RH_NULL and non-storage newParent in Rewrap --- src/fwtpm/fwtpm_command.c | 19 +++++++++++++++---- tests/fwtpm_unit_tests.c | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 769bab9b..42e56f8a 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -5636,11 +5636,22 @@ static TPM_RC FwCmd_Rewrap(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = (TPM_RC_HANDLE | TPM_RC_1); } - /* Look up newParent */ - if (rc == 0 && newParentH != TPM_RH_NULL) { - newParent = FwFindObject(ctx, newParentH); - if (newParent == NULL) + /* Look up newParent. Rewrap must re-encrypt under a storage key; a + * TPM_RH_NULL newParent would emit the sensitive area in the clear. */ + if (rc == 0) { + if (newParentH == TPM_RH_NULL) { rc = (TPM_RC_HANDLE | TPM_RC_2); + } + else { + newParent = FwFindObject(ctx, newParentH); + if (newParent == NULL) { + rc = (TPM_RC_HANDLE | TPM_RC_2); + } + else if (!(newParent->pub.objectAttributes & TPMA_OBJECT_restricted) + || !(newParent->pub.objectAttributes & TPMA_OBJECT_decrypt)) { + rc = TPM_RC_ATTRIBUTES; + } + } } #ifdef DEBUG_WOLFTPM diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 530450fb..abe15393 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7905,6 +7905,38 @@ static void test_fwtpm_loadexternal_symcipher_bad_keysize_rejected(void) fwtpm_pass("LoadExternal SYMCIPHER bad keySz (SIZE):", 0); } +/* Rewrap must re-encrypt under a storage parent. A TPM_RH_NULL newParent + * would serialize the unwrapped TPMT_SENSITIVE in the clear, so it must be + * rejected per Part 3 Sec.23.4.2. */ +static void test_fwtpm_rewrap_null_newparent_rejected(void) +{ + FWTPM_CTX ctx; + int pos, rspSize = 0; + byte secret[8]; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + memset(secret, 0x5A, sizeof(secret)); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Rewrap); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_NULL); pos += 4; /* oldParent */ + PutU32BE(gCmd + pos, TPM_RH_NULL); pos += 4; /* newParent */ + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU16BE(gCmd + pos, (UINT16)sizeof(secret)); pos += 2; /* inDuplicate */ + memcpy(gCmd + pos, secret, sizeof(secret)); pos += sizeof(secret); + PutU16BE(gCmd + pos, 0); pos += 2; /* name */ + PutU16BE(gCmd + pos, 0); pos += 2; /* inSymSeed */ + PutU32BE(gCmd + 2, (UINT32)pos); + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), (TPM_RC_HANDLE | TPM_RC_2)); + + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tRewrap(NULL newParent) rejected:\tPassed\n"); +} + /* ================================================================== */ /* Group D: Hash/HMAC Sequences */ /* ================================================================== */ @@ -8553,6 +8585,9 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_quote_ecdaa_scheme(); test_fwtpm_sign_ecdaa_scheme(); test_fwtpm_certify_creation_ecdaa_scheme(); + test_fwtpm_ecdh_keygen_signkey_returns_attributes(); + test_fwtpm_ecdh_zgen_signkey_returns_attributes(); + test_fwtpm_zgen_2phase_signkey_returns_attributes(); #endif #endif #endif From a62c5b124b7e6526f725fef65d2d1d34d5839720 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:13:42 -0700 Subject: [PATCH 03/50] F-5104 - Require restricted signing key in fwTPM Quote --- src/fwtpm/fwtpm_command.c | 9 ++++ tests/fwtpm_unit_tests.c | 95 ++++++++++++++++++++++++++++++++++----- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 42e56f8a..a7bc547f 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -11827,6 +11827,15 @@ static TPM_RC FwCmd_Quote(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_HANDLE; } } + /* Quote requires a restricted signing key (Part 3 Sec.18.4) */ + if (rc == 0) { + if (!(sigObj->pub.objectAttributes & TPMA_OBJECT_sign)) + rc = TPM_RC_KEY; + } + if (rc == 0) { + if (!(sigObj->pub.objectAttributes & TPMA_OBJECT_restricted)) + rc = TPM_RC_ATTRIBUTES; + } if (rc == 0) { rc = FwParseAttestParams(cmd, cmdSize, cmdTag, diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index abe15393..d81b2dea 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -6531,12 +6531,11 @@ static UINT32 CreatePrimaryHelper(FWTPM_CTX* ctx, TPM_ALG_ID alg) #if defined(HAVE_ECC) && !defined(FWTPM_NO_ATTESTATION) && \ !defined(FWTPM_NO_NV) -/* Build a non-restricted ECC-P256 sign-capable primary (for tests that - * require TPMA_OBJECT_sign and a key with no scheme bound at create time). - * Only consumed by the attestation tests nested inside the NV-tests - * section, so gated to avoid -Werror=unused-function in - * FWTPM_NO_ATTESTATION or FWTPM_NO_NV builds. */ -static int BuildCreatePrimaryEccSignCmd(byte* buf) +/* Build an ECC-P256 sign-capable primary with caller-supplied attributes + * and no scheme bound at create time. Only consumed by the attestation + * tests nested inside the NV-tests section, so gated to avoid + * -Werror=unused-function in FWTPM_NO_ATTESTATION or FWTPM_NO_NV builds. */ +static int BuildCreatePrimaryEccSignCmd(byte* buf, UINT32 attributes) { int pos = 0; int pubAreaStart, pubAreaLen; @@ -6563,9 +6562,7 @@ static int BuildCreatePrimaryEccSignCmd(byte* buf) PutU16BE(buf + pos, 0); pos += 2; PutU16BE(buf + pos, TPM_ALG_ECC); pos += 2; PutU16BE(buf + pos, TPM_ALG_SHA256); pos += 2; - /* fixedTPM | fixedParent | sensitiveDataOrigin | userWithAuth | noDA | - * sign (non-restricted, sign-only) */ - PutU32BE(buf + pos, 0x00040472); pos += 4; + PutU32BE(buf + pos, attributes); pos += 4; PutU16BE(buf + pos, 0); pos += 2; /* authPolicy */ PutU16BE(buf + pos, TPM_ALG_NULL); pos += 2; /* sym.algorithm = NULL */ PutU16BE(buf + pos, TPM_ALG_NULL); pos += 2; /* scheme = NULL */ @@ -6587,7 +6584,18 @@ static int BuildCreatePrimaryEccSignCmd(byte* buf) static UINT32 CreatePrimaryEccSignHelper(FWTPM_CTX* ctx) { int cmdSz, rspSize = 0; - cmdSz = BuildCreatePrimaryEccSignCmd(gCmd); + /* fixedTPM|fixedParent|sensitiveDataOrigin|userWithAuth|noDA|sign */ + cmdSz = BuildCreatePrimaryEccSignCmd(gCmd, 0x00040472); + FWTPM_ProcessCommand(ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + if (GetRspRC(gRsp) != TPM_RC_SUCCESS) return 0; + return GetU32BE(gRsp + TPM2_HEADER_SIZE); +} + +static UINT32 CreatePrimaryEccSignRestrictedHelper(FWTPM_CTX* ctx) +{ + int cmdSz, rspSize = 0; + /* As above plus restricted: a valid attestation key (AIK) */ + cmdSz = BuildCreatePrimaryEccSignCmd(gCmd, 0x00050472); FWTPM_ProcessCommand(ctx, gCmd, cmdSz, gRsp, &rspSize, 0); if (GetRspRC(gRsp) != TPM_RC_SUCCESS) return 0; return GetU32BE(gRsp + TPM2_HEADER_SIZE); @@ -7160,7 +7168,7 @@ static void test_fwtpm_quote_ecdaa_scheme(void) memset(&ctx, 0, sizeof(ctx)); AssertIntEQ(fwtpm_test_startup(&ctx), 0); - keyH = CreatePrimaryHelper(&ctx, TPM_ALG_ECC); + keyH = CreatePrimaryEccSignRestrictedHelper(&ctx); AssertIntNE(keyH, 0); /* Build TPM2_Quote with ECDAA inScheme. */ @@ -7398,6 +7406,69 @@ static void test_fwtpm_zgen_2phase_signkey_returns_attributes(void) FWTPM_Cleanup(&ctx); printf("Test fwTPM:\tZGen_2Phase(sign key) rejected:\tPassed\n"); } + +/* Quote requires a restricted signing key per Part 3 Sec.18.4. Build the + * command once and run it against keys that violate each requirement. */ +static int BuildQuoteCmd(byte* buf, UINT32 signHandle) +{ + int pos = 0; + PutU16BE(buf + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(buf + pos, 0); pos += 4; + PutU32BE(buf + pos, TPM_CC_Quote); pos += 4; + PutU32BE(buf + pos, signHandle); pos += 4; + pos = AppendPwAuth(buf, pos, NULL, 0); + PutU16BE(buf + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(buf + pos, TPM_ALG_NULL); pos += 2; /* inScheme */ + PutU32BE(buf + pos, 1); pos += 4; /* PCRselect count */ + PutU16BE(buf + pos, TPM_ALG_SHA256); pos += 2; + buf[pos++] = 3; buf[pos++] = 0; buf[pos++] = 0; buf[pos++] = 0; + PutU32BE(buf + 2, (UINT32)pos); + return pos; +} + +static void test_fwtpm_quote_decrypt_key_returns_key(void) +{ + FWTPM_CTX ctx; + int cmdSz, rspSize = 0; + UINT32 keyH; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + /* Restricted decrypt (storage) key has no TPMA_OBJECT_sign */ + keyH = CreatePrimaryHelper(&ctx, TPM_ALG_ECC); + AssertIntNE(keyH, 0); + + cmdSz = BuildQuoteCmd(gCmd, keyH); + FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_KEY); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tQuote(decrypt key) rejected:\tPassed\n"); +} + +static void test_fwtpm_quote_unrestricted_sign_returns_attributes(void) +{ + FWTPM_CTX ctx; + int cmdSz, rspSize = 0; + UINT32 keyH; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + /* Non-restricted signing key must not be usable for attestation */ + keyH = CreatePrimaryEccSignHelper(&ctx); + AssertIntNE(keyH, 0); + + cmdSz = BuildQuoteCmd(gCmd, keyH); + FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_ATTRIBUTES); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tQuote(unrestricted sign) rejected:\tPassed\n"); +} #endif /* HAVE_ECC */ /* NV_Certify with size=0 and offset=0 must emit TPMS_NV_DIGEST_CERTIFY_INFO @@ -8588,6 +8659,8 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_ecdh_keygen_signkey_returns_attributes(); test_fwtpm_ecdh_zgen_signkey_returns_attributes(); test_fwtpm_zgen_2phase_signkey_returns_attributes(); + test_fwtpm_quote_decrypt_key_returns_key(); + test_fwtpm_quote_unrestricted_sign_returns_attributes(); #endif #endif #endif From 991ae59be440fb1f922b15a101c5f951544cc289 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:28:02 -0700 Subject: [PATCH 04/50] F-4841 F-5832 - Enforce NV read authorization in PolicyNV and PolicyAuthorizeNV --- src/fwtpm/fwtpm_command.c | 18 +++++++-- tests/fwtpm_unit_tests.c | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index a7bc547f..8edd315f 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -69,6 +69,8 @@ static TPM_RC FwParseAttestParams(TPM2_Packet* cmd, int cmdSize, #ifndef FWTPM_NO_NV static FWTPM_NvIndex* FwFindNvIndex(FWTPM_CTX* ctx, TPMI_RH_NV_INDEX nvIndex); +static TPM_RC FwNvCheckAccess(TPM_HANDLE authHandle, + TPMI_RH_NV_INDEX nvHandle, UINT32 attributes, int isWrite); #endif static FWTPM_Object* FwFindObject(FWTPM_CTX* ctx, TPM_HANDLE handle); #ifdef WOLFTPM_V185 @@ -9367,8 +9369,6 @@ static TPM_RC FwCmd_PolicyNV(FWTPM_CTX* ctx, TPM2_Packet* cmd, } #endif - (void)authHandle; - /* Find NV index */ if (rc == 0) { nv = FwFindNvIndex(ctx, nvIndex); @@ -9377,6 +9377,12 @@ static TPM_RC FwCmd_PolicyNV(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } + /* Verify caller is authorized to read the NV index */ + if (rc == 0) { + rc = FwNvCheckAccess(authHandle, nvIndex, + nv->nvPublic.attributes, 0); + } + /* Find policy session */ if (rc == 0) { sess = FwFindSession(ctx, sessHandle); @@ -10382,6 +10388,12 @@ static TPM_RC FwCmd_PolicyAuthorizeNV(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = FW_NV_HANDLE_ERR_2; } + /* Verify caller is authorized to read the NV index */ + if (rc == 0) { + rc = FwNvCheckAccess(authHandle, nvHandle, + nv->nvPublic.attributes, 0); + } + if (rc == 0 && !nv->written) { rc = TPM_RC_NV_UNINITIALIZED; } @@ -10423,8 +10435,6 @@ static TPM_RC FwCmd_PolicyAuthorizeNV(FWTPM_CTX* ctx, TPM2_Packet* cmd, printf("fwTPM: PolicyAuthorizeNV(auth=0x%x, nv=0x%x, sess=0x%x)\n", authHandle, nvHandle, sessHandle); #endif - (void)authHandle; - /* Step 1: Reset policyDigest to zero */ XMEMSET(sess->policyDigest.buffer, 0, dSz); sess->policyDigest.size = (UINT16)dSz; diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index d81b2dea..bf735795 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -6988,6 +6988,86 @@ static void test_fwtpm_policyauthorize_null_ticket_rejected(void) fwtpm_pass("PolicyAuthorize zero-ticket (TICKET):", 0); } +#ifndef FWTPM_NO_NV +/* PolicyNV and PolicyAuthorizeNV must verify the caller is authorized to + * read the NV index. An OWNER authHandle against an index without + * TPMA_NV_OWNERREAD must be rejected with TPM_RC_NV_AUTHORIZATION. */ +static void test_fwtpm_policynv_owner_read_denied(void) +{ + FWTPM_CTX ctx; + int pos, cmdSz, rspSize = 0; + UINT32 sessH; + UINT32 nvIdx = 0x01500051; + UINT32 attrs = TPMA_NV_OWNERWRITE | TPMA_NV_AUTHREAD | TPMA_NV_NO_DA; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + cmdSz = BuildNvDefineCmd(gCmd, nvIdx, 8, attrs); + FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + sessH = StartSessionHelper(&ctx, TPM_SE_TRIAL); + AssertIntNE(sessH, 0); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_PolicyNV); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; /* authHandle */ + PutU32BE(gCmd + pos, nvIdx); pos += 4; + PutU32BE(gCmd + pos, sessH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU16BE(gCmd + pos, 0); pos += 2; /* operandB */ + PutU16BE(gCmd + pos, 0); pos += 2; /* offset */ + PutU16BE(gCmd + pos, 0); pos += 2; /* operation */ + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_NV_AUTHORIZATION); + + FlushHandle(&ctx, sessH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tPolicyNV OWNER read denied:\tPassed\n"); +} + +static void test_fwtpm_policyauthorizenv_owner_read_denied(void) +{ + FWTPM_CTX ctx; + int pos, cmdSz, rspSize = 0; + UINT32 sessH; + UINT32 nvIdx = 0x01500052; + UINT32 attrs = TPMA_NV_OWNERWRITE | TPMA_NV_AUTHREAD | TPMA_NV_NO_DA; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + cmdSz = BuildNvDefineCmd(gCmd, nvIdx, 8, attrs); + FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + sessH = StartSessionHelper(&ctx, TPM_SE_TRIAL); + AssertIntNE(sessH, 0); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_PolicyAuthorizeNV); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; /* authHandle */ + PutU32BE(gCmd + pos, nvIdx); pos += 4; + PutU32BE(gCmd + pos, sessH); 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_NV_AUTHORIZATION); + + FlushHandle(&ctx, sessH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tPolicyAuthorizeNV OWNER read denied:\tPassed\n"); +} +#endif /* !FWTPM_NO_NV */ + #endif /* !FWTPM_NO_POLICY */ /* ================================================================== */ @@ -8643,6 +8723,10 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_policy_pcr(); test_fwtpm_policy_ticket_zero_digest_rejected(); test_fwtpm_policyauthorize_null_ticket_rejected(); +#ifndef FWTPM_NO_NV + test_fwtpm_policynv_owner_read_denied(); + test_fwtpm_policyauthorizenv_owner_read_denied(); +#endif #endif /* NV operations */ From cb32e61d269941314c01e776c1713f551dd4e182 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:32:19 -0700 Subject: [PATCH 05/50] F-5654 F-5655 - Validate selftest params and report test status --- src/fwtpm/fwtpm_command.c | 52 +++++++++++++++++++++++++----------- tests/fwtpm_unit_tests.c | 55 +++++++++++++++++++++++++++++++++++++++ wolftpm/fwtpm/fwtpm.h | 3 +++ 3 files changed, 95 insertions(+), 15 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 8edd315f..2f211cf1 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -830,6 +830,7 @@ static TPM_RC FwCmd_SelfTest(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, } if (rc == 0) { + ctx->selfTestRun = 1; FwRspFinalize(rsp, TPM_ST_NO_SESSIONS, TPM_RC_SUCCESS); } @@ -840,38 +841,59 @@ static TPM_RC FwCmd_SelfTest(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, static TPM_RC FwCmd_IncrementalSelfTest(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, TPM2_Packet* rsp, UINT16 cmdTag) { + TPM_RC rc = TPM_RC_SUCCESS; + UINT32 toTestCount = 0; + UINT32 i; + UINT16 alg; + (void)ctx; - (void)cmd; - (void)cmdSize; (void)cmdTag; + /* Require the TPML_ALG toTest parameter (count + count*TPM_ALG_ID) */ + if (cmdSize < TPM2_HEADER_SIZE + 4) { + rc = TPM_RC_COMMAND_SIZE; + } + if (rc == 0) { + TPM2_Packet_ParseU32(cmd, &toTestCount); + if (cmdSize < TPM2_HEADER_SIZE + 4 + (int)(toTestCount * sizeof(alg))) + rc = TPM_RC_COMMAND_SIZE; + } + if (rc == 0) { + for (i = 0; i < toTestCount; i++) + TPM2_Packet_ParseU16(cmd, &alg); + } + #ifdef DEBUG_WOLFTPM - printf("fwTPM: IncrementalSelfTest\n"); + if (rc == 0) + printf("fwTPM: IncrementalSelfTest(toTest=%u)\n", (unsigned)toTestCount); #endif - /* TODO: IncrementalSelfTest is currently a no-op stub. A real - * implementation would track per-algorithm CAST status and run any - * tests from toTest[] that have not yet passed. Returning an empty - * toDoList signals "nothing left to test" which is acceptable for the - * non-FIPS configuration but must be revisited for FIPS builds. */ - TPM2_Packet_AppendU32(rsp, 0); /* toDoList count = 0 */ - FwRspFinalize(rsp, TPM_ST_NO_SESSIONS, TPM_RC_SUCCESS); - return TPM_RC_SUCCESS; /* always succeeds */ + /* No-op stub: report nothing left to test via an empty toDoList. + * Acceptable for non-FIPS; must be revisited for FIPS builds. */ + if (rc == 0) { + TPM2_Packet_AppendU32(rsp, 0); /* toDoList count = 0 */ + FwRspFinalize(rsp, TPM_ST_NO_SESSIONS, TPM_RC_SUCCESS); + } + return rc; } /* --- TPM2_GetTestResult (CC 0x017C) --- */ static TPM_RC FwCmd_GetTestResult(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, TPM2_Packet* rsp, UINT16 cmdTag) { - (void)ctx; (void)cmd; (void)cmdSize; (void)cmdTag; + UINT32 testResult; + + (void)cmd; (void)cmdSize; (void)cmdTag; + + /* Report NEEDS_TEST until TPM2_SelfTest has completed successfully */ + testResult = ctx->selfTestRun ? TPM_RC_SUCCESS : TPM_RC_NEEDS_TEST; /* outData (TPM2B_MAX_BUFFER) - empty */ TPM2_Packet_AppendU16(rsp, 0); - /* testResult (TPM_RC) - success */ - TPM2_Packet_AppendU32(rsp, TPM_RC_SUCCESS); + TPM2_Packet_AppendU32(rsp, testResult); FwRspFinalize(rsp, TPM_ST_NO_SESSIONS, TPM_RC_SUCCESS); - return TPM_RC_SUCCESS; /* always succeeds */ + return TPM_RC_SUCCESS; } /* --- TPM2_GetRandom (CC 0x017B) --- */ diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index bf735795..9de12430 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7714,6 +7714,59 @@ static void test_fwtpm_incremental_selftest(void) fwtpm_pass("IncrementalSelfTest/GetResult:", 0); } +static void test_fwtpm_incremental_selftest_truncated_returns_cmd_size(void) +{ + FWTPM_CTX ctx; + int rspSize = 0; + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + /* Header only: the required TPML_ALG toTest parameter is missing */ + BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 10, TPM_CC_IncrementalSelfTest); + FWTPM_ProcessCommand(&ctx, gCmd, 10, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_COMMAND_SIZE); + + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tIncrementalSelfTest truncated (CMD_SIZE):\tPassed\n"); +} + +static void test_fwtpm_gettestresult_needs_test_then_success(void) +{ + FWTPM_CTX ctx; + int cmdSz, rspSize = 0; + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(FWTPM_Init(&ctx), 0); + + /* Startup only -- do not run SelfTest yet */ + cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 12, TPM_CC_Startup); + PutU16BE(gCmd + cmdSz, TPM_SU_CLEAR); cmdSz += 2; + PutU32BE(gCmd + 2, (UINT32)cmdSz); + FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 10, TPM_CC_GetTestResult); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, 10, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + AssertIntEQ(GetU32BE(gRsp + TPM2_HEADER_SIZE + 2), TPM_RC_NEEDS_TEST); + + /* After SelfTest completes, status becomes SUCCESS */ + cmdSz = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 11, TPM_CC_SelfTest); + gCmd[cmdSz++] = 1; + PutU32BE(gCmd + 2, (UINT32)cmdSz); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 10, TPM_CC_GetTestResult); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, 10, gRsp, &rspSize, 0); + AssertIntEQ(GetU32BE(gRsp + TPM2_HEADER_SIZE + 2), TPM_RC_SUCCESS); + + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tGetTestResult NEEDS_TEST then SUCCESS:\tPassed\n"); +} + static void test_fwtpm_pcr_reset(void) { FWTPM_CTX ctx; @@ -8752,6 +8805,8 @@ int fwtpm_unit_tests(int argc, char *argv[]) /* Hierarchy & misc */ test_fwtpm_test_parms(); test_fwtpm_incremental_selftest(); + test_fwtpm_incremental_selftest_truncated_returns_cmd_size(); + test_fwtpm_gettestresult_needs_test_then_success(); /* Destructive tests last (Clear changes state) */ test_fwtpm_change_eps(); diff --git a/wolftpm/fwtpm/fwtpm.h b/wolftpm/fwtpm/fwtpm.h index 87a738f8..cbf9c469 100644 --- a/wolftpm/fwtpm/fwtpm.h +++ b/wolftpm/fwtpm/fwtpm.h @@ -714,6 +714,9 @@ typedef struct FWTPM_CTX { /* ContextSave sequence counter (monotonic, reset on init) */ UINT64 contextSeqCounter; + /* Set once TPM2_SelfTest has completed successfully */ + int selfTestRun; + #ifdef HAVE_ECC /* EC_Ephemeral commit counter and key storage (volatile) */ UINT16 ecEphemeralCounter; From 84705f08cac4575c163dcfe366cf1b486f5ee020 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:36:18 -0700 Subject: [PATCH 06/50] F-5721 - Enforce DRTM PCR locality in fwTPM PCR_Event --- src/fwtpm/fwtpm_command.c | 15 +++++++++++++-- tests/fwtpm_unit_tests.c | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 2f211cf1..235a7c50 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -1840,6 +1840,19 @@ static TPM_RC FwCmd_PCR_Event(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } + /* DRTM PCRs are locality-restricted (Part 1 Sec.11.4.6): + * PCR 17 requires locality 4; PCRs 18-22 require locality 3 or 4. */ + if (rc == 0) { + pcrIndex = pcrHandle - PCR_FIRST; + 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); } @@ -1866,8 +1879,6 @@ static TPM_RC FwCmd_PCR_Event(FWTPM_CTX* ctx, TPM2_Packet* cmd, } if (rc == 0) { - pcrIndex = pcrHandle - PCR_FIRST; - /* SHA-256 bank */ bankAlgs[0] = TPM_ALG_SHA256; digestSz[0] = TPM2_GetHashDigestSize(TPM_ALG_SHA256); diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 9de12430..1bf8c0ce 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -792,6 +792,44 @@ static void test_fwtpm_pcr_extend_and_read(void) fwtpm_pass("PCR_Extend + Read(16):", 0); } +/* PCR_Event into DRTM PCR 17 must require locality 4 (Part 1 Sec.11.4.6). */ +static void test_fwtpm_pcr_event_drtm_locality_enforced(void) +{ + FWTPM_CTX ctx; + int pos, rspSize = 0; + byte ev[4]; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + memset(ev, 0xAB, sizeof(ev)); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_PCR_Event); pos += 4; + PutU32BE(gCmd + pos, 17); pos += 4; /* pcrHandle = PCR 17 */ + PutU32BE(gCmd + pos, 9); pos += 4; /* auth area size */ + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; /* nonce */ + gCmd[pos++] = 0; /* attributes */ + PutU16BE(gCmd + pos, 0); pos += 2; /* hmac */ + PutU16BE(gCmd + pos, (UINT16)sizeof(ev)); pos += 2; + memcpy(gCmd + pos, ev, sizeof(ev)); pos += sizeof(ev); + PutU32BE(gCmd + 2, (UINT32)pos); + + /* Locality 0: rejected */ + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_LOCALITY); + + /* Locality 4: allowed */ + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 4); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tPCR_Event DRTM locality enforced:\tPassed\n"); +} + /* 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 @@ -8638,6 +8676,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) /* PCR operations */ test_fwtpm_pcr_read(); test_fwtpm_pcr_extend_and_read(); + test_fwtpm_pcr_event_drtm_locality_enforced(); test_fwtpm_pcr_extend_empty_pw_rejected_after_setauth(); test_fwtpm_pcr_reset(); test_fwtpm_pcr_reset_locality_enforced(); From 9245b19203780e65d3391dc82e2656180fd2194b Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:41:02 -0700 Subject: [PATCH 07/50] F-5719 - Bind and enforce PolicyLocality constraint on policy sessions --- src/fwtpm/fwtpm_command.c | 14 ++++++++ tests/fwtpm_unit_tests.c | 67 +++++++++++++++++++++++++++++++++++++++ wolftpm/fwtpm/fwtpm.h | 1 + 3 files changed, 82 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 235a7c50..d443cd38 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -9160,6 +9160,13 @@ static TPM_RC FwCmd_PolicyLocality(FWTPM_CTX* ctx, TPM2_Packet* cmd, NULL, 0, 0) != 0) { rc = TPM_RC_FAILURE; } + /* Bind the locality constraint (intersect across calls) */ + if (rc == 0) { + if (sess->requiredLocality == 0) + sess->requiredLocality = locality; + else + sess->requiredLocality &= locality; + } } if (rc == 0) { FwRspNoParams(rsp, cmdTag); @@ -15645,6 +15652,13 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx, TPM_ST_NO_SESSIONS, TPM_RC_POLICY_FAIL); return TPM_RC_SUCCESS; } + /* Enforce any PolicyLocality constraint bound to the session */ + if (pSess->requiredLocality != 0 && + !((1 << ctx->activeLocality) & pSess->requiredLocality)) { + *rspSize = FwBuildErrorResponse(rspBuf, + TPM_ST_NO_SESSIONS, TPM_RC_LOCALITY); + return TPM_RC_SUCCESS; + } } else if (authPolicy != NULL && authPolicy->size == 0 && cmdAuths[pj].cmdHmacSize == 0) { diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 1bf8c0ce..cb8e03ca 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7104,6 +7104,72 @@ static void test_fwtpm_policyauthorizenv_owner_read_denied(void) FWTPM_Cleanup(&ctx); printf("Test fwTPM:\tPolicyAuthorizeNV OWNER read denied:\tPassed\n"); } + +/* PolicyLocality must bind a locality constraint that is enforced when the + * policy session authorizes an entity. A session satisfying a locality-4 + * policy must not authorize a command issued at locality 0. */ +static void test_fwtpm_policy_locality_enforced(void) +{ + FWTPM_CTX ctx; + int pos, cmdSz, rspSize = 0; + UINT32 sessH; + UINT16 dSz; + byte digest[64]; + UINT32 nvIdx = 0x01500061; + UINT32 nvAttrs = TPMA_NV_OWNERWRITE | TPMA_NV_OWNERREAD | TPMA_NV_NO_DA; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + sessH = StartSessionHelper(&ctx, TPM_SE_POLICY); + AssertIntNE(sessH, 0); + + /* PolicyLocality(bit 4 = locality 4 only) */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_PolicyLocality); pos += 4; + PutU32BE(gCmd + pos, sessH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + gCmd[pos++] = 0x10; + PutU32BE(gCmd + 2, (UINT32)pos); + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + /* Read back the resulting policyDigest */ + AssertIntEQ(SendPolicyCmd(&ctx, TPM_CC_PolicyGetDigest, sessH), + TPM_RC_SUCCESS); + dSz = GetU16BE(gRsp + TPM2_HEADER_SIZE + 4); + AssertIntEQ(dSz, 32); + memcpy(digest, gRsp + TPM2_HEADER_SIZE + 6, dSz); + + /* Bind that policy to the owner hierarchy */ + 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); + PutU16BE(gCmd + pos, dSz); pos += 2; + memcpy(gCmd + pos, digest, dSz); pos += dSz; + 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_SUCCESS); + + /* Authorize an owner command via the policy session at locality 0. + * The digest matches but locality 4 is required. */ + cmdSz = BuildNvDefineCmd(gCmd, nvIdx, 8, nvAttrs); + PutU32BE(gCmd + 18, sessH); /* replace TPM_RS_PW with the policy session */ + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_LOCALITY); + + FlushHandle(&ctx, sessH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tPolicyLocality enforced:\tPassed\n"); +} #endif /* !FWTPM_NO_NV */ #endif /* !FWTPM_NO_POLICY */ @@ -8818,6 +8884,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) #ifndef FWTPM_NO_NV test_fwtpm_policynv_owner_read_denied(); test_fwtpm_policyauthorizenv_owner_read_denied(); + test_fwtpm_policy_locality_enforced(); #endif #endif diff --git a/wolftpm/fwtpm/fwtpm.h b/wolftpm/fwtpm/fwtpm.h index cbf9c469..3a3fcad3 100644 --- a/wolftpm/fwtpm/fwtpm.h +++ b/wolftpm/fwtpm/fwtpm.h @@ -529,6 +529,7 @@ typedef struct FWTPM_Session { TPM2B_DIGEST cpHashA; /* PolicyCpHash: locked once set */ TPM2B_DIGEST nameHash; /* PolicyNameHash: locked once set */ int isPPRequired; /* PolicyPhysicalPresence flag */ + int requiredLocality; /* PolicyLocality bitmap (0 = unset) */ } FWTPM_Session; /* NV index slot (user NV RAM) */ From b1a4528393375f90f822ce27e3ead06e19f82edd Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:43:27 -0700 Subject: [PATCH 08/50] F-5677 - Reject newMaxTries=0 in DictionaryAttackParameters --- src/fwtpm/fwtpm_command.c | 5 +++++ tests/fwtpm_unit_tests.c | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index d443cd38..23669fad 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -11441,6 +11441,11 @@ static TPM_RC FwCmd_DictionaryAttackParameters(FWTPM_CTX* ctx, TPM2_Packet_ParseU32(cmd, &lockoutRecovery); } + /* newMaxTries of 0 would permanently disable lockout enforcement */ + if (rc == 0 && newMaxTries == 0) { + rc = TPM_RC_VALUE; + } + if (rc == 0 && lockHandle != TPM_RH_LOCKOUT) { rc = TPM_RC_HIERARCHY; } diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index cb8e03ca..9f5187ae 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -8117,6 +8117,31 @@ static void test_fwtpm_da_parameters_and_reset(void) FWTPM_Cleanup(&ctx); fwtpm_pass("DA Parameters/LockReset:", 0); } + +/* newMaxTries=0 must be rejected: it would disable lockout permanently. */ +static void test_fwtpm_da_parameters_zero_maxtries_rejected(void) +{ + FWTPM_CTX ctx; + int pos, rspSize = 0; + 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_DictionaryAttackParameters); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_LOCKOUT); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU32BE(gCmd + pos, 0); pos += 4; /* newMaxTries = 0 */ + PutU32BE(gCmd + pos, 60); pos += 4; + PutU32BE(gCmd + pos, 300); pos += 4; + PutU32BE(gCmd + 2, (UINT32)pos); + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_VALUE); + + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tDA Parameters maxTries=0 rejected:\tPassed\n"); +} #endif /* !FWTPM_NO_DA */ static void test_fwtpm_read_public(void) @@ -8868,6 +8893,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_set_primary_policy_bad_size_rejected(); #ifndef FWTPM_NO_DA test_fwtpm_da_parameters_and_reset(); + test_fwtpm_da_parameters_zero_maxtries_rejected(); #endif /* Policy */ From 4e7c88b995c3d6479d5c7f0c5fff0218c9712c1a Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:45:14 -0700 Subject: [PATCH 09/50] F-5722 - Enforce per-hierarchy persistent sub-range in EvictControl --- src/fwtpm/fwtpm_command.c | 13 +++++++++++++ tests/fwtpm_unit_tests.c | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 23669fad..58f11eab 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -3757,6 +3757,19 @@ static TPM_RC FwCmd_EvictControl(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_HIERARCHY; } + /* Per TPM 2.0 Part 3 Sec.28, platformAuth owns the PLATFORM_PERSISTENT + * sub-range and owner/endorsement auth the range below it; neither may + * manage a handle in the other's sub-range. */ + if (rc == 0) { + if (authHandle == TPM_RH_PLATFORM) { + if (persistentHandle < PLATFORM_PERSISTENT) + rc = TPM_RC_RANGE; + } + else if (persistentHandle >= PLATFORM_PERSISTENT) { + rc = TPM_RC_RANGE; + } + } + #ifdef DEBUG_WOLFTPM if (rc == 0) { printf("fwTPM: EvictControl(auth=0x%x, obj=0x%x, persist=0x%x)\n", diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 9f5187ae..6244b974 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -8426,6 +8426,40 @@ static void test_fwtpm_evict_control(void) fwtpm_pass("EvictControl (persist/remove):", 0); } +/* Owner auth must not persist into the platform sub-range (Part 3 Sec.28). */ +static void test_fwtpm_evict_control_cross_hierarchy_rejected(void) +{ + FWTPM_CTX ctx; + int pos, rspSize = 0; + UINT32 keyH; + + 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; /* owner auth */ + PutU32BE(gCmd + pos, keyH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU32BE(gCmd + pos, 0x81800001); pos += 4; /* platform sub-range */ + PutU32BE(gCmd + 2, (UINT32)pos); + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_RANGE); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tEvictControl cross-hierarchy rejected:\tPassed\n"); +} + /* 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 @@ -8872,6 +8906,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_loadexternal_symcipher_bad_keysize_rejected(); test_fwtpm_rewrap_null_newparent_rejected(); test_fwtpm_evict_control(); + test_fwtpm_evict_control_cross_hierarchy_rejected(); test_fwtpm_evict_control_bad_persistent_handle_rejected(); test_fwtpm_evict_control_persistent_object_rejected(); test_fwtpm_context_save(); From 6f3c20fe071a2e89d1b591995a2183e84e472b0c Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:50:01 -0700 Subject: [PATCH 10/50] F-4953 F-5095 - Enforce key-declared signing scheme over wire scheme --- src/fwtpm/fwtpm_crypto.c | 33 ++++++++++------- tests/fwtpm_unit_tests.c | 77 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 12 deletions(-) diff --git a/src/fwtpm/fwtpm_crypto.c b/src/fwtpm/fwtpm_crypto.c index 6a7cea80..457a03fa 100644 --- a/src/fwtpm/fwtpm_crypto.c +++ b/src/fwtpm/fwtpm_crypto.c @@ -3790,19 +3790,28 @@ int FwComputeNvName(FWTPM_NvIndex* nv, byte* buf, UINT16* sz) void FwResolveSignScheme(FWTPM_Object* obj, UINT16* sigScheme, UINT16* sigHashAlg) { - if (*sigScheme == TPM_ALG_NULL) { - if (obj->pub.type == TPM_ALG_RSA) { - *sigScheme = obj->pub.parameters.rsaDetail.scheme.scheme; - *sigHashAlg = obj->pub.parameters.rsaDetail.scheme.details - .anySig.hashAlg; - } - else if (obj->pub.type == TPM_ALG_ECC) { - *sigScheme = obj->pub.parameters.eccDetail.scheme.scheme; - *sigHashAlg = obj->pub.parameters.eccDetail.scheme.details - .any.hashAlg; - } + UINT16 keyScheme = TPM_ALG_NULL; + UINT16 keyHashAlg = TPM_ALG_NULL; + + if (obj->pub.type == TPM_ALG_RSA) { + keyScheme = obj->pub.parameters.rsaDetail.scheme.scheme; + keyHashAlg = obj->pub.parameters.rsaDetail.scheme.details + .anySig.hashAlg; + } + else if (obj->pub.type == TPM_ALG_ECC) { + keyScheme = obj->pub.parameters.eccDetail.scheme.scheme; + keyHashAlg = obj->pub.parameters.eccDetail.scheme.details + .any.hashAlg; + } + + /* A key that mandates a scheme overrides any wire-supplied scheme and + * hash. Honoring a differing wire pair would let an attacker downgrade + * a restricted SHA-256 attestation key to e.g. SHA-1. */ + if (keyScheme != TPM_ALG_NULL) { + *sigScheme = keyScheme; + *sigHashAlg = keyHashAlg; } - if (*sigScheme == TPM_ALG_NULL) { + else if (*sigScheme == TPM_ALG_NULL) { *sigScheme = (obj->pub.type == TPM_ALG_RSA) ? TPM_ALG_RSASSA : TPM_ALG_ECDSA; } diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 6244b974..fb3c187b 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7653,6 +7653,82 @@ static void test_fwtpm_quote_unrestricted_sign_returns_attributes(void) FWTPM_Cleanup(&ctx); printf("Test fwTPM:\tQuote(unrestricted sign) rejected:\tPassed\n"); } + +/* A key that declares a signing scheme must not be downgraded by a wire + * inScheme: signing with ECDSA-SHA1 against an ECDSA-SHA256 key must emit a + * SHA-256 signature, not SHA-1. */ +static void test_fwtpm_sign_scheme_downgrade_rejected(void) +{ + FWTPM_CTX ctx; + int pos, pubStart, sensStart, rspSize = 0; + UINT32 keyH; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + /* CreatePrimary: ECC sign key declaring scheme ECDSA-SHA256 */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_CreatePrimary); pos += 4; + PutU32BE(gCmd + pos, TPM_RH_OWNER); pos += 4; + PutU32BE(gCmd + pos, 9); pos += 4; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; + gCmd[pos++] = 0; + PutU16BE(gCmd + pos, 0); pos += 2; + sensStart = pos; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + sensStart, (UINT16)(pos - sensStart - 2)); + pubStart = pos; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_ECC); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; + PutU32BE(gCmd + pos, 0x00040472); pos += 4; /* sign, non-restricted */ + PutU16BE(gCmd + pos, 0); pos += 2; /* authPolicy */ + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* sym */ + PutU16BE(gCmd + pos, TPM_ALG_ECDSA); pos += 2; /* declared scheme */ + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; /* scheme hashAlg */ + PutU16BE(gCmd + pos, TPM_ECC_NIST_P256); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* kdf */ + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pubStart, (UINT16)(pos - pubStart - 2)); + PutU16BE(gCmd + pos, 0); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + 2, (UINT32)pos); + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + keyH = GetU32BE(gRsp + TPM2_HEADER_SIZE); + AssertIntNE(keyH, 0); + + /* Sign with wire inScheme ECDSA-SHA1 */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Sign); pos += 4; + PutU32BE(gCmd + pos, keyH); pos += 4; + pos = AppendPwAuth(gCmd, pos, NULL, 0); + PutU16BE(gCmd + pos, 32); pos += 2; + memset(gCmd + pos, 0xAB, 32); pos += 32; + PutU16BE(gCmd + pos, TPM_ALG_ECDSA); pos += 2; + PutU16BE(gCmd + pos, TPM_ALG_SHA1); pos += 2; + PutU16BE(gCmd + pos, TPM_ST_HASHCHECK); pos += 2; + PutU32BE(gCmd + pos, TPM_RH_NULL); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + /* TPMT_SIGNATURE: sigAlg(2) then hashAlg(2) after paramSize */ + AssertIntEQ(GetU16BE(gRsp + TPM2_HEADER_SIZE + 4 + 2), TPM_ALG_SHA256); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tSign scheme downgrade rejected:\tPassed\n"); +} #endif /* HAVE_ECC */ /* NV_Certify with size=0 and offset=0 must emit TPMS_NV_DIGEST_CERTIFY_INFO @@ -8965,6 +9041,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_zgen_2phase_signkey_returns_attributes(); test_fwtpm_quote_decrypt_key_returns_key(); test_fwtpm_quote_unrestricted_sign_returns_attributes(); + test_fwtpm_sign_scheme_downgrade_rejected(); #endif #endif #endif From ef59e32fcec2abf8444a2019558bae6114fcc256 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:55:14 -0700 Subject: [PATCH 11/50] F-5280 - Reject non-session response tag for sessioned commands --- src/tpm2.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/tpm2.c b/src/tpm2.c index 94b17ab9..afe6fb40 100644 --- a/src/tpm2.c +++ b/src/tpm2.c @@ -466,6 +466,7 @@ static TPM_RC TPM2_SendCommandAuth(TPM2_CTX* ctx, TPM2_Packet* packet, { TPM_RC rc = TPM_RC_FAILURE; TPM_ST tag; + TPM_ST respTag; TPM_CC cmdCode; BYTE *cmd; UINT32 cmdSz, respSz; @@ -524,10 +525,18 @@ static TPM_RC TPM2_SendCommandAuth(TPM2_CTX* ctx, TPM2_Packet* packet, /* restart the unmarshalling position */ packet->pos = 0; - TPM2_Packet_ParseU16(packet, &tag); + TPM2_Packet_ParseU16(packet, &respTag); + + /* A command sent with sessions must receive a sessioned response. A + * man-in-the-middle flipping the response tag to TPM_ST_NO_SESSIONS + * would otherwise skip HMAC verification entirely. */ + if (rc == TPM_RC_SUCCESS && tag == TPM_ST_SESSIONS && + respTag != TPM_ST_SESSIONS) { + rc = TPM_RC_HMAC; + } /* Is auth session required for this TPM command? */ - if (rc == TPM_RC_SUCCESS && tag == TPM_ST_SESSIONS) { + if (rc == TPM_RC_SUCCESS && respTag == TPM_ST_SESSIONS) { rc = TPM2_ResponseProcess(ctx, packet, info, cmdCode, respSz); } From 9df47ef09b2cad2bd6fafd67be15066e669f513f Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:56:47 -0700 Subject: [PATCH 12/50] F-5647 - Reject negative authSz in key auth wrapper APIs --- src/tpm2_wrap.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index 659c47e0..1cdc7c7f 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -2806,6 +2806,9 @@ int wolfTPM2_ChangeAuthKey(WOLFTPM2_DEV* dev, WOLFTPM2_KEY* key, changeIn.objectHandle = key->handle.hndl; changeIn.parentHandle = parent->hndl; if (auth) { + if (authSz < 0) { + return BAD_FUNC_ARG; + } /* Note: returns error instead of truncating for security (v3.11+) */ if (authSz > (int)sizeof(changeIn.newAuth.buffer)) { return BUFFER_E; @@ -3022,6 +3025,9 @@ int wolfTPM2_CreateLoadedKey(WOLFTPM2_DEV* dev, WOLFTPM2_KEYBLOB* keyBlob, if (auth) { TPM2B_AUTH* pAuth = &createLoadedIn.inSensitive.sensitive.userAuth; int nameAlgDigestSz = TPM2_GetHashDigestSize(publicTemplate->nameAlg); + if (authSz < 0) { + return BAD_FUNC_ARG; + } if (nameAlgDigestSz > 0) { if (authSz > nameAlgDigestSz) { authSz = nameAlgDigestSz; @@ -9047,6 +9053,9 @@ int wolfTPM2_CreateKeySeal_ex(WOLFTPM2_DEV* dev, WOLFTPM2_KEYBLOB* keyBlob, createIn.parentHandle = parent->hndl; if (auth) { TPM2B_AUTH* pAuth = &createIn.inSensitive.sensitive.userAuth; + if (authSz < 0) { + return BAD_FUNC_ARG; + } if (authSz > (int)sizeof(pAuth->buffer)) { return BUFFER_E; } From b8cdf54fc8ef47d9e2c9ce27a8e353e15aeb19cc Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:57:39 -0700 Subject: [PATCH 13/50] F-4840 - Require non-empty session key for parameter encryption --- src/fwtpm/fwtpm_command.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 58f11eab..2ab33c48 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -15541,9 +15541,13 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx, #ifndef FWTPM_NO_PARAM_ENC /* Detect encryption session (first non-PW with - * symmetric alg) */ + * symmetric alg). A session with an empty + * sessionKey (unsalted and unbound) derives a + * wire-observable key, so it must not be used + * for parameter encryption. */ if (encSess == NULL && - sess->symmetric.algorithm != TPM_ALG_NULL) { + sess->symmetric.algorithm != TPM_ALG_NULL && + sess->sessionKey.size > 0) { encSess = sess; /* decrypt attr = client encrypted cmd param */ if ((attribs & TPMA_SESSION_decrypt) From 6ee458221bff462446af1c14eba6d1fa17a268f8 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 11:59:53 -0700 Subject: [PATCH 14/50] F-4842 F-5098 - Sanitize NV journal DA maxTries and object handle --- src/fwtpm/fwtpm_nv.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index 2d63bf8d..da293596 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -48,6 +48,11 @@ /* TLV header size: tag(2) + length(2) */ #define TLV_HDR_SIZE 4 +/* Dictionary-attack bounds used to sanitize values replayed from the + * integrity-unprotected NV journal. */ +#define FWTPM_DA_DEFAULT_MAX_TRIES 32 +#define FWTPM_DA_MAX_TRIES_LIMIT 0xFFFF + /* ========================================================================= */ /* File-based NV backend */ /* ========================================================================= */ @@ -534,6 +539,12 @@ static int FwNvUnmarshalObject(const byte* buf, word32* pos, word32 maxSz, XMEMSET(obj, 0, sizeof(FWTPM_Object)); rc = FwNvUnmarshalU32(buf, pos, maxSz, &obj->handle); + /* A persistent object handle must stay in range or a later FwFindObject + * lookup could resolve a transient handle to this slot. */ + if (rc == 0 && + (obj->handle < PERSISTENT_FIRST || obj->handle > PERSISTENT_LAST)) { + rc = TPM_RC_VALUE; + } if (rc == 0) { rc = FwNvUnmarshalPublic(buf, pos, maxSz, &obj->pub); } @@ -827,6 +838,12 @@ static int FwNvProcessEntry(FWTPM_CTX* ctx, UINT16 tag, FwNvUnmarshalU32(value, &vPos, vMax, &ctx->daMaxTries); FwNvUnmarshalU32(value, &vPos, vMax, &ctx->daRecoveryTime); FwNvUnmarshalU32(value, &vPos, vMax, &ctx->daLockoutRecovery); + /* A tampered journal must not disable lockout with 0 or a value + * so large the gate never engages */ + if (ctx->daMaxTries == 0 || + ctx->daMaxTries > FWTPM_DA_MAX_TRIES_LIMIT) { + ctx->daMaxTries = FWTPM_DA_DEFAULT_MAX_TRIES; + } ctx->daFailedTries = 0; /* volatile - reset on load */ #endif break; From c03db2d23d71aed2858404574fc521d0a2871182 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 12:07:31 -0700 Subject: [PATCH 15/50] F-5678 - Populate attestation clockInfo from live TPM state --- src/fwtpm/fwtpm_command.c | 53 ++++++++++++++++++++++++--------------- src/fwtpm/fwtpm_nv.c | 8 +++++- tests/fwtpm_unit_tests.c | 32 +++++++++++++++++++++++ wolftpm/fwtpm/fwtpm.h | 2 ++ 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 2ab33c48..1ec83025 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -712,6 +712,17 @@ static TPM_RC FwCmd_Startup(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, rc = wc_RNG_GenerateBlock(&ctx->rng, ctx->nullSeed, FWTPM_SEED_SIZE); if (rc != 0) rc = TPM_RC_FAILURE; + + /* TPM Reset: bump persisted resetCount, clear restartCount */ + if (rc == 0) { + ctx->resetCount++; + ctx->restartCount = 0; + FWTPM_NV_SaveFlags(ctx); + } + } + else { + /* TPM Restart/Resume: bump volatile restartCount */ + ctx->restartCount++; } ctx->wasStarted = 1; @@ -10050,10 +10061,8 @@ static TPM_RC FwCmd_PolicyCounterTimer(FWTPM_CTX* ctx, TPM2_Packet* cmd, /* clockInfo.clock (8 bytes) */ FwStoreU64BE(timeInfo + p, t); p += 8; /* resetCount(4) + restartCount(4) + safe(1) */ - timeInfo[p++] = 0; timeInfo[p++] = 0; - timeInfo[p++] = 0; timeInfo[p++] = 0; - timeInfo[p++] = 0; timeInfo[p++] = 0; - timeInfo[p++] = 0; timeInfo[p++] = 0; + FwStoreU32BE(timeInfo + p, ctx->resetCount); p += 4; + FwStoreU32BE(timeInfo + p, ctx->restartCount); p += 4; timeInfo[p++] = 1; /* safe = YES */ if ((UINT32)offset + operandBSz > (UINT32)p) { @@ -11776,8 +11785,18 @@ static TPM_RC FwCmd_EncryptDecrypt2(FWTPM_CTX* ctx, TPM2_Packet* cmd, /* Helper: Serialize TPMS_ATTEST common header into pkt. * Returns: number of bytes written (up to serialized position in pkt). */ #ifndef FWTPM_NO_ATTESTATION -static void FwAppendAttestCommonHeader(TPM2_Packet* pkt, UINT16 type, - const TPM2B_NAME* qualifiedSigner, const TPM2B_DATA* extraData) +/* Append a TPMS_CLOCK_INFO from live TPM state (clock + reset/restart). */ +static void FwAppendClockInfo(FWTPM_CTX* ctx, TPM2_Packet* pkt) +{ + TPM2_Packet_AppendU64(pkt, FWTPM_Clock_GetMs(ctx)); + TPM2_Packet_AppendU32(pkt, ctx->resetCount); + TPM2_Packet_AppendU32(pkt, ctx->restartCount); + TPM2_Packet_AppendU8(pkt, YES); /* safe */ +} + +static void FwAppendAttestCommonHeader(FWTPM_CTX* ctx, TPM2_Packet* pkt, + UINT16 type, const TPM2B_NAME* qualifiedSigner, + const TPM2B_DATA* extraData) { /* magic */ TPM2_Packet_AppendU32(pkt, TPM_GENERATED_VALUE); @@ -11791,10 +11810,7 @@ static void FwAppendAttestCommonHeader(TPM2_Packet* pkt, UINT16 type, TPM2_Packet_AppendU16(pkt, extraData->size); TPM2_Packet_AppendBytes(pkt, (byte*)extraData->buffer, extraData->size); /* clockInfo: clock(8) + resetCount(4) + restartCount(4) + safe(1) */ - TPM2_Packet_AppendU64(pkt, 0); /* clock */ - TPM2_Packet_AppendU32(pkt, 0); /* resetCount */ - TPM2_Packet_AppendU32(pkt, 0); /* restartCount */ - TPM2_Packet_AppendU8(pkt, 1); /* safe = YES */ + FwAppendClockInfo(ctx, pkt); /* firmwareVersion */ TPM2_Packet_AppendU64(pkt, ((UINT64)FWTPM_VERSION_MAJOR << 32) | FWTPM_VERSION_MINOR); @@ -11934,7 +11950,7 @@ static TPM_RC FwCmd_Quote(FWTPM_CTX* ctx, TPM2_Packet* cmd, attestPkt.pos = 0; attestPkt.size = (int)FWTPM_MAX_ATTEST_BUF; - FwAppendAttestCommonHeader(&attestPkt, TPM_ST_ATTEST_QUOTE, + FwAppendAttestCommonHeader(ctx, &attestPkt, TPM_ST_ATTEST_QUOTE, &sigObj->name, &qualifyingData); /* attested.quote: pcrSelect (TPML_PCR_SELECTION) + @@ -12063,7 +12079,7 @@ static TPM_RC FwCmd_Certify(FWTPM_CTX* ctx, TPM2_Packet* cmd, attestPkt.pos = 0; attestPkt.size = (int)FWTPM_MAX_ATTEST_BUF; - FwAppendAttestCommonHeader(&attestPkt, TPM_ST_ATTEST_CERTIFY, + FwAppendAttestCommonHeader(ctx, &attestPkt, TPM_ST_ATTEST_CERTIFY, &sigObj->name, &qualifyingData); /* attested.certify: name + qualifiedName */ @@ -12253,7 +12269,7 @@ static TPM_RC FwCmd_CertifyCreation(FWTPM_CTX* ctx, TPM2_Packet* cmd, attestPkt.pos = 0; attestPkt.size = (int)FWTPM_MAX_ATTEST_BUF; - FwAppendAttestCommonHeader(&attestPkt, TPM_ST_ATTEST_CREATION, + FwAppendAttestCommonHeader(ctx, &attestPkt, TPM_ST_ATTEST_CREATION, &sigObj->name, &qualifyingData); /* attested.creation: objectName (TPM2B_NAME) + @@ -12316,16 +12332,13 @@ static TPM_RC FwCmd_GetTime(FWTPM_CTX* ctx, TPM2_Packet* cmd, attestPkt.pos = 0; attestPkt.size = (int)FWTPM_MAX_PUB_BUF; - FwAppendAttestCommonHeader(&attestPkt, TPM_ST_ATTEST_TIME, + FwAppendAttestCommonHeader(ctx, &attestPkt, TPM_ST_ATTEST_TIME, &sigObj->name, &qualifyingData); /* attested.time: TPMS_TIME_INFO (time + clockInfo) + * firmwareVersion */ - TPM2_Packet_AppendU64(&attestPkt, 0); /* time */ - TPM2_Packet_AppendU64(&attestPkt, 0); /* clockInfo.clock */ - TPM2_Packet_AppendU32(&attestPkt, 0); /* clockInfo.resetCount */ - TPM2_Packet_AppendU32(&attestPkt, 0); /* clockInfo.restartCount */ - TPM2_Packet_AppendU8(&attestPkt, 1); /* clockInfo.safe */ + TPM2_Packet_AppendU64(&attestPkt, FWTPM_Clock_GetMs(ctx)); /* time */ + FwAppendClockInfo(ctx, &attestPkt); TPM2_Packet_AppendU64(&attestPkt, ((UINT64)FWTPM_VERSION_MAJOR << 32) | FWTPM_VERSION_MINOR); /* firmwareVersion */ @@ -12465,7 +12478,7 @@ static TPM_RC FwCmd_NV_Certify(FWTPM_CTX* ctx, TPM2_Packet* cmd, attestPkt.pos = 0; attestPkt.size = (int)FWTPM_MAX_ATTEST_BUF; - FwAppendAttestCommonHeader(&attestPkt, attestType, + FwAppendAttestCommonHeader(ctx, &attestPkt, attestType, &sigObj->name, &qualifyingData); if (digestMode) { diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index da293596..da1de556 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -846,6 +846,10 @@ static int FwNvProcessEntry(FWTPM_CTX* ctx, UINT16 tag, } ctx->daFailedTries = 0; /* volatile - reset on load */ #endif + /* resetCount trails the DA fields in newer journals */ + if (vPos + 4 <= vMax) { + FwNvUnmarshalU32(value, &vPos, vMax, &ctx->resetCount); + } break; } @@ -1340,6 +1344,7 @@ int FWTPM_NV_Save(FWTPM_CTX* ctx) FwNvMarshalU32(buf, &pos, bufSz, ctx->daRecoveryTime); FwNvMarshalU32(buf, &pos, bufSz, ctx->daLockoutRecovery); #endif + FwNvMarshalU32(buf, &pos, bufSz, ctx->resetCount); rc = FwNvAppendEntry(ctx, FWTPM_NV_TAG_FLAGS, buf, (UINT16)pos); } @@ -1613,7 +1618,7 @@ int FWTPM_NV_SavePcrAuth(FWTPM_CTX* ctx) int FWTPM_NV_SaveFlags(FWTPM_CTX* ctx) { int rc; - byte buf[1 + 12]; /* flags + DA params */ + byte buf[1 + 12 + 4]; /* flags + DA params + resetCount */ word32 pos = 0; if (ctx == NULL) { @@ -1627,6 +1632,7 @@ int FWTPM_NV_SaveFlags(FWTPM_CTX* ctx) FwNvMarshalU32(buf, &pos, sizeof(buf), ctx->daRecoveryTime); FwNvMarshalU32(buf, &pos, sizeof(buf), ctx->daLockoutRecovery); #endif + FwNvMarshalU32(buf, &pos, sizeof(buf), ctx->resetCount); rc = FwNvAppendEntry(ctx, FWTPM_NV_TAG_FLAGS, buf, (UINT16)pos); return rc; diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index fb3c187b..0e047e57 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7729,6 +7729,37 @@ static void test_fwtpm_sign_scheme_downgrade_rejected(void) FWTPM_Cleanup(&ctx); printf("Test fwTPM:\tSign scheme downgrade rejected:\tPassed\n"); } + +/* TPMS_ATTEST clockInfo must carry live TPM state, not hardcoded zeros. + * After Startup(CLEAR) the resetCount is non-zero, so a Quote attest must + * reflect that. */ +static void test_fwtpm_quote_clockinfo_resetcount_nonzero(void) +{ + FWTPM_CTX ctx; + int cmdSz, rspSize = 0, clockOff; + UINT32 keyH; + UINT16 nameSz, extraSz; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + keyH = CreatePrimaryEccSignRestrictedHelper(&ctx); + AssertIntNE(keyH, 0); + + cmdSz = BuildQuoteCmd(gCmd, keyH); + FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + /* header(10)+paramSize(4)+attestSz(2): magic,type,qualifiedSigner@22 */ + nameSz = GetU16BE(gRsp + 22); + extraSz = GetU16BE(gRsp + 24 + nameSz); + clockOff = 24 + nameSz + 2 + extraSz; /* clock(8) then resetCount(4) */ + AssertIntNE(GetU32BE(gRsp + clockOff + 8), 0); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tQuote clockInfo resetCount nonzero:\tPassed\n"); +} #endif /* HAVE_ECC */ /* NV_Certify with size=0 and offset=0 must emit TPMS_NV_DIGEST_CERTIFY_INFO @@ -9042,6 +9073,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_quote_decrypt_key_returns_key(); test_fwtpm_quote_unrestricted_sign_returns_attributes(); test_fwtpm_sign_scheme_downgrade_rejected(); + test_fwtpm_quote_clockinfo_resetcount_nonzero(); #endif #endif #endif diff --git a/wolftpm/fwtpm/fwtpm.h b/wolftpm/fwtpm/fwtpm.h index 3a3fcad3..325dee13 100644 --- a/wolftpm/fwtpm/fwtpm.h +++ b/wolftpm/fwtpm/fwtpm.h @@ -634,6 +634,8 @@ typedef struct FWTPM_CTX { #endif int activeLocality; UINT64 clockOffset; /* Clock offset set by ClockSet */ + UINT32 resetCount; /* TPM Reset count, persisted across boots */ + UINT32 restartCount; /* TPM Restart/Resume count, volatile */ /* PCR state: [pcrIndex][bank][digest bytes] */ byte pcrDigest[IMPLEMENTATION_PCR][FWTPM_PCR_BANKS][TPM_MAX_DIGEST_SIZE]; From 60aef67a053fdbe4c70e65c4bebbd00093361246 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 12:12:52 -0700 Subject: [PATCH 16/50] F-4673 - Bind context sequence counter to blob and reject replay --- src/fwtpm/fwtpm_command.c | 11 ++++++++- src/fwtpm/fwtpm_crypto.c | 18 +++++++++++++++ tests/fwtpm_unit_tests.c | 48 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 1ec83025..40463b0b 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -3077,7 +3077,7 @@ static TPM_RC FwCmd_ContextLoad(FWTPM_CTX* ctx, TPM2_Packet* cmd, UINT32 magic = 0, version = 0; (void)cmdTag; - (void)seqHi; (void)seqLo; (void)hierarchy; + (void)hierarchy; if (cmdSize < TPM2_HEADER_SIZE + 18) { rc = TPM_RC_COMMAND_SIZE; @@ -3091,6 +3091,13 @@ static TPM_RC FwCmd_ContextLoad(FWTPM_CTX* ctx, TPM2_Packet* cmd, TPM2_Packet_ParseU16(cmd, &blobSz); } + /* Replay protection: a saved context is bound to the sequence counter + * value it was created with, and each successful load consumes it. */ + if (rc == 0 && + (((UINT64)seqHi << 32) | (UINT64)seqLo) != ctx->contextSeqCounter) { + rc = TPM_RC_INTEGRITY; + } + /* Validate minimum blob size (magic + version = 8 bytes) */ if (rc == 0 && blobSz < 8) { rc = TPM_RC_SIZE; @@ -3189,6 +3196,8 @@ static TPM_RC FwCmd_ContextLoad(FWTPM_CTX* ctx, TPM2_Packet* cmd, #ifdef DEBUG_WOLFTPM printf("fwTPM: ContextLoad(handle=0x%x)\n", savedHandle); #endif + /* Consume this sequence value so the same blob cannot be replayed */ + ctx->contextSeqCounter++; TPM2_Packet_AppendU32(rsp, savedHandle); FwRspFinalize(rsp, TPM_ST_NO_SESSIONS, TPM_RC_SUCCESS); } diff --git a/src/fwtpm/fwtpm_crypto.c b/src/fwtpm/fwtpm_crypto.c index 457a03fa..95b63097 100644 --- a/src/fwtpm/fwtpm_crypto.c +++ b/src/fwtpm/fwtpm_crypto.c @@ -2219,6 +2219,18 @@ int FwUnwrapPrivate(FWTPM_Object* parent, /* Context blob wrap/unwrap (ContextSave/ContextLoad) */ /* ================================================================== */ +/* Fold a 64-bit value into an HMAC as big-endian, used to bind the context + * sequence counter into the blob MAC for replay protection. */ +static int FwHmacUpdateU64(Hmac* hmac, UINT64 v) +{ + byte b[8]; + int i; + for (i = 0; i < 8; i++) { + b[i] = (byte)(v >> (56 - 8 * i)); + } + return wc_HmacUpdate(hmac, b, (word32)sizeof(b)); +} + /* Encrypt-then-MAC context blob protection using the per-boot key. * Layout: iv(16) | ciphertext(plainSz) | hmac(32) * Returns 0 on success, sets *outSz. */ @@ -2271,6 +2283,9 @@ int FwWrapContextBlob(FWTPM_CTX* ctx, if (rc == 0) { rc = wc_HmacUpdate(hmac, out, AES_BLOCK_SIZE + plainSz); } + if (rc == 0) { + rc = FwHmacUpdateU64(hmac, ctx->contextSeqCounter); + } if (rc == 0) { rc = wc_HmacFinal(hmac, out + AES_BLOCK_SIZE + plainSz); } @@ -2328,6 +2343,9 @@ int FwUnwrapContextBlob(FWTPM_CTX* ctx, if (rc == 0) { rc = wc_HmacUpdate(hmac, in, AES_BLOCK_SIZE + cipherSz); } + if (rc == 0) { + rc = FwHmacUpdateU64(hmac, ctx->contextSeqCounter); + } if (rc == 0) { rc = wc_HmacFinal(hmac, computedHmac); } diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 0e047e57..5b24909e 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -8482,6 +8482,53 @@ static void test_fwtpm_context_save(void) fwtpm_pass("ContextSave:", 0); } +/* A saved context must load at most once; replaying the same blob is + * rejected to prevent resurrecting a satisfied policy session. */ +static void test_fwtpm_context_load_replay_rejected(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, ctxSz, pos; + UINT32 keyH; + byte savedCtx[512]; + + 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); + + BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 14, TPM_CC_ContextSave); + PutU32BE(gCmd + 10, keyH); + FWTPM_ProcessCommand(&ctx, gCmd, 14, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + ctxSz = rspSize - TPM2_HEADER_SIZE; /* TPMS_CONTEXT follows the header */ + AssertIntGT(ctxSz, 0); + memcpy(savedCtx, gRsp + TPM2_HEADER_SIZE, ctxSz); + + /* First load succeeds */ + pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_ContextLoad); + memcpy(gCmd + pos, savedCtx, ctxSz); pos += ctxSz; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + /* Replaying the identical blob is rejected */ + pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_ContextLoad); + memcpy(gCmd + pos, savedCtx, ctxSz); pos += ctxSz; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntNE(GetRspRC(gRsp), TPM_RC_SUCCESS); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tContextLoad replay rejected:\tPassed\n"); +} + static void test_fwtpm_evict_control(void) { FWTPM_CTX ctx; @@ -9017,6 +9064,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_evict_control_bad_persistent_handle_rejected(); test_fwtpm_evict_control_persistent_object_rejected(); test_fwtpm_context_save(); + test_fwtpm_context_load_replay_rejected(); /* Crypto */ test_fwtpm_hash(); From 2f059c34b69084a06ebdf4c0dc2811ff115e068b Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 12:15:35 -0700 Subject: [PATCH 17/50] F-5101 F-5108 - Flush transient state on command client change --- src/fwtpm/fwtpm_command.c | 22 +++++++++++++++++++++ src/fwtpm/fwtpm_io.c | 4 ++++ tests/fwtpm_unit_tests.c | 37 +++++++++++++++++++++++++++++++++++ wolftpm/fwtpm/fwtpm_command.h | 8 ++++++++ 4 files changed, 71 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 40463b0b..b915db2a 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -2403,6 +2403,28 @@ static void FwFlushAllSessions(FWTPM_CTX* ctx) } } +void FWTPM_ResetCommandClient(FWTPM_CTX* ctx) +{ + int i; + if (ctx == NULL) { + return; + } + FwFlushAllObjects(ctx); + FwFlushAllSessions(ctx); + for (i = 0; i < FWTPM_MAX_HASH_SEQ; i++) { + if (ctx->hashSeq[i].used) { + FwFreeHashSeq(&ctx->hashSeq[i]); + } + } +#ifdef WOLFTPM_V185 + for (i = 0; i < FWTPM_MAX_SIGN_SEQ; i++) { + if (ctx->signSeq[i].used) { + FwFreeSignSeq(&ctx->signSeq[i]); + } + } +#endif +} + /* --- TPM2_CreatePrimary (CC 0x0131) --- */ static TPM_RC FwCmd_CreatePrimary(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, TPM2_Packet* rsp, UINT16 cmdTag) diff --git a/src/fwtpm/fwtpm_io.c b/src/fwtpm/fwtpm_io.c index d26f94d0..5e0035be 100644 --- a/src/fwtpm/fwtpm_io.c +++ b/src/fwtpm/fwtpm_io.c @@ -681,6 +681,8 @@ int FWTPM_IO_ServerLoop(FWTPM_CTX* ctx) printf("fwTPM: command connection replaced\n"); #endif CloseSocket(cmdFd); + /* New client must not inherit prior transient state */ + FWTPM_ResetCommandClient(ctx); } cmdFd = newFd; } @@ -699,6 +701,8 @@ int FWTPM_IO_ServerLoop(FWTPM_CTX* ctx) if (HandleCommandConnection(ctx, cmdFd) != TPM_RC_SUCCESS) { CloseSocket(cmdFd); cmdFd = FWTPM_INVALID_FD; + /* Drop transient state when the client disconnects */ + FWTPM_ResetCommandClient(ctx); } } } diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 5b24909e..4b375355 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -8529,6 +8529,42 @@ static void test_fwtpm_context_load_replay_rejected(void) printf("Test fwTPM:\tContextLoad replay rejected:\tPassed\n"); } +/* When the command client changes, transient objects must be flushed so a + * replacement client cannot enumerate and use the previous client's handles. */ +static void test_fwtpm_reset_command_client_flushes_transient(void) +{ + FWTPM_CTX ctx; + int rspSize = 0; + UINT32 keyH; + + 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); + + /* Object usable before the client change */ + BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 14, TPM_CC_ContextSave); + PutU32BE(gCmd + 10, keyH); + FWTPM_ProcessCommand(&ctx, gCmd, 14, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + FWTPM_ResetCommandClient(&ctx); + + /* Handle no longer resolves after the reset */ + BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 14, TPM_CC_ContextSave); + PutU32BE(gCmd + 10, keyH); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, 14, gRsp, &rspSize, 0); + AssertIntNE(GetRspRC(gRsp), TPM_RC_SUCCESS); + + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tResetCommandClient flushes transient:\tPassed\n"); +} + static void test_fwtpm_evict_control(void) { FWTPM_CTX ctx; @@ -9065,6 +9101,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_evict_control_persistent_object_rejected(); test_fwtpm_context_save(); test_fwtpm_context_load_replay_rejected(); + test_fwtpm_reset_command_client_flushes_transient(); /* Crypto */ test_fwtpm_hash(); diff --git a/wolftpm/fwtpm/fwtpm_command.h b/wolftpm/fwtpm/fwtpm_command.h index 9c052fac..705b309a 100644 --- a/wolftpm/fwtpm/fwtpm_command.h +++ b/wolftpm/fwtpm/fwtpm_command.h @@ -56,6 +56,14 @@ WOLFTPM_API int FWTPM_ProcessCommand(FWTPM_CTX* ctx, const byte* cmdBuf, int cmdSize, byte* rspBuf, int* rspSize, int locality); +/*! + \brief Flush all transient objects, sessions, and sequences. + + Called when the active command client connection is replaced or closed so + a subsequent client cannot inherit the previous client's transient state. +*/ +WOLFTPM_API void FWTPM_ResetCommandClient(FWTPM_CTX* ctx); + #ifdef __cplusplus } /* extern "C" */ #endif From 6c9a64efd7444fdf5e025b459c4fa8bcb664dbe7 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 12:31:58 -0700 Subject: [PATCH 18/50] F-5102 F-5119 F-5833 - Authenticate NV journal with integrity MAC --- src/fwtpm/fwtpm_nv.c | 171 +++++++++++++++++++++++++++++++++++++-- tests/fwtpm_unit_tests.c | 80 ++++++++++++++++++ wolftpm/fwtpm/fwtpm.h | 5 ++ 3 files changed, 251 insertions(+), 5 deletions(-) diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index da1de556..70420418 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -42,6 +42,15 @@ #include #include +#include + +#if !defined(NO_FILESYSTEM) && !defined(_WIN32) + #include +#endif + +#define FWTPM_NV_KEY_SIZE 32 +#define FWTPM_NV_MAC_SIZE WC_SHA256_DIGEST_SIZE + #include #include @@ -153,7 +162,8 @@ static FWTPM_NV_HAL fwNvDefaultHal = { FwNvFileWrite, FwNvFileErase, (void*)FWTPM_NV_FILE, - FWTPM_NV_MAX_SIZE + FWTPM_NV_MAX_SIZE, + NULL /* get_integrity_key: file backend uses a key file */ }; #endif /* !NO_FILESYSTEM */ @@ -638,6 +648,114 @@ static int FwNvUnmarshalPrimaryCache(const byte* buf, word32* pos, return rc; } +/* ========================================================================= */ +/* Journal Integrity */ +/* ========================================================================= */ + +#if !defined(NO_FILESYSTEM) +/* Load, or create on first use, the sibling key file for the default file + * backend so journal integrity is enabled without integrator action. */ +static int FwNvLoadOrCreateKeyFile(FWTPM_CTX* ctx, byte* key, word32* keySz) +{ + const char* nvPath = (const char*)ctx->nvHal.ctx; + char keyPath[256]; + size_t nvLen; + FILE* f; + int ok = 0; + + if (nvPath == NULL) { + return 0; + } + nvLen = XSTRLEN(nvPath); + if (nvLen + 5 > sizeof(keyPath)) { /* ".key" + NUL */ + return 0; + } + XMEMCPY(keyPath, nvPath, nvLen); + XMEMCPY(keyPath + nvLen, ".key", 5); + + f = fopen(keyPath, "rb"); + if (f != NULL) { + ok = ((int)fread(key, 1, FWTPM_NV_KEY_SIZE, f) == FWTPM_NV_KEY_SIZE); + fclose(f); + } + else { + if (wc_RNG_GenerateBlock(&ctx->rng, key, FWTPM_NV_KEY_SIZE) != 0) { + return 0; + } + f = fopen(keyPath, "wb"); + if (f != NULL) { + ok = ((int)fwrite(key, 1, FWTPM_NV_KEY_SIZE, f) + == FWTPM_NV_KEY_SIZE); + fclose(f); + #if !defined(_WIN32) + chmod(keyPath, S_IRUSR | S_IWUSR); + #endif + } + } + if (ok) { + *keySz = FWTPM_NV_KEY_SIZE; + return 1; + } + TPM2_ForceZero(key, FWTPM_NV_KEY_SIZE); + return 0; +} +#endif /* !NO_FILESYSTEM */ + +/* Resolve the journal integrity key: a platform-provided device secret if + * the HAL supplies one, else the default file backend's key file. */ +static int FwNvGetIntegrityKey(FWTPM_CTX* ctx, byte* key, word32* keySz) +{ + *keySz = 0; + if (ctx->nvHal.get_integrity_key != NULL) { + if (ctx->nvHal.get_integrity_key(ctx->nvHal.ctx, key, keySz) == 0 && + *keySz > 0) { + return 1; + } + return 0; + } +#if !defined(NO_FILESYSTEM) + if (ctx->nvHal.read == FwNvFileRead) { + return FwNvLoadOrCreateKeyFile(ctx, key, keySz); + } +#endif + return 0; +} + +/* HMAC-SHA256 over the journal body [header .. writePos). */ +static int FwNvComputeJournalMac(FWTPM_CTX* ctx, const byte* key, + word32 keySz, byte* macOut) +{ + FWTPM_NV_HAL* hal = &ctx->nvHal; + FWTPM_DECLARE_VAR(hmac, Hmac); + byte chunk[256]; + word32 off = sizeof(FWTPM_NV_HEADER); + int rc; + + FWTPM_ALLOC_VAR(hmac, Hmac); + + rc = wc_HmacInit(hmac, NULL, INVALID_DEVID); + if (rc == 0) { + rc = wc_HmacSetKey(hmac, WC_SHA256, key, keySz); + } + while (rc == 0 && off < ctx->nvWritePos) { + word32 n = ctx->nvWritePos - off; + if (n > sizeof(chunk)) { + n = sizeof(chunk); + } + rc = hal->read(hal->ctx, off, chunk, n); + if (rc == 0) { + rc = wc_HmacUpdate(hmac, chunk, n); + } + off += n; + } + if (rc == 0) { + rc = wc_HmacFinal(hmac, macOut); + } + wc_HmacFree(hmac); + FWTPM_FREE_VAR(hmac); + return rc; +} + /* ========================================================================= */ /* Journal Operations */ /* ========================================================================= */ @@ -648,12 +766,29 @@ static int FwNvWriteHeader(FWTPM_CTX* ctx) byte hdr[sizeof(FWTPM_NV_HEADER)]; /* 4 x UINT32 = 16 bytes */ FWTPM_NV_HAL* hal = &ctx->nvHal; + byte key[FWTPM_NV_KEY_SIZE]; + byte mac[FWTPM_NV_MAC_SIZE]; + word32 keySz = 0; + int rc; + FwStoreU32LE(hdr + 0, FWTPM_NV_MAGIC); FwStoreU32LE(hdr + 4, FWTPM_NV_VERSION); FwStoreU32LE(hdr + 8, ctx->nvWritePos); FwStoreU32LE(hdr + 12, hal->maxSize); - return hal->write(hal->ctx, 0, hdr, sizeof(hdr)); + rc = hal->write(hal->ctx, 0, hdr, sizeof(hdr)); + + /* Refresh the trailing journal MAC so a tampered journal is detected + * on the next load. The MAC sits at writePos and is rewritten after + * every append. */ + if (rc == TPM_RC_SUCCESS && FwNvGetIntegrityKey(ctx, key, &keySz)) { + rc = FwNvComputeJournalMac(ctx, key, keySz, mac); + if (rc == TPM_RC_SUCCESS) { + rc = hal->write(hal->ctx, ctx->nvWritePos, mac, sizeof(mac)); + } + TPM2_ForceZero(key, sizeof(key)); + } + return rc; } /* Append a single TLV entry to the journal */ @@ -669,8 +804,8 @@ static int FwNvAppendEntry(FWTPM_CTX* ctx, UINT16 tag, return TPM_RC_FAILURE; } - /* Check if journal has space */ - if (ctx->nvWritePos + entrySize > hal->maxSize) { + /* Reserve room for the trailing journal MAC written after each append */ + if (ctx->nvWritePos + entrySize + FWTPM_NV_MAC_SIZE > hal->maxSize) { /* If already compacting, NV is genuinely full */ if (ctx->nvCompacting) { return TPM_RC_NV_SPACE; @@ -681,7 +816,7 @@ static int FwNvAppendEntry(FWTPM_CTX* ctx, UINT16 tag, return rc; } /* After compaction, check again */ - if (ctx->nvWritePos + entrySize > hal->maxSize) { + if (ctx->nvWritePos + entrySize + FWTPM_NV_MAC_SIZE > hal->maxSize) { return TPM_RC_NV_SPACE; } } @@ -1028,6 +1163,8 @@ int FWTPM_NV_Init(FWTPM_CTX* ctx) byte tlvHdr[TLV_HDR_SIZE]; byte* valueBuf = NULL; word32 valueBufSz = FWTPM_NV_MAX_ENTRY; + byte vKey[FWTPM_NV_KEY_SIZE]; + word32 vKeySz = 0; if (ctx == NULL) { return BAD_FUNC_ARG; @@ -1074,6 +1211,30 @@ int FWTPM_NV_Init(FWTPM_CTX* ctx) rc = TPM_RC_NV_UNINITIALIZED; } } + + /* Authenticate the journal before replaying it. A failed MAC means the + * journal was tampered, so it is discarded and fresh state generated + * rather than loading forged objects, clock, PCR state, or keys. */ + if (rc == TPM_RC_SUCCESS && + hdr.magic == FWTPM_NV_MAGIC && + hdr.version == FWTPM_NV_VERSION) { + ctx->nvWritePos = hdr.writePos; + if (FwNvGetIntegrityKey(ctx, vKey, &vKeySz)) { + byte storedMac[FWTPM_NV_MAC_SIZE]; + byte calcMac[FWTPM_NV_MAC_SIZE]; + if (ctx->nvWritePos + FWTPM_NV_MAC_SIZE > hal->maxSize || + hal->read(hal->ctx, ctx->nvWritePos, storedMac, + FWTPM_NV_MAC_SIZE) != TPM_RC_SUCCESS || + FwNvComputeJournalMac(ctx, vKey, vKeySz, calcMac) + != TPM_RC_SUCCESS || + TPM2_ConstantCompare(storedMac, calcMac, + FWTPM_NV_MAC_SIZE) != 0) { + rc = TPM_RC_NV_UNINITIALIZED; + } + TPM2_ForceZero(vKey, sizeof(vKey)); + } + } + if (rc == TPM_RC_SUCCESS && hdr.magic == FWTPM_NV_MAGIC && hdr.version == FWTPM_NV_VERSION) { diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 4b375355..4217dc6e 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7284,6 +7284,85 @@ static void test_fwtpm_nv_read_public(void) fwtpm_pass("NV_ReadPublic:", 0); } +/* In-memory NV backend with an integrity key, to prove a tampered journal is + * rejected on load (forged objects/clock/PCR state are not replayed). */ +#define TNV_SIZE (32 * 1024) +static byte gTnvBuf[TNV_SIZE]; +static int TnvRead(void* c, word32 off, byte* buf, word32 sz) +{ + (void)c; + if ((size_t)off + sz > sizeof(gTnvBuf)) return TPM_RC_FAILURE; + memcpy(buf, gTnvBuf + off, sz); + return TPM_RC_SUCCESS; +} +static int TnvWrite(void* c, word32 off, const byte* buf, word32 sz) +{ + (void)c; + if ((size_t)off + sz > sizeof(gTnvBuf)) return TPM_RC_FAILURE; + memcpy(gTnvBuf + off, buf, sz); + return TPM_RC_SUCCESS; +} +static int TnvKey(void* c, byte* key, word32* keySz) +{ + (void)c; + memset(key, 0x5A, 32); + *keySz = 32; + return 0; +} +static void TnvSetHal(FWTPM_CTX* ctx) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->nvHal.read = TnvRead; + ctx->nvHal.write = TnvWrite; + ctx->nvHal.maxSize = sizeof(gTnvBuf); + ctx->nvHal.get_integrity_key = TnvKey; +} + +static void test_fwtpm_nv_journal_tamper_rejected(void) +{ + FWTPM_CTX ctx; + int rspSize = 0, cmdSz, pos; + UINT32 nvIdx = 0x01500070; + UINT32 attrs = TPMA_NV_OWNERWRITE | TPMA_NV_OWNERREAD | TPMA_NV_NO_DA; + + memset(gTnvBuf, 0, sizeof(gTnvBuf)); + + /* Provision an NV index and persist the MAC'd journal */ + TnvSetHal(&ctx); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + cmdSz = BuildNvDefineCmd(gCmd, nvIdx, 8, attrs); + FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + AssertIntEQ(FWTPM_NV_Save(&ctx), TPM_RC_SUCCESS); + FWTPM_Cleanup(&ctx); + + /* Untampered reload: the index persists */ + TnvSetHal(&ctx); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_NV_ReadPublic); + PutU32BE(gCmd + pos, nvIdx); 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); + + /* Flip a journal byte past the header, then reload: the journal MAC + * fails so the forged state is discarded and the index is gone. */ + gTnvBuf[sizeof(FWTPM_NV_HEADER) + 1] ^= 0xFF; + TnvSetHal(&ctx); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_NV_ReadPublic); + PutU32BE(gCmd + pos, nvIdx); pos += 4; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntNE(GetRspRC(gRsp), TPM_RC_SUCCESS); + FWTPM_Cleanup(&ctx); + + printf("Test fwTPM:\tNV journal tamper rejected:\tPassed\n"); +} + static void test_fwtpm_nv_counter(void) { FWTPM_CTX ctx; @@ -9145,6 +9224,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) #ifndef FWTPM_NO_NV test_fwtpm_nv_define_write_read(); test_fwtpm_nv_read_public(); + test_fwtpm_nv_journal_tamper_rejected(); test_fwtpm_nv_counter(); #ifndef FWTPM_NO_ATTESTATION test_fwtpm_nv_certify_digest_mode(); diff --git a/wolftpm/fwtpm/fwtpm.h b/wolftpm/fwtpm/fwtpm.h index 325dee13..cc911810 100644 --- a/wolftpm/fwtpm/fwtpm.h +++ b/wolftpm/fwtpm/fwtpm.h @@ -595,6 +595,11 @@ struct FWTPM_NV_HAL_S { int (*erase)(void* ctx, word32 offset, word32 size); /* Optional */ void* ctx; word32 maxSize; /* Total NV region size */ + /* Override the NV-journal integrity key with a platform device secret + * (e.g. hardware-fused or host-TPM backed). Return 0 and set *keySz on + * success. When NULL the default file backend uses an auto-created key + * file; integrity verification is always performed when a key exists. */ + int (*get_integrity_key)(void* ctx, byte* key, word32* keySz); }; /* Clock HAL callbacks (optional - if not set, clockOffset used directly) */ From 89eb04527873b3574569df1951a8af1b96ac40c3 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 12:35:28 -0700 Subject: [PATCH 19/50] F-5646 - Document empty-auth risk on key load/import helpers --- wolftpm/tpm2_wrap.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/wolftpm/tpm2_wrap.h b/wolftpm/tpm2_wrap.h index 1b1e85df..0c25f2fd 100644 --- a/wolftpm/tpm2_wrap.h +++ b/wolftpm/tpm2_wrap.h @@ -1276,10 +1276,16 @@ WOLFTPM_API int wolfTPM2_LoadRsaPrivateKey(WOLFTPM2_DEV* dev, \param scheme value of TPMI_ALG_RSA_SCHEME type, specifying the RSA scheme \param hashAlg value of TPMI_ALG_HASH type, specifying the TPM hashing algorithm + \warning If key->handle.auth is empty the key is loaded with empty + authorization and any caller holding the handle can use it. Set + key->handle.auth (e.g. via wolfTPM2_SetKeyAuthPassword) before loading + to require a password. + \sa wolfTPM2_LoadRsaPrivateKey \sa wolfTPM2_LoadPrivateKey \sa wolfTPM2_ImportRsaPrivateKey \sa wolfTPM2_LoadEccPrivateKey + \sa wolfTPM2_SetKeyAuthPassword */ WOLFTPM_API int wolfTPM2_LoadRsaPrivateKey_ex(WOLFTPM2_DEV* dev, const WOLFTPM2_KEY* parentKey, WOLFTPM2_KEY* key, @@ -1434,9 +1440,15 @@ WOLFTPM_API int wolfTPM2_ImportEccPrivateKeySeed(WOLFTPM2_DEV* dev, \param eccPriv pointer to a byte buffer containing the private material \param eccPrivSz integer value of word32 type, specifying the private material size + \warning If key->handle.auth is empty the key is loaded with empty + authorization and any caller holding the handle can use it. Set + key->handle.auth (e.g. via wolfTPM2_SetKeyAuthPassword) before loading + to require a password. + \sa wolfTPM2_ImportEccPrivateKey \sa wolfTPM2_LoadEccPublicKey \sa wolfTPM2_LoadPrivateKey + \sa wolfTPM2_SetKeyAuthPassword */ WOLFTPM_API int wolfTPM2_LoadEccPrivateKey(WOLFTPM2_DEV* dev, const WOLFTPM2_KEY* parentKey, WOLFTPM2_KEY* key, @@ -3270,8 +3282,14 @@ WOLFTPM_API int wolfTPM2_HmacFinish(WOLFTPM2_DEV* dev, WOLFTPM2_HMAC* hmac, \param keyBuf pointer to key material \param keySz size of key material in bytes + \warning If key->handle.auth is empty the key is loaded with empty + authorization and any caller holding the handle can use it. Set + key->handle.auth (e.g. via wolfTPM2_SetKeyAuthPassword) before loading + to require a password. + \sa wolfTPM2_EncryptDecryptBlock \sa wolfTPM2_EncryptDecrypt + \sa wolfTPM2_SetKeyAuthPassword */ WOLFTPM_API int wolfTPM2_LoadSymmetricKey(WOLFTPM2_DEV* dev, WOLFTPM2_KEY* key, int alg, const byte* keyBuf, word32 keySz); From 77f502279ef0276f14a023fd7bf86f45783c7efb Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 12:51:38 -0700 Subject: [PATCH 20/50] F-5105 F-5106 F-5107 - Require signing key for attestation commands --- src/fwtpm/fwtpm_crypto.c | 6 ++++++ tests/fwtpm_unit_tests.c | 41 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/fwtpm/fwtpm_crypto.c b/src/fwtpm/fwtpm_crypto.c index 95b63097..ace3bd36 100644 --- a/src/fwtpm/fwtpm_crypto.c +++ b/src/fwtpm/fwtpm_crypto.c @@ -3875,6 +3875,12 @@ TPM_RC FwSignAttest(FWTPM_CTX* ctx, FWTPM_Object* obj, int digestSz; enum wc_HashType wcHash; + /* Attestation must be signed by a signing key (Part 3 Sec.18). Reject + * decrypt-only keys so a forged attestation cannot be produced. */ + if (!(obj->pub.objectAttributes & TPMA_OBJECT_sign)) { + return TPM_RC_KEY; + } + /* Resolve scheme/hash from key if NULL */ FwResolveSignScheme(obj, &sigScheme, &sigHashAlg); diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 4217dc6e..0104c858 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7839,6 +7839,44 @@ static void test_fwtpm_quote_clockinfo_resetcount_nonzero(void) FWTPM_Cleanup(&ctx); printf("Test fwTPM:\tQuote clockInfo resetCount nonzero:\tPassed\n"); } + +/* Attestation commands must reject a decrypt-only signing key. Certify + * exercises the shared FwSignAttest gate that also guards GetTime and + * NV_Certify. */ +static void test_fwtpm_certify_decrypt_key_returns_key(void) +{ + FWTPM_CTX ctx; + int pos, rspSize = 0; + UINT32 keyH; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + keyH = CreatePrimaryHelper(&ctx, TPM_ALG_ECC); /* restricted decrypt */ + AssertIntNE(keyH, 0); + + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_Certify); pos += 4; + PutU32BE(gCmd + pos, keyH); pos += 4; /* objectHandle */ + PutU32BE(gCmd + pos, keyH); pos += 4; /* signHandle */ + PutU32BE(gCmd + pos, 18); pos += 4; /* two PW auth sessions */ + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; PutU16BE(gCmd + pos, 0); + pos += 2; + PutU32BE(gCmd + pos, TPM_RS_PW); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; gCmd[pos++] = 0; PutU16BE(gCmd + pos, 0); + pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; /* qualifyingData */ + PutU16BE(gCmd + pos, TPM_ALG_NULL); pos += 2; /* inScheme */ + PutU32BE(gCmd + 2, (UINT32)pos); + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_KEY); + + FlushHandle(&ctx, keyH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tCertify(decrypt key) rejected:\tPassed\n"); +} #endif /* HAVE_ECC */ /* NV_Certify with size=0 and offset=0 must emit TPMS_NV_DIGEST_CERTIFY_INFO @@ -7862,7 +7900,7 @@ static void test_fwtpm_nv_certify_digest_mode(void) * does not enforce sign attribute, so a restricted-decrypt key suffices * to exercise the attest-tag path under test). */ #ifdef HAVE_ECC - keyH = CreatePrimaryHelper(&ctx, TPM_ALG_ECC); + keyH = CreatePrimaryEccSignHelper(&ctx); /* attestation needs a sign key */ #else keyH = CreatePrimaryHelper(&ctx, TPM_ALG_RSA); #endif @@ -9239,6 +9277,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_quote_unrestricted_sign_returns_attributes(); test_fwtpm_sign_scheme_downgrade_rejected(); test_fwtpm_quote_clockinfo_resetcount_nonzero(); + test_fwtpm_certify_decrypt_key_returns_key(); #endif #endif #endif From e98e527997da7e5effaf2a9711dd7c1708ed64a3 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 12:55:09 -0700 Subject: [PATCH 21/50] F-4845 F-4848 F-5096 F-5015 - Enforce key scheme/hash in RSA and HMAC --- src/fwtpm/fwtpm_command.c | 30 ++++++++++++++++++++++++++++-- src/fwtpm/fwtpm_crypto.c | 14 ++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index b915db2a..ccdb1f09 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -6585,8 +6585,18 @@ static TPM_RC FwCmd_RSA_Encrypt(FWTPM_CTX* ctx, TPM2_Packet* cmd, /* Use the key's scheme if set, otherwise keep NULL */ if (obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL) { encScheme = obj->pub.parameters.rsaDetail.scheme.scheme; + encHashAlg = obj->pub.parameters.rsaDetail.scheme.details + .anySig.hashAlg; } } + /* A key with a declared scheme hash must not be used with a + * different wire hash (Part 3 Sec.17.2) */ + else if (rc == 0 && encHashAlg != TPM_ALG_NULL && + obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL && + encHashAlg != obj->pub.parameters.rsaDetail.scheme.details + .anySig.hashAlg) { + rc = TPM_RC_SCHEME; + } #ifdef DEBUG_WOLFTPM printf("fwTPM: RSA_Encrypt(handle=0x%x, scheme=0x%x, msgSz=%d)\n", @@ -6738,8 +6748,18 @@ static TPM_RC FwCmd_RSA_Decrypt(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0 && decScheme == TPM_ALG_NULL) { if (obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL) { decScheme = obj->pub.parameters.rsaDetail.scheme.scheme; + decHashAlg = obj->pub.parameters.rsaDetail.scheme.details + .anySig.hashAlg; } } + /* A key with a declared scheme hash must not be used with a + * different wire hash (Part 3 Sec.17.2) */ + else if (rc == 0 && decHashAlg != TPM_ALG_NULL && + obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL && + decHashAlg != obj->pub.parameters.rsaDetail.scheme.details + .anySig.hashAlg) { + rc = TPM_RC_SCHEME; + } #ifdef DEBUG_WOLFTPM printf("fwTPM: RSA_Decrypt(handle=0x%x, scheme=0x%x, ctSz=%d)\n", @@ -6958,9 +6978,15 @@ static TPM_RC FwCmd_HMAC(FWTPM_CTX* ctx, TPM2_Packet* cmd, /* Parse hashAlg */ TPM2_Packet_ParseU16(cmd, (UINT16*)&hashAlg); - /* If hashAlg is NULL, use the key's nameAlg */ + /* NULL means use the key's HMAC scheme hash; a non-NULL value must + * match it (Part 3 Sec.17.4) rather than any wire-chosen hash. */ if (hashAlg == TPM_ALG_NULL) { - hashAlg = obj->pub.nameAlg; + hashAlg = obj->pub.parameters.keyedHashDetail.scheme.details + .hmac.hashAlg; + } + else if (hashAlg != obj->pub.parameters.keyedHashDetail.scheme + .details.hmac.hashAlg) { + rc = TPM_RC_VALUE; } ht = FwGetWcHashType(hashAlg); diff --git a/src/fwtpm/fwtpm_crypto.c b/src/fwtpm/fwtpm_crypto.c index ace3bd36..e16d1e3b 100644 --- a/src/fwtpm/fwtpm_crypto.c +++ b/src/fwtpm/fwtpm_crypto.c @@ -3629,6 +3629,20 @@ TPM_RC FwVerifySignatureCore(FWTPM_Object* obj, rsaInit = 1; } + /* A key with a declared scheme hash must not verify a signature + * made under a different (e.g. downgraded) hash, and the digest + * length must match the signature hash (Part 3 Sec.20.2). */ + if (rc == 0 && + obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL && + sig->signature.rsassa.hash != + obj->pub.parameters.rsaDetail.scheme.details.anySig.hashAlg) { + rc = TPM_RC_SCHEME; + } + if (rc == 0 && digestSz != + TPM2_GetHashDigestSize(sig->signature.rsassa.hash)) { + rc = TPM_RC_SIZE; + } + if (rc == 0) { if (pad == WC_RSA_PSS_PAD) { FWTPM_DECLARE_BUF(decSig, FWTPM_MAX_PUB_BUF); From 7a4b94e985dab7b1751e9c8d4112aaf0b08ca4c1 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 12:57:08 -0700 Subject: [PATCH 22/50] F-4956 F-4846 - Enforce tpmKey decrypt attr and cipher mode match --- src/fwtpm/fwtpm_command.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index ccdb1f09..c629a7ae 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -8077,6 +8077,13 @@ static TPM_RC FwCmd_StartAuthSession(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (keyObj == NULL) { rc = TPM_RC_HANDLE; } + /* tpmKey must be a restricted decryption key (Part 1 Sec.11.2.2); + * a signing-only key would let an attacker derive the session key. */ + if (rc == 0 && + (!(keyObj->pub.objectAttributes & TPMA_OBJECT_decrypt) || + !(keyObj->pub.objectAttributes & TPMA_OBJECT_restricted))) { + rc = TPM_RC_KEY; + } if (rc == 0) { rc = FwDecryptSeed(ctx, keyObj, encSalt, encSaltSize, @@ -11678,6 +11685,11 @@ static TPM_RC FwEncryptDecryptCore(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0 && mode == TPM_ALG_NULL) { mode = obj->pub.parameters.symDetail.sym.mode.sym; } + /* A non-NULL wire mode must match the key's configured mode; otherwise + * e.g. ECB could be applied to a CFB key (Part 3 Sec.12.6.1). */ + else if (rc == 0 && mode != obj->pub.parameters.symDetail.sym.mode.sym) { + rc = TPM_RC_MODE; + } #ifdef DEBUG_WOLFTPM if (rc == 0) { From 08d327bfab9581fd117a58a55e99addef39f5a7a Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 12:59:10 -0700 Subject: [PATCH 23/50] F-5679 F-5279 - EventSequenceComplete locality and ClockSet bound --- src/fwtpm/fwtpm_command.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index c629a7ae..0ebc0a64 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -2227,7 +2227,10 @@ static TPM_RC FwCmd_ClockSet(FWTPM_CTX* ctx, TPM2_Packet* cmd, /* New time must be >= current (can only advance) */ if (rc == 0) { UINT64 currentTime = FWTPM_Clock_GetMs(ctx); - if (newTime < currentTime) { + /* Clock may move forward but at most 2^32-1 ms per call (Part 1 + * Sec.17.5.3); reject a far-future value that would freeze it. */ + if (newTime < currentTime || + newTime > currentTime + ((UINT64)1 << 32)) { rc = TPM_RC_VALUE; } } @@ -7622,8 +7625,17 @@ static TPM_RC FwCmd_EventSequenceComplete(FWTPM_CTX* ctx, TPM2_Packet* cmd, /* Extend the result into the PCR */ if (rc == 0 && pcrHandle <= PCR_LAST) { pcrIndex = pcrHandle - PCR_FIRST; + /* DRTM PCRs are locality-restricted (Part 1 Sec.11.4.6): + * PCR 17 requires locality 4; PCRs 18-22 require locality 3 or 4. */ + 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; + } bank = FwGetPcrBankIndex(seqHashAlg); - if (bank >= 0 && digestSz > 0) { + if (rc == 0 && bank >= 0 && digestSz > 0) { wcHash = FwGetWcHashType(seqHashAlg); XMEMCPY(concat, ctx->pcrDigest[pcrIndex][bank], digestSz); XMEMCPY(concat + digestSz, digest, digestSz); From 0d95be5f653b8f8c20898f5510a7ebff400dc60e Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:00:39 -0700 Subject: [PATCH 24/50] F-5099 F-5109 - Validate authPolicy size and nvIndex range on NV load --- src/fwtpm/fwtpm_nv.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index 70420418..b69b5ad1 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -347,6 +347,12 @@ static int FwNvUnmarshalPublic(const byte* buf, word32* pos, word32 maxSz, if (pkt.pos <= 0 || (word32)pkt.pos > (maxSz - *pos)) { return TPM_RC_FAILURE; } + /* authPolicy.size must equal the nameAlg digest size (Part 3 Sec.31.3) */ + if (pub2b.publicArea.authPolicy.size > 0 && + (int)pub2b.publicArea.authPolicy.size != + TPM2_GetHashDigestSize(pub2b.publicArea.nameAlg)) { + return TPM_RC_FAILURE; + } XMEMCPY(pub, &pub2b.publicArea, sizeof(TPMT_PUBLIC)); *pos += pkt.pos; @@ -413,6 +419,13 @@ static int FwNvUnmarshalNvPublic(const byte* buf, word32* pos, word32 maxSz, { int rc; rc = FwNvUnmarshalU32(buf, pos, maxSz, &nvPub->nvIndex); + /* nvIndex must be in the NV handle range or a later lookup could be + * confused with another handle class (Part 2 Sec.7.4). */ + if (rc == 0 && + (nvPub->nvIndex < NV_INDEX_FIRST || + nvPub->nvIndex > NV_INDEX_LAST)) { + rc = TPM_RC_FAILURE; + } if (rc == 0) { rc = FwNvUnmarshalU16(buf, pos, maxSz, &nvPub->nameAlg); } From 5617f43aae5c49da97217632ce8d0ae94f0ee3bb Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:06:36 -0700 Subject: [PATCH 25/50] F-5277 F-5122 - Store and enforce policy cpHashA command binding --- src/fwtpm/fwtpm_command.c | 58 ++++++++++++++++++++++++++++++++---- tests/fwtpm_unit_tests.c | 62 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 6 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 0ebc0a64..53068a6f 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -8856,6 +8856,8 @@ static TPM_RC FwCmd_PolicySecret(FWTPM_CTX* ctx, TPM2_Packet* cmd, UINT16 nonceTpmSz, cpHashASz, policyRefSz = 0; INT32 expiration; byte policyRef[64]; + byte nonceTpmBuf[TPM_MAX_DIGEST_SIZE]; + byte cpHashBuf[TPM_MAX_DIGEST_SIZE]; byte entityName[sizeof(TPM2B_NAME)]; int entityNameSz = 0; FWTPM_Session* sess; @@ -8868,19 +8870,25 @@ static TPM_RC FwCmd_PolicySecret(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0) { TPM2_Packet_ParseU16(cmd, &nonceTpmSz); - if (cmd->pos + nonceTpmSz > cmdSize) { - rc = TPM_RC_COMMAND_SIZE; + if (nonceTpmSz > (UINT16)sizeof(nonceTpmBuf) || + cmd->pos + nonceTpmSz > cmdSize) { + rc = TPM_RC_SIZE; } } if (rc == 0) { - cmd->pos += nonceTpmSz; + if (nonceTpmSz > 0) { + TPM2_Packet_ParseBytes(cmd, nonceTpmBuf, nonceTpmSz); + } TPM2_Packet_ParseU16(cmd, &cpHashASz); - if (cmd->pos + cpHashASz > cmdSize) { - rc = TPM_RC_COMMAND_SIZE; + if (cpHashASz > (UINT16)sizeof(cpHashBuf) || + cmd->pos + cpHashASz > cmdSize) { + rc = TPM_RC_SIZE; } } if (rc == 0) { - cmd->pos += cpHashASz; + if (cpHashASz > 0) { + TPM2_Packet_ParseBytes(cmd, cpHashBuf, cpHashASz); + } TPM2_Packet_ParseU16(cmd, &policyRefSz); if (policyRefSz > (UINT16)sizeof(policyRef)) { rc = TPM_RC_SIZE; @@ -8909,6 +8917,16 @@ static TPM_RC FwCmd_PolicySecret(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_AUTH_TYPE; } + /* A supplied nonceTPM must match the session nonce (Part 3 Sec.23.4), + * preventing replay of a PolicySecret authorization to another session. */ + if (rc == 0 && nonceTpmSz > 0) { + if (nonceTpmSz != sess->nonceTPM.size || + TPM2_ConstantCompare(nonceTpmBuf, sess->nonceTPM.buffer, + nonceTpmSz) != 0) { + rc = TPM_RC_VALUE; + } + } + if (rc == 0) { /* Auth verification for authHandle is handled by the command dispatch * framework (FWTPM_ProcessCommand) via the authorization area, not @@ -8926,6 +8944,12 @@ static TPM_RC FwCmd_PolicySecret(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } + /* Bind the command to cpHashA so policy enforcement can verify it */ + if (rc == 0 && cpHashASz > 0) { + sess->cpHashA.size = cpHashASz; + XMEMCPY(sess->cpHashA.buffer, cpHashBuf, cpHashASz); + } + if (rc == 0) { /* Response: timeout(TPM2B size=0) + ticket(TPMT_TK_AUTH) */ paramStart = FwRspParamsBegin(rsp, cmdTag, ¶mSzPos); @@ -9431,6 +9455,12 @@ static TPM_RC FwCmd_PolicySigned(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } + /* Bind the command to cpHashA so policy enforcement can verify it */ + if (rc == 0 && cpHashASz > 0) { + sess->cpHashA.size = cpHashASz; + XMEMCPY(sess->cpHashA.buffer, cpHashBuf, cpHashASz); + } + if (rc == 0) { /* Response: timeout(TPM2B size=0) + ticket(TPMT_TK_AUTH) */ paramStart = FwRspParamsBegin(rsp, cmdTag, ¶mSzPos); @@ -15775,6 +15805,22 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx, TPM_ST_NO_SESSIONS, TPM_RC_LOCALITY); return TPM_RC_SUCCESS; } + /* Enforce any PolicyCpHash command binding: the command's + * cpHash must equal the value the policy committed to. */ + if (pSess->cpHashA.size > 0) { + byte ccpHash[TPM_MAX_DIGEST_SIZE]; + int ccpHashSz = 0; + if (FwComputeCpHash(pSess->authHash, cmdCode, + cmdBuf, cmdSize, cmdHandles, cmdHandleCnt, + ctx, cpStart, ccpHash, &ccpHashSz) != 0 || + (int)pSess->cpHashA.size != ccpHashSz || + TPM2_ConstantCompare(pSess->cpHashA.buffer, + ccpHash, (word32)ccpHashSz) != 0) { + *rspSize = FwBuildErrorResponse(rspBuf, + TPM_ST_NO_SESSIONS, TPM_RC_POLICY_FAIL); + return TPM_RC_SUCCESS; + } + } } else if (authPolicy != NULL && authPolicy->size == 0 && cmdAuths[pj].cmdHmacSize == 0) { diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 0104c858..90485c53 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -7170,6 +7170,67 @@ static void test_fwtpm_policy_locality_enforced(void) FWTPM_Cleanup(&ctx); printf("Test fwTPM:\tPolicyLocality enforced:\tPassed\n"); } + +/* PolicyCpHash binds a session to a specific command; a command whose real + * cpHash differs must be rejected even when the policyDigest matches. */ +static void test_fwtpm_policy_cphash_enforced(void) +{ + FWTPM_CTX ctx; + int pos, cmdSz, rspSize = 0; + UINT32 sessH; + UINT16 dSz; + byte digest[64]; + byte cph[32]; + UINT32 nvIdx = 0x01500062; + UINT32 nvAttrs = TPMA_NV_OWNERWRITE | TPMA_NV_OWNERREAD | TPMA_NV_NO_DA; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + memset(cph, 0xCC, sizeof(cph)); + + sessH = StartSessionHelper(&ctx, TPM_SE_POLICY); + AssertIntNE(sessH, 0); + + /* Bind the session to a cpHash no real command will produce */ + pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_PolicyCpHash); + PutU32BE(gCmd + pos, sessH); pos += 4; + PutU16BE(gCmd + pos, (UINT16)sizeof(cph)); pos += 2; + memcpy(gCmd + pos, cph, sizeof(cph)); pos += sizeof(cph); + PutU32BE(gCmd + 2, (UINT32)pos); + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + + AssertIntEQ(SendPolicyCmd(&ctx, TPM_CC_PolicyGetDigest, sessH), + TPM_RC_SUCCESS); + dSz = GetU16BE(gRsp + TPM2_HEADER_SIZE + 4); + AssertIntEQ(dSz, 32); + memcpy(digest, gRsp + TPM2_HEADER_SIZE + 6, dSz); + + 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); + PutU16BE(gCmd + pos, dSz); pos += 2; + memcpy(gCmd + pos, digest, dSz); pos += dSz; + 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_SUCCESS); + + /* The NV_DefineSpace cpHash will not match the bound cpHashA */ + cmdSz = BuildNvDefineCmd(gCmd, nvIdx, 8, nvAttrs); + PutU32BE(gCmd + 18, sessH); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, cmdSz, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_POLICY_FAIL); + + FlushHandle(&ctx, sessH); + FWTPM_Cleanup(&ctx); + printf("Test fwTPM:\tPolicyCpHash enforced:\tPassed\n"); +} #endif /* !FWTPM_NO_NV */ #endif /* !FWTPM_NO_POLICY */ @@ -9255,6 +9316,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_policynv_owner_read_denied(); test_fwtpm_policyauthorizenv_owner_read_denied(); test_fwtpm_policy_locality_enforced(); + test_fwtpm_policy_cphash_enforced(); #endif #endif From 194d3bf03cae89fdac9ca63961397168df754f9e Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:10:55 -0700 Subject: [PATCH 26/50] F-5278 F-5723 - Exempt NO_DA NV from lockout and persist GlobalWriteLock --- src/fwtpm/fwtpm_command.c | 48 ++++++++++++++++++++++++++++++--------- src/fwtpm/fwtpm_nv.c | 7 ++++-- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 53068a6f..8e858643 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -11509,6 +11509,8 @@ static TPM_RC FwCmd_NV_GlobalWriteLock(FWTPM_CTX* ctx, TPM2_Packet* cmd, printf("fwTPM: NV_GlobalWriteLock(auth=0x%x)\n", authHandle); #endif ctx->globalNvWriteLock = 1; + /* Persist so the lock survives a daemon restart (Resume) */ + FWTPM_NV_SaveFlags(ctx); FwRspNoParams(rsp, cmdTag); } @@ -15463,6 +15465,23 @@ static const FWTPM_CMD_ENTRY* FwFindCmdEntry(TPM_CC cc) return NULL; } +/* Return 1 if the handle is an NV index with TPMA_NV_NO_DA set. Such an + * index is exempt from dictionary-attack lockout (Part 1 Sec.23.2). */ +static int FwHandleIsNoDA(FWTPM_CTX* ctx, TPM_HANDLE handle) +{ +#ifndef FWTPM_NO_NV + if ((handle & 0xFF000000) == (NV_INDEX_FIRST & 0xFF000000)) { + FWTPM_NvIndex* nv = FwFindNvIndex(ctx, handle); + if (nv != NULL && (nv->nvPublic.attributes & TPMA_NV_NO_DA)) { + return 1; + } + } +#else + (void)ctx; (void)handle; +#endif + return 0; +} + /* ================================================================== */ /* Public API: FWTPM_ProcessCommand */ /* ================================================================== */ @@ -15852,7 +15871,8 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx, cmdCode != TPM_CC_DictionaryAttackLockReset && cmdCode != TPM_CC_DictionaryAttackParameters && cmdCode != TPM_CC_StartAuthSession && - cmdCode != TPM_CC_FlushContext) { + cmdCode != TPM_CC_FlushContext && + !(cmdHandleCnt > 0 && FwHandleIsNoDA(ctx, cmdHandles[0]))) { *rspSize = FwBuildErrorResponse(rspBuf, TPM_ST_NO_SESSIONS, TPM_RC_LOCKOUT); return TPM_RC_SUCCESS; @@ -15908,11 +15928,14 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx, "0x%x (CC=0x%x)\n", entityH, cmdCode); #endif #ifndef FWTPM_NO_DA - ctx->daFailedTries++; - if (ctx->daFailedTries >= ctx->daMaxTries) { - *rspSize = FwBuildErrorResponse(rspBuf, - TPM_ST_NO_SESSIONS, TPM_RC_LOCKOUT); - return TPM_RC_SUCCESS; + /* A failed auth against a NO_DA index must not feed lockout */ + if (!FwHandleIsNoDA(ctx, entityH)) { + ctx->daFailedTries++; + if (ctx->daFailedTries >= ctx->daMaxTries) { + *rspSize = FwBuildErrorResponse(rspBuf, + TPM_ST_NO_SESSIONS, TPM_RC_LOCKOUT); + return TPM_RC_SUCCESS; + } } #endif *rspSize = FwBuildErrorResponse(rspBuf, @@ -16002,11 +16025,14 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx, "0x%x (CC=0x%x)\n", entityH, cmdCode); #endif #ifndef FWTPM_NO_DA - ctx->daFailedTries++; - if (ctx->daFailedTries >= ctx->daMaxTries) { - *rspSize = FwBuildErrorResponse(rspBuf, - TPM_ST_NO_SESSIONS, TPM_RC_LOCKOUT); - return TPM_RC_SUCCESS; + /* A failed auth against a NO_DA index must not feed lockout */ + if (!FwHandleIsNoDA(ctx, entityH)) { + ctx->daFailedTries++; + if (ctx->daFailedTries >= ctx->daMaxTries) { + *rspSize = FwBuildErrorResponse(rspBuf, + TPM_ST_NO_SESSIONS, TPM_RC_LOCKOUT); + return TPM_RC_SUCCESS; + } } #endif *rspSize = FwBuildErrorResponse(rspBuf, diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index b69b5ad1..f378cf31 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -982,6 +982,7 @@ static int FwNvProcessEntry(FWTPM_CTX* ctx, UINT16 tag, UINT8 flags8 = 0; FwNvUnmarshalU8(value, &vPos, vMax, &flags8); ctx->disableClear = (flags8 & 0x01) ? 1 : 0; + ctx->globalNvWriteLock = (flags8 & 0x02) ? 1 : 0; #ifndef FWTPM_NO_DA FwNvUnmarshalU32(value, &vPos, vMax, &ctx->daMaxTries); FwNvUnmarshalU32(value, &vPos, vMax, &ctx->daRecoveryTime); @@ -1512,7 +1513,8 @@ int FWTPM_NV_Save(FWTPM_CTX* ctx) if (rc == 0) { pos = 0; FwNvMarshalU8(buf, &pos, bufSz, - (UINT8)(ctx->disableClear ? 0x01 : 0x00)); + (UINT8)((ctx->disableClear ? 0x01 : 0x00) | + (ctx->globalNvWriteLock ? 0x02 : 0x00))); #ifndef FWTPM_NO_DA FwNvMarshalU32(buf, &pos, bufSz, ctx->daMaxTries); FwNvMarshalU32(buf, &pos, bufSz, ctx->daRecoveryTime); @@ -1800,7 +1802,8 @@ int FWTPM_NV_SaveFlags(FWTPM_CTX* ctx) } FwNvMarshalU8(buf, &pos, sizeof(buf), - (UINT8)(ctx->disableClear ? 0x01 : 0x00)); + (UINT8)((ctx->disableClear ? 0x01 : 0x00) | + (ctx->globalNvWriteLock ? 0x02 : 0x00))); #ifndef FWTPM_NO_DA FwNvMarshalU32(buf, &pos, sizeof(buf), ctx->daMaxTries); FwNvMarshalU32(buf, &pos, sizeof(buf), ctx->daRecoveryTime); From 32a823c44d524e7f1b402d8fb7d3ee895c3baa16 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:12:45 -0700 Subject: [PATCH 27/50] F-5834 F-5281 F-5118 - Fix ephemeral DoS and PKCS1 unpad OOB/padding --- src/fwtpm/fwtpm_command.c | 9 ++++++--- src/tpm2_asn.c | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 8e858643..a1edd288 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -13433,9 +13433,12 @@ static TPM_RC FwCmd_ZGen_2Phase(FWTPM_CTX* ctx, TPM2_Packet* cmd, TPM2_ForceZero(z1yBuf, sizeof(z1yBuf)); TPM2_ForceZero(z2xBuf, sizeof(z2xBuf)); TPM2_ForceZero(z2yBuf, sizeof(z2yBuf)); - /* Zero ephemeral key — it was consumed and must not be reused */ - TPM2_ForceZero(ctx->ecEphemeralKey, sizeof(ctx->ecEphemeralKey)); - ctx->ecEphemeralKeySz = 0; + /* Only consume the ephemeral on success; an error path must not let an + * unauthenticated caller destroy a victim's pending ephemeral. */ + if (rc == 0) { + TPM2_ForceZero(ctx->ecEphemeralKey, sizeof(ctx->ecEphemeralKey)); + ctx->ecEphemeralKeySz = 0; + } if (privAInit) wc_ecc_free(privKeyA); if (ephInit) wc_ecc_free(privEph); if (peerInit) wc_ecc_free(peerPub); diff --git a/src/tpm2_asn.c b/src/tpm2_asn.c index bab41b60..8faaa8de 100644 --- a/src/tpm2_asn.c +++ b/src/tpm2_asn.c @@ -381,7 +381,10 @@ int TPM2_ASN_RsaUnpadPkcsv15(uint8_t** pSig, int* sigSz) break; idx++; } - if (idx < *sigSz && sig[idx++] == 0x00) { + /* Require the null separator to be in bounds (avoids a 1-byte + * over-read) and at least 8 padding bytes per PKCS#1 v1.5 Sec.9.2 + * (also rejects the all-0xFF Bleichenbacher variant). */ + if (idx < *sigSz && (idx - 2) >= 8 && sig[idx++] == 0x00) { rc = 0; *pSig = &sig[idx]; *sigSz -= idx; From 204573faa2b75a2353699e655170c99839f8552b Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:16:30 -0700 Subject: [PATCH 28/50] F-4161 F-4843 - Reject oversized auth and undersized session nonce --- src/fwtpm/fwtpm_command.c | 6 ++++++ src/tpm2_wrap.c | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index a1edd288..0612f8db 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -8043,6 +8043,12 @@ static TPM_RC FwCmd_StartAuthSession(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } + /* nonceCaller must carry at least the 16-octet minimum (Part 1 + * Sec.19.6.8); a tiny/zero nonce weakens the param-encryption IV. */ + if (rc == 0 && nonceCallerSize < 16) { + rc = TPM_RC_SIZE; + } + #ifdef DEBUG_WOLFTPM if (rc == 0) { printf("fwTPM: StartAuthSession(type=%d, hash=0x%x, tpmKey=0x%x, " diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index 1cdc7c7f..bc668074 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -2890,8 +2890,9 @@ int wolfTPM2_CreateKey(WOLFTPM2_DEV* dev, WOLFTPM2_KEYBLOB* keyBlob, TPM2B_AUTH* pAuth = &createIn.inSensitive.sensitive.userAuth; int nameAlgDigestSz = TPM2_GetHashDigestSize(publicTemplate->nameAlg); if (nameAlgDigestSz > 0) { + /* Reject oversized auth rather than silently truncating it */ if (authSz > nameAlgDigestSz) { - authSz = nameAlgDigestSz; + return BUFFER_E; } XMEMCPY(pAuth->buffer, auth, authSz); if (authSz < nameAlgDigestSz) { From 5f8c180771879617fad7a881a31cdc0af8c54ac1 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:18:30 -0700 Subject: [PATCH 29/50] F-4954 - Require TPM_RH_NULL hierarchy for LoadExternal private key --- src/fwtpm/fwtpm_command.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 0612f8db..68644c38 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -4612,7 +4612,12 @@ static TPM_RC FwCmd_LoadExternal(FWTPM_CTX* ctx, TPM2_Packet* cmd, } if (rc == 0) { TPM2_Packet_ParseU32(cmd, &hierarchy); - (void)hierarchy; + /* Private key material may only be loaded under TPM_RH_NULL + * (Part 3 Sec.18.4); otherwise the object would claim a real + * hierarchy and yield a spoofed TPM_ST_VERIFIED ticket. */ + if (inPrivSize > 0 && hierarchy != TPM_RH_NULL) { + rc = TPM_RC_HIERARCHY; + } } #ifdef DEBUG_WOLFTPM From c6b15aabf626931e4483cc3e5f8ce62096634b31 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:21:26 -0700 Subject: [PATCH 30/50] F-4955 - Derive credential keys under EK nameAlg not hardcoded SHA-256 --- src/fwtpm/fwtpm_command.c | 4 ++-- src/fwtpm/fwtpm_crypto.c | 7 +++++-- wolftpm/fwtpm/fwtpm_crypto.h | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 68644c38..1663a93e 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -12784,7 +12784,7 @@ static TPM_RC FwCmd_MakeCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, /* Derive symmetric and HMAC keys from seed */ if (rc == 0) { - rc = FwCredentialDeriveKeys(seed, seedSz, + rc = FwCredentialDeriveKeys(keyObj->pub.nameAlg, seed, seedSz, objectName.name, objectName.size, symKey, (int)sizeof(symKey), hmacKey, (int)sizeof(hmacKey)); @@ -12980,7 +12980,7 @@ static TPM_RC FwCmd_ActivateCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, /* Derive symmetric and HMAC keys from seed */ if (rc == 0) { objName = &activateObj->name; - rc = FwCredentialDeriveKeys(seed, seedSzInt, + rc = FwCredentialDeriveKeys(keyObj->pub.nameAlg, seed, seedSzInt, objName->name, objName->size, symKey, (int)sizeof(symKey), hmacKey, (int)sizeof(hmacKey)); diff --git a/src/fwtpm/fwtpm_crypto.c b/src/fwtpm/fwtpm_crypto.c index e16d1e3b..658bd91f 100644 --- a/src/fwtpm/fwtpm_crypto.c +++ b/src/fwtpm/fwtpm_crypto.c @@ -3930,6 +3930,7 @@ TPM_RC FwSignAttest(FWTPM_CTX* ctx, FWTPM_Object* obj, /* Derive AES symmetric key ("STORAGE") and HMAC key ("INTEGRITY") from seed. * Per TPM 2.0 Part 1 Section 24. */ TPM_RC FwCredentialDeriveKeys( + TPMI_ALG_HASH nameAlg, const byte* seed, int seedSz, const byte* name, int nameSz, byte* symKey, int symKeySz, @@ -3937,12 +3938,14 @@ TPM_RC FwCredentialDeriveKeys( { int kdfRc; - kdfRc = TPM2_KDFa_ex(TPM_ALG_SHA256, seed, seedSz, + /* Derive under the credentialed key's nameAlg, not a hardcoded SHA-256, + * so a SHA-384 EK is not silently downgraded. */ + kdfRc = TPM2_KDFa_ex(nameAlg, seed, seedSz, "STORAGE", name, nameSz, NULL, 0, symKey, symKeySz); if (kdfRc != symKeySz) { return TPM_RC_FAILURE; } - kdfRc = TPM2_KDFa_ex(TPM_ALG_SHA256, seed, seedSz, + kdfRc = TPM2_KDFa_ex(nameAlg, seed, seedSz, "INTEGRITY", NULL, 0, NULL, 0, hmacKey, hmacKeySz); if (kdfRc != hmacKeySz) { TPM2_ForceZero(symKey, symKeySz); diff --git a/wolftpm/fwtpm/fwtpm_crypto.h b/wolftpm/fwtpm/fwtpm_crypto.h index 37131d8a..63a26668 100644 --- a/wolftpm/fwtpm/fwtpm_crypto.h +++ b/wolftpm/fwtpm/fwtpm_crypto.h @@ -352,6 +352,7 @@ TPM_RC FwSignAttest(FWTPM_CTX* ctx, FWTPM_Object* obj, #ifndef FWTPM_NO_CREDENTIAL TPM_RC FwCredentialDeriveKeys( + TPMI_ALG_HASH nameAlg, const byte* seed, int seedSz, const byte* name, int nameSz, byte* symKey, int symKeySz, From 6332a9bd3674a4b4b5d93d47b4656146c7f7dd30 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:23:34 -0700 Subject: [PATCH 31/50] F-4847 - Require inner-wrap path for encryptedDuplication objects --- src/fwtpm/fwtpm_command.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 1663a93e..ef84dd24 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -5291,6 +5291,13 @@ static TPM_RC FwCmd_Duplicate(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (symAlg == TPM_ALG_NULL) { rc = TPM_RC_SYMMETRIC; } + /* The outer-wrap path to a real parent does not also apply the + * mandatory inner wrap, which would leave the new parent able to + * recover the sensitive. Require the inner-wrap export path + * (newParent == TPM_RH_NULL) for such objects. */ + else if (newParentHandle != TPM_RH_NULL) { + rc = TPM_RC_SYMMETRIC; + } } /* Find new parent (if not TPM_RH_NULL) */ From 0f157679f25df612840422799d42125ad349ffc3 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:26:44 -0700 Subject: [PATCH 32/50] F-5653 F-5644 - Bounds-check NV key read and propagate NV load errors --- examples/nvram/read.c | 16 ++++++++++++++++ src/fwtpm/fwtpm_nv.c | 7 ++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/examples/nvram/read.c b/examples/nvram/read.c index f3e57cc3..67286fe5 100644 --- a/examples/nvram/read.c +++ b/examples/nvram/read.c @@ -211,6 +211,14 @@ int TPM2_NVRAM_Read_Example(void* userCtx, int argc, char *argv[]) printf("Successfully read public key part from NV\n\n"); offset += readSize; + /* pub.size comes from NV and must fit the destination buffer */ + if (readSize != sizeof(keyBlob.pub.size) || + sizeof(UINT16) + keyBlob.pub.size > sizeof(pubAreaBuffer)) { + printf("Invalid public key size marker from NV\n"); + rc = BUFFER_E; + goto exit; + } + readSize = sizeof(UINT16) + keyBlob.pub.size; /* account for TPM2B size marker */ printf("Trying to read %d bytes of public key part from NV\n", keyBlob.pub.size); rc = wolfTPM2_NVReadAuth(&dev, &nv, nvIndex, @@ -244,6 +252,14 @@ int TPM2_NVRAM_Read_Example(void* userCtx, int argc, char *argv[]) printf("Successfully read size marker from NV\n\n"); offset += readSize; + /* priv.size comes from NV and must fit the destination buffer */ + if (readSize != sizeof(keyBlob.priv.size) || + keyBlob.priv.size > sizeof(keyBlob.priv.buffer)) { + printf("Invalid private key size marker from NV\n"); + rc = BUFFER_E; + goto exit; + } + readSize = keyBlob.priv.size; printf("Trying to read %d bytes of private key part from NV\n", readSize); rc = wolfTPM2_NVReadAuth(&dev, &nv, nvIndex, diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index f378cf31..1ea2c254 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -1283,6 +1283,9 @@ int FWTPM_NV_Init(FWTPM_CTX* ctx) newBuf = (byte*)XMALLOC(len, NULL, DYNAMIC_TYPE_TMP_BUFFER); if (newBuf == NULL) { + /* Do not silently report success on an allocation + * failure mid-journal */ + rc = TPM_RC_MEMORY; break; } XFREE(valueBuf, NULL, DYNAMIC_TYPE_TMP_BUFFER); @@ -1307,7 +1310,9 @@ int FWTPM_NV_Init(FWTPM_CTX* ctx) (int)hdr.version, (int)ctx->nvWritePos, (int)((ctx->nvWritePos - sizeof(FWTPM_NV_HEADER)))); #endif - rc = TPM_RC_SUCCESS; + /* rc already reflects the scan: SUCCESS for a clean or + * end-of-journal stop, or a genuine read/alloc error to propagate + * rather than masking as success. */ } else { /* No valid NV image — generate fresh hierarchy seeds */ From 944058ae2919aefa24d6c9f914116d028d59b47a Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:27:58 -0700 Subject: [PATCH 33/50] F-5123 F-5124 - fsync NV writes for crash durability --- src/fwtpm/fwtpm_nv.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index 1ea2c254..8c08f02b 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -46,6 +46,7 @@ #if !defined(NO_FILESYSTEM) && !defined(_WIN32) #include + #include #endif #define FWTPM_NV_KEY_SIZE 32 @@ -131,6 +132,11 @@ static int FwNvFileWrite(void* ctx, word32 offset, const byte* buf, } ret = (int)fwrite(buf, 1, size, f); + /* Flush to stable storage so a crash cannot lose committed NV state */ + fflush(f); +#if !defined(_WIN32) + fsync(fileno(f)); +#endif fclose(f); if (ret != (int)size) { From 4ac2bd2d221036ec2d13cc26d9a7f52e6e7ffdfe Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:31:15 -0700 Subject: [PATCH 34/50] F-4675 F-4677 F-5125 - Harden fwTPM command-port signals and shm perms --- src/fwtpm/fwtpm_io.c | 16 +++++++++++++++- src/fwtpm/fwtpm_tis_shm.c | 12 ++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/fwtpm/fwtpm_io.c b/src/fwtpm/fwtpm_io.c index 5e0035be..85d85451 100644 --- a/src/fwtpm/fwtpm_io.c +++ b/src/fwtpm/fwtpm_io.c @@ -436,7 +436,15 @@ static int HandleCommandConnection(FWTPM_CTX* ctx, int clientFd) return TPM_RC_SUCCESS; } - /* Handle platform signals on command port */ + /* State-mutating platform signals belong on the platform port, not the + * unauthenticated command port; reject them here. */ + if (tssCmd == FWTPM_TCP_SIGNAL_POWER_OFF || + tssCmd == FWTPM_TCP_SIGNAL_RESET || + tssCmd == FWTPM_TCP_STOP) { + return TPM_RC_FAILURE; + } + + /* Handle remaining (non state-mutating) platform signals on command port */ if (IsMssimSignal(tssCmd)) { return HandleMssimSignal(ctx, clientFd, tssCmd); } @@ -677,6 +685,12 @@ int FWTPM_IO_ServerLoop(FWTPM_CTX* ctx) SOCKET_T newFd = accept(ctx->io.listenFd, NULL, NULL); if (newFd != FWTPM_INVALID_FD) { if (cmdFd != FWTPM_INVALID_FD) { + /* Consume any select-confirmed in-flight command on the + * old connection before dropping it, so a pending + * request is not silently lost. */ + if (FD_ISSET(cmdFd, &readFds)) { + HandleCommandConnection(ctx, cmdFd); + } #ifdef DEBUG_WOLFTPM printf("fwTPM: command connection replaced\n"); #endif diff --git a/src/fwtpm/fwtpm_tis_shm.c b/src/fwtpm/fwtpm_tis_shm.c index 995a764b..a0bc5d40 100644 --- a/src/fwtpm/fwtpm_tis_shm.c +++ b/src/fwtpm/fwtpm_tis_shm.c @@ -46,6 +46,7 @@ #ifdef HAVE_UNISTD_H #include +#include #endif #include @@ -68,6 +69,7 @@ static int TisShmInit(void* ctx, FWTPM_TIS_REGS** regs) { FWTPM_TIS_SHM_CTX* shm = (FWTPM_TIS_SHM_CTX*)ctx; int fd; + struct stat shmStat; /* Threat model: fwtpm_server is a dev/test tool and is NOT intended to * run setuid or as a privileged daemon. O_NOFOLLOW + mode 0600 is * sufficient for non-privileged execution. We intentionally avoid @@ -90,6 +92,16 @@ static int TisShmInit(void* ctx, FWTPM_TIS_REGS** regs) return -1; } + /* A pre-existing file may carry attacker-controlled ownership or + * permissions; require our own UID and force 0600 (O_CREAT without + * O_EXCL cannot prevent the pre-creation race on its own). */ + if (fstat(fd, &shmStat) != 0 || shmStat.st_uid != getuid() || + fchmod(fd, S_IRUSR | S_IWUSR) != 0) { + fprintf(stderr, "fwTPM TIS: shm ownership/permission check failed\n"); + close(fd); + return -1; + } + if (ftruncate(fd, (off_t)sizeof(FWTPM_TIS_REGS)) < 0) { fprintf(stderr, "fwTPM TIS: ftruncate failed: %d (%s)\n", errno, strerror(errno)); From 4a874c6c4ff1cccec88c69f35b3b071da8114ce1 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:32:28 -0700 Subject: [PATCH 35/50] F-5643 - Skip DER decode when PEM-to-DER conversion fails --- src/tpm2_wrap.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index bc668074..83b396cd 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -4315,8 +4315,8 @@ int wolfTPM2_ImportPublicKeyBuffer(WOLFTPM2_DEV* dev, int keyType, derSz = inSz; } - /* Handle DER Import */ - if (keyType == TPM_ALG_RSA) { + /* Handle DER Import (skip if PEM-to-DER conversion above failed) */ + if (rc == 0 && keyType == TPM_ALG_RSA) { #ifndef NO_RSA rc = wolfTPM2_DecodeRsaDer(derBuf, derSz, &key->pub, NULL, objectAttributes); @@ -4324,7 +4324,7 @@ int wolfTPM2_ImportPublicKeyBuffer(WOLFTPM2_DEV* dev, int keyType, rc = NOT_COMPILED_IN; #endif } - else if (keyType == TPM_ALG_ECC) { + else if (rc == 0 && keyType == TPM_ALG_ECC) { #ifdef HAVE_ECC rc = wolfTPM2_DecodeEccDer(derBuf, derSz, &key->pub, NULL, objectAttributes); @@ -4386,15 +4386,15 @@ int wolfTPM2_ImportPrivateKeyBuffer(WOLFTPM2_DEV* dev, derSz = inSz; } - /* Handle DER Import */ - if (keyType == TPM_ALG_RSA) { + /* Handle DER Import (skip if PEM-to-DER conversion above failed) */ + if (rc == 0 && keyType == TPM_ALG_RSA) { #ifndef NO_RSA rc = wolfTPM2_DecodeRsaDer(derBuf, derSz, pub, &sens, objectAttributes); #else rc = NOT_COMPILED_IN; #endif } - else if (keyType == TPM_ALG_ECC) { + else if (rc == 0 && keyType == TPM_ALG_ECC) { #ifdef HAVE_ECC rc = wolfTPM2_DecodeEccDer(derBuf, derSz, pub, &sens, objectAttributes); #else From ef022dd43db834b0eb2368c103996bd4dbd2b4fd Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:34:29 -0700 Subject: [PATCH 36/50] F-5127 - Guard wolfSPDM_Finish against missing KeyExchange state --- src/spdm/spdm_session.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/spdm/spdm_session.c b/src/spdm/spdm_session.c index 15485722..2467442b 100644 --- a/src/spdm/spdm_session.c +++ b/src/spdm/spdm_session.c @@ -112,6 +112,12 @@ int wolfSPDM_Finish(WOLFSPDM_CTX* ctx) word32 decSz = sizeof(decBuf); int rc; + /* FINISH is only valid after a successful KEY_EXCHANGE; otherwise the + * session keys are unestablished (zero-entropy). */ + if (ctx == NULL || ctx->state < WOLFSPDM_STATE_KEY_EX) { + return WOLFSPDM_E_BAD_STATE; + } + rc = wolfSPDM_BuildFinish(ctx, finishBuf, &finishSz); /* FINISH must be sent encrypted (HANDSHAKE_IN_THE_CLEAR not negotiated) */ From 34930558e354257e6a07ab4c053633179c07dc9b Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 13:36:18 -0700 Subject: [PATCH 37/50] F-5648 F-5649 - Add HMAC verify truncation and compute buffer-size tests --- tests/unit_tests.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 9c7f9aa6..07a7f26c 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -1036,6 +1036,19 @@ static void test_TPM2_HmacCompute(void) digest, digestSz); AssertIntEQ(TPM_RC_INTEGRITY, rc); + /* A truncated (short) expected HMAC must be rejected on the length check */ + rc = TPM2_HmacVerify(TPM_ALG_SHA256, + hmacKey, 4, hmacData, 28, NULL, 0, + hmacExp, sizeof(hmacExp) - 1); + AssertIntEQ(TPM_RC_INTEGRITY, rc); + + /* An output buffer smaller than the digest must be rejected */ + digestSz = 31; + rc = TPM2_HmacCompute(TPM_ALG_SHA256, + hmacKey, 4, hmacData, 28, NULL, 0, + digest, &digestSz); + AssertIntEQ(BUFFER_E, rc); + printf("Test TPM Wrapper: %-40s Passed\n", "HmacCompute:"); #else printf("Test TPM Wrapper: %-40s Skipped\n", "HmacCompute:"); From 5453e6ff262c1ba042aac823c9454683e9f6d6c5 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 14:24:11 -0700 Subject: [PATCH 38/50] F-5654 F-5719 F-5277 F-5122 F-4845 F-4955 - Harden fwTPM fixes per review --- src/fwtpm/fwtpm_command.c | 82 ++++++++++++++++++++++++++++++--------- tests/fwtpm_unit_tests.c | 7 ++++ wolftpm/fwtpm/fwtpm.h | 3 +- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index ef84dd24..57326641 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -866,8 +866,12 @@ static TPM_RC FwCmd_IncrementalSelfTest(FWTPM_CTX* ctx, TPM2_Packet* cmd, } if (rc == 0) { TPM2_Packet_ParseU32(cmd, &toTestCount); - if (cmdSize < TPM2_HEADER_SIZE + 4 + (int)(toTestCount * sizeof(alg))) + /* Bound by division to avoid the count*size multiply overflowing and + * wrapping the size check (which would allow an unbounded loop). */ + if (toTestCount > (UINT32)((cmdSize - (TPM2_HEADER_SIZE + 4)) / + (int)sizeof(alg))) { rc = TPM_RC_COMMAND_SIZE; + } } if (rc == 0) { for (i = 0; i < toTestCount; i++) @@ -6993,15 +6997,23 @@ static TPM_RC FwCmd_HMAC(FWTPM_CTX* ctx, TPM2_Packet* cmd, /* Parse hashAlg */ TPM2_Packet_ParseU16(cmd, (UINT16*)&hashAlg); - /* NULL means use the key's HMAC scheme hash; a non-NULL value must - * match it (Part 3 Sec.17.4) rather than any wire-chosen hash. */ - if (hashAlg == TPM_ALG_NULL) { - hashAlg = obj->pub.parameters.keyedHashDetail.scheme.details - .hmac.hashAlg; + /* If the key binds an HMAC scheme, a NULL wire hash uses it and a + * non-NULL wire hash must match it (Part 3 Sec.17.4). A key with an + * unbound (NULL) scheme lets the caller choose, defaulting to the + * key's nameAlg. */ + if (obj->pub.parameters.keyedHashDetail.scheme.scheme + != TPM_ALG_NULL) { + if (hashAlg == TPM_ALG_NULL) { + hashAlg = obj->pub.parameters.keyedHashDetail.scheme + .details.hmac.hashAlg; + } + else if (hashAlg != obj->pub.parameters.keyedHashDetail.scheme + .details.hmac.hashAlg) { + rc = TPM_RC_VALUE; + } } - else if (hashAlg != obj->pub.parameters.keyedHashDetail.scheme - .details.hmac.hashAlg) { - rc = TPM_RC_VALUE; + else if (hashAlg == TPM_ALG_NULL) { + hashAlg = obj->pub.nameAlg; } ht = FwGetWcHashType(hashAlg); @@ -8962,10 +8974,21 @@ static TPM_RC FwCmd_PolicySecret(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } - /* Bind the command to cpHashA so policy enforcement can verify it */ + /* Bind the command to cpHashA so policy enforcement can verify it. + * Lock-once: if already bound, a different value must be rejected + * rather than silently replacing the earlier binding. */ if (rc == 0 && cpHashASz > 0) { - sess->cpHashA.size = cpHashASz; - XMEMCPY(sess->cpHashA.buffer, cpHashBuf, cpHashASz); + if (sess->cpHashA.size > 0) { + if (sess->cpHashA.size != cpHashASz || + TPM2_ConstantCompare(sess->cpHashA.buffer, cpHashBuf, + cpHashASz) != 0) { + rc = TPM_RC_CPHASH; + } + } + else { + sess->cpHashA.size = cpHashASz; + XMEMCPY(sess->cpHashA.buffer, cpHashBuf, cpHashASz); + } } if (rc == 0) { @@ -9302,12 +9325,15 @@ static TPM_RC FwCmd_PolicyLocality(FWTPM_CTX* ctx, TPM2_Packet* cmd, NULL, 0, 0) != 0) { rc = TPM_RC_FAILURE; } - /* Bind the locality constraint (intersect across calls) */ + /* Bind the locality constraint (intersect across calls). A flag + * marks it present so a bitmap of 0 stays an unsatisfiable + * constraint rather than reading as "unset". */ if (rc == 0) { - if (sess->requiredLocality == 0) + if (!sess->hasRequiredLocality) sess->requiredLocality = locality; else sess->requiredLocality &= locality; + sess->hasRequiredLocality = 1; } } if (rc == 0) { @@ -9473,10 +9499,21 @@ static TPM_RC FwCmd_PolicySigned(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } - /* Bind the command to cpHashA so policy enforcement can verify it */ + /* Bind the command to cpHashA so policy enforcement can verify it. + * Lock-once: if already bound, a different value must be rejected + * rather than silently replacing the earlier binding. */ if (rc == 0 && cpHashASz > 0) { - sess->cpHashA.size = cpHashASz; - XMEMCPY(sess->cpHashA.buffer, cpHashBuf, cpHashASz); + if (sess->cpHashA.size > 0) { + if (sess->cpHashA.size != cpHashASz || + TPM2_ConstantCompare(sess->cpHashA.buffer, cpHashBuf, + cpHashASz) != 0) { + rc = TPM_RC_CPHASH; + } + } + else { + sess->cpHashA.size = cpHashASz; + XMEMCPY(sess->cpHashA.buffer, cpHashBuf, cpHashASz); + } } if (rc == 0) { @@ -12723,6 +12760,11 @@ static TPM_RC FwCmd_MakeCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_HANDLE; } } + /* Credential wrap/unwrap integrity is SHA-256 only; reject other + * nameAlgs rather than emit a mixed-hash (non-interoperable) blob. */ + if (rc == 0 && keyObj->pub.nameAlg != TPM_ALG_SHA256) { + rc = TPM_RC_HASH; + } /* MakeCredential has no auth area */ @@ -12902,6 +12944,10 @@ static TPM_RC FwCmd_ActivateCredential(FWTPM_CTX* ctx, TPM2_Packet* cmd, rc = TPM_RC_HANDLE; } } + /* Credential wrap/unwrap integrity is SHA-256 only (see MakeCredential) */ + if (rc == 0 && keyObj->pub.nameAlg != TPM_ALG_SHA256) { + rc = TPM_RC_HASH; + } /* Skip auth area */ if (rc == 0 && cmdTag == TPM_ST_SESSIONS) { @@ -15839,7 +15885,7 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx, return TPM_RC_SUCCESS; } /* Enforce any PolicyLocality constraint bound to the session */ - if (pSess->requiredLocality != 0 && + if (pSess->hasRequiredLocality && !((1 << ctx->activeLocality) & pSess->requiredLocality)) { *rspSize = FwBuildErrorResponse(rspBuf, TPM_ST_NO_SESSIONS, TPM_RC_LOCALITY); diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index 90485c53..fda8b142 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -8115,6 +8115,13 @@ static void test_fwtpm_incremental_selftest_truncated_returns_cmd_size(void) FWTPM_ProcessCommand(&ctx, gCmd, 10, gRsp, &rspSize, 0); AssertIntEQ(GetRspRC(gRsp), TPM_RC_COMMAND_SIZE); + /* Overflow case: a huge count must be rejected, not wrap the size check */ + BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 14, TPM_CC_IncrementalSelfTest); + PutU32BE(gCmd + 10, 0xFFFFFFFFu); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, 14, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_COMMAND_SIZE); + FWTPM_Cleanup(&ctx); printf("Test fwTPM:\tIncrementalSelfTest truncated (CMD_SIZE):\tPassed\n"); } diff --git a/wolftpm/fwtpm/fwtpm.h b/wolftpm/fwtpm/fwtpm.h index cc911810..95d62329 100644 --- a/wolftpm/fwtpm/fwtpm.h +++ b/wolftpm/fwtpm/fwtpm.h @@ -529,7 +529,8 @@ typedef struct FWTPM_Session { TPM2B_DIGEST cpHashA; /* PolicyCpHash: locked once set */ TPM2B_DIGEST nameHash; /* PolicyNameHash: locked once set */ int isPPRequired; /* PolicyPhysicalPresence flag */ - int requiredLocality; /* PolicyLocality bitmap (0 = unset) */ + int requiredLocality; /* PolicyLocality bitmap */ + int hasRequiredLocality; /* 1 once PolicyLocality has been called */ } FWTPM_Session; /* NV index slot (user NV RAM) */ From d7638f9f2734baee8ad22ce83948fae85b90af42 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 14:24:11 -0700 Subject: [PATCH 39/50] F-5655 - Parse GetTestResult testResult as full 32-bit value --- src/tpm2.c | 2 +- wolftpm/tpm2.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tpm2.c b/src/tpm2.c index afe6fb40..09fa2841 100644 --- a/src/tpm2.c +++ b/src/tpm2.c @@ -979,7 +979,7 @@ TPM_RC TPM2_GetTestResult(GetTestResult_Out* out) if (wireSize > out->outData.size) TPM2_Packet_ParseBytes(&packet, NULL, wireSize - out->outData.size); - TPM2_Packet_ParseU16(&packet, &out->testResult); + TPM2_Packet_ParseU32(&packet, &out->testResult); } TPM2_ReleaseLock(ctx); diff --git a/wolftpm/tpm2.h b/wolftpm/tpm2.h index e4973706..30569ab3 100644 --- a/wolftpm/tpm2.h +++ b/wolftpm/tpm2.h @@ -2271,7 +2271,7 @@ WOLFTPM_API TPM_RC TPM2_IncrementalSelfTest(IncrementalSelfTest_In* in, typedef struct { TPM2B_MAX_BUFFER outData; - UINT16 testResult; /* TPM_RC */ + UINT32 testResult; /* full 4-byte TPM_RC value on the wire */ } GetTestResult_Out; WOLFTPM_API TPM_RC TPM2_GetTestResult(GetTestResult_Out* out); From d42f61034c7541e2c00843d000b22337caced335 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 14:24:11 -0700 Subject: [PATCH 40/50] F-4161 - Reject oversized auth in CreateLoadedKey and CreatePrimaryKey --- src/tpm2_wrap.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index 83b396cd..3454305c 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -2705,8 +2705,10 @@ int wolfTPM2_CreatePrimaryKey_ex(WOLFTPM2_DEV* dev, WOLFTPM2_PKEY* pkey, * primary keys and may differ from other TPM stacks that accept * shorter auth values as-is. */ if (nameAlgDigestSz > 0) { + /* Reject oversized auth rather than silently truncating it, + * consistent with wolfTPM2_CreateKey */ if (authSz > nameAlgDigestSz) { - authSz = nameAlgDigestSz; + return BUFFER_E; } XMEMCPY(createPriAuth->buffer, auth, authSz); if (authSz < nameAlgDigestSz) { @@ -3030,8 +3032,10 @@ int wolfTPM2_CreateLoadedKey(WOLFTPM2_DEV* dev, WOLFTPM2_KEYBLOB* keyBlob, return BAD_FUNC_ARG; } if (nameAlgDigestSz > 0) { + /* Reject oversized auth rather than silently truncating it, + * consistent with wolfTPM2_CreateKey */ if (authSz > nameAlgDigestSz) { - authSz = nameAlgDigestSz; + return BUFFER_E; } XMEMCPY(pAuth->buffer, auth, authSz); if (authSz < nameAlgDigestSz) { From 684de58a4a5ad4c720c0e28bc2acfba99fa7415d Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 14:24:11 -0700 Subject: [PATCH 41/50] F-4677 - Drop dead command-port power-signal branches --- src/fwtpm/fwtpm_io.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/fwtpm/fwtpm_io.c b/src/fwtpm/fwtpm_io.c index 85d85451..0166301c 100644 --- a/src/fwtpm/fwtpm_io.c +++ b/src/fwtpm/fwtpm_io.c @@ -293,14 +293,10 @@ static int HandlePlatformCommand(FWTPM_CTX* ctx, int clientFd) static int HandleMssimSignal(FWTPM_CTX* ctx, int clientFd, UINT32 tssCmd) { UINT32 netVal; + /* State-mutating signals (POWER_OFF/RESET) are rejected before reaching + * here on the command port; only POWER_ON is handled. */ if (tssCmd == FWTPM_TCP_SIGNAL_POWER_ON) ctx->powerOn = 1; - else if (tssCmd == FWTPM_TCP_SIGNAL_POWER_OFF) { - ctx->powerOn = 0; - ctx->wasStarted = 0; - } - else if (tssCmd == FWTPM_TCP_SIGNAL_RESET) - ctx->wasStarted = 0; #ifdef DEBUG_WOLFTPM printf("fwTPM: Cmd-port signal %u (ack)\n", tssCmd); #endif From 675d336ecd8bf772e93e59bf20719e3bc9df8076 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 14:24:11 -0700 Subject: [PATCH 42/50] F-5102 - Create NV journal key file with O_EXCL and O_NOFOLLOW --- src/fwtpm/fwtpm_nv.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index 8c08f02b..8e868bc0 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -47,6 +47,7 @@ #if !defined(NO_FILESYSTEM) && !defined(_WIN32) #include #include + #include #endif #define FWTPM_NV_KEY_SIZE 32 @@ -681,6 +682,10 @@ static int FwNvLoadOrCreateKeyFile(FWTPM_CTX* ctx, byte* key, word32* keySz) size_t nvLen; FILE* f; int ok = 0; +#if !defined(_WIN32) + int kfd; + int kflags = O_CREAT | O_EXCL | O_WRONLY; +#endif if (nvPath == NULL) { return 0; @@ -701,15 +706,26 @@ static int FwNvLoadOrCreateKeyFile(FWTPM_CTX* ctx, byte* key, word32* keySz) if (wc_RNG_GenerateBlock(&ctx->rng, key, FWTPM_NV_KEY_SIZE) != 0) { return 0; } + #if !defined(_WIN32) + /* O_EXCL prevents a pre-creation race; O_NOFOLLOW blocks a symlink + * swap to an attacker-chosen target. */ + #ifdef O_NOFOLLOW + kflags |= O_NOFOLLOW; + #endif + kfd = open(keyPath, kflags, S_IRUSR | S_IWUSR); + if (kfd >= 0) { + ok = (write(kfd, key, FWTPM_NV_KEY_SIZE) == + (ssize_t)FWTPM_NV_KEY_SIZE); + close(kfd); + } + #else f = fopen(keyPath, "wb"); if (f != NULL) { ok = ((int)fwrite(key, 1, FWTPM_NV_KEY_SIZE, f) == FWTPM_NV_KEY_SIZE); fclose(f); - #if !defined(_WIN32) - chmod(keyPath, S_IRUSR | S_IWUSR); - #endif } + #endif } if (ok) { *keySz = FWTPM_NV_KEY_SIZE; From 57f49c79f81013660bd1c7f68a09633f373f9b29 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Tue, 2 Jun 2026 14:30:52 -0700 Subject: [PATCH 43/50] F-4673 - Track live context sequences for any-order single-use load --- src/fwtpm/fwtpm_command.c | 45 ++++++++++++++++++++++++++++-------- src/fwtpm/fwtpm_crypto.c | 8 +++---- tests/fwtpm_unit_tests.c | 38 ++++++++++++++++++++++-------- wolftpm/fwtpm/fwtpm.h | 4 ++++ wolftpm/fwtpm/fwtpm_crypto.h | 4 ++-- 5 files changed, 73 insertions(+), 26 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 57326641..0ad95638 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -703,6 +703,8 @@ static TPM_RC FwCmd_Startup(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, } } ctx->globalNvWriteLock = 0; + /* Saved contexts are invalidated by TPM Reset */ + ctx->contextLiveCount = 0; #ifdef HAVE_ECC ctx->ecEphemeralCounter = 0; ctx->ecEphemeralKeySz = 0; @@ -3041,6 +3043,11 @@ static TPM_RC FwCmd_ContextSave(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0) { /* TPMS_CONTEXT: sequence(8) | savedHandle(4) | hierarchy(4) | blob */ ctx->contextSeqCounter++; + /* Record this context as live so it loads at most once, in any order */ + if (ctx->contextLiveCount < (int)(sizeof(ctx->contextLive) / + sizeof(ctx->contextLive[0]))) { + ctx->contextLive[ctx->contextLiveCount++] = ctx->contextSeqCounter; + } seqHi = (UINT32)(ctx->contextSeqCounter >> 32); seqLo = (UINT32)(ctx->contextSeqCounter & 0xFFFFFFFFu); TPM2_Packet_AppendU32(rsp, seqHi); @@ -3054,8 +3061,8 @@ static TPM_RC FwCmd_ContextSave(FWTPM_CTX* ctx, TPM2_Packet* cmd, byte wrappedBuf[AES_BLOCK_SIZE + sizeof(FWTPM_Session) + WC_SHA256_DIGEST_SIZE]; int wrappedSz = 0; - rc = FwWrapContextBlob(ctx, (const byte*)sess, - (int)sizeof(FWTPM_Session), + rc = FwWrapContextBlob(ctx, ctx->contextSeqCounter, + (const byte*)sess, (int)sizeof(FWTPM_Session), wrappedBuf, (int)sizeof(wrappedBuf), &wrappedSz); if (rc == 0) { blobSz = 4 + 4 + (UINT16)wrappedSz; @@ -3104,6 +3111,9 @@ static TPM_RC FwCmd_ContextLoad(FWTPM_CTX* ctx, TPM2_Packet* cmd, UINT32 seqHi, seqLo, savedHandle, hierarchy; UINT16 blobSz = 0; UINT32 magic = 0, version = 0; + UINT64 loadSeq = 0; + int liveIdx = -1; + int liveScan; (void)cmdTag; (void)hierarchy; @@ -3120,11 +3130,20 @@ static TPM_RC FwCmd_ContextLoad(FWTPM_CTX* ctx, TPM2_Packet* cmd, TPM2_Packet_ParseU16(cmd, &blobSz); } - /* Replay protection: a saved context is bound to the sequence counter - * value it was created with, and each successful load consumes it. */ - if (rc == 0 && - (((UINT64)seqHi << 32) | (UINT64)seqLo) != ctx->contextSeqCounter) { - rc = TPM_RC_INTEGRITY; + /* Replay protection: the context must be a live (saved, not-yet-loaded) + * sequence. A load consumes it; independent saved contexts may load in + * any order. */ + if (rc == 0) { + loadSeq = ((UINT64)seqHi << 32) | (UINT64)seqLo; + for (liveScan = 0; liveScan < ctx->contextLiveCount; liveScan++) { + if (ctx->contextLive[liveScan] == loadSeq) { + liveIdx = liveScan; + break; + } + } + if (liveIdx < 0) { + rc = TPM_RC_INTEGRITY; + } } /* Validate minimum blob size (magic + version = 8 bytes) */ @@ -3164,7 +3183,7 @@ static TPM_RC FwCmd_ContextLoad(FWTPM_CTX* ctx, TPM2_Packet* cmd, byte wrappedBuf[AES_BLOCK_SIZE + sizeof(FWTPM_Session) + WC_SHA256_DIGEST_SIZE]; TPM2_Packet_ParseBytes(cmd, wrappedBuf, (int)dataLen); - rc = FwUnwrapContextBlob(ctx, wrappedBuf, (int)dataLen, + rc = FwUnwrapContextBlob(ctx, loadSeq, wrappedBuf, (int)dataLen, (byte*)&restored, (int)sizeof(restored), &restoredSz); TPM2_ForceZero(wrappedBuf, sizeof(wrappedBuf)); if (rc != 0) { @@ -3225,8 +3244,14 @@ static TPM_RC FwCmd_ContextLoad(FWTPM_CTX* ctx, TPM2_Packet* cmd, #ifdef DEBUG_WOLFTPM printf("fwTPM: ContextLoad(handle=0x%x)\n", savedHandle); #endif - /* Consume this sequence value so the same blob cannot be replayed */ - ctx->contextSeqCounter++; + /* Consume the sequence so this blob cannot be replayed */ + if (liveIdx >= 0 && liveIdx < ctx->contextLiveCount) { + for (liveScan = liveIdx; liveScan < ctx->contextLiveCount - 1; + liveScan++) { + ctx->contextLive[liveScan] = ctx->contextLive[liveScan + 1]; + } + ctx->contextLiveCount--; + } TPM2_Packet_AppendU32(rsp, savedHandle); FwRspFinalize(rsp, TPM_ST_NO_SESSIONS, TPM_RC_SUCCESS); } diff --git a/src/fwtpm/fwtpm_crypto.c b/src/fwtpm/fwtpm_crypto.c index 658bd91f..7410746a 100644 --- a/src/fwtpm/fwtpm_crypto.c +++ b/src/fwtpm/fwtpm_crypto.c @@ -2234,7 +2234,7 @@ static int FwHmacUpdateU64(Hmac* hmac, UINT64 v) /* Encrypt-then-MAC context blob protection using the per-boot key. * Layout: iv(16) | ciphertext(plainSz) | hmac(32) * Returns 0 on success, sets *outSz. */ -int FwWrapContextBlob(FWTPM_CTX* ctx, +int FwWrapContextBlob(FWTPM_CTX* ctx, UINT64 seq, const byte* plain, int plainSz, byte* out, int outBufSz, int* outSz) { @@ -2284,7 +2284,7 @@ int FwWrapContextBlob(FWTPM_CTX* ctx, rc = wc_HmacUpdate(hmac, out, AES_BLOCK_SIZE + plainSz); } if (rc == 0) { - rc = FwHmacUpdateU64(hmac, ctx->contextSeqCounter); + rc = FwHmacUpdateU64(hmac, seq); } if (rc == 0) { rc = wc_HmacFinal(hmac, out + AES_BLOCK_SIZE + plainSz); @@ -2306,7 +2306,7 @@ int FwWrapContextBlob(FWTPM_CTX* ctx, } /* Verify-then-decrypt context blob. Returns 0 on success, sets *outSz. */ -int FwUnwrapContextBlob(FWTPM_CTX* ctx, +int FwUnwrapContextBlob(FWTPM_CTX* ctx, UINT64 seq, const byte* in, int inSz, byte* out, int outBufSz, int* outSz) { @@ -2344,7 +2344,7 @@ int FwUnwrapContextBlob(FWTPM_CTX* ctx, rc = wc_HmacUpdate(hmac, in, AES_BLOCK_SIZE + cipherSz); } if (rc == 0) { - rc = FwHmacUpdateU64(hmac, ctx->contextSeqCounter); + rc = FwHmacUpdateU64(hmac, seq); } if (rc == 0) { rc = wc_HmacFinal(hmac, computedHmac); diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index fda8b142..e777364f 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -8672,9 +8672,10 @@ static void test_fwtpm_context_save(void) static void test_fwtpm_context_load_replay_rejected(void) { FWTPM_CTX ctx; - int rspSize = 0, ctxSz, pos; + int rspSize = 0, ctxSzA, ctxSzB, pos; UINT32 keyH; - byte savedCtx[512]; + byte savedCtxA[512]; + byte savedCtxB[512]; memset(&ctx, 0, sizeof(ctx)); AssertIntEQ(fwtpm_test_startup(&ctx), 0); @@ -8685,33 +8686,50 @@ static void test_fwtpm_context_load_replay_rejected(void) #endif AssertIntNE(keyH, 0); + /* Save context A, then save context B while A is still outstanding */ BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 14, TPM_CC_ContextSave); PutU32BE(gCmd + 10, keyH); FWTPM_ProcessCommand(&ctx, gCmd, 14, gRsp, &rspSize, 0); AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); - ctxSz = rspSize - TPM2_HEADER_SIZE; /* TPMS_CONTEXT follows the header */ - AssertIntGT(ctxSz, 0); - memcpy(savedCtx, gRsp + TPM2_HEADER_SIZE, ctxSz); + ctxSzA = rspSize - TPM2_HEADER_SIZE; + AssertIntGT(ctxSzA, 0); + memcpy(savedCtxA, gRsp + TPM2_HEADER_SIZE, ctxSzA); - /* First load succeeds */ + BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 14, TPM_CC_ContextSave); + PutU32BE(gCmd + 10, keyH); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, 14, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + ctxSzB = rspSize - TPM2_HEADER_SIZE; + memcpy(savedCtxB, gRsp + TPM2_HEADER_SIZE, ctxSzB); + + /* Load A first (older blob, B still outstanding) — must succeed */ pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_ContextLoad); - memcpy(gCmd + pos, savedCtx, ctxSz); pos += ctxSz; + memcpy(gCmd + pos, savedCtxA, ctxSzA); pos += ctxSzA; PutU32BE(gCmd + 2, (UINT32)pos); rspSize = 0; FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); - /* Replaying the identical blob is rejected */ + /* Replaying A is rejected (single-use) */ pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_ContextLoad); - memcpy(gCmd + pos, savedCtx, ctxSz); pos += ctxSz; + memcpy(gCmd + pos, savedCtxA, ctxSzA); pos += ctxSzA; PutU32BE(gCmd + 2, (UINT32)pos); rspSize = 0; FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); AssertIntNE(GetRspRC(gRsp), TPM_RC_SUCCESS); + /* B is still loadable after A was consumed (any-order multi-context) */ + pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_ContextLoad); + memcpy(gCmd + pos, savedCtxB, ctxSzB); pos += ctxSzB; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + FlushHandle(&ctx, keyH); FWTPM_Cleanup(&ctx); - printf("Test fwTPM:\tContextLoad replay rejected:\tPassed\n"); + printf("Test fwTPM:\tContextLoad replay + multi-context:\tPassed\n"); } /* When the command client changes, transient objects must be flushed so a diff --git a/wolftpm/fwtpm/fwtpm.h b/wolftpm/fwtpm/fwtpm.h index 95d62329..0736ce8c 100644 --- a/wolftpm/fwtpm/fwtpm.h +++ b/wolftpm/fwtpm/fwtpm.h @@ -722,6 +722,10 @@ typedef struct FWTPM_CTX { /* ContextSave sequence counter (monotonic, reset on init) */ UINT64 contextSeqCounter; + /* Live (saved-but-not-yet-loaded) context sequences. A context loads at + * most once and saved contexts may load in any order. */ + UINT64 contextLive[FWTPM_MAX_OBJECTS + FWTPM_MAX_SESSIONS]; + int contextLiveCount; /* Set once TPM2_SelfTest has completed successfully */ int selfTestRun; diff --git a/wolftpm/fwtpm/fwtpm_crypto.h b/wolftpm/fwtpm/fwtpm_crypto.h index 63a26668..fa20d1a5 100644 --- a/wolftpm/fwtpm/fwtpm_crypto.h +++ b/wolftpm/fwtpm/fwtpm_crypto.h @@ -255,10 +255,10 @@ int FwUnwrapPrivate(FWTPM_Object* parent, /* --- Context blob wrap/unwrap (ContextSave/Load) --- */ -int FwWrapContextBlob(FWTPM_CTX* ctx, +int FwWrapContextBlob(FWTPM_CTX* ctx, UINT64 seq, const byte* plain, int plainSz, byte* out, int outBufSz, int* outSz); -int FwUnwrapContextBlob(FWTPM_CTX* ctx, +int FwUnwrapContextBlob(FWTPM_CTX* ctx, UINT64 seq, const byte* in, int inSz, byte* out, int outBufSz, int* outSz); From bf010679a1a35724da2ba8fcdd536e99148a4a1d Mon Sep 17 00:00:00 2001 From: aidan garske Date: Wed, 3 Jun 2026 09:52:57 -0700 Subject: [PATCH 44/50] fwtpm: keep transient state across command-port reconnects --- src/fwtpm/fwtpm_io.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/fwtpm/fwtpm_io.c b/src/fwtpm/fwtpm_io.c index 0166301c..edff1999 100644 --- a/src/fwtpm/fwtpm_io.c +++ b/src/fwtpm/fwtpm_io.c @@ -691,8 +691,6 @@ int FWTPM_IO_ServerLoop(FWTPM_CTX* ctx) printf("fwTPM: command connection replaced\n"); #endif CloseSocket(cmdFd); - /* New client must not inherit prior transient state */ - FWTPM_ResetCommandClient(ctx); } cmdFd = newFd; } @@ -711,8 +709,9 @@ int FWTPM_IO_ServerLoop(FWTPM_CTX* ctx) if (HandleCommandConnection(ctx, cmdFd) != TPM_RC_SUCCESS) { CloseSocket(cmdFd); cmdFd = FWTPM_INVALID_FD; - /* Drop transient state when the client disconnects */ - FWTPM_ResetCommandClient(ctx); + /* Transient state persists across command connections: the + * mssim transport reconnects per command for one logical TPM, + * so a clean disconnect must not flush handles. */ } } } From 16a4741e239ab908156f5dbae5690a1c79625c98 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Wed, 3 Jun 2026 09:52:57 -0700 Subject: [PATCH 45/50] fwtpm: address PR review for scheme checks, context-save, and NV durability --- src/fwtpm/fwtpm_command.c | 65 +++++++++++++++++++++++++-------------- src/fwtpm/fwtpm_crypto.c | 20 +++++++----- src/fwtpm/fwtpm_nv.c | 55 +++++++++++++++++++++++++-------- src/tpm2_wrap.c | 2 +- 4 files changed, 99 insertions(+), 43 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 0ad95638..1f7e5d1b 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -719,7 +719,7 @@ static TPM_RC FwCmd_Startup(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, if (rc == 0) { ctx->resetCount++; ctx->restartCount = 0; - FWTPM_NV_SaveFlags(ctx); + rc = FWTPM_NV_SaveFlags(ctx); } } else { @@ -2236,7 +2236,7 @@ static TPM_RC FwCmd_ClockSet(FWTPM_CTX* ctx, TPM2_Packet* cmd, /* Clock may move forward but at most 2^32-1 ms per call (Part 1 * Sec.17.5.3); reject a far-future value that would freeze it. */ if (newTime < currentTime || - newTime > currentTime + ((UINT64)1 << 32)) { + newTime > currentTime + 0xFFFFFFFFULL) { rc = TPM_RC_VALUE; } } @@ -2420,6 +2420,8 @@ void FWTPM_ResetCommandClient(FWTPM_CTX* ctx) } FwFlushAllObjects(ctx); FwFlushAllSessions(ctx); + /* Saved-context replay set belongs to the prior client */ + ctx->contextLiveCount = 0; for (i = 0; i < FWTPM_MAX_HASH_SEQ; i++) { if (ctx->hashSeq[i].used) { FwFreeHashSeq(&ctx->hashSeq[i]); @@ -3040,14 +3042,19 @@ static TPM_RC FwCmd_ContextSave(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } + if (rc == 0 && + ctx->contextLiveCount >= (int)(sizeof(ctx->contextLive) / + sizeof(ctx->contextLive[0]))) { + /* No room to record another live context; emitting a blob we cannot + * track would have it rejected as non-live at load time. */ + rc = TPM_RC_OBJECT_MEMORY; + } + if (rc == 0) { /* TPMS_CONTEXT: sequence(8) | savedHandle(4) | hierarchy(4) | blob */ ctx->contextSeqCounter++; /* Record this context as live so it loads at most once, in any order */ - if (ctx->contextLiveCount < (int)(sizeof(ctx->contextLive) / - sizeof(ctx->contextLive[0]))) { - ctx->contextLive[ctx->contextLiveCount++] = ctx->contextSeqCounter; - } + ctx->contextLive[ctx->contextLiveCount++] = ctx->contextSeqCounter; seqHi = (UINT32)(ctx->contextSeqCounter >> 32); seqLo = (UINT32)(ctx->contextSeqCounter & 0xFFFFFFFFu); TPM2_Packet_AppendU32(rsp, seqHi); @@ -6633,13 +6640,18 @@ static TPM_RC FwCmd_RSA_Encrypt(FWTPM_CTX* ctx, TPM2_Packet* cmd, .anySig.hashAlg; } } - /* A key with a declared scheme hash must not be used with a - * different wire hash (Part 3 Sec.17.2) */ - else if (rc == 0 && encHashAlg != TPM_ALG_NULL && - obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL && - encHashAlg != obj->pub.parameters.rsaDetail.scheme.details - .anySig.hashAlg) { - rc = TPM_RC_SCHEME; + /* A key with a fixed scheme must not be used with a different wire + * scheme or hash (Part 3 Sec.17.2) */ + else if (rc == 0 && + obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL) { + if (encScheme != obj->pub.parameters.rsaDetail.scheme.scheme) { + rc = TPM_RC_SCHEME; + } + else if (encHashAlg != TPM_ALG_NULL && + encHashAlg != obj->pub.parameters.rsaDetail.scheme.details + .anySig.hashAlg) { + rc = TPM_RC_SCHEME; + } } #ifdef DEBUG_WOLFTPM @@ -6796,13 +6808,18 @@ static TPM_RC FwCmd_RSA_Decrypt(FWTPM_CTX* ctx, TPM2_Packet* cmd, .anySig.hashAlg; } } - /* A key with a declared scheme hash must not be used with a - * different wire hash (Part 3 Sec.17.2) */ - else if (rc == 0 && decHashAlg != TPM_ALG_NULL && - obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL && - decHashAlg != obj->pub.parameters.rsaDetail.scheme.details - .anySig.hashAlg) { - rc = TPM_RC_SCHEME; + /* A key with a fixed scheme must not be used with a different wire + * scheme or hash (Part 3 Sec.17.2) */ + else if (rc == 0 && + obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL) { + if (decScheme != obj->pub.parameters.rsaDetail.scheme.scheme) { + rc = TPM_RC_SCHEME; + } + else if (decHashAlg != TPM_ALG_NULL && + decHashAlg != obj->pub.parameters.rsaDetail.scheme.details + .anySig.hashAlg) { + rc = TPM_RC_SCHEME; + } } #ifdef DEBUG_WOLFTPM @@ -11590,8 +11607,9 @@ static TPM_RC FwCmd_NV_GlobalWriteLock(FWTPM_CTX* ctx, TPM2_Packet* cmd, #endif ctx->globalNvWriteLock = 1; /* Persist so the lock survives a daemon restart (Resume) */ - FWTPM_NV_SaveFlags(ctx); - FwRspNoParams(rsp, cmdTag); + rc = FWTPM_NV_SaveFlags(ctx); + if (rc == 0) + FwRspNoParams(rsp, cmdTag); } return rc; @@ -15911,7 +15929,8 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx, } /* Enforce any PolicyLocality constraint bound to the session */ if (pSess->hasRequiredLocality && - !((1 << ctx->activeLocality) & pSess->requiredLocality)) { + (ctx->activeLocality > 4 || + !((1u << ctx->activeLocality) & pSess->requiredLocality))) { *rspSize = FwBuildErrorResponse(rspBuf, TPM_ST_NO_SESSIONS, TPM_RC_LOCALITY); return TPM_RC_SUCCESS; diff --git a/src/fwtpm/fwtpm_crypto.c b/src/fwtpm/fwtpm_crypto.c index 7410746a..3cce0232 100644 --- a/src/fwtpm/fwtpm_crypto.c +++ b/src/fwtpm/fwtpm_crypto.c @@ -3629,14 +3629,20 @@ TPM_RC FwVerifySignatureCore(FWTPM_Object* obj, rsaInit = 1; } - /* A key with a declared scheme hash must not verify a signature - * made under a different (e.g. downgraded) hash, and the digest - * length must match the signature hash (Part 3 Sec.20.2). */ + /* A key with a fixed scheme must not verify a signature made + * under a different scheme (e.g. RSASSA vs RSAPSS) or a different + * (e.g. downgraded) hash (Part 3 Sec.20.2). */ if (rc == 0 && - obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL && - sig->signature.rsassa.hash != - obj->pub.parameters.rsaDetail.scheme.details.anySig.hashAlg) { - rc = TPM_RC_SCHEME; + obj->pub.parameters.rsaDetail.scheme.scheme != TPM_ALG_NULL) { + if (sig->sigAlg != + obj->pub.parameters.rsaDetail.scheme.scheme) { + rc = TPM_RC_SCHEME; + } + else if (sig->signature.rsassa.hash != + obj->pub.parameters.rsaDetail.scheme.details + .anySig.hashAlg) { + rc = TPM_RC_SCHEME; + } } if (rc == 0 && digestSz != TPM2_GetHashDigestSize(sig->signature.rsassa.hash)) { diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index 8e868bc0..e32634a7 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -134,9 +134,15 @@ static int FwNvFileWrite(void* ctx, word32 offset, const byte* buf, ret = (int)fwrite(buf, 1, size, f); /* Flush to stable storage so a crash cannot lose committed NV state */ - fflush(f); + if (fflush(f) != 0) { + fclose(f); + return TPM_RC_FAILURE; + } #if !defined(_WIN32) - fsync(fileno(f)); + if (fsync(fileno(f)) != 0) { + fclose(f); + return TPM_RC_FAILURE; + } #endif fclose(f); @@ -680,11 +686,15 @@ static int FwNvLoadOrCreateKeyFile(FWTPM_CTX* ctx, byte* key, word32* keySz) const char* nvPath = (const char*)ctx->nvHal.ctx; char keyPath[256]; size_t nvLen; - FILE* f; int ok = 0; #if !defined(_WIN32) int kfd; + int rfd; int kflags = O_CREAT | O_EXCL | O_WRONLY; + int rflags = O_RDONLY; + struct stat kst; +#else + FILE* f; #endif if (nvPath == NULL) { @@ -697,18 +707,28 @@ static int FwNvLoadOrCreateKeyFile(FWTPM_CTX* ctx, byte* key, word32* keySz) XMEMCPY(keyPath, nvPath, nvLen); XMEMCPY(keyPath + nvLen, ".key", 5); - f = fopen(keyPath, "rb"); - if (f != NULL) { - ok = ((int)fread(key, 1, FWTPM_NV_KEY_SIZE, f) == FWTPM_NV_KEY_SIZE); - fclose(f); +#if !defined(_WIN32) + /* O_NOFOLLOW blocks a symlink swap to an attacker-chosen target on both + * the read and create paths. */ + #ifdef O_NOFOLLOW + rflags |= O_NOFOLLOW; + #endif + rfd = open(keyPath, rflags); + if (rfd >= 0) { + /* Only trust a regular file we own with no group/other access */ + if (fstat(rfd, &kst) == 0 && S_ISREG(kst.st_mode) && + kst.st_uid == geteuid() && + (kst.st_mode & (S_IRWXG | S_IRWXO)) == 0) { + ok = (read(rfd, key, FWTPM_NV_KEY_SIZE) == + (ssize_t)FWTPM_NV_KEY_SIZE); + } + close(rfd); } else { if (wc_RNG_GenerateBlock(&ctx->rng, key, FWTPM_NV_KEY_SIZE) != 0) { return 0; } - #if !defined(_WIN32) - /* O_EXCL prevents a pre-creation race; O_NOFOLLOW blocks a symlink - * swap to an attacker-chosen target. */ + /* O_EXCL prevents a pre-creation race */ #ifdef O_NOFOLLOW kflags |= O_NOFOLLOW; #endif @@ -718,15 +738,26 @@ static int FwNvLoadOrCreateKeyFile(FWTPM_CTX* ctx, byte* key, word32* keySz) (ssize_t)FWTPM_NV_KEY_SIZE); close(kfd); } - #else + } +#else + f = fopen(keyPath, "rb"); + if (f != NULL) { + ok = ((int)fread(key, 1, FWTPM_NV_KEY_SIZE, f) == FWTPM_NV_KEY_SIZE); + fclose(f); + } + else { + if (wc_RNG_GenerateBlock(&ctx->rng, key, FWTPM_NV_KEY_SIZE) != 0) { + return 0; + } f = fopen(keyPath, "wb"); if (f != NULL) { ok = ((int)fwrite(key, 1, FWTPM_NV_KEY_SIZE, f) == FWTPM_NV_KEY_SIZE); fclose(f); } - #endif } +#endif + if (ok) { *keySz = FWTPM_NV_KEY_SIZE; return 1; diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index 3454305c..c3f88b38 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -2875,7 +2875,7 @@ int wolfTPM2_CreateKey(WOLFTPM2_DEV* dev, WOLFTPM2_KEYBLOB* keyBlob, Create_Out createOut; if (dev == NULL || keyBlob == NULL || parent == NULL || - publicTemplate == NULL) { + publicTemplate == NULL || authSz < 0) { return BAD_FUNC_ARG; } From 61f7fec929507f72bf622129565ede3995d2f1ac Mon Sep 17 00:00:00 2001 From: aidan garske Date: Wed, 3 Jun 2026 10:25:48 -0700 Subject: [PATCH 46/50] F-5835, F-5100 - Validate firmware data-size bound and NV reserved attribute bits --- examples/firmware/ifx_fw_extract.c | 3 ++- src/fwtpm/fwtpm_nv.c | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/firmware/ifx_fw_extract.c b/examples/firmware/ifx_fw_extract.c index d90397a3..11a8b4af 100644 --- a/examples/firmware/ifx_fw_extract.c +++ b/examples/firmware/ifx_fw_extract.c @@ -180,7 +180,8 @@ static int extractFW( } READ_BE32(size32, fw, fw_size, offset); - if (offset + size32 > fw_size) { + /* offset <= fw_size here; subtract to avoid size_t wrap on 32-bit */ + if (size32 > fw_size - offset) { LOG("FW file too short"); return -1; } diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index e32634a7..a8edba0b 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -445,6 +445,12 @@ static int FwNvUnmarshalNvPublic(const byte* buf, word32* pos, word32 maxSz, if (rc == 0) { rc = FwNvUnmarshalU32(buf, pos, maxSz, &nvPub->attributes); } + /* Reserved TPMA_NV bits [9:8] and [24:20] must be clear (Part 2 + * Sec.13.4); a crafted journal entry could otherwise install an index + * with undefined access-control semantics. */ + if (rc == 0 && (nvPub->attributes & 0x01F00300UL) != 0) { + rc = TPM_RC_FAILURE; + } if (rc == 0) { rc = FwNvUnmarshalDigest(buf, pos, maxSz, &nvPub->authPolicy); } From 5699061b53ae789650987f130856a5c5c11f0c2e Mon Sep 17 00:00:00 2001 From: aidan garske Date: Wed, 3 Jun 2026 10:57:13 -0700 Subject: [PATCH 47/50] fwtpm: scope context replay to sessions and accept unsalted param-enc --- src/fwtpm/fwtpm_command.c | 37 +++++++++++++++------------ tests/fwtpm_unit_tests.c | 54 +++++++++++++++++++++++---------------- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 1f7e5d1b..83987b17 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -3042,19 +3042,22 @@ static TPM_RC FwCmd_ContextSave(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } - if (rc == 0 && + /* Only session contexts carry single-use replay tracking (the policy + * replay concern); object contexts are freely reloadable, so tracking + * them would exhaust the small live set under normal multi-load use. */ + if (rc == 0 && isSession && ctx->contextLiveCount >= (int)(sizeof(ctx->contextLive) / sizeof(ctx->contextLive[0]))) { - /* No room to record another live context; emitting a blob we cannot - * track would have it rejected as non-live at load time. */ rc = TPM_RC_OBJECT_MEMORY; } if (rc == 0) { /* TPMS_CONTEXT: sequence(8) | savedHandle(4) | hierarchy(4) | blob */ ctx->contextSeqCounter++; - /* Record this context as live so it loads at most once, in any order */ - ctx->contextLive[ctx->contextLiveCount++] = ctx->contextSeqCounter; + if (isSession) { + /* Record so this session context loads at most once, any order */ + ctx->contextLive[ctx->contextLiveCount++] = ctx->contextSeqCounter; + } seqHi = (UINT32)(ctx->contextSeqCounter >> 32); seqLo = (UINT32)(ctx->contextSeqCounter & 0xFFFFFFFFu); TPM2_Packet_AppendU32(rsp, seqHi); @@ -3137,10 +3140,13 @@ static TPM_RC FwCmd_ContextLoad(FWTPM_CTX* ctx, TPM2_Packet* cmd, TPM2_Packet_ParseU16(cmd, &blobSz); } - /* Replay protection: the context must be a live (saved, not-yet-loaded) - * sequence. A load consumes it; independent saved contexts may load in - * any order. */ - if (rc == 0) { + /* Replay protection applies to session contexts only: a saved session + * must be a live (not-yet-loaded) sequence and a load consumes it, so a + * satisfied policy session cannot be replayed. Object contexts are + * reloadable any number of times and are not tracked. */ + if (rc == 0 && + ((savedHandle & 0xFF000000) == HMAC_SESSION_FIRST || + (savedHandle & 0xFF000000) == POLICY_SESSION_FIRST)) { loadSeq = ((UINT64)seqHi << 32) | (UINT64)seqLo; for (liveScan = 0; liveScan < ctx->contextLiveCount; liveScan++) { if (ctx->contextLive[liveScan] == loadSeq) { @@ -15793,14 +15799,13 @@ int FWTPM_ProcessCommand(FWTPM_CTX* ctx, } #ifndef FWTPM_NO_PARAM_ENC - /* Detect encryption session (first non-PW with - * symmetric alg). A session with an empty - * sessionKey (unsalted and unbound) derives a - * wire-observable key, so it must not be used - * for parameter encryption. */ + /* Detect encryption session (first non-PW with a + * symmetric alg). Unsalted/unbound sessions are + * accepted for client compatibility; over the + * loopback transport their key is only observable + * to a local peer. */ if (encSess == NULL && - sess->symmetric.algorithm != TPM_ALG_NULL && - sess->sessionKey.size > 0) { + sess->symmetric.algorithm != TPM_ALG_NULL) { encSess = sess; /* decrypt attr = client encrypted cmd param */ if ((attribs & TPMA_SESSION_decrypt) diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index e777364f..ba1d4ff0 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -8672,10 +8672,10 @@ static void test_fwtpm_context_save(void) static void test_fwtpm_context_load_replay_rejected(void) { FWTPM_CTX ctx; - int rspSize = 0, ctxSzA, ctxSzB, pos; - UINT32 keyH; - byte savedCtxA[512]; - byte savedCtxB[512]; + int rspSize = 0, ctxSz, pos; + UINT32 keyH, sessH; + byte savedObj[MAX_CONTEXT_SIZE]; + byte savedSess[MAX_CONTEXT_SIZE]; memset(&ctx, 0, sizeof(ctx)); AssertIntEQ(fwtpm_test_startup(&ctx), 0); @@ -8686,50 +8686,60 @@ static void test_fwtpm_context_load_replay_rejected(void) #endif AssertIntNE(keyH, 0); - /* Save context A, then save context B while A is still outstanding */ + /* Object contexts are freely reloadable (real TPM behavior, relied on by + * tpm2-tools): save once, load the same blob twice. */ BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 14, TPM_CC_ContextSave); PutU32BE(gCmd + 10, keyH); FWTPM_ProcessCommand(&ctx, gCmd, 14, gRsp, &rspSize, 0); AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); - ctxSzA = rspSize - TPM2_HEADER_SIZE; - AssertIntGT(ctxSzA, 0); - memcpy(savedCtxA, gRsp + TPM2_HEADER_SIZE, ctxSzA); + ctxSz = rspSize - TPM2_HEADER_SIZE; + AssertIntGT(ctxSz, 0); + memcpy(savedObj, gRsp + TPM2_HEADER_SIZE, ctxSz); - BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 14, TPM_CC_ContextSave); - PutU32BE(gCmd + 10, keyH); + pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_ContextLoad); + memcpy(gCmd + pos, savedObj, ctxSz); pos += ctxSz; + PutU32BE(gCmd + 2, (UINT32)pos); rspSize = 0; - FWTPM_ProcessCommand(&ctx, gCmd, 14, gRsp, &rspSize, 0); + FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); - ctxSzB = rspSize - TPM2_HEADER_SIZE; - memcpy(savedCtxB, gRsp + TPM2_HEADER_SIZE, ctxSzB); - /* Load A first (older blob, B still outstanding) — must succeed */ pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_ContextLoad); - memcpy(gCmd + pos, savedCtxA, ctxSzA); pos += ctxSzA; + memcpy(gCmd + pos, savedObj, ctxSz); pos += ctxSz; PutU32BE(gCmd + 2, (UINT32)pos); rspSize = 0; FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); - /* Replaying A is rejected (single-use) */ + /* Session contexts are single-use: a satisfied policy/HMAC session must + * not be replayable. Save a session, load once, then reject the replay. */ + sessH = StartSessionHelper(&ctx, TPM_SE_HMAC); + AssertIntNE(sessH, 0); + BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 14, TPM_CC_ContextSave); + PutU32BE(gCmd + 10, sessH); + rspSize = 0; + FWTPM_ProcessCommand(&ctx, gCmd, 14, gRsp, &rspSize, 0); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + ctxSz = rspSize - TPM2_HEADER_SIZE; + AssertIntGT(ctxSz, 0); + memcpy(savedSess, gRsp + TPM2_HEADER_SIZE, ctxSz); + pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_ContextLoad); - memcpy(gCmd + pos, savedCtxA, ctxSzA); pos += ctxSzA; + memcpy(gCmd + pos, savedSess, ctxSz); pos += ctxSz; PutU32BE(gCmd + 2, (UINT32)pos); rspSize = 0; FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); - AssertIntNE(GetRspRC(gRsp), TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); - /* B is still loadable after A was consumed (any-order multi-context) */ pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_ContextLoad); - memcpy(gCmd + pos, savedCtxB, ctxSzB); pos += ctxSzB; + memcpy(gCmd + pos, savedSess, ctxSz); pos += ctxSz; PutU32BE(gCmd + 2, (UINT32)pos); rspSize = 0; FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0); - AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + AssertIntNE(GetRspRC(gRsp), TPM_RC_SUCCESS); FlushHandle(&ctx, keyH); FWTPM_Cleanup(&ctx); - printf("Test fwTPM:\tContextLoad replay + multi-context:\tPassed\n"); + printf("Test fwTPM:\tContextLoad object reload + session replay:\tPassed\n"); } /* When the command client changes, transient objects must be flushed so a From 40716ac612ca4c0f903e0bbbb5fbfa41fdf55a30 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Wed, 3 Jun 2026 11:15:46 -0700 Subject: [PATCH 48/50] tests: create restricted AIK for tpm2-tools quote --- scripts/tpm2_tools_test.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/tpm2_tools_test.sh b/scripts/tpm2_tools_test.sh index d255e8ce..06610d6d 100755 --- a/scripts/tpm2_tools_test.sh +++ b/scripts/tpm2_tools_test.sh @@ -522,10 +522,12 @@ flush_transient run_test "createprimary for attestation" \ tpm2_createprimary -C o -g sha256 -G rsa -c "$TEST_TMPDIR/att_primary.ctx" -# Create child AIK for signing +# Create child AIK for signing. Quote requires a restricted signing key +# (TPM 2.0 Part 3 Sec.18.4), so set the canonical attestation-key attributes. run_test "create AIK (RSA signing key)" \ tpm2_create -C "$TEST_TMPDIR/att_primary.ctx" \ -g sha256 -G rsa:rsassa:null \ + -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|sign" \ -u "$TEST_TMPDIR/aik.pub" -r "$TEST_TMPDIR/aik.priv" run_test "load AIK" \ From f98029caf6c312a3b051b9d89b3f6c7a157d8848 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Wed, 3 Jun 2026 11:28:19 -0700 Subject: [PATCH 49/50] fwtpm: guard FwHandleIsNoDA definition under FWTPM_NO_DA --- src/fwtpm/fwtpm_command.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 83987b17..1d831047 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -15581,6 +15581,7 @@ static const FWTPM_CMD_ENTRY* FwFindCmdEntry(TPM_CC cc) return NULL; } +#ifndef FWTPM_NO_DA /* Return 1 if the handle is an NV index with TPMA_NV_NO_DA set. Such an * index is exempt from dictionary-attack lockout (Part 1 Sec.23.2). */ static int FwHandleIsNoDA(FWTPM_CTX* ctx, TPM_HANDLE handle) @@ -15597,6 +15598,7 @@ static int FwHandleIsNoDA(FWTPM_CTX* ctx, TPM_HANDLE handle) #endif return 0; } +#endif /* !FWTPM_NO_DA */ /* ================================================================== */ /* Public API: FWTPM_ProcessCommand */ From 21a950149e7b9192f51ac09966c6168bbcd44a68 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Wed, 3 Jun 2026 11:49:09 -0700 Subject: [PATCH 50/50] fwtpm: make FWTPM_NV_SaveFlags a no-op success when NV is disabled --- src/fwtpm/fwtpm_nv.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/fwtpm/fwtpm_nv.c b/src/fwtpm/fwtpm_nv.c index a8edba0b..d3394d77 100644 --- a/src/fwtpm/fwtpm_nv.c +++ b/src/fwtpm/fwtpm_nv.c @@ -1857,6 +1857,13 @@ int FWTPM_NV_SavePcrAuth(FWTPM_CTX* ctx) int FWTPM_NV_SaveFlags(FWTPM_CTX* ctx) { +#ifdef FWTPM_NO_NV + /* No persistence without NV; nothing to save (callers treat as success) */ + if (ctx == NULL) { + return BAD_FUNC_ARG; + } + return TPM_RC_SUCCESS; +#else int rc; byte buf[1 + 12 + 4]; /* flags + DA params + resetCount */ word32 pos = 0; @@ -1877,6 +1884,7 @@ int FWTPM_NV_SaveFlags(FWTPM_CTX* ctx) rc = FwNvAppendEntry(ctx, FWTPM_NV_TAG_FLAGS, buf, (UINT16)pos); return rc; +#endif /* FWTPM_NO_NV */ } int FWTPM_NV_SaveClock(FWTPM_CTX* ctx)