From 4ead81671367c58f86a9155aaff92d3f0d5a524b Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 22 Apr 2026 09:06:05 -0700 Subject: [PATCH] fwTPM PQC: finish v1.85 protocol wiring for Sign/Verify over mssim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server-side handler fixes: - FwCmd_SignSequenceStart / VerifySequenceStart: call FwSkipAuthArea when cmdTag == TPM_ST_SESSIONS. Without it, the 4-byte authAreaSize prefix was mis-parsed as the auth / context TPM2B size fields, producing mis-aligned context bytes (ctxSz=9 on sign, 0 on verify) so the μ fed into FIPS 204 differed between the two handlers — verify always failed. - FwCmd_SignSequenceStart / VerifySequenceStart: emit the output sequenceHandle before FwRspParamsBegin, matching TPM 2.0 response framing (handles precede the SESSIONS paramSize). - FwCmd_CreatePrimary: add MLDSA / HASH_MLDSA / MLKEM arms to the hashUnique switch so the unique template actually binds into KDFa derivation. - FwCmd_TestParms: accept PQC algs (MLKEM / MLDSA / HASH_MLDSA). Client-side wrapper fixes: - wolfTPM2_CopyPubT: add MLDSA / HASH_MLDSA / MLKEM cases. Previous switch fell through, leaving unique.mlkem / .mldsa as zero-filled buffers after a successful CreatePrimary (Jay's reported bug). - GetKeyTemplateSize: add PQC parameter-set-aware sizes. - wolfTPM2_SetKeyTemplate_Unique: add PQC arms. - TPM2_SignSequenceComplete: add CMD_FLAG_AUTH_USER2 (Table 124 requires USER auth on both @seq and @key handles). - TPM2_VerifySequenceComplete: remove extra buffer field (Table 118 has no buffer parameter); add CMD_FLAG_AUTH_USER1. examples/pqc/pqc_mssim_e2e.c: tighten validation — check_pub_populated catches CopyPubT-class regressions, MLKEM-768 Encap/Decap secrets must match, HashMLDSA-65 SignDigest emits a DIGEST_VERIFIED ticket. --- examples/pqc/pqc_mssim_e2e.c | 30 ++++++++++ src/fwtpm/fwtpm_command.c | 48 +++++++++++++++- src/tpm2.c | 40 +++++++++++-- src/tpm2_wrap.c | 108 +++++++++++++++++++++++++++++++++++ 4 files changed, 219 insertions(+), 7 deletions(-) diff --git a/examples/pqc/pqc_mssim_e2e.c b/examples/pqc/pqc_mssim_e2e.c index 9510101e..211de549 100644 --- a/examples/pqc/pqc_mssim_e2e.c +++ b/examples/pqc/pqc_mssim_e2e.c @@ -36,6 +36,25 @@ #if !defined(WOLFTPM2_NO_WRAPPER) && defined(WOLFTPM_V185) +/* Guard against the CopyPubT-class bug where the server-side key exists + * and the handle works, but the client-side TPM2B buffer is zero-filled + * (Part 2 Table 225 unique arm never copied). */ +static int check_pub_populated(const char* label, const byte* buf, + UINT16 gotSize, UINT16 wantSize) +{ + int i; + if (gotSize != wantSize) { + printf("%s.size = %u (expected %u)\n", label, gotSize, wantSize); + return -1; + } + for (i = 0; i < wantSize; i++) { + if (buf[i] != 0) return 0; + } + printf("%s.buffer is all zero (client-side unique-arm copy dropped)\n", + label); + return -1; +} + static int test_mlkem_roundtrip(WOLFTPM2_DEV* dev) { WOLFTPM2_KEY mlkem; @@ -64,6 +83,11 @@ static int test_mlkem_roundtrip(WOLFTPM2_DEV* dev) return rc; } + rc = check_pub_populated("mlkem.unique", + mlkem.pub.publicArea.unique.mlkem.buffer, + mlkem.pub.publicArea.unique.mlkem.size, 1184); + if (rc != 0) goto cleanup; + rc = wolfTPM2_Encapsulate(dev, &mlkem, ct, &ctSz, ss1, &ss1Sz); if (rc != 0) { printf("Encapsulate rc=%d\n", rc); @@ -126,6 +150,12 @@ static int test_hash_mldsa_digest_roundtrip(WOLFTPM2_DEV* dev) return rc; } + /* HashMLDSA shares the mldsa arm of TPMU_PUBLIC_ID. */ + rc = check_pub_populated("mldsa.unique", + mldsa.pub.publicArea.unique.mldsa.buffer, + mldsa.pub.publicArea.unique.mldsa.size, 1952); + if (rc != 0) goto cleanup; + rc = wolfTPM2_SignDigest(dev, &mldsa, digest, sizeof(digest), NULL, 0, diff --git a/src/fwtpm/fwtpm_command.c b/src/fwtpm/fwtpm_command.c index 48a2aedf..aa13841d 100644 --- a/src/fwtpm/fwtpm_command.c +++ b/src/fwtpm/fwtpm_command.c @@ -1312,6 +1312,11 @@ static TPM_RC FwCmd_TestParms(FWTPM_CTX* ctx, TPM2_Packet* cmd, int cmdSize, #endif case TPM_ALG_HMAC: case TPM_ALG_NULL: + #ifdef WOLFTPM_V185 + case TPM_ALG_MLDSA: + case TPM_ALG_HASH_MLDSA: + case TPM_ALG_MLKEM: + #endif /* Supported - skip remaining type-specific params */ break; default: @@ -2289,6 +2294,24 @@ static TPM_RC FwCmd_CreatePrimary(FWTPM_CTX* ctx, TPM2_Packet* cmd, uBuf = inPublic->publicArea.unique.sym.buffer; uSz = (int)inPublic->publicArea.unique.sym.size; break; +#ifdef WOLFTPM_V185 + /* MLDSA / HASH_MLDSA / MLKEM: only feed user-supplied unique + * bytes into hashUnique, not the raw buffer. A size==0 arm + * must not read the uninitialized buffer pointer. */ + case TPM_ALG_MLDSA: + case TPM_ALG_HASH_MLDSA: + if (inPublic->publicArea.unique.mldsa.size > 0) { + uBuf = inPublic->publicArea.unique.mldsa.buffer; + uSz = (int)inPublic->publicArea.unique.mldsa.size; + } + break; + case TPM_ALG_MLKEM: + if (inPublic->publicArea.unique.mlkem.size > 0) { + uBuf = inPublic->publicArea.unique.mlkem.buffer; + uSz = (int)inPublic->publicArea.unique.mlkem.size; + } + break; +#endif /* WOLFTPM_V185 */ default: break; } @@ -13022,6 +13045,15 @@ static TPM_RC FwCmd_SignSequenceStart(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } + /* Skip auth area when the client used ST_SESSIONS. SignSequenceStart + * has no mandatory auth (Table 89 Auth Index: None) but clients may + * still emit a password session. Without skipping, the 4-byte + * authAreaSize prefix gets mis-parsed as the TPM2B_AUTH and + * TPM2B_SIGNATURE_CTX size fields. */ + if (rc == 0 && cmdTag == TPM_ST_SESSIONS) { + rc = FwSkipAuthArea(cmd, cmdSize); + } + /* Parse auth (TPM2B_AUTH) */ if (rc == 0) { TPM2_Packet_ParseU16(cmd, &authSz); @@ -13067,8 +13099,11 @@ static TPM_RC FwCmd_SignSequenceStart(FWTPM_CTX* ctx, TPM2_Packet* cmd, } if (rc == 0) { - paramStart = FwRspParamsBegin(rsp, cmdTag, ¶mSzPos); + /* sequenceHandle is an output handle per Table 89 — must be + * emitted BEFORE the parameterSize field, not inside the + * parameter area. */ TPM2_Packet_AppendU32(rsp, seqHandle); + paramStart = FwRspParamsBegin(rsp, cmdTag, ¶mSzPos); FwRspParamsEnd(rsp, cmdTag, paramSzPos, paramStart); } else if (seq != NULL) { @@ -13110,6 +13145,13 @@ static TPM_RC FwCmd_VerifySequenceStart(FWTPM_CTX* ctx, TPM2_Packet* cmd, } } + /* Skip auth area when tag is ST_SESSIONS — Table 87 Auth Index: None, + * but clients may still emit a password session that otherwise + * desynchronises the auth / hint / context TPM2B parse. */ + if (rc == 0 && cmdTag == TPM_ST_SESSIONS) { + rc = FwSkipAuthArea(cmd, cmdSize); + } + if (rc == 0) { TPM2_Packet_ParseU16(cmd, &authSz); if (authSz > sizeof(((TPM2B_AUTH*)0)->buffer)) { @@ -13158,8 +13200,10 @@ static TPM_RC FwCmd_VerifySequenceStart(FWTPM_CTX* ctx, TPM2_Packet* cmd, } if (rc == 0) { - paramStart = FwRspParamsBegin(rsp, cmdTag, ¶mSzPos); + /* sequenceHandle is an output handle per Table 87 — emitted + * before parameterSize. */ TPM2_Packet_AppendU32(rsp, seqHandle); + paramStart = FwRspParamsBegin(rsp, cmdTag, ¶mSzPos); FwRspParamsEnd(rsp, cmdTag, paramSzPos, paramStart); } else if (seq != NULL) { diff --git a/src/tpm2.c b/src/tpm2.c index 87f5088c..bc98ce89 100644 --- a/src/tpm2.c +++ b/src/tpm2.c @@ -3414,7 +3414,10 @@ TPM_RC TPM2_SignSequenceComplete(SignSequenceComplete_In* in, TPM2_Packet packet; CmdInfo_t info = {0,0,0,0}; info.inHandleCnt = 2; - info.flags = (CMD_FLAG_ENC2 | CMD_FLAG_AUTH_USER1); + /* Part 3 §20.6 Table 124: both @sequenceHandle and @keyHandle + * require USER authorization. */ + info.flags = (CMD_FLAG_ENC2 | CMD_FLAG_AUTH_USER1 | + CMD_FLAG_AUTH_USER2); TPM2_Packet_Init(ctx, &packet); @@ -3457,7 +3460,10 @@ TPM_RC TPM2_VerifySequenceComplete(VerifySequenceComplete_In* in, TPM2_Packet packet; CmdInfo_t info = {0,0,0,0}; info.inHandleCnt = 2; - info.flags = (CMD_FLAG_ENC2); + /* Part 3 §20.3 Table 118: @sequenceHandle requires USER auth; + * keyHandle has no auth. The framework needs the USER1 flag so + * the auth area matches what the server parses under ST_SESSIONS. */ + info.flags = (CMD_FLAG_ENC2 | CMD_FLAG_AUTH_USER1); TPM2_Packet_Init(ctx, &packet); @@ -3466,9 +3472,8 @@ TPM_RC TPM2_VerifySequenceComplete(VerifySequenceComplete_In* in, st = TPM2_Packet_AppendAuth(&packet, ctx, &info); - TPM2_Packet_AppendU16(&packet, in->buffer.size); - TPM2_Packet_AppendBytes(&packet, in->buffer.buffer, in->buffer.size); - + /* Part 3 §20.3 Table 118: parameters are {signature} only — no + * buffer field. Message was accumulated via SequenceUpdate. */ TPM2_Packet_AppendSignature(&packet, &in->signature); TPM2_Packet_Finalize(&packet, st, TPM_CC_VerifySequenceComplete); @@ -7326,6 +7331,31 @@ void TPM2_PrintPublicArea(const TPM2B_PUBLIC* pub) TPM2_PrintBin(pub->publicArea.unique.ecc.y.buffer, pub->publicArea.unique.ecc.y.size); #endif break; +#ifdef WOLFTPM_V185 + case TPM_ALG_MLDSA: + case TPM_ALG_HASH_MLDSA: + printf(" %s: parameterSet 0x%X, unique size %d\n", + (pub->publicArea.type == TPM_ALG_MLDSA) + ? "ML-DSA" : "Hash-ML-DSA", + (pub->publicArea.type == TPM_ALG_MLDSA) + ? pub->publicArea.parameters.mldsaDetail.parameterSet + : pub->publicArea.parameters.hash_mldsaDetail.parameterSet, + pub->publicArea.unique.mldsa.size); + #ifdef WOLFTPM_DEBUG_VERBOSE + TPM2_PrintBin(pub->publicArea.unique.mldsa.buffer, + pub->publicArea.unique.mldsa.size); + #endif + break; + case TPM_ALG_MLKEM: + printf(" ML-KEM: parameterSet 0x%X, unique size %d\n", + pub->publicArea.parameters.mlkemDetail.parameterSet, + pub->publicArea.unique.mlkem.size); + #ifdef WOLFTPM_DEBUG_VERBOSE + TPM2_PrintBin(pub->publicArea.unique.mlkem.buffer, + pub->publicArea.unique.mlkem.size); + #endif + break; +#endif /* WOLFTPM_V185 */ default: /* derive does not seem to have specific fields in the parameters struct */ printf("Derive Type: unique label size %d, context size %d\n", diff --git a/src/tpm2_wrap.c b/src/tpm2_wrap.c index 148e9f2d..e3ecd409 100644 --- a/src/tpm2_wrap.c +++ b/src/tpm2_wrap.c @@ -8482,6 +8482,31 @@ static int GetKeyTemplateSize(TPMT_PUBLIC* publicTemplate) case TPM_ALG_SYMCIPHER: ret = publicTemplate->parameters.symDetail.sym.keyBits.sym / 8; break; + #ifdef WOLFTPM_V185 + case TPM_ALG_MLDSA: + case TPM_ALG_HASH_MLDSA: { + TPMI_MLDSA_PARAMETER_SET ps = + (publicTemplate->type == TPM_ALG_MLDSA) + ? publicTemplate->parameters.mldsaDetail.parameterSet + : publicTemplate->parameters.hash_mldsaDetail.parameterSet; + /* Per Part 2 Table 207 MLDSA public-key sizes. */ + if (ps == TPM_MLDSA_44) ret = 1312; + else if (ps == TPM_MLDSA_65) ret = 1952; + else if (ps == TPM_MLDSA_87) ret = 2592; + else ret = BAD_FUNC_ARG; + break; + } + case TPM_ALG_MLKEM: { + TPMI_MLKEM_PARAMETER_SET ps = + publicTemplate->parameters.mlkemDetail.parameterSet; + /* Per Part 2 Table 204 MLKEM public-key sizes. */ + if (ps == TPM_MLKEM_512) ret = 800; + else if (ps == TPM_MLKEM_768) ret = 1184; + else if (ps == TPM_MLKEM_1024) ret = 1568; + else ret = BAD_FUNC_ARG; + break; + } + #endif /* WOLFTPM_V185 */ case TPM_ALG_KEYEDHASH: default: ret = BAD_FUNC_ARG; @@ -8565,6 +8590,49 @@ int wolfTPM2_SetKeyTemplate_Unique(TPMT_PUBLIC* publicTemplate, } publicTemplate->unique.sym.size = uniqueSz; break; +#ifdef WOLFTPM_V185 + /* TPMU_PUBLIC_ID shares the mldsa arm between MLDSA and HASH_MLDSA + * (Part 2 Table 225 note — one union field, two selectors). */ + case TPM_ALG_MLDSA: + case TPM_ALG_HASH_MLDSA: + if (uniqueSz == 0) { + uniqueSz = keySz; + } + else if (uniqueSz > keySz) { + uniqueSz = keySz; + } + if (uniqueSz > (int)sizeof(publicTemplate->unique.mldsa.buffer)) { + uniqueSz = + (int)sizeof(publicTemplate->unique.mldsa.buffer); /* truncate */ + } + if (unique == NULL) { + XMEMSET(publicTemplate->unique.mldsa.buffer, 0, uniqueSz); + } + else { + XMEMCPY(publicTemplate->unique.mldsa.buffer, unique, uniqueSz); + } + publicTemplate->unique.mldsa.size = uniqueSz; + break; + case TPM_ALG_MLKEM: + if (uniqueSz == 0) { + uniqueSz = keySz; + } + else if (uniqueSz > keySz) { + uniqueSz = keySz; + } + if (uniqueSz > (int)sizeof(publicTemplate->unique.mlkem.buffer)) { + uniqueSz = + (int)sizeof(publicTemplate->unique.mlkem.buffer); /* truncate */ + } + if (unique == NULL) { + XMEMSET(publicTemplate->unique.mlkem.buffer, 0, uniqueSz); + } + else { + XMEMCPY(publicTemplate->unique.mlkem.buffer, unique, uniqueSz); + } + publicTemplate->unique.mlkem.size = uniqueSz; + break; +#endif /* WOLFTPM_V185 */ case TPM_ALG_KEYEDHASH: /* not supported */ ret = BAD_FUNC_ARG; @@ -8946,6 +9014,46 @@ static void wolfTPM2_CopyPubT(TPMT_PUBLIC* out, const TPMT_PUBLIC* in) wolfTPM2_CopyEccParam(&out->unique.ecc.y, &in->unique.ecc.y); break; +#ifdef WOLFTPM_V185 + case TPM_ALG_MLDSA: + out->parameters.mldsaDetail.parameterSet = + in->parameters.mldsaDetail.parameterSet; + out->parameters.mldsaDetail.allowExternalMu = + in->parameters.mldsaDetail.allowExternalMu; + out->unique.mldsa.size = in->unique.mldsa.size; + if (out->unique.mldsa.size > (UINT16)sizeof(out->unique.mldsa.buffer)) { + out->unique.mldsa.size = (UINT16)sizeof(out->unique.mldsa.buffer); + } + XMEMCPY(out->unique.mldsa.buffer, in->unique.mldsa.buffer, + out->unique.mldsa.size); + break; + case TPM_ALG_HASH_MLDSA: + out->parameters.hash_mldsaDetail.parameterSet = + in->parameters.hash_mldsaDetail.parameterSet; + out->parameters.hash_mldsaDetail.hashAlg = + in->parameters.hash_mldsaDetail.hashAlg; + /* TPMU_PUBLIC_ID shares the mldsa arm between MLDSA and HASH_MLDSA + * (Part 2 Table 225 note — one union field, two selectors). */ + out->unique.mldsa.size = in->unique.mldsa.size; + if (out->unique.mldsa.size > (UINT16)sizeof(out->unique.mldsa.buffer)) { + out->unique.mldsa.size = (UINT16)sizeof(out->unique.mldsa.buffer); + } + XMEMCPY(out->unique.mldsa.buffer, in->unique.mldsa.buffer, + out->unique.mldsa.size); + break; + case TPM_ALG_MLKEM: + wolfTPM2_CopySymmetric(&out->parameters.mlkemDetail.symmetric, + &in->parameters.mlkemDetail.symmetric); + out->parameters.mlkemDetail.parameterSet = + in->parameters.mlkemDetail.parameterSet; + out->unique.mlkem.size = in->unique.mlkem.size; + if (out->unique.mlkem.size > (UINT16)sizeof(out->unique.mlkem.buffer)) { + out->unique.mlkem.size = (UINT16)sizeof(out->unique.mlkem.buffer); + } + XMEMCPY(out->unique.mlkem.buffer, in->unique.mlkem.buffer, + out->unique.mlkem.size); + break; +#endif /* WOLFTPM_V185 */ default: wolfTPM2_CopySymmetric(&out->parameters.asymDetail.symmetric, &in->parameters.asymDetail.symmetric);