On June 3, 2026, Let's Encrypt committed to Merkle Tree Certificates
(MTC) as its path to post-quantum certificates
(https://letsencrypt.org/2026/06/03/pq-certs). The problem MTC solves:
ML-DSA-44 signatures are roughly 2,420 bytes versus 64 bytes for
ECDSA-P256, so a naive PQ swap pushes a typical handshake past 10 KB,
which reportedly breaks about 5% of real-world connections. Let's
Encrypt targets a staging environment in late 2026 and production in
2027. Chrome ships preliminary support, and Google/Cloudflare have
been running a live feasibility study since February 2026.
This issue records an assessment of what implementation work would be
needed for tlshd to interoperate with MTC.
The mechanism
MTC is standardized in the IETF PLANTS working group as
draft-ietf-plants-merkle-tree-certs
(https://datatracker.ietf.org/doc/draft-ietf-plants-merkle-tree-certs/),
with negotiation built on draft-ietf-tls-trust-anchor-ids
(https://datatracker.ietf.org/doc/draft-ietf-tls-trust-anchor-ids/).
Two design points matter most:
The certificate remains DER-encoded X.509. The CA appends entries to
an append-only issuance log and periodically signs Merkle subtree
checkpoints, so one PQ signature covers thousands of certificates. An
individual certificate's signatureValue carries an MTCProof structure
(subtree parameters, a Merkle inclusion proof, and cosignatures) under
a new signatureAlgorithm OID, id-alg-mtcProof (1.3.6.1.4.1.44363.47.0).
Two profiles exist: a "standalone" form carrying full cosignatures,
self-contained but larger; and a "landmark-relative" form that omits
all signatures because the relying party has pre-fetched "landmark"
subtree hashes out of band. The landmark-relative form produces the
size win: the entire authentication path is one signature, one public
key, and one inclusion proof.
Negotiation uses the trust_anchors TLS extension. The client
advertises trust anchor IDs in ClientHello (landmark groups are
identified by per-CA OID arcs); the server selects a matching
certification path and signals the match with an empty trust_anchors
extension in the first CertificateEntry of the Certificate message.
When a fresh landmark match exists the server sends the
landmark-relative certificate; otherwise it falls back to a standalone
MTC or a classic X.509 chain. The extension also defines a retry
mechanism (the client retries with an ID selected from the server's
EncryptedExtensions list) and an optional DNS hint via a
tls-trust-anchors SvcParamKey.
A client must verify inclusion proofs and cosignatures, and run a
background fetcher keeping up to 2 * max_active_landmarks landmark
hashes per CA current. A server must hold multiple certificate forms
per key and select by trust anchor ID.
Implementation impact
The work splits into three layers.
GnuTLS (the bulk, and the blocker). A search of the GnuTLS issue
tracker found no MTC or trust-anchor-IDs work at all. GnuTLS would
need: recognition of id-alg-mtcProof and MTCProof verification in the
certificate path; the trust_anchors extension on both client and
server sides; and trust anchor ID plumbing in credential selection.
The server side requires emitting an extension in a CertificateEntry,
which GnuTLS's public API does not expose, so this is core-library
work that tlshd cannot bolt on with gnutls_session_ext_register().
Because both MTC forms are valid X.509, a partial shortcut exists:
tlshd installs its own verify callbacks and could in principle verify
a standalone MTCProof itself from the raw peer DER, but that forfeits
the landmark-relative form, which is the entire size win, and
reimplements crypto that belongs in the library.
tlshd proper (modest). Config options for an MTC certificate
alongside the classic and PQ certificates, following the existing
dual-cert pattern (x509.pq.certificate / x509.pq.private_key in
src/tlshd/config.c with signature-algorithm-based selection in the
retrieve callbacks); configuration of trusted MTC CAs and cosigner
keys plus a landmark store path; and landmark refresh, though
periodic fetching is arguably a job for a system service rather than
the handshake daemon. The handshake netlink protocol likely needs
nothing: the auth mode stays X.509.
Client-side peerid handling (actionable now, independent of MTC).
Commit efeda80 ("tlshd: Kernel should not parse incoming client
certificates") removed the server-side add_key(2) call because the
kernel's X.509 parser rejects certificates with signature algorithms
it does not recognize, turning a successfully authenticated mTLS
session into one that looks unauthenticated (TLS_NO_PEERID) to NFSD.
That commit anticipated the client side having the same problem, and
it does: tlshd_client_x509_verify_function() still inserts every peer
certificate via tlshd_keyring_create_cert() (src/tlshd/client.c:387,
src/tlshd/keyring.c:236). A server presenting an ML-DSA certificate
today, or an MTC certificate later, fails kernel-side parsing and the
client-side consumer sees TLS_NO_PEERID. Extending the fixed-peerid
approach to the client is PQ-readiness work needed before MTC ever
enters the picture.
Relevance and timing
MTC presumes a CA running batch-issuance logs and clients fetching
landmarks, which is WebPKI-scale infrastructure. tlshd deployments
(NFS, NVMe/TCP) mostly use private CAs or PSKs, where standing up an
MTC issuance log is heavy. The realistic scenario where MTC matters
is RPC-with-TLS across the WAN using Let's Encrypt certificates. The
standalone form would interoperate there even without landmark
distribution, at handshake sizes still below a classic ML-DSA chain.
No tlshd work beyond the client-side peerid cleanup is actionable
yet. Both drafts are still moving (draft-ietf-plants-merkle-tree-certs
is at -04, expiring November 2026), and Let's Encrypt will not have
staging issuance until late 2026. Worth tracking: the two drafts,
GnuTLS upstream, and whether a standalone-form-only subset would serve
the WAN use case as a first step. A GnuTLS feature request
referencing the Let's Encrypt timeline may be the most useful near-term
action.
References
On June 3, 2026, Let's Encrypt committed to Merkle Tree Certificates
(MTC) as its path to post-quantum certificates
(https://letsencrypt.org/2026/06/03/pq-certs). The problem MTC solves:
ML-DSA-44 signatures are roughly 2,420 bytes versus 64 bytes for
ECDSA-P256, so a naive PQ swap pushes a typical handshake past 10 KB,
which reportedly breaks about 5% of real-world connections. Let's
Encrypt targets a staging environment in late 2026 and production in
2027. Chrome ships preliminary support, and Google/Cloudflare have
been running a live feasibility study since February 2026.
This issue records an assessment of what implementation work would be
needed for tlshd to interoperate with MTC.
The mechanism
MTC is standardized in the IETF PLANTS working group as
draft-ietf-plants-merkle-tree-certs
(https://datatracker.ietf.org/doc/draft-ietf-plants-merkle-tree-certs/),
with negotiation built on draft-ietf-tls-trust-anchor-ids
(https://datatracker.ietf.org/doc/draft-ietf-tls-trust-anchor-ids/).
Two design points matter most:
The certificate remains DER-encoded X.509. The CA appends entries to
an append-only issuance log and periodically signs Merkle subtree
checkpoints, so one PQ signature covers thousands of certificates. An
individual certificate's signatureValue carries an MTCProof structure
(subtree parameters, a Merkle inclusion proof, and cosignatures) under
a new signatureAlgorithm OID, id-alg-mtcProof (1.3.6.1.4.1.44363.47.0).
Two profiles exist: a "standalone" form carrying full cosignatures,
self-contained but larger; and a "landmark-relative" form that omits
all signatures because the relying party has pre-fetched "landmark"
subtree hashes out of band. The landmark-relative form produces the
size win: the entire authentication path is one signature, one public
key, and one inclusion proof.
Negotiation uses the trust_anchors TLS extension. The client
advertises trust anchor IDs in ClientHello (landmark groups are
identified by per-CA OID arcs); the server selects a matching
certification path and signals the match with an empty trust_anchors
extension in the first CertificateEntry of the Certificate message.
When a fresh landmark match exists the server sends the
landmark-relative certificate; otherwise it falls back to a standalone
MTC or a classic X.509 chain. The extension also defines a retry
mechanism (the client retries with an ID selected from the server's
EncryptedExtensions list) and an optional DNS hint via a
tls-trust-anchors SvcParamKey.
A client must verify inclusion proofs and cosignatures, and run a
background fetcher keeping up to 2 * max_active_landmarks landmark
hashes per CA current. A server must hold multiple certificate forms
per key and select by trust anchor ID.
Implementation impact
The work splits into three layers.
GnuTLS (the bulk, and the blocker). A search of the GnuTLS issue
tracker found no MTC or trust-anchor-IDs work at all. GnuTLS would
need: recognition of id-alg-mtcProof and MTCProof verification in the
certificate path; the trust_anchors extension on both client and
server sides; and trust anchor ID plumbing in credential selection.
The server side requires emitting an extension in a CertificateEntry,
which GnuTLS's public API does not expose, so this is core-library
work that tlshd cannot bolt on with gnutls_session_ext_register().
Because both MTC forms are valid X.509, a partial shortcut exists:
tlshd installs its own verify callbacks and could in principle verify
a standalone MTCProof itself from the raw peer DER, but that forfeits
the landmark-relative form, which is the entire size win, and
reimplements crypto that belongs in the library.
tlshd proper (modest). Config options for an MTC certificate
alongside the classic and PQ certificates, following the existing
dual-cert pattern (x509.pq.certificate / x509.pq.private_key in
src/tlshd/config.c with signature-algorithm-based selection in the
retrieve callbacks); configuration of trusted MTC CAs and cosigner
keys plus a landmark store path; and landmark refresh, though
periodic fetching is arguably a job for a system service rather than
the handshake daemon. The handshake netlink protocol likely needs
nothing: the auth mode stays X.509.
Client-side peerid handling (actionable now, independent of MTC).
Commit efeda80 ("tlshd: Kernel should not parse incoming client
certificates") removed the server-side add_key(2) call because the
kernel's X.509 parser rejects certificates with signature algorithms
it does not recognize, turning a successfully authenticated mTLS
session into one that looks unauthenticated (TLS_NO_PEERID) to NFSD.
That commit anticipated the client side having the same problem, and
it does: tlshd_client_x509_verify_function() still inserts every peer
certificate via tlshd_keyring_create_cert() (src/tlshd/client.c:387,
src/tlshd/keyring.c:236). A server presenting an ML-DSA certificate
today, or an MTC certificate later, fails kernel-side parsing and the
client-side consumer sees TLS_NO_PEERID. Extending the fixed-peerid
approach to the client is PQ-readiness work needed before MTC ever
enters the picture.
Relevance and timing
MTC presumes a CA running batch-issuance logs and clients fetching
landmarks, which is WebPKI-scale infrastructure. tlshd deployments
(NFS, NVMe/TCP) mostly use private CAs or PSKs, where standing up an
MTC issuance log is heavy. The realistic scenario where MTC matters
is RPC-with-TLS across the WAN using Let's Encrypt certificates. The
standalone form would interoperate there even without landmark
distribution, at handshake sizes still below a classic ML-DSA chain.
No tlshd work beyond the client-side peerid cleanup is actionable
yet. Both drafts are still moving (draft-ietf-plants-merkle-tree-certs
is at -04, expiring November 2026), and Let's Encrypt will not have
staging issuance until late 2026. Worth tracking: the two drafts,
GnuTLS upstream, and whether a standalone-form-only subset would serve
the WAN use case as a first step. A GnuTLS feature request
referencing the Let's Encrypt timeline may be the most useful near-term
action.
References