From c9a169ba86be6774b4b7696592b81abb2d753ca9 Mon Sep 17 00:00:00 2001 From: deadcafe Date: Wed, 21 Oct 2026 23:16:49 +0530 Subject: [PATCH] fwtpm: reject ML-DSA signing with no private seed SignSequenceComplete and SignDigest signed with obj->privKey without checking that a private seed was loaded. For a public-only ML-DSA or Hash-ML-DSA key from TPM2_LoadExternal they returned success with a signature from the all-zero seed, which does not verify against the key's public area. Return TPM_RC_KEY when privKeySize is not MAX_MLDSA_PRIV_SEED_SIZE, matching FwSignAttestMldsa. For sequences the check is in Complete, where the key is used: the object at keyHandle can differ from the one given to Start while keeping the same Name. --- src/fwtpm/fwtpm_command.c | 15 +++++ tests/fwtpm_unit_tests.c | 121 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 112a2132..8f52af7e 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -18910,6 +18910,14 @@ static TPM_RC FwCmd_SignSequenceComplete(FWTPM_CTX* ctx, TPM2_Packet* cmd, if (rc == 0 && !(keyObj->pub.objectAttributes & TPMA_OBJECT_sign)) { rc = TPM_RC_KEY; } + /* Reject a public-only ML-DSA object: its all-zero seed derives a + * universally reproducible key. Checked here rather than at + * SequenceStart because a public-only twin shares the key's Name. */ + if (rc == 0 && (keyObj->pub.type == TPM_ALG_MLDSA || + keyObj->pub.type == TPM_ALG_HASH_MLDSA) && + keyObj->privKeySize != MAX_MLDSA_PRIV_SEED_SIZE) { + rc = TPM_RC_KEY; + } /* Part 3 Sec.20.6.1: the x509sign attribute of keyHandle MUST NOT be SET * (TPM_RC_ATTRIBUTES). Keys with x509sign restrict what digests can be * signed for X.509 cert use; SignSequenceComplete is not the right @@ -19576,6 +19584,13 @@ static TPM_RC FwCmd_SignDigest(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, if (rc == 0 && !(obj->pub.objectAttributes & TPMA_OBJECT_sign)) { rc = TPM_RC_KEY; } + /* Reject a public-only ML-DSA object: its all-zero seed derives a + * universally reproducible key. */ + if (rc == 0 && (obj->pub.type == TPM_ALG_MLDSA || + obj->pub.type == TPM_ALG_HASH_MLDSA) && + obj->privKeySize != MAX_MLDSA_PRIV_SEED_SIZE) { + rc = TPM_RC_KEY; + } /* Part 3 Sec.20.7.1: x509sign restricts the key to X.509-cert signing * and is rejected outright here with TPM_RC_ATTRIBUTES. Restricted diff --git a/tests/fwtpm_unit_tests.c b/tests/fwtpm_unit_tests.c index dbf9060e..069aed9a 100644 --- a/tests/fwtpm_unit_tests.c +++ b/tests/fwtpm_unit_tests.c @@ -6072,6 +6072,126 @@ static void test_fwtpm_mldsa_quote_public_only_returns_key(void) } #endif /* WOLFTPM_MLDSA_SIGN && WOLFTPM_MLDSA_VERIFY && !FWTPM_NO_ATTESTATION */ +/* LoadExternal a public-only ML-DSA-44 (or Hash-ML-DSA-44/SHA-256) signing + * key and return its handle. */ +static UINT32 LoadPublicOnlyMldsa(FWTPM_CTX* ctx, UINT16 algType) +{ + int rspSize = 0, pos = 0, pubStart; + + PutU16BE(gCmd + pos, TPM_ST_NO_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_LoadExternal); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; /* inPrivate empty */ + pubStart = pos; + PutU16BE(gCmd + pos, 0); pos += 2; /* size placeholder */ + PutU16BE(gCmd + pos, algType); pos += 2; /* type */ + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; /* nameAlg */ + PutU32BE(gCmd + pos, 0x00040040); pos += 4; /* sign|userWithAuth */ + PutU16BE(gCmd + pos, 0); pos += 2; /* authPolicy */ + PutU16BE(gCmd + pos, TPM_MLDSA_44); pos += 2; /* parameterSet */ + if (algType == TPM_ALG_HASH_MLDSA) { + PutU16BE(gCmd + pos, TPM_ALG_SHA256); pos += 2; /* hashAlg */ + } + else { + gCmd[pos++] = NO; /* allowExternalMu */ + } + PutU16BE(gCmd + pos, sizeof(gNistMldsa44Pk)); pos += 2; + memcpy(gCmd + pos, gNistMldsa44Pk, sizeof(gNistMldsa44Pk)); + pos += sizeof(gNistMldsa44Pk); + PutU16BE(gCmd + pubStart, (UINT16)(pos - pubStart - 2)); + PutU32BE(gCmd + pos, TPM_RH_NULL); pos += 4; /* hierarchy */ + PutU32BE(gCmd + 2, (UINT32)pos); + AssertIntEQ(FWTPM_ProcessCommand(ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + return GetU32BE(gRsp + TPM2_HEADER_SIZE); +} + +/* A public-only ML-DSA key has no private seed. SignSequenceComplete and + * SignDigest must return TPM_RC_KEY rather than sign from an all-zero seed. */ +static void test_fwtpm_mldsa_sign_public_only_returns_key(void) +{ + FWTPM_CTX ctx; + int rspSize, pos; + UINT32 handle, seqHandle; + + memset(&ctx, 0, sizeof(ctx)); + AssertIntEQ(fwtpm_test_startup(&ctx), 0); + + handle = LoadPublicOnlyMldsa(&ctx, TPM_ALG_MLDSA); + + /* SignSequenceStart (no auth, empty context) */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_NO_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_SignSequenceStart); pos += 4; + PutU32BE(gCmd + pos, handle); pos += 4; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU16BE(gCmd + pos, 0); pos += 2; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_SUCCESS); + seqHandle = GetU32BE(gRsp + TPM2_HEADER_SIZE); + + /* SignSequenceComplete must refuse: no private seed. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_SignSequenceComplete); pos += 4; + PutU32BE(gCmd + pos, seqHandle); pos += 4; + PutU32BE(gCmd + pos, handle); pos += 4; + PutU32BE(gCmd + pos, 18); 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; + 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, 4); pos += 2; /* buffer */ + memset(gCmd + pos, 0xAA, 4); pos += 4; + PutU32BE(gCmd + 2, (UINT32)pos); + rspSize = 0; + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_KEY); + + FlushHandle(&ctx, seqHandle); + FlushHandle(&ctx, handle); + +#ifdef WOLFTPM_HASH_MLDSA + handle = LoadPublicOnlyMldsa(&ctx, TPM_ALG_HASH_MLDSA); + + /* SignDigest must refuse: no private seed. */ + pos = 0; + PutU16BE(gCmd + pos, TPM_ST_SESSIONS); pos += 2; + PutU32BE(gCmd + pos, 0); pos += 4; + PutU32BE(gCmd + pos, TPM_CC_SignDigest); pos += 4; + PutU32BE(gCmd + pos, handle); 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; + PutU16BE(gCmd + pos, 0); pos += 2; /* context empty */ + PutU16BE(gCmd + pos, 32); pos += 2; /* digest */ + memset(gCmd + pos, 0xAA, 32); pos += 32; + 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; + AssertIntEQ(FWTPM_ProcessCommand(&ctx, gCmd, pos, gRsp, &rspSize, 0), + TPM_RC_SUCCESS); + AssertIntEQ(GetRspRC(gRsp), TPM_RC_KEY); + + FlushHandle(&ctx, handle); +#endif /* WOLFTPM_HASH_MLDSA */ + + FWTPM_Cleanup(&ctx); + fwtpm_pass("MLDSA sign public-only key rejected:", 1); +} + /* TPM2_LoadExternal must reject ML public areas with an unsupported parameter * set, an out-of-range allowExternalMu, an invalid Hash-ML-DSA hash, invalid * ML-KEM symmetric parameters, or a public key size inconsistent with the @@ -19140,6 +19260,7 @@ int fwtpm_unit_tests(int argc, char *argv[]) test_fwtpm_signseqcomplete_truncated_buffer(); test_fwtpm_verifyseqcomplete_neg(); test_fwtpm_signdigest_neg(); + test_fwtpm_mldsa_sign_public_only_returns_key(); #ifdef WOLFTPM_HASH_MLDSA test_fwtpm_signdigest_malformed_hashcheck_tag(); test_fwtpm_signdigest_restricted_null_ticket_returns_ticket();