From dacb7eff55fc3389bea2870f0393d6fa8fbae62b Mon Sep 17 00:00:00 2001 From: deadcafe Date: Wed, 21 Oct 2026 22:58:34 +0530 Subject: [PATCH 1/5] Keep slot-1 session in SignSequenceComplete wolfTPM2_SignSequenceComplete authorizes keyHandle with wolfTPM2_SetAuthHandle(dev, 1, ...), which resets session slot 1 to password auth. A caller's parameter-encryption HMAC session in slot 1 was silently dropped, so the next command (e.g. wolfTPM2_Decapsulate) returned its secret unencrypted and the session's rolling nonce was lost. Save slot 1 before the command and restore it afterwards. When the saved session is an HMAC session and slot 2 is free, run it from slot 2 for this command, where the TPM takes extra encrypt and audit sessions, so it also protects the sign. Copy it back with the nonces the command advanced. Authorizing the key through the slot-1 session instead would fail with TPM_RC_AUTH_FAIL for keys with an authValue, since an unbound HMAC session does not carry the key's authValue. Extend the ML-DSA key-auth sign test to sign with an HMAC session in slot 1, check the session is kept and its nonce rolled, and verify the signature through the same session. --- src/tpm2_wrap.c | 24 ++++++++++++++++++++++++ tests/unit_tests.c | 27 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index 13d02786..4e12017b 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -6056,6 +6056,10 @@ int wolfTPM2_SignSequenceComplete(WOLFTPM2_DEV* dev, SignSequenceComplete_In signSeqCompleteIn; SignSequenceComplete_Out signSeqCompleteOut; WOLFTPM2_HANDLE seqHandleObj; + TPM2_AUTH_SESSION session1; +#if MAX_SESSION_NUM > 2 + int moved = 0; +#endif if (dev == NULL || key == NULL || sig == NULL || sigSz == NULL) { return BAD_FUNC_ARG; @@ -6077,6 +6081,17 @@ int wolfTPM2_SignSequenceComplete(WOLFTPM2_DEV* dev, XMEMSET(&seqHandleObj, 0, sizeof(seqHandleObj)); seqHandleObj.hndl = sequenceHandle; wolfTPM2_SetAuthHandle(dev, 0, &seqHandleObj); + /* SetAuthHandle resets slot 1 to password auth; restore the caller's + * slot-1 session (e.g. parameter encryption) after the command. While + * slot 2 is free, that session also protects this command from there. */ + XMEMCPY(&session1, &dev->session[1], sizeof(session1)); +#if MAX_SESSION_NUM > 2 + if (TPM2_IS_HMAC_SESSION(session1.sessionHandle) && + dev->session[2].sessionHandle == 0) { + XMEMCPY(&dev->session[2], &session1, sizeof(session1)); + moved = 1; + } +#endif wolfTPM2_SetAuthHandle(dev, 1, &key->handle); XMEMSET(&signSeqCompleteIn, 0, sizeof(signSeqCompleteIn)); @@ -6089,6 +6104,15 @@ int wolfTPM2_SignSequenceComplete(WOLFTPM2_DEV* dev, XMEMSET(&signSeqCompleteOut, 0, sizeof(signSeqCompleteOut)); rc = TPM2_SignSequenceComplete(&signSeqCompleteIn, &signSeqCompleteOut); +#if MAX_SESSION_NUM > 2 + if (moved) { + /* keep the nonces the command advanced */ + XMEMCPY(&session1, &dev->session[2], sizeof(session1)); + TPM2_ForceZero(&dev->session[2], sizeof(dev->session[2])); + } +#endif + XMEMCPY(&dev->session[1], &session1, sizeof(session1)); + TPM2_ForceZero(&session1, sizeof(session1)); if (rc == TPM_RC_SUCCESS) { /* Extract signature based on algorithm */ if (signSeqCompleteOut.signature.sigAlg == TPM_ALG_ECDSA || diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 9347c5d0..61dd966c 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -10543,6 +10543,10 @@ static void test_wolfTPM2_MLDSA_SignSequence_NonEmptyAuth(WOLFTPM2_DEV* dev, int rc; WOLFTPM2_KEY key; TPMT_PUBLIC pub; +#ifndef WOLFTPM2_NO_WOLFCRYPT + WOLFTPM2_SESSION sess; + TPM2B_NONCE nonceTPM; +#endif static const byte gAuth[] = { 'p','q','c','_','a','u','t','h' }; byte sig[5000]; int sigSz = (int)sizeof(sig); @@ -10563,6 +10567,29 @@ static void test_wolfTPM2_MLDSA_SignSequence_NonEmptyAuth(WOLFTPM2_DEV* dev, test_wolfTPM2_MLDSA_SignSequence(dev, &key, gMsg, msgSz, sig, &sigSz); +#ifndef WOLFTPM2_NO_WOLFCRYPT + /* Signing must leave a caller's slot-1 param-encryption session in + * place; replacing it with password auth sends the next command's + * secrets (e.g. a Decapsulate shared secret) in the clear. */ + rc = wolfTPM2_StartSession(dev, &sess, NULL, NULL, TPM_SE_HMAC, + TPM_ALG_CFB); + AssertIntEQ(rc, 0); + rc = wolfTPM2_SetAuthSession(dev, 1, &sess, (TPMA_SESSION_decrypt | + TPMA_SESSION_encrypt | TPMA_SESSION_continueSession)); + AssertIntEQ(rc, 0); + XMEMCPY(&nonceTPM, &dev->session[1].nonceTPM, sizeof(nonceTPM)); + sigSz = (int)sizeof(sig); + test_wolfTPM2_MLDSA_SignSequence(dev, &key, gMsg, msgSz, sig, &sigSz); + AssertIntEQ(dev->session[1].sessionHandle, sess.handle.hndl); + /* The sign ran the session (nonce rolled); verifying through it checks + * the encrypted message and the session's nonces both survived. */ + AssertIntNE(XMEMCMP(&nonceTPM, &dev->session[1].nonceTPM, + sizeof(nonceTPM)), 0); + test_wolfTPM2_MLDSA_VerifySequence(dev, &key, gMsg, msgSz, sig, sigSz); + wolfTPM2_SetAuthSession(dev, 1, NULL, 0); + wolfTPM2_UnloadHandle(dev, &sess.handle); +#endif + wolfTPM2_UnloadHandle(dev, &key.handle); printf("Test TPM Wrapper: %-40s Passed\n", "ML-DSA Sign Seq w/ key auth:"); From 5c08a963cd0e0c69ee2815bf14de5b58123ab059 Mon Sep 17 00:00:00 2001 From: deadcafe Date: Wed, 21 Oct 2026 23:39:56 +0530 Subject: [PATCH 2/5] Guard slot-1 restore for policy and 1-slot builds - With MAX_SESSION_NUM=1 (--enable-smallstack --disable-wolfcrypt) the slot-1 save and restore read and wrote past dev->session. Compile them only when a second slot exists. - A policy-auth key keeps its policy session in slot 1 and the command advances its nonce; restoring the snapshot rolled that nonce back. Skip the save and restore for policyAuth keys. - With MAX_SESSION_NUM=2 the session cannot move to slot 2, so its nonce does not roll. Check the rolled nonce only when slot 2 exists. --- src/tpm2_wrap.c | 41 +++++++++++++++++++++++++++-------------- tests/unit_tests.c | 7 +++++-- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index 4e12017b..2f0336a7 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -6056,7 +6056,10 @@ int wolfTPM2_SignSequenceComplete(WOLFTPM2_DEV* dev, SignSequenceComplete_In signSeqCompleteIn; SignSequenceComplete_Out signSeqCompleteOut; WOLFTPM2_HANDLE seqHandleObj; +#if MAX_SESSION_NUM > 1 TPM2_AUTH_SESSION session1; + int restore = 0; +#endif #if MAX_SESSION_NUM > 2 int moved = 0; #endif @@ -6081,15 +6084,21 @@ int wolfTPM2_SignSequenceComplete(WOLFTPM2_DEV* dev, XMEMSET(&seqHandleObj, 0, sizeof(seqHandleObj)); seqHandleObj.hndl = sequenceHandle; wolfTPM2_SetAuthHandle(dev, 0, &seqHandleObj); +#if MAX_SESSION_NUM > 1 /* SetAuthHandle resets slot 1 to password auth; restore the caller's * slot-1 session (e.g. parameter encryption) after the command. While - * slot 2 is free, that session also protects this command from there. */ - XMEMCPY(&session1, &dev->session[1], sizeof(session1)); -#if MAX_SESSION_NUM > 2 - if (TPM2_IS_HMAC_SESSION(session1.sessionHandle) && - dev->session[2].sessionHandle == 0) { - XMEMCPY(&dev->session[2], &session1, sizeof(session1)); - moved = 1; + * slot 2 is free, that session also protects this command from there. + * A policy-auth key runs on the slot-1 session itself; keep it live. */ + if (!key->handle.policyAuth) { + XMEMCPY(&session1, &dev->session[1], sizeof(session1)); + restore = 1; + #if MAX_SESSION_NUM > 2 + if (TPM2_IS_HMAC_SESSION(session1.sessionHandle) && + dev->session[2].sessionHandle == 0) { + XMEMCPY(&dev->session[2], &session1, sizeof(session1)); + moved = 1; + } + #endif } #endif wolfTPM2_SetAuthHandle(dev, 1, &key->handle); @@ -6104,15 +6113,19 @@ int wolfTPM2_SignSequenceComplete(WOLFTPM2_DEV* dev, XMEMSET(&signSeqCompleteOut, 0, sizeof(signSeqCompleteOut)); rc = TPM2_SignSequenceComplete(&signSeqCompleteIn, &signSeqCompleteOut); -#if MAX_SESSION_NUM > 2 - if (moved) { - /* keep the nonces the command advanced */ - XMEMCPY(&session1, &dev->session[2], sizeof(session1)); - TPM2_ForceZero(&dev->session[2], sizeof(dev->session[2])); +#if MAX_SESSION_NUM > 1 + if (restore) { + #if MAX_SESSION_NUM > 2 + if (moved) { + /* keep the nonces the command advanced */ + XMEMCPY(&session1, &dev->session[2], sizeof(session1)); + TPM2_ForceZero(&dev->session[2], sizeof(dev->session[2])); + } + #endif + XMEMCPY(&dev->session[1], &session1, sizeof(session1)); + TPM2_ForceZero(&session1, sizeof(session1)); } #endif - XMEMCPY(&dev->session[1], &session1, sizeof(session1)); - TPM2_ForceZero(&session1, sizeof(session1)); if (rc == TPM_RC_SUCCESS) { /* Extract signature based on algorithm */ if (signSeqCompleteOut.signature.sigAlg == TPM_ALG_ECDSA || diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 61dd966c..f0499cd3 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -10581,10 +10581,13 @@ static void test_wolfTPM2_MLDSA_SignSequence_NonEmptyAuth(WOLFTPM2_DEV* dev, sigSz = (int)sizeof(sig); test_wolfTPM2_MLDSA_SignSequence(dev, &key, gMsg, msgSz, sig, &sigSz); AssertIntEQ(dev->session[1].sessionHandle, sess.handle.hndl); - /* The sign ran the session (nonce rolled); verifying through it checks - * the encrypted message and the session's nonces both survived. */ +#if MAX_SESSION_NUM > 2 + /* With a free slot 2 the sign ran the session (nonce rolled) */ AssertIntNE(XMEMCMP(&nonceTPM, &dev->session[1].nonceTPM, sizeof(nonceTPM)), 0); +#endif + /* Verifying through the session checks the encrypted message and the + * session's nonces both survived. */ test_wolfTPM2_MLDSA_VerifySequence(dev, &key, gMsg, msgSz, sig, sigSz); wolfTPM2_SetAuthSession(dev, 1, NULL, 0); wolfTPM2_UnloadHandle(dev, &sess.handle); From 65decab4fc900901b6f8c525c72ccfcca9822865 Mon Sep 17 00:00:00 2001 From: deadcafe Date: Thu, 22 Oct 2026 00:03:21 +0530 Subject: [PATCH 3/5] Clear bound Name when moving session to slot 2 The slot-1 session copied into slot 2 kept its Name. For a session bound to that entity (name == bindName), TPM2_ParamEncBindKey() then appended the bind authValue to the parameter-encryption key, but slot 2 authorizes no handle, so the TPM used the session key alone. The command HMAC still validated, while the TPM decrypted the message to other bytes and signed those. Clear the Name in slot 2 and put the caller's Name back on restore, unless the response retired the session. Add a bound-session regression that verifies the signature over the original message. --- src/tpm2_wrap.c | 10 +++++++++- tests/unit_tests.c | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index 2f0336a7..59b480a0 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -6096,6 +6096,9 @@ int wolfTPM2_SignSequenceComplete(WOLFTPM2_DEV* dev, if (TPM2_IS_HMAC_SESSION(session1.sessionHandle) && dev->session[2].sessionHandle == 0) { XMEMCPY(&dev->session[2], &session1, sizeof(session1)); + /* slot 2 authorizes no handle; a bound Name would fold the bind + * authValue into the param-enc key, which the TPM does not */ + dev->session[2].name.size = 0; moved = 1; } #endif @@ -6117,7 +6120,12 @@ int wolfTPM2_SignSequenceComplete(WOLFTPM2_DEV* dev, if (restore) { #if MAX_SESSION_NUM > 2 if (moved) { - /* keep the nonces the command advanced */ + /* keep the nonces the command advanced; a retired session + * stays cleared */ + if (dev->session[2].sessionHandle == session1.sessionHandle) { + XMEMCPY(&dev->session[2].name, &session1.name, + sizeof(session1.name)); + } XMEMCPY(&session1, &dev->session[2], sizeof(session1)); TPM2_ForceZero(&dev->session[2], sizeof(dev->session[2])); } diff --git a/tests/unit_tests.c b/tests/unit_tests.c index f0499cd3..33479c4b 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -10591,6 +10591,26 @@ static void test_wolfTPM2_MLDSA_SignSequence_NonEmptyAuth(WOLFTPM2_DEV* dev, test_wolfTPM2_MLDSA_VerifySequence(dev, &key, gMsg, msgSz, sig, sigSz); wolfTPM2_SetAuthSession(dev, 1, NULL, 0); wolfTPM2_UnloadHandle(dev, &sess.handle); + + /* Same with a session bound to the key and slot 1 holding the key's + * Name (as the NV wrappers leave it): while the session runs from + * slot 2 the bind authValue must stay out of the param-enc key. */ + rc = wolfTPM2_StartSession(dev, &sess, NULL, &key.handle, TPM_SE_HMAC, + TPM_ALG_CFB); + AssertIntEQ(rc, 0); + rc = wolfTPM2_SetAuthSession(dev, 1, &sess, (TPMA_SESSION_decrypt | + TPMA_SESSION_encrypt | TPMA_SESSION_continueSession)); + AssertIntEQ(rc, 0); + rc = wolfTPM2_SetAuthHandleName(dev, 1, &key.handle); + AssertIntEQ(rc, 0); + sigSz = (int)sizeof(sig); + test_wolfTPM2_MLDSA_SignSequence(dev, &key, gMsg, msgSz, sig, &sigSz); + AssertIntEQ(dev->session[1].sessionHandle, sess.handle.hndl); + AssertIntEQ(dev->session[1].name.size, key.handle.name.size); + /* Verify with password auth: a wrong decrypt above signed other bytes */ + wolfTPM2_SetAuthSession(dev, 1, NULL, 0); + test_wolfTPM2_MLDSA_VerifySequence(dev, &key, gMsg, msgSz, sig, sigSz); + wolfTPM2_UnloadHandle(dev, &sess.handle); #endif wolfTPM2_UnloadHandle(dev, &key.handle); From 5be741380c432e7545c2ec11a41301270844ad3b Mon Sep 17 00:00:00 2001 From: deadcafe Date: Thu, 22 Oct 2026 00:18:12 +0530 Subject: [PATCH 4/5] Skip slot-1 session test when signing unsupported When the TPM rejects SignSequenceStart, the sign helper sets sigSz to zero and returns. The slot-1 session checks still ran, and with a free slot 2 the nonce assertion failed because nothing advanced the nonce, aborting the suite. Unload the key and return once the first sign produces no signature. --- tests/unit_tests.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 33479c4b..fa1a499a 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -10566,6 +10566,11 @@ static void test_wolfTPM2_MLDSA_SignSequence_NonEmptyAuth(WOLFTPM2_DEV* dev, AssertIntEQ(rc, 0); test_wolfTPM2_MLDSA_SignSequence(dev, &key, gMsg, msgSz, sig, &sigSz); + if (sigSz == 0) { + /* sign sequence unsupported; the helper reported the skip */ + wolfTPM2_UnloadHandle(dev, &key.handle); + return; + } #ifndef WOLFTPM2_NO_WOLFCRYPT /* Signing must leave a caller's slot-1 param-encryption session in From 3db79c0ab357ca8dd8dbf32b84aa402fabc9ce03 Mon Sep 17 00:00:00 2001 From: deadcafe Date: Thu, 22 Oct 2026 04:32:39 +0530 Subject: [PATCH 5/5] Test policy-auth SignSequenceComplete slot 1 Cover the policyAuth branch: sign with a PolicyAuthValue-only ML-DSA key whose policy session sits in slot 1, then assert the session handle stays, its nonceTPM advances, and a second policy + sign on the same session succeeds. The session is bound so the wrapper's PolicyAuthValue HMAC key has a session key in front of the authValue. --- tests/unit_tests.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/unit_tests.c b/tests/unit_tests.c index fa1a499a..0bdc1347 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -10623,6 +10623,82 @@ static void test_wolfTPM2_MLDSA_SignSequence_NonEmptyAuth(WOLFTPM2_DEV* dev, "ML-DSA Sign Seq w/ key auth:"); } +#ifndef WOLFTPM2_NO_WOLFCRYPT +/* A policy-authorized key signs on the slot-1 policy session itself, so + * SignSequenceComplete must leave that session in slot 1 with the nonces + * the command rolled; a second sign on it fails if they went stale. */ +static void test_wolfTPM2_MLDSA_SignSequence_PolicyAuth(WOLFTPM2_DEV* dev, + const TPMT_PUBLIC* mldsaPub) +{ + int rc; + WOLFTPM2_KEY key; + WOLFTPM2_SESSION sess; + TPMT_PUBLIC pub; + TPM2B_NONCE nonceTPM; + word32 polSz; + static const byte gAuth[] = { 'p','o','l','_','a','u','t','h' }; + byte sig[5000]; + int sigSz = (int)sizeof(sig); + static const byte gMsg[] = "Policy-auth ML-DSA test message"; + int msgSz = (int)sizeof(gMsg) - 1; + + XMEMSET(&key, 0, sizeof(key)); + XMEMCPY(&pub, mldsaPub, sizeof(pub)); + /* policy-only: authPolicy = PolicyAuthValue, no userWithAuth */ + pub.objectAttributes &= ~TPMA_OBJECT_userWithAuth; + XMEMSET(pub.authPolicy.buffer, 0, sizeof(pub.authPolicy.buffer)); + polSz = (word32)TPM2_GetHashDigestSize(pub.nameAlg); + AssertIntEQ(wolfTPM2_PolicyHash(pub.nameAlg, pub.authPolicy.buffer, + &polSz, TPM_CC_PolicyAuthValue, NULL, 0), 0); + pub.authPolicy.size = (UINT16)polSz; + + rc = wolfTPM2_CreatePrimaryKey(dev, &key, TPM_RH_OWNER, &pub, + gAuth, (int)sizeof(gAuth)); + if (TEST_V185_UNSUPPORTED(rc, TPM_ALG_MLDSA)) { + printf("Test TPM Wrapper: %-40s Skipped (not supported)\n", + "ML-DSA Sign Seq w/ policy auth:"); + return; + } + AssertIntEQ(rc, 0); + key.handle.policyAuth = 1; + + /* bound: the PolicyAuthValue HMAC key layout needs a session key */ + rc = wolfTPM2_StartSession(dev, &sess, NULL, &key.handle, TPM_SE_POLICY, + TPM_ALG_NULL); + AssertIntEQ(rc, 0); + AssertIntEQ(wolfTPM2_PolicyAuthValue(dev, &sess, gAuth, + (int)sizeof(gAuth)), 0); + rc = wolfTPM2_SetAuthSession(dev, 1, &sess, TPMA_SESSION_continueSession); + AssertIntEQ(rc, 0); + XMEMCPY(&nonceTPM, &dev->session[1].nonceTPM, sizeof(nonceTPM)); + + test_wolfTPM2_MLDSA_SignSequence(dev, &key, gMsg, msgSz, sig, &sigSz); + if (sigSz == 0) { + /* sign sequence unsupported; the helper reported the skip */ + goto done; + } + AssertIntEQ(dev->session[1].sessionHandle, sess.handle.hndl); + AssertIntNE(XMEMCMP(&nonceTPM, &dev->session[1].nonceTPM, + sizeof(nonceTPM)), 0); + + /* the TPM reset the policy digest on use; satisfy it again and sign */ + AssertIntEQ(wolfTPM2_PolicyAuthValue(dev, &sess, NULL, 0), 0); + sigSz = (int)sizeof(sig); + test_wolfTPM2_MLDSA_SignSequence(dev, &key, gMsg, msgSz, sig, &sigSz); + AssertIntEQ(dev->session[1].sessionHandle, sess.handle.hndl); + + wolfTPM2_SetAuthSession(dev, 1, NULL, 0); + test_wolfTPM2_MLDSA_VerifySequence(dev, &key, gMsg, msgSz, sig, sigSz); + printf("Test TPM Wrapper: %-40s Passed\n", + "ML-DSA Sign Seq w/ policy auth:"); + +done: + wolfTPM2_SetAuthSession(dev, 1, NULL, 0); + wolfTPM2_UnloadHandle(dev, &sess.handle); + wolfTPM2_UnloadHandle(dev, &key.handle); +} +#endif + /* Regression for the VerifySequenceComplete data-chain fix. * * The wrapper used to silently drop the data/dataSz arguments; the fix @@ -11119,6 +11195,9 @@ static void test_wolfTPM2_PQC(void) } test_TPM2_SignSequenceStart_NoSession(&dev, &mldsaKey); test_wolfTPM2_MLDSA_SignSequence_NonEmptyAuth(&dev, &mldsaPub); +#ifndef WOLFTPM2_NO_WOLFCRYPT + test_wolfTPM2_MLDSA_SignSequence_PolicyAuth(&dev, &mldsaPub); +#endif wolfTPM2_UnloadHandle(&dev, &mldsaKey.handle); mldsa_done: