Skip to content

Add TPM 2.0 v1.85 PQC (ML-DSA and ML-KEM) Support#445

Draft
aidangarske wants to merge 5 commits intowolfSSL:masterfrom
aidangarske:v185-pq-support
Draft

Add TPM 2.0 v1.85 PQC (ML-DSA and ML-KEM) Support#445
aidangarske wants to merge 5 commits intowolfSSL:masterfrom
aidangarske:v185-pq-support

Conversation

@aidangarske
Copy link
Member

@aidangarske aidangarske commented Dec 26, 2025

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_SignSequenceStart
  • TPM2_VerifySequenceStart
  • TPM2_SignSequenceComplete
  • TPM2_VerifySequenceComplete
  • TPM2_SignDigest
  • TPM2_VerifyDigestSignature

These 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 commands

  • New structure tags:

    • TPM_ST_MESSAGE_VERIFIED
    • TPM_ST_DIGEST_VERIFIED
  • New TPM2B types:

    • TPM2B_SIGNATURE_CTX
    • TPM2B_KEM_CIPHERTEXT
    • TPM2B_SHARED_SECRET
  • New input/output command structures added to tpm2.h

Updated RC4 additions and types

  • Added PQC key structures: TPM2B_PUBLIC_KEY_MLDSA, TPM2B_PRIVATE_KEY_MLDSA, TPM2B_PUBLIC_KEY_MLKEM, TPM2B_PRIVATE_KEY_MLKEM
  • Added parameter structures: TPMS_MLDSA_PARMS, TPMS_HASH_MLDSA_PARMS, TPMS_MLKEM_PARMS
  • Updated unions: TPMU_PUBLIC_PARMS, TPMU_PUBLIC_ID, TPMU_SENSITIVE_COMPOSITE with PQC members
  • Added TPM_RC_EXT_MU error code (RC_FMT1 + 0x02B)
  • Added size constants: MAX_MLDSA_PUB_SIZE, MAX_MLDSA_SIG_SIZE, MAX_MLKEM_PUB_SIZE, etc.
  • Added packet serialization for PQC public parms, unique fields, and sensitive data
  • Added key template helpers: wolfTPM2_GetKeyTemplate_MLDSA, wolfTPM2_GetKeyTemplate_HASH_MLDSA, wolfTPM2_GetKeyTemplate_MLKEM
  • Added unit tests for key templates (non-TPM dependent) and PQC command tests (skip gracefully on unsupported TPMs)

Testing

unit.c unit testing

  • test_wolfTPM2_MLDSA_*
  • test_wolfTPM2_MLKEM_*

TODO:

  • Test with actual hardware / TCG spec 185 sim

@aidangarske aidangarske self-assigned this Dec 26, 2025
@dgarske dgarske self-requested a review December 29, 2025 17:44
@dgarske dgarske self-assigned this Jan 2, 2026
@dgarske dgarske removed their assignment Jan 28, 2026
@dgarske dgarske removed their request for review January 28, 2026 20:59
Copy link

@tdjCisco tdjCisco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't the command/response buffers need updating

Copy link

@tdjCisco tdjCisco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like bus encryption with PQC key is missing

Copy link

@tdjCisco tdjCisco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dgarske dgarske self-assigned this Mar 5, 2026
Copilot AI review requested due to automatic review settings March 11, 2026 19:20
@aidangarske aidangarske review requested due to automatic review settings March 11, 2026 19:22
Copilot AI review requested due to automatic review settings March 19, 2026 19:44
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-v185 option 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.

Copy link

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copilot AI review requested due to automatic review settings March 19, 2026 22:01
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_CTX is defined with MAX_SIGNATURE_CTX_SIZE (255) and the wrappers enforce that limit. This example (and the similar context examples for wolfTPM2_SignDigest / verify functions) is misleading and will fail with BUFFER_E if copied verbatim. Concrete fix: update examples to use MAX_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants