From 5699061b53ae789650987f130856a5c5c11f0c2e Mon Sep 17 00:00:00 2001 From: aidan garske Date: Wed, 3 Jun 2026 10:57:13 -0700 Subject: [PATCH] fwtpm: scope context replay to sessions and accept unsalted param-enc --- src/fwtpm/fwtpm_command.c | 37 ++++++++++++--------- tests/fwtpm_unit_tests.c | 68 ++++++++++++++++++++++----------------- 2 files changed, 60 insertions(+), 45 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); - 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, 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) */ 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); + + /* 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, 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); + + pos = BuildCmdHeader(gCmd, TPM_ST_NO_SESSIONS, 0, TPM_CC_ContextLoad); + 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); - /* 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 + 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