ML-DSA certificate chains: the 184 bytes that break TLS handshakes

Swap a three-certificate chain from ECDSA to ML-DSA-65 and its TLS 1.3 Certificate message grows from 1,157 bytes to 16,568. The default mbedTLS input buffer holds 16,384. That's 1.1% over, which is enough to fail every handshake that presents the chain. Nothing in your crypto inventory will flag it.

Where the bytes come from

ML-DSA-65 (FIPS 204) has a 1,952-byte public key and a 3,309-byte signature. In an X.509 certificate, each of those sits inside a DER BIT STRING: a tag byte, a length, and one "unused bits" byte before the payload. Once the content passes 255 bytes, the length takes three bytes (0x82 plus two):

subjectPublicKey1 + 3 + 1 + 1,9521,957 B
signatureValue1 + 3 + 1 + 3,3093,314 B
everything elsenames, validity, extensions, AlgorithmIdentifiers244 B
one certificate5,515 B

The 244 bytes is a deliberately minimal profile, measured against certificates generated with OpenSSL 3.6.1. Real certificates carry SANs, policies, SCTs and AIA extensions, so treat every number below as a lower bound.

TLS 1.3 (RFC 8446 §4.4.2) wraps the chain in a Certificate handshake message. It has an 8-byte header: handshake type (1), length (3), request-context length (1) and certificate-list length (3). Each certificate entry adds 5 bytes: a 3-byte length and 2 bytes of extensions length.

header1 + 3 + 1 + 38 B
3 × entry3 × (5 + 5,515)16,560 B
Certificate messagevs. MBEDTLS_SSL_IN_CONTENT_LEN 16,38416,568 B

Why 16,384 is a wall

mbedTLS reassembles each incoming handshake message in its input buffer, and MBEDTLS_SSL_IN_CONTENT_LEN sets that buffer's size. The default is 214 = 16,384 bytes, which is also the largest plaintext record TLS allows (RFC 8446 §5.1). A Certificate message larger than the buffer can't be processed, so the handshake fails.

Tokenkeep classifies this sink as a CRITICAL hard ceiling, because the setting is already at the protocol's record maximum: no config change gets you more room. Embedded builds often shrink the buffer to 4 or 8 KiB to save RAM, and then even an ML-DSA-44 chain breaks.

How the algorithms compare

AlgorithmOne cert1-cert chain2-cert chain3-cert chainvs. 16,384 B
Ed255193463597101,061fits
ECDSA P-2563783917741,157fits
RSA-20487667791,5502,321fits
FN-DSA-5121,8171,8303,6525,474fits
ML-DSA-443,9863,9997,99011,981fits
ML-DSA-655,5155,52811,04816,568+184
ML-DSA-877,4737,48614,96422,442+6,058
SLH-DSA-128s8,1408,15316,29824,443+8,059

Bytes. Every certificate in the chain uses the same algorithm for its subject key and its issuer's signature, with minimal 244 B metadata.

Why your inventory misses it

A Cryptographic Bill of Materials finds the call that signs a certificate and reports ECDSA. It's right about that. What it doesn't see is the rest of the picture, which is spread across files no crypto scanner reads:

The failure only shows up when those three facts are put together, which is what Tokenkeep's constraint graph does. The scanner reads MBEDTLS_SSL_IN_CONTENT_LEN from the header and the chain from a [tls_chain] record, then solves the projected message size for any target algorithm.

$ tokenkeep-pqc-scan --path . --target-algo ML-DSA-65
 [CRITICAL] tls/mbedtls_config.h:2   16,568 B > 16,384 B   +184 B · 1.01× limit
            RUNTIME FAILURE IMMINENT: Certificate message exceeds the TLS
            stack's record buffer; handshake aborts

Four ways out

  1. Send the leaf alone. A one-certificate message is 5,528 bytes. Intermediates are fetched through the Authority Information Access extension (RFC 5280 §4.2.2.1). This is the template Tokenkeep PQC Enterprise emits, and it only works if your clients can fetch AIA.
  2. Shorten the chain. Two ML-DSA-65 certificates come to 11,048 bytes and fit with room to spare. If an intermediate exists only for organisational reasons, dropping it buys back 5,520 bytes.
  3. Pick the parameter set per tier. ML-DSA-44 at all three tiers is 11,981 bytes. Many PKIs could use ML-DSA-65 for the root and intermediates and a smaller set for short-lived leaves. Run the scan once per candidate before you commit.
  4. Don't count on compression. TLS certificate compression (RFC 8879) does well on names and extensions, but PQC keys and signatures are high-entropy, so most of the 16,568 bytes won't shrink.

See it for your own stack

Paste your mbedtls_config.h into the checker. It runs the engine's own parser in your browser.

Related