Add TPM 2.0 v1.85 PQC (ML-DSA and ML-KEM) and fwTPM PQC Support #445
Merged
dgarske merged 51 commits intowolfSSL:masterfrom Apr 29, 2026
Merged
Add TPM 2.0 v1.85 PQC (ML-DSA and ML-KEM) and fwTPM PQC Support #445dgarske merged 51 commits intowolfSSL:masterfrom
dgarske merged 51 commits intowolfSSL:masterfrom
Conversation
tdjCisco
reviewed
Jan 12, 2026
tdjCisco
reviewed
Jan 26, 2026
tdjCisco
reviewed
Feb 26, 2026
tdjCisco
left a comment
There was a problem hiding this comment.
Won't the command/response buffers need updating
tdjCisco
reviewed
Feb 26, 2026
tdjCisco
left a comment
There was a problem hiding this comment.
Looks like bus encryption with PQC key is missing
tdjCisco
reviewed
Feb 26, 2026
tdjCisco
left a comment
There was a problem hiding this comment.
I think updates to TPMU_PUBLIC_PARMS, new TPMS_MLDSA_PARMS and TPMS_MLKEM_PARMS, TPMU_PUBLIC_ID, TPMU_SENSITIVE_COMPOSITE, TPM2_Packet_AppendPublicParms, TPM2_Packet_AppendPublicArea, wolfTPM2_GetKeyTemplate_MLKEM and MLDSA are needed.
f326166 to
e910410
Compare
56264db to
fc1f89f
Compare
fc1f89f to
eb4df24
Compare
Per TCG TPM 2.0 Library v1.85 Part 1 §24 (p.316) and Part 2 Table 222,
v1.85 adds ML-KEM as a valid key-exchange type for encryptedSalt
(TPM2_StartAuthSession), inSymSeed (Duplicate/Import/Rewrap), and
credentialBlob (ActivateCredential/MakeCredential). The caller
encapsulates under the TPM's ML-KEM public key: the 32-byte shared
secret becomes the session salt, the ciphertext goes on the wire.
Changes:
- wolftpm/tpm2.h: add mlkem[MAX_MLKEM_CT_SIZE] arm to
TPMU_ENCRYPTED_SECRET union (gated on WOLFTPM_V185). Without this the
union sized at MAX_RSA_KEY_BYTES (512) could not hold an ML-KEM-768
ciphertext (1088 bytes) let alone ML-KEM-1024 (1568 bytes).
- src/tpm2_wrap.c: new static wolfTPM2_EncryptSecret_MLKEM helper that
inits an MlKemKey from tpmKey->pub.unique.mlkem, calls
wc_MlKemKey_Encapsulate, writes shared secret to data->buffer and
ciphertext to secret->secret. Dispatch switch in wolfTPM2_EncryptSecret
gains a TPM_ALG_MLKEM case. ML-DSA intentionally omitted — signing
keys have no encrypt operation (spec Table 222 has no mldsa arm).
- tests/unit_tests.c: test_wolfTPM2_EncryptSecret now creates a real
MLKEM-768 primary and asserts data.size == 32 and secret.size == 1088.
examples/keygen/keygen:
- New -mldsa[=44|65|87], -hash_mldsa[=44|65|87], -mlkem[=512|768|1024]
options alongside existing -rsa/-ecc/-sym/-keyedhash. Dispatches to
wolfTPM2_GetKeyTemplate_{MLDSA,HASH_MLDSA,MLKEM}, then CreateKey under
the SRK parent. AIK template path correctly rejects PQC (AIKs are
RSA/ECC only per TCG).
- Param-set parser defaults: MLDSA-65, MLKEM-768, SHA-256 pre-hash for
Hash-ML-DSA.
examples/pqc/mlkem_encap (new):
- CreatePrimary MLKEM (512/768/1024) then Encapsulate + Decapsulate,
asserting the two shared secrets match byte-for-byte. Companion to
pqc_mssim_e2e but focused on the KEM wrappers alone.
examples/run_examples.sh:
- Detects WOLFTPM_V185 from config.h, runs keygen+keyload round-trip
for all 9 PQC variants (same pattern used by RSA/ECC blocks above).
All 9 pass against fwtpm_server.
Main README:
- New ## Post-Quantum Cryptography (v1.85) section between fwTPM and
TPM 2.0 Overview. Covers supported algorithms (ML-DSA-44/65/87,
Hash-ML-DSA, ML-KEM-512/768/1024), exact wolfSSL + wolfTPM build
config (--enable-dilithium --enable-mlkem --enable-experimental ...
for wolfSSL; --enable-fwtpm --enable-v185 for wolfTPM), and a
make check pointer.
- Existing feature-list bullet at line 41 now points to the new
section instead of directly to docs/FWTPM.md.
examples/pqc/README.md:
- Rewrite around three audience splits: (1) build steps, (2) run
everything with make check, (3) per-example details.
- New sections for mlkem_encap and the -mldsa/-hash_mldsa/-mlkem
options on examples/keygen/keygen.
- Drop stale --enable-swtpm reference (wrong flag; caused reviewer
confusion).
- Point users at the existing tests/fwtpm_check.sh and
tests/pqc_mssim_e2e.sh for targeted reruns without the full classical
suite.
Documentation split (no duplication):
- Top-level README - build + I just want to run it
- examples/pqc/README.md per-example usage
- docs/FWTPM.md#tpm-20-v185-post-quantum-support -> server internals
(commands, primary-key derivation, buffer constants, spec
interpretation decisions)
FwCmd_CreateLoaded previously handled only RSA/ECC/KEYEDHASH/SYMCIPHER;
any caller issuing TPM2_CreateLoaded with an MLDSA or MLKEM template
hit the default case and got TPM_RC_TYPE. The switch now mirrors the
same three PQC arms that FwCmd_Create landed earlier (src/fwtpm/
fwtpm_command.c lines 3649-3689):
- TPM_ALG_MLDSA / TPM_ALG_HASH_MLDSA: draw a 32-byte Xi seed via
wc_RNG_GenerateBlock, expand through FwGenerateMldsaKey, write the
expanded public key to inPublic.unique.mldsa. The wire-format private
portion is the 32-byte seed.
- TPM_ALG_MLKEM: same pattern with a 64-byte d||z seed and
FwGenerateMlkemKey.
Both arms guarded on WOLFTPM_V185 to keep the non-PQC build unchanged.
Test coverage:
- new test_fwtpm_create_loaded_mldsa creates an RSA SRK then
CreateLoaded an MLDSA-65 child under it, asserts TPM_RC_SUCCESS and
a non-zero transient handle, flushes both.
- new test_fwtpm_create_loaded_mlkem is the MLKEM-768 mirror.
- new BuildCreateLoadedCmd helper reuses BuildCreatePrimaryCmd's
TPMT_PUBLIC emitter and rewrites command code + parent handle,
stripping the trailing outsideInfo + creationPCR fields that
CreatePrimary has but CreateLoaded does not (spec Part 3 §30.2).
- Note: server's FwCmd_CreateLoaded accepts only loaded-object parents,
not hierarchy handles; tests CreatePrimary first to get a usable
parent. Hierarchy-as-parent is a separate v1.38 spec feature tracked
for follow-up.
The harness was Phase-1 scaffolding: driver does JSON well-formedness only; 6 of 8 fixtures carry TBD_PHASE_* placeholders; no byte-level spec assertion. Its unique value (catching client and server both misread the spec the same wa) requires a second v1.85 implementation to compare against — none exists today, and wolfTPM's wire correctness is already covered end-to-end by: - tests/fwtpm_unit.test (88 cases, in-process) - tests/unit.test via mssim (31 wrapper cases) - tests/pqc_mssim_e2e.sh (dedicated PQC round-trip) - examples/run_examples.sh (18-way keygen+keyload matrix) - scripts/tpm2_tools_test.sh (308 tpm2-tools compatibility cases) Rebuild from scratch if/when a second v1.85 TPM ships for interop.
tests/fuzz/tpm2.dict gains:
- 8 new command codes (Encapsulate 0x1A7, Decapsulate 0x1A8,
SignSequenceStart 0x1AA, SignSequenceComplete 0x1A4,
VerifySequenceStart 0x1A9, VerifySequenceComplete 0x1A3,
SignDigest 0x1A6, VerifyDigestSignature 0x1A5)
- 3 PQC algorithm IDs (MLKEM 0x00A0, MLDSA 0x00A1, HASH_MLDSA 0x00A2)
- 6 parameter-set values (MLKEM 512/768/1024, MLDSA 44/65/87)
- 3 new response codes (RC_EXT_MU, RC_ONE_SHOT_SIGNATURE,
RC_SIGN_CONTEXT_KEY)
tests/fuzz/gen_corpus.py gains 10 PQC seed files:
- pqc_encapsulate_mlkem, pqc_decapsulate_mlkem
- pqc_signseqstart_mldsa, pqc_signseqcomplete_mldsa
- pqc_verifyseqstart_mldsa, pqc_verifyseqcomplete_mldsa
- pqc_signdigest_mldsa, pqc_verifydigestsig_mldsa
- pqc_createprimary_mlkem, pqc_createprimary_mldsa
These give libFuzzer starting shapes for the 8 new command paths rather
than relying on coverage-guided discovery from scratch.
.github/workflows/fuzz.yml matrix expanded:
- Existing 2 entries (fuzz-full, fuzz-smoke) classical-only, unchanged
flags, zero regression risk
- New fuzz-full-pqc (10-min weekly) and fuzz-smoke-pqc (60s every PR)
build wolfSSL with --enable-dilithium --enable-mlkem --enable-
experimental --enable-harden and wolfTPM with --enable-v185, use
-max_len=8192 to accommodate MLDSA-87 sigs (4627 bytes).
Closes 13 spec-conformance findings flagged by two TCG compliance reviews
of the v1.85 PQC handlers. Each fix maps to a specific Part 2 / Part 3
section; all are exercised by negative test fixtures in
tests/fwtpm_unit_tests.c that bite-verify each fix in isolation.
Spec-RC corrections (one-line each):
- Drop TPMA_ML_PARAMETER_SET_extMu from TPM_PT_ML_PARAMETER_SETS — Part 2
§12.2.3.6 (no μ-direct sign API in wolfCrypt yet).
- Sign* handlers return TPM_RC_SCHEME (not TPM_RC_KEY) for valid keys
with unsupported scheme — Part 3 §17.5.1 / §20.7.1.
- SignDigest / VerifyDigestSignature return TPM_RC_ATTRIBUTES (not
TPM_RC_EXT_MU) when key's allowExternalMu=NO — EXT_MU is reserved for
capability errors, ATTRIBUTES for key-attribute errors.
Validation additions:
- SignDigest rejects restricted and x509sign keys at entry
(TPM_RC_ATTRIBUTES, Part 3 §20.7.1).
- VerifyDigestSignature enforces sigHashAlg == key.hashAlg
(TPM_RC_SCHEME, Part 3 §20.4.1) and digest size == hashAlg digest size
(TPM_RC_SIZE).
- CreatePrimary / Create / CreateLoaded / TestParms reject MLDSA with
allowExternalMu=YES at object-creation time (TPM_RC_EXT_MU, Part 2
§12.2.3.6) instead of letting the request succeed and fail later.
- TestParms validates ML-DSA / Hash-ML-DSA / ML-KEM parameterSet ranges.
- SignSequenceComplete rejects restricted keys signing messages whose
first 4 bytes are TPM_GENERATED_VALUE (0xFF544347) (TPM_RC_VALUE,
Part 3 §20.6.1).
- SignSequenceComplete rejects x509sign keys (TPM_RC_ATTRIBUTES).
- TPM_RC_ONE_SHOT_SIGNATURE moved from SequenceUpdate to
SignSequenceComplete (Part 3 §20.6.1: it's a Complete-time RC about
non-empty sequence, not an Update-time RC).
Structural fixes:
- TPMA_OBJECT_x509sign (bit 19, Part 2 §8.3.3 v1.85) added to the enum
in wolftpm/tpm2.h and enforced in the two sign-side handlers.
- TPM_PT_ML_PARAMETER_SETS bits gated on wolfCrypt build symbols
(WOLFSSL_HAVE_MLKEM / KYBER_*, WOLFSSL_WC_DILITHIUM / HAVE_DILITHIUM)
so the capability matches what the build actually delivers.
- wolfTPM2_EncryptSecret_MLKEM applies KDFa(SECRET, ct, pub) over the
ML-KEM shared secret per Part 1 §47.4 Eq 66 (Labeled-KEM); previous
code emitted raw K as the salt.
- VerifySequenceComplete and VerifyDigestSignature emit hierarchy-bound
TPMT_TK_VERIFIED tickets (Part 2 §10.6.5) instead of the
TPM_RH_NULL + empty-HMAC stub.
Tests (tests/fwtpm_unit_tests.c, 8 new + 4 updated, all under fwtpm_unit):
- 8 new spec-bite negatives, one per finding where a single-RC test is
meaningful. Each was bite-verified by reverting its source fix and
confirming the test fails with a different RC.
- 4 existing negatives updated to assert the new spec-mandated RCs and
reshaped where the rejection point moved (SequenceUpdate → Complete).
CI:
- New .github/workflows/pqc-examples.yml: builds + smoke-runs the v1.85
examples + invokes tests/check_doc_constants.sh on each PR.
- tests/check_doc_constants.sh greps every FWTPM_* size/seed/digest
constant from wolftpm/fwtpm/fwtpm.h and asserts each is mentioned in
docs/FWTPM.md (catches doc drift like the v1.85
FWTPM_MAX_COMMAND_SIZE 4096→8192 bump). docs/FWTPM.md gains 6 missing
entries (CMD_AUTHS, SENSITIVE_SIZE, SIGN_SEQ, SYM_KEY_SIZE,
HMAC_KEY_SIZE, HMAC_DIGEST_SIZE) so the check passes clean.
- fuzz.yml / fwtpm-test.yml / make-test-swtpm.yml / sanitizer.yml each
gain a v1.85 matrix entry running the same checks under
--enable-v185 + wolfSSL --enable-dilithium --enable-mlkem
--enable-experimental.
Closes 13 findings across two reviews of the v1.85 PQC paths.
Tickets (TPMT_TK_VERIFIED / TPMT_TK_HASHCHECK / TPMT_TK_CREATION):
- FwAppendTicket binds tag (always) and metadata (DIGEST_VERIFIED only)
into the HMAC per Part 2 §10.6.5 Eq (5). Streamed via chunked
wc_HmacUpdate, no temp buffer. All 5 callers updated; the hand-rolled
VerifyDigestSignature path collapses into FwAppendTicket.
- FWTPM_Object gains a hierarchy field, captured at every load/create
site (CreatePrimary, Load, LoadExternal, CreateLoaded). Replaces
hardcoded TPM_RH_OWNER in VerifySignature, VerifySequenceComplete,
VerifyDigestSignature, ContextSave, and Create's creation ticket.
- VerifySequenceComplete snapshots the verified digest before
wc_HashFinal so Hash-ML-DSA tickets bind (digest || keyName) rather
than just keyName — pre-fix, two distinct verified digests on the
same key produced byte-identical tickets (universal reuse).
Authorization:
- Sign/VerifySequenceStart split TPM_RC_KEY (non-signing key, e.g.
ML-KEM) from TPM_RC_SCHEME (signing key, scheme unsupported) using
TPMA_OBJECT_sign per Part 3 §17.5.1 / §17.6.1.
- SignDigest restricted-key path validates TPMT_TK_HASHCHECK HMAC
per Part 3 §20.7.1 instead of blanket-rejecting; x509sign keeps
the TPM_RC_ATTRIBUTES short-circuit.
- Decapsulate, SignDigest, SignSequenceComplete reject NO_SESSIONS
with TPM_RC_AUTH_MISSING (Auth Role: USER, Tables 62/124/126).
Restricted-key TPM_GENERATED_VALUE check:
- FWTPM_SignSeq.firstBytes[4] populated by SequenceUpdate covers the
Hash-ML-DSA path where bytes are otherwise consumed by hashCtx;
topped-up from the Complete trailing buffer for Pure-MLDSA one-shot.
Closes the Update-then-empty-Complete bypass.
Client-side (src/tpm2.c):
- TPM2_VerifySequenceComplete defensively dispatches on validation.tag
for TPMU_TK_VERIFIED_META, mirroring TPM2_VerifyDigestSignature.
Other:
- TPM2_Packet_AppendSensitive caps mldsa/mlkem .size to buffer length.
- pqc_mssim_e2e.c zeroizes ss1/ss2 on cleanup.
- Untrack examples/pqc/pqc_mssim_e2e (libtool wrapper with hardcoded
/home/aidangarske path; .gitignore already covered it).
- #pragma message at WOLFTPM_V185 build-time flagging that the PQC
primary-key KDFa labels are interpretation pending TCG Part 4 v1.85;
suppressible via -DWOLFTPM_V185_LABELS_ACK.
Tests: 11 new fixtures in tests/fwtpm_unit_tests.c, 4 existing tests
updated to assert new spec-mandated RCs. fwtpm_unit.test reports 105
passing, zero failures.
Fixes 5 v1.85 PR CI/build issues:
1. src/tpm2_wrap.c: add #include <wolfssl/wolfcrypt/mlkem.h> inside the
v185 MLKEM guard. Builds with --disable-fwtpm against wolfSSL with
--enable-mlkem failed because the MLKEM symbol declarations were
only pulled in transitively by src/fwtpm/fwtpm_crypto.c.
2. src/fwtpm/fwtpm_command.c: switch FWTPM_ALLOC_BUF(privKeyDer) to
FWTPM_CALLOC_BUF in 4 sites (Create, Load, LoadExternal, Import,
CreateLoaded). MSan-v185 flagged uninit-value reads in SocketSend
originating from FwCmd_Create's keyedHash branch — when caller
supplies undersized inSensitive material, FwComputeUniqueHash hashed
beyond what was written. Zero-initialising the buffer eliminates the
class of issue.
3. examples/keygen/keygen.c: pass allowExternalMu=NO for MLDSA. The
v1.85 EXT_MU enforcement now correctly rejects allowExternalMu=YES
at object creation per Part 2 §12.2.3.6.
4. .github/workflows/make-test-swtpm.yml: convert v185-pqc-swtpm lane
to build-only. swtpm has no v1.85 PQC, so unit.test PQC blocks fail
on TPM_RC_SIZE; runtime PQC coverage stays in the fwtpm-v185 lane.
5. .github/workflows/sanitizer.yml: UBSan-v185 now uses the same
sanitizer flags as the classical UBSan lane (drops ).
Pre-existing wolfSSL UB at misc.c:117 (440<<24 in Hash_df) only
surfaces under -fsanitize=integer.
Code quality / defensive fixes:
- TPM2_Encap/Decap: drop bare scope braces, hoist wireSize locals
- FwCmd_SequenceUpdate: clarify Pure ML-DSA sign accumulation comment
- FwAllocSignSeq: _Static_assert transient slot range stays valid
- keygen: drop unused hashMldsaHash local, pass TPM_ALG_SHA256 directly
- FwCmd_Encapsulate: skip auth area when cmdTag == TPM_ST_SESSIONS
- writeKeyBlob: restore no-op TPM_RC_SUCCESS in NO_FILESYSTEM build
- FwCmd_SignDigest restricted-key ticket compare: TPM2_ConstantCompare
- FwCmd_VerifySequenceComplete: hard-fail if ticket data binding lost
(no silent fallback that emits a weakened ticket)
- wolfTPM2_VerifySequenceComplete: validate sigSz before SequenceUpdate
so BUFFER_E does not leak the TPM-side sequence handle
- FwCmd_VerifySequenceComplete: heap-allocate ~1KB ticketData via
FWTPM_DECLARE_BUF / FWTPM_ALLOC_BUF (matches sibling buffers)
v1.85 capability + scope:
- GetCapability: report TPM_PT_FIRMWARE_SVN/MAX_SVN = 0
- Allow Pure ML-DSA streaming via SequenceUpdate per FIPS 204 (SHAKE256
absorbing is incremental); SignSequenceComplete concatenates msgBuf
with the trailing complete-time buffer and signs the full message
- Document v1.85 scope: Encap/Decap is ML-KEM only; Sign/VerifySequence
and SignDigest/VerifyDigestSignature are ML-DSA / Hash-ML-DSA only
(classical schemes still go via TPM2_Sign / TPM2_VerifySignature)
TCG ticket wire-format fixes (security):
- TPMT_TK_HASHCHECK: SignDigest now validates tag = TPM_ST_HASHCHECK
unconditionally (TPM_RC_TAG); wolfTPM2_SignDigest wrapper synthesizes
the NULL Hashcheck instead of sending tag=0/hierarchy=0 from XMEMSET
- NULL Verified Tickets: FwAppendTicket no longer appends metadata
bytes when hierarchy == TPM_RH_NULL; client parser conditions
metaAlg consumption on hierarchy != TPM_RH_NULL (Part 2 §10.6.5)
Embedded RAM auto-shrink (v1.85):
- New FWTPM_MAX_MLDSA_{SIG,PUB}_SIZE / FWTPM_MAX_MLKEM_{CT,PUB}_SIZE
resolve to the largest enabled parameter set via wolfCrypt's
WOLFSSL_NO_ML_DSA_{44,65,87} / WOLFSSL_NO_KYBER{512,768,1024} gates
- FWTPM_MAX_DER_SIG_BUF, FWTPM_MAX_PUB_BUF, FWTPM_MAX_KEM_CT_BUF
derive from those (no per-board override needed)
- FWTPM_MAX_COMMAND_SIZE / FWTPM_TIS_FIFO_SIZE only lift to 8192 when
MLDSA-65 or MLDSA-87 is enabled; MLDSA-44-only and MLKEM-only
v1.85 builds stay at 4096
- docs/FWTPM.md: per-build size table + override + small-stack notes
Test coverage:
- examples/run_examples.sh: invoke pqc/mldsa_sign and pqc/mlkem_encap
inside the v1.85 block
- tests/fwtpm_unit_tests.c:
* SignDigest with malformed HASHCHECK tag rejected (TPM_RC_TAG)
* FwAppendTicket NULL DIGEST_VERIFIED emits no metadata
* SignSeqComplete Pure-MLDSA streaming (FIPS 204 §6) — replaces
obsolete one-shot rejection assertion
- tests/unit_tests.c:
* Hash-ML-DSA SignSeqUpdate streaming end-to-end + arg validation
* TPMT_SIGNATURE round-trip for ML-DSA / Hash-ML-DSA arms
* TPM2B_PUBLIC round-trip for ML-DSA / Hash-ML-DSA / ML-KEM arms
Build / portability:
- Drop #pragma message in fwtpm_crypto.c (MSVC-incompatible)
- Replace non-ASCII section sign with Sec. across all sources/docs
Configure:
- Add --enable-pqc alias for --enable-v185 (same WOLFTPM_V185 macro)
- Auto-detect: when --enable-fwtpm + wolfCrypt has dilithium.h+mlkem.h
and neither flag is set, configure auto-enables PQC; --disable-pqc
opts out
- Both flags probe the wolfSSL PQC headers and fail at configure time
with a clear hint when missing
Spec / security hardening:
- VerifySequenceComplete now emits TPM_ST_DIGEST_VERIFIED (with hashAlg
metadata) for Hash-ML-DSA tickets, MESSAGE_VERIFIED for Pure ML-DSA
(was mis-tagging digests as messages, breaking PolicyTicket consumers)
- Sign/VerifySequenceComplete: free the slot on TPM_RC_SIGN_CONTEXT_KEY
too, so wrong-key Complete cannot exhaust FWTPM_MAX_SIGN_SEQ slots
(CWE-772 DoS)
- TestParms PQC arms return TPM_RC_PARMS (spec-correct) instead of
TPM_RC_VALUE; reject MLDSA/MLKEM parameter sets not actually compiled
in; parse TPMS_MLKEM_PARMS.symmetric via TPM2_Packet_ParseSymmetric
- GetCapability TPMA_ML_PARAMETER_SETS gates each MLDSA/MLKEM bit on the
per-set wolfCrypt availability macro (subset builds advertise truth)
- TPM2_VerifySignature client parser now defensive: only consume the
v1.85 metaAlg when tag==DIGEST_VERIFIED && hierarchy!=RH_NULL
- VerifyDigestSignature: hard-fail on keyName overflow instead of
silently emitting a ticket missing the name binding
- TPM_GENERATED_VALUE prefix check guarded with rc==0
- Drop dead (void)cmdSize casts in Sign/VerifySequenceStart
- wolfTPM2_EncryptSecret_MLKEM: track wc_InitRng_ex/wc_MlKemKey_Init
success flags so Free is only called on initialized state
- UBSan-v185 sanitizer cflags: explicitly disable signed-integer-overflow
and shift checks (matches the comment about wolfSSL Hash_df 440<<24)
Embedded RAM:
- FWTPM_NV_PUBAREA_EST derives from FWTPM_MAX_MLDSA_PUB_SIZE /
FWTPM_MAX_MLKEM_PUB_SIZE auto-shrink macros (subset builds save NV)
- tpm2_types.h MAX_MLDSA_*/MAX_MLKEM_* stay at worst-case (ABI floor
for TPM2B wire buffers) with comment
Tests:
- Negative test for Hash-MLDSA VerifySeqComplete ticket tag
- Negative test exposing sign-seq slot leak on TPM_RC_SIGN_CONTEXT_KEY
- Roundtrip test for wolfTPM2_SignDigest + VerifyDigestSignature
Documentation:
- README, FWTPM.md, fwtpm/README.md, examples/pqc/README.md mention
both --enable-pqc and --enable-v185 + auto-detect
- README wolfSSL line: --enable-pkcallbacks + WC_RSA_NO_PADDING
- fwtpm/README.md: drop FWTPM_SPEC_* labels (macros never existed),
remove v1.85 Additions table (all 8 commands implemented), update
coverage table to 137/113/24 (82%); note remaining gaps are
inherited v1.59/v1.84 commands, not PQC
- fwtpm_nv.h:52: clarify 2592 vs 2720 math (PQC pub key + header slack)
…support
Bring TPM 2.0 v1.85 fwTPM implementation to full TCG compliance for the
sign/verify, KEM, and key-exchange paths. This unblocks interop with the
TCG simulator and removes four scope gaps surfaced by Skoll TCG review.
TCG Phase B fixes (4 of 4):
- TCG-HIGH-3: TPM2_VerifySequenceComplete now binds the streamed message
bytes in the TPMT_TK_VERIFIED ticket per Part 2 Sec.10.6.5 Eq (5).
Hash-ML-DSA SequenceUpdate mirrors bytes into seq->msgBuf alongside
the hash accumulator, so the ticket builder has the material at
Complete time. Pure ML-DSA already had this; both paths now share one
ticket-data construction.
- TCG-HIGH-2: ECC DHKEM Encapsulate/Decapsulate per RFC 9180 Sec.4.1 +
Part 2 Sec.12.2.3.5. Added FwEncapsulateEcdhDhkem/FwDecapsulateEcdhDhkem
helpers in fwtpm_crypto.c implementing LabeledExtract/LabeledExpand over
HKDF. Curve-hash pairings: P-256/SHA256 (kem_id 0x0010), P-384/SHA384
(0x0011), P-521/SHA512 (0x0012). Wire ciphertext is SEC1-uncompressed
ephemeral public key. Added TPM_ALG_HKDF=0x001F to wolftpm/tpm2.h.
- TCG-HIGH-4: ML-KEM Labeled KEM in FwEncryptSeed/FwDecryptSeed per
Part 1 Sec.47.4 Eq.66:
seed = KDFa(nameAlg, K, label, ciphertext, publicKey, bits)
This unlocks ML-KEM as TPM2_StartAuthSession salt key, TPM2_Import
newParent, TPM2_MakeCredential issuer, etc.
- TCG-HIGH-1: Classical RSA/ECC sign+verify in v1.85 sequence/digest
commands. Extended FwCmd_SignDigest, FwCmd_VerifyDigestSignature,
FwCmd_SignSequenceStart/Complete, FwCmd_VerifySequenceStart/Complete,
and the SequenceUpdate dispatch to accept TPM_ALG_RSA/TPM_ALG_ECC keys.
Reuses existing FwSignDigestAndAppend + FwVerifySignatureCore helpers;
signature scheme/hashAlg resolved from the key's metadata.
Earlier review-cycle fixes also included in this commit:
- src/tpm2.c: TPM2_VerifySequenceComplete/VerifyDigestSignature use
TPM2_Packet_ParseU16Buf for atomic clamp+skip of the validation digest
field. TPM2_Encapsulate CmdInfo flags switched to CMD_FLAG_DEC2 (the
protected value is the first response param, not a command param).
- src/tpm2_wrap.c: wolfTPM2_VerifySequenceComplete moved per-key-type
sigSz validation ahead of the internal SequenceUpdate to avoid leaking
the sequence handle on BUFFER_E (CWE-772). Added ECC arm. Added defensive
ForceZero in wolfTPM2_EncryptSecret_MLKEM on the failure path so partial
KDFa output cannot leak via callers that ignore rc.
- src/tpm2_packet.c: comment fix MAX_MLDSA_KEY_BYTES -> MAX_MLDSA_PRIV_SEED_SIZE.
- wolftpm/tpm2.h: TPM_RC_PARMS moved out of WOLFTPM_V185 guard (it has
been part of TCG Part 2 since v1.16, not v1.85 — Windows CI fix).
- tests/unit_tests.c: align tab-formatted Test TPM Wrapper output lines
to the same column as the rest.
Tests:
- 6 new fwtpm_unit tests (Hash-MLDSA ticket binds message, ECC DHKEM
roundtrip, ML-KEM seed roundtrip, ECDSA SignDigest+VerifyDigestSignature
roundtrip, ECDSA SignSequence+VerifySequence roundtrip).
- 1 existing negative test updated: VerifyDigestSignature with RSASSA on
ML-DSA key now returns TPM_RC_KEY (key-type mismatch) instead of
TPM_RC_SCHEME (which previously meant scheme unsupported).
Validation: tests/fwtpm_unit.test 117/0; tests/fwtpm_check.sh 2/0/1 (incl.
308/0 tpm2-tools compat against fwtpm_server); make check FAIL: 0.
bce749c to
d518bff
Compare
dgarske
approved these changes
Apr 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR adds TPM 2.0 Library Specification v1.85 PQC support to wolfTPM, covering both client-side wrappers and an in-tree fwTPM server implementation that is the first open-source PQC-capable TPM 2.0 built from spec.
Implements ML-DSA (FIPS 204) for signing, ML-KEM (FIPS 203) for key encapsulation, and Hash-ML-DSA pre-hash variant. All three parameter sets per algorithm: ML-DSA-{44, 65, 87} and ML-KEM-{512, 768, 1024}.
Spec basis: TCG TPM 2.0 Library Specification v185 rc4, TCG Algorithm Registry v2.0 rc2, FIPS 203, FIPS 204.
New TPM 2.0 v1.85 Commands
8 new commands implemented end-to-end (client wrapper + marshaling + fwTPM server handler):
TPM2_Encapsulate0x1A7TPM2_Decapsulate0x1A8TPM2_SignSequenceStart0x1AATPM2_VerifySequenceStart0x1A9TPM2_SignSequenceComplete0x1A4TPM2_VerifySequenceComplete0x1A3TPM_ST_MESSAGE_VERIFIEDticketTPM2_SignDigest0x1A6TPM2_VerifyDigestSignature0x1A5TPM_ST_DIGEST_VERIFIEDticketNew Types, Enums, and Structures
TPM_ALG_MLKEM(0x00A0),TPM_ALG_MLDSA(0x00A1),TPM_ALG_HASH_MLDSA(0x00A2)TPM_MLKEM_{512,768,1024},TPM_MLDSA_{44,65,87}TPM_ST_MESSAGE_VERIFIED(0x8026),TPM_ST_DIGEST_VERIFIED(0x8027)TPM_RC_EXT_MU(RC_FMT1+0x02B),TPM_RC_ONE_SHOT_SIGNATURE(RC_FMT1+0x02C),TPM_RC_SIGN_CONTEXT_KEY(RC_FMT1+0x02D)TPM2B_SIGNATURE_CTX,TPM2B_SIGNATURE_HINT,TPM2B_KEM_CIPHERTEXT,TPM2B_SHARED_SECRET,TPM2B_PUBLIC_KEY_MLDSA/MLKEM,TPM2B_PRIVATE_KEY_MLDSA/MLKEMTPMS_MLDSA_PARMS,TPMS_HASH_MLDSA_PARMS,TPMS_MLKEM_PARMSTPMU_PUBLIC_PARMS,TPMU_PUBLIC_ID,TPMU_SENSITIVE_COMPOSITE,TPMU_SIGNATURE,TPMU_ENCRYPTED_SECRET(newmlkemarm per v185 Table 222),TPMU_TK_VERIFIED_METATPMA_ML_PARAMETER_SETbitfield,TPM_PT_ML_PARAMETER_SETSproperty, wired intoTPM_CAP_ALGS/TPM_CAP_COMMANDS/TPM_CAP_TPM_PROPERTIESTPMT_TK_VERIFIEDgains[tag]metadata(TPM_ALG_ID for digest-verified),digest→hmacrename per v185MAX_MLDSA_PUB_SIZE=2592,MAX_MLDSA_SIG_SIZE=4627,MAX_MLKEM_PUB_SIZE=1568, seed sizesfwTPM Server PQC Support
All 8 new commands implemented in the in-tree fwTPM server (
src/fwtpm/):"MLDSA","HASH_MLDSA","MLKEM")FwCmd_Create,FwCmd_CreatePrimary,FwCmd_CreateLoadedall gained MLDSA/HASH_MLDSA/MLKEM armsSequenceStart/SequenceUpdate/SequenceCompletewith Pure-MLDSA one-shot enforcement per §17.5 (returnsTPM_RC_ONE_SHOT_SIGNATUREonSequenceUpdate)FWTPM_MAX_COMMAND_SIZE,FWTPM_MAX_PUB_BUF,FWTPM_MAX_DER_SIG_BUF) conditionally lifted underWOLFTPM_V185so classical ABI stays stable when the flag is offClient-Side Wrappers and Examples
tpm2_wrap.c:wolfTPM2_Encapsulate,wolfTPM2_Decapsulate,wolfTPM2_SignSequenceStart/Update/Complete,wolfTPM2_VerifySequenceStart/Update/Complete,wolfTPM2_SignDigest,wolfTPM2_VerifyDigestSignature,wolfTPM2_GetKeyTemplate_{MLDSA, HASH_MLDSA, MLKEM},wolfTPM2_EncryptSecret_MLKEMexamples/pqc/mldsa_sign.c—-mldsa=44|65|87, full sign-sequence + verify-sequence round-trip against fwTPMexamples/pqc/mlkem_encap.c—-mlkem=512|768|1024, encap/decap shared-secret round-tripexamples/keygenextended with-mldsa,-hash_mldsa,-mlkemoptions (keygen + keyload round-trip validated across all 18 parameter combinations in CI)Testing
Unit tests (
make check)TPM_PT_ML_PARAMETER_SETScapability reporting, NV persistence round-trips, MLDSA-87 + MLKEM-1024 max-buffer pressure, Hash-ML-DSA sequences for all three parameter sets, sign-determinism KATs against pinned NIST vectors, slot exhaustion, long-message accumulation boundarytest_wolfTPM2_MLDSA_SignSequence,test_wolfTPM2_MLDSA_VerifySequence,test_wolfTPM2_MLKEM_Encapsulate,test_wolfTPM2_MLKEM_Decapsulate,test_wolfTPM2_MLKEM_RoundTrip,test_wolfTPM2_PQC_KeyTemplates,test_wolfTPM2_PQC_Sizestests/pqc_mssim_e2e.sh) — launchesfwtpm_serversubprocess, points wolfTPM2 client wrappers at it, exercises MLDSA Hash-sequence sign/verify and MLKEM encap/decapexamples/run_examples.sh)Fuzz CI
.github/workflows/fuzz.ymlextended with 2 new matrix entries (fuzz-full-pqc,fuzz-smoke-pqc) that exercise v1.85 command-code paths with PQC-aware dictionary + seed corpus. Classical fuzz entries unchanged.Build
Files
wolftpm/tpm2.h,wolftpm/tpm2_types.h,wolftpm/tpm2_wrap.h— PQC types, constants, wrapper declarationssrc/tpm2.c,src/tpm2_packet.c,src/tpm2_wrap.c— marshaling + wrapperssrc/fwtpm/fwtpm_command.c,src/fwtpm/fwtpm_crypto.c— fwTPM server PQC handlers + crypto helpersexamples/pqc/mldsa_sign.c,examples/pqc/mlkem_encap.c— end-to-end demosexamples/keygen/keygen.c— PQC optionstests/unit_tests.c,tests/fwtpm_unit_tests.c,tests/pqc_mssim_e2e.sh,tests/pqc_kat_vectors.h— test coverageREADME.md,docs/FWTPM.md,examples/pqc/README.md— usage docs.github/workflows/fuzz.yml,tests/fuzz/gen_corpus.py,tests/fuzz/tpm2.dict— fuzz matrix extensions