Add TPM 2.0 v1.85 PQC (ML-DSA and ML-KEM) Support#445
Add TPM 2.0 v1.85 PQC (ML-DSA and ML-KEM) Support#445aidangarske wants to merge 5 commits intowolfSSL:masterfrom
Conversation
tdjCisco
left a comment
There was a problem hiding this comment.
Won't the command/response buffers need updating
tdjCisco
left a comment
There was a problem hiding this comment.
Looks like bus encryption with PQC key is missing
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
There was a problem hiding this comment.
Pull request overview
This PR introduces initial TPM 2.0 Library Spec v1.85 Post-Quantum Cryptography (PQC) support to wolfTPM by adding ML-DSA (Dilithium) signing/verification sequence commands and ML-KEM (Kyber) encapsulation/decapsulation commands, along with the required types, constants, packet marshalling, wrappers, and unit tests.
Changes:
- Adds v1.85 PQ algorithm identifiers, parameter sets, command codes, response codes, tags, and PQC-related TPM2B/TPMS structures.
- Implements new v1.85 command marshalling/unmarshalling and wrapper APIs for ML-DSA sequences/digest signing and ML-KEM KEM operations.
- Adds an autotools
--enable-v185option and introduces unit tests for PQC templates/sizes and (currently) TPM-dependent PQC command paths.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 23 comments.
Show a summary per file
| File | Description |
|---|---|
configure.ac |
Adds --enable-v185 build flag to enable v1.85 PQC support. |
wolftpm/tpm2.h |
Introduces new v1.85 algorithms, parameter sets, command codes, RC/tag additions, and new PQC structures. |
wolftpm/tpm2_types.h |
Adds PQC sizing constants used by the new types and APIs. |
wolftpm/tpm2_wrap.h |
Declares new v1.85 wrapper APIs and key-template helpers with Doxygen docs. |
src/tpm2.c |
Implements low-level marshalling for new v1.85 PQC commands and adds alg-name strings. |
src/tpm2_packet.c |
Extends packet serialization/parsing for PQ public parms/unique/sensitive and ML-DSA signatures. |
src/tpm2_wrap.c |
Adds wrapper functions for new v1.85 commands and key-template helper implementations. |
tests/unit_tests.c |
Adds PQC template/size tests and TPM-dependent PQC command tests (intended to skip on unsupported TPMs). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #445
Scan targets checked: wolftpm-bugs, wolftpm-src
Findings: 9
High (3)
Wrong key type check for ML-DSA in wolfTPM2_VerifySequenceComplete
File: src/tpm2_wrap.c:4700-4730
Function: wolfTPM2_VerifySequenceComplete
Category: Logic errors
In the #ifdef WOLFTPM_V185 else branch (for key types other than ECC/RSA), the code checks key->pub.publicArea.type == TPM_ALG_KEYEDHASH to detect ML-DSA keys. However, ML-DSA keys have type == TPM_ALG_MLDSA or TPM_ALG_HASH_MLDSA, not TPM_ALG_KEYEDHASH. This means scheme always remains TPM_ALG_NULL, the scheme-based detection always fails, and the code falls through to the unreliable size-based heuristic (sigSz >= 2000 && sigSz <= 5000). The same incorrect logic is copy-pasted into wolfTPM2_VerifyDigestSignature.
if (key->pub.publicArea.type == TPM_ALG_KEYEDHASH) {
/* KEYEDHASH keys may have ML-DSA scheme */
/* The scheme is in keyedHashDetail.scheme.scheme */
scheme = key->pub.publicArea.parameters.keyedHashDetail.scheme.scheme;
}
/* Check if it's an ML-DSA algorithm from key scheme */
if (scheme == TPM_ALG_MLDSA || scheme == TPM_ALG_HASH_MLDSA) {Recommendation: Check for the actual ML-DSA key types directly: if (key->pub.publicArea.type == TPM_ALG_MLDSA) { signature.sigAlg = TPM_ALG_MLDSA; ... } else if (key->pub.publicArea.type == TPM_ALG_HASH_MLDSA) { signature.sigAlg = TPM_ALG_HASH_MLDSA; ... }. This should replace both the KEYEDHASH check and the size-based fallback heuristic.
Missing bounds check on ML-DSA signature size in TPM2_Packet_ParseSignature
File: src/tpm2_packet.c:1003-1008
Function: TPM2_Packet_ParseSignature
Category: Buffer overflows
When parsing an ML-DSA signature from a TPM response, sig->signature.mldsa.signature.size is read directly from the packet and used as the length for TPM2_Packet_ParseBytes without any bounds check against the buffer capacity. The TPMS_SIGNATURE_ML_DSA struct uses TPM2B_MAX_BUFFER for the signature field, which has a buffer of MAX_DIGEST_BUFFER bytes. A malformed or malicious TPM response with an oversized size field would cause a buffer overflow. Compare with the new TPM2_Packet_ParsePublic code which correctly clamps mldsa.size against MAX_MLDSA_PUB_SIZE and mlkem.size against MAX_MLKEM_PUB_SIZE.
case TPM_ALG_MLDSA:
case TPM_ALG_HASH_MLDSA:
TPM2_Packet_ParseU16(packet, &sig->signature.mldsa.hash);
TPM2_Packet_ParseU16(packet, &sig->signature.mldsa.signature.size);
TPM2_Packet_ParseBytes(packet, sig->signature.mldsa.signature.buffer,
sig->signature.mldsa.signature.size);
break;Recommendation: Add a bounds check before TPM2_Packet_ParseBytes, clamping sig->signature.mldsa.signature.size to the buffer capacity (e.g., MAX_DIGEST_BUFFER or the actual buffer size of TPM2B_MAX_BUFFER). Follow the same pattern used in TPM2_Packet_ParsePublic for ML-DSA/ML-KEM public keys.
Missing bounds checks on TPM2_Encapsulate and TPM2_Decapsulate response sizes
File: src/tpm2.c:3525-3613
Function: TPM2_Encapsulate / TPM2_Decapsulate
Category: Buffer overflows
In TPM2_Encapsulate, the response fields out->sharedSecret.size and out->ciphertext.size are parsed from the TPM response via TPM2_Packet_ParseU16 and immediately used as the byte count for TPM2_Packet_ParseBytes without validating against MAX_SHARED_SECRET_SIZE (64) and MAX_KEM_CIPHERTEXT_SIZE (2048). Similarly, in TPM2_Decapsulate, out->sharedSecret.size is used without bounds validation. A malformed TPM response with a size field larger than the buffer would overflow the fixed-size TPM2B_SHARED_SECRET or TPM2B_KEM_CIPHERTEXT structures.
/* TPM2_Encapsulate */
TPM2_Packet_ParseU16(&packet, &out->sharedSecret.size);
TPM2_Packet_ParseBytes(&packet, out->sharedSecret.buffer,
out->sharedSecret.size);
TPM2_Packet_ParseU16(&packet, &out->ciphertext.size);
TPM2_Packet_ParseBytes(&packet, out->ciphertext.buffer,
out->ciphertext.size);Recommendation: Add bounds checks after each TPM2_Packet_ParseU16 call: clamp out->sharedSecret.size to MAX_SHARED_SECRET_SIZE and out->ciphertext.size to MAX_KEM_CIPHERTEXT_SIZE before calling TPM2_Packet_ParseBytes, following the same clamping pattern used in TPM2_Packet_ParsePublic.
Medium (5)
Missing bounds check in TPM2_Packet_ParseSignature for ML-DSA signatures
File: src/tpm2_packet.c:1000-1009
Function: TPM2_Packet_ParseSignature
Category: Incorrect sizeof/type usage
The ML-DSA case in TPM2_Packet_ParseSignature parses sig->signature.mldsa.signature.size from the packet and immediately uses it as the length for TPM2_Packet_ParseBytes without validating that it fits in the destination buffer. The signature field is TPM2B_MAX_BUFFER which has a buffer of MAX_DIGEST_BUFFER bytes (typically 1024). ML-DSA-87 signatures are 4627 bytes, which would overflow this buffer. By contrast, TPM2_Packet_ParsePublic correctly bounds-checks ML-DSA and ML-KEM public key sizes before parsing bytes (e.g., if (pub->publicArea.unique.mldsa.size > MAX_MLDSA_PUB_SIZE)).
case TPM_ALG_MLDSA:
case TPM_ALG_HASH_MLDSA:
TPM2_Packet_ParseU16(packet, &sig->signature.mldsa.hash);
TPM2_Packet_ParseU16(packet, &sig->signature.mldsa.signature.size);
TPM2_Packet_ParseBytes(packet, sig->signature.mldsa.signature.buffer,
sig->signature.mldsa.signature.size);
break;Recommendation: Add a bounds check after parsing the size, similar to the public key parsing pattern: if (sig->signature.mldsa.signature.size > MAX_DIGEST_BUFFER) { sig->signature.mldsa.signature.size = MAX_DIGEST_BUFFER; }. Additionally, consider whether TPM2B_MAX_BUFFER (typically 1024 bytes) is the correct type for TPMS_SIGNATURE_ML_DSA.signature given that ML-DSA signatures can be up to 4627 bytes. A dedicated type with MAX_MLDSA_SIG_SIZE capacity may be needed.
Same wrong key type check copy-pasted into wolfTPM2_VerifyDigestSignature
File: src/tpm2_wrap.c:4870-4900
Function: wolfTPM2_VerifyDigestSignature
Category: Copy-paste errors
The wolfTPM2_VerifyDigestSignature function contains the same incorrect TPM_ALG_KEYEDHASH check as wolfTPM2_VerifySequenceComplete. For ML-DSA keys where key->pub.publicArea.type == TPM_ALG_MLDSA, the code enters the else branch, checks for TPM_ALG_KEYEDHASH (which is false), leaves scheme as TPM_ALG_NULL, and falls through to the fragile signature-size heuristic. This is a copy-paste of the same logic error from wolfTPM2_VerifySequenceComplete.
/* Try to get scheme from key if available */
if (key->pub.publicArea.type == TPM_ALG_KEYEDHASH) {
/* KEYEDHASH keys may have ML-DSA scheme */
/* The scheme is in keyedHashDetail.scheme.scheme */
scheme = key->pub.publicArea.parameters.keyedHashDetail.scheme.scheme;
}Recommendation: Apply the same fix as wolfTPM2_VerifySequenceComplete: check for TPM_ALG_MLDSA and TPM_ALG_HASH_MLDSA key types directly instead of TPM_ALG_KEYEDHASH.
Missing bounds check on validation.digest.size in VerifySequenceComplete and VerifyDigestSignature
File: src/tpm2.c:3395-3413
Function: TPM2_VerifySequenceComplete / TPM2_VerifyDigestSignature
Category: Buffer overflows
In both TPM2_VerifySequenceComplete and TPM2_VerifyDigestSignature, the response parsing reads out->validation.digest.size from the TPM response and uses it directly as the length for TPM2_Packet_ParseBytes into out->validation.digest.buffer. The TPMT_TK_VERIFIED.digest field is a TPM2B_DIGEST with a fixed-size buffer. No bounds check is performed to ensure the parsed size does not exceed the buffer capacity.
TPM2_Packet_ParseU16(&packet, &out->validation.digest.size);
TPM2_Packet_ParseBytes(&packet,
out->validation.digest.buffer,
out->validation.digest.size);Recommendation: Add a bounds check after parsing validation.digest.size, clamping it to the maximum digest buffer size before calling TPM2_Packet_ParseBytes.
Missing ForceZero on shared secret material in Encapsulate/Decapsulate wrappers
File: src/tpm2_wrap.c:4880-4970
Function: wolfTPM2_Encapsulate / wolfTPM2_Decapsulate
Category: Missing ForceZero
In wolfTPM2_Encapsulate, the encapsulateOut structure containing the shared secret (TPM2B_SHARED_SECRET) is left on the stack without being zeroed after the secret is copied to the caller's buffer. Similarly, in wolfTPM2_Decapsulate, decapsulateOut.sharedSecret is not zeroed. KEM shared secrets are high-value cryptographic material, and leaving them in stack memory could allow recovery through memory disclosure vulnerabilities or cold-boot attacks.
/* wolfTPM2_Encapsulate - after XMEMCPY to caller buffer */
XMEMCPY(sharedSecret, encapsulateOut.sharedSecret.buffer, ...);
*sharedSecretSz = encapsulateOut.sharedSecret.size;
/* No ForceZero(encapsulateOut.sharedSecret.buffer, ...) */
return rc;Recommendation: Add ForceZero(&encapsulateOut, sizeof(encapsulateOut)) before returning from wolfTPM2_Encapsulate, and ForceZero(&decapsulateOut, sizeof(decapsulateOut)) before returning from wolfTPM2_Decapsulate. This follows the wolfSSL convention for clearing sensitive material from stack.
Fragile signature algorithm detection by size heuristic
File: src/tpm2_wrap.c:4700-4730
Function: wolfTPM2_VerifySequenceComplete / wolfTPM2_VerifyDigestSignature
Category: Parameter marshalling errors
In both wolfTPM2_VerifySequenceComplete and wolfTPM2_VerifyDigestSignature, when the key type is not ECC, RSA, or a recognizable ML-DSA scheme, the code falls back to guessing the algorithm based on signature size: if (sigSz >= 2000 && sigSz <= 5000) assumes ML-DSA. This heuristic could misidentify non-ML-DSA signatures (e.g., large RSA-4096 signatures are 512 bytes, but future algorithms or padded signatures in the 2000-5000 range would be misclassified). Furthermore, the key type check uses TPM_ALG_KEYEDHASH to detect ML-DSA keys, but ML-DSA keys should have TPM_ALG_MLDSA or TPM_ALG_HASH_MLDSA as their type, not TPM_ALG_KEYEDHASH.
else if (sigSz >= 2000 && sigSz <= 5000) {
/* Likely ML-DSA signature based on size */
signature.sigAlg = TPM_ALG_MLDSA;
signature.signature.mldsa.hash = TPM_ALG_SHA3_256;Recommendation: Check key->pub.publicArea.type directly against TPM_ALG_MLDSA and TPM_ALG_HASH_MLDSA instead of using TPM_ALG_KEYEDHASH. Remove the size-based heuristic fallback and return BAD_FUNC_ARG for unknown key types. The algorithm should be determined deterministically from key metadata, not guessed from signature length.
Low (1)
Negative contextSz passed to XMEMCPY without check in wrapper functions
File: src/tpm2_wrap.c:4460-4480
Function: wolfTPM2_SignSequenceStart / wolfTPM2_VerifySequenceStart
Category: Buffer overflows
In wolfTPM2_SignSequenceStart and wolfTPM2_VerifySequenceStart, contextSz is checked against the upper bound (sizeof(buffer)) but there is no check for negative values before contextSz is cast to UINT16. While the if (context != NULL && contextSz > 0) guard on XMEMCPY prevents the copy, the line signSeqStartIn.context.size = (UINT16)contextSz still executes with a negative value, resulting in a large UINT16 size field being sent to the TPM. For example, contextSz = -1 would pass the > sizeof(buffer) check (as a signed comparison) and set context.size = 65535.
if (contextSz > (int)sizeof(signSeqStartIn.context.buffer)) {
return BUFFER_E;
}
...
signSeqStartIn.context.size = (UINT16)contextSz;
if (context != NULL && contextSz > 0) {
XMEMCPY(signSeqStartIn.context.buffer, context, contextSz);
}Recommendation: Add an explicit check if (contextSz < 0) return BAD_FUNC_ARG; at the start of these functions, or change the bounds check to if (contextSz < 0 || contextSz > (int)sizeof(...)).
This review was generated automatically by Fenrir. Findings are non-blocking.
- Add TPM2B_MLDSA_SIGNATURE type with proper 4627-byte buffer for ML-DSA-87
signatures instead of reusing TPM2B_MAX_BUFFER (1024 bytes)
- Add bounds checking and byte skipping for MLDSA/MLKEM public key parsing
in TPM2_Packet_ParsePublic to prevent buffer overflow
- Add bounds checking for ML-DSA signature parsing in
TPM2_Packet_ParseSignature with proper wire size tracking
- Add bounds checking to Encapsulate/Decapsulate response parsing
(sharedSecret and ciphertext buffers)
- Add negative size validation for contextSz, digestSz, dataSz parameters
in wrapper functions: wolfTPM2_SignSequenceStart, wolfTPM2_SignSequenceComplete,
wolfTPM2_VerifySequenceStart, wolfTPM2_VerifySequenceComplete,
wolfTPM2_SignDigest, wolfTPM2_VerifyDigestSignature
- Fix misleading MAX_SIGNATURE_CTX_SIZE comment - this is for domain
separation context (255 bytes), not signature size
- Change TPMT_PUBLIC size check from assertion to warning for embedded
systems compatibility
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
wolftpm/tpm2_wrap.h:1
- The new PQC wrapper examples show a 1024-byte context buffer, but
TPM2B_SIGNATURE_CTXis defined withMAX_SIGNATURE_CTX_SIZE(255) and the wrappers enforce that limit. This example (and the similar context examples forwolfTPM2_SignDigest/ verify functions) is misleading and will fail withBUFFER_Eif copied verbatim. Concrete fix: update examples to useMAX_SIGNATURE_CTX_SIZE(or <=255) and/or explicitly mention the max context size.
/* tpm2_wrap.h
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Description
This PR adds initial support for TPM 2.0 Library Specification v1.85 PQC APIs to wolfTPM.
It implements new ML-DSA (Dilithium) and ML-KEM (Kyber) commands that were not present in the v1.84 RFC.
New TPM v1.85 Features Added
ML-DSA (Dilithium) - Signature & Verification
TPM2_SignSequenceStartTPM2_VerifySequenceStartTPM2_SignSequenceCompleteTPM2_VerifySequenceCompleteTPM2_SignDigestTPM2_VerifyDigestSignatureThese commands add context-based and sequence-based signing/verification required for PQ signature schemes.
ML-KEM (Kyber) - Key Encapsulation
TPM2_Encapsulate(public-key operation)TPM2_Decapsulate(private-key operation)Supports generation and recovery of shared secrets via PQ KEM.
New Types, Enums, and Structures
New
TPM_CC_*command codes for all v1.85 PQ commandsNew structure tags:
TPM_ST_MESSAGE_VERIFIEDTPM_ST_DIGEST_VERIFIEDNew TPM2B types:
TPM2B_SIGNATURE_CTXTPM2B_KEM_CIPHERTEXTTPM2B_SHARED_SECRETNew input/output command structures added to
tpm2.hUpdated RC4 additions and types
Testing
unit.c unit testing
test_wolfTPM2_MLDSA_*test_wolfTPM2_MLKEM_*TODO: