Skip to content

Chapter 28: TLS 1.3 migration

Once the endpoint’s TLS stack already supports X25519MLKEM768, adding the hybrid is a configuration change and the protocol negotiation handles the rest. Where a component does not yet support the hybrid, the work begins instead with a runtime upgrade, a vendor-supplied path, or an architectural change that routes post-quantum traffic around the component. Ch 27 gave the construction: the combiner, the wire format, the at-least-one-holds security argument. Ch 28 gives the operator’s playbook for the same endpoint under an internal deadline, on a live fleet, with monitoring and rollback.

The threat the rollout blunts is harvest now, decrypt later, applied to session confidentiality. An attacker who records ciphertext today can decrypt it later if a large-scale quantum computer becomes available. Ch 1 walks that model. X25519MLKEM768 defends the recorded session against that adversary. It does not defend against an active attacker who can authenticate as the server today, leaked TLS secrets, compromised session-ticket or pre-shared-key (PSK) material, or a forged certificate chain that enables a live man-in-the-middle (MITM) attack. Certificate-level and PKI migration are covered in Ch 29. Operator tooling for this chapter lives at solutions/ch28-tls-migration/.

The Ch 25 CBOM records a tls_endpoint_api touchpoint with the following starting state: TLS 1.2, ECDHE-ECDSA-AES256-GCM-SHA384, curve P-256, deployed 2022-06-15. The Ch 26 agility policy classifies this endpoint as externally-facing and subject to the organization’s post-quantum migration plan. That plan tracks the NCSC 2025 milestones: 2028 discovery, 2031 first-wave deployment, 2035 end-of-migration (UK National Cyber Security Centre, 2025). The first-wave milestone applies to this endpoint.

Week one of the rollout is an inventory pass. Every client and server in the path of the endpoint gets classified ready, gated, or blocked. Ready means the deployed version already supports X25519MLKEM768. Gated means the library supports the hybrid at a newer version than the runtime currently running. Blocked means the library is absent from the version matrix, so the classifier knows of no version that supports the hybrid. The inventory sets the order of operations for configuration, canary, and rollout.

TLS 1.3 negotiates a single NamedGroup for key establishment through the supported_groups and key_share extensions (RFC 9846 §4.3.7, §4.3.8 (Rescorla, 2026)). The client lists supported groups in preference order and may provide key shares for a subset of them. The server selects one mutually supported group, either by accepting an offered key share in ServerHello or by requesting a different mutually supported group with HelloRetryRequest. The selection policy is server-side and need not honor the client’s order. Negotiation integrity is provided by the TLS 1.3 transcript hash: any modification to the handshake messages after they enter the transcript invalidates the derived keys. An attacker cannot rewrite the chosen group silently once both sides complete the handshake.

This chapter cites RFC 9846 rather than RFC 8446. RFC 9846 respecified TLS 1.3 in July 2026, retaining the same protocol version number and obsoleting RFC 8446 (Rescorla, 2018, 2026). Its Section 1.2 lists the technical changes. Five of them reach an operator running this rollout.

Change (RFC 9846 §1.2)What it means on a rollout
KeyShare reuse forbidden (§4.3.8)Generate a fresh share per connection; still accept a peer that reuses
TLS 1.0 and 1.1 must not be negotiatedPairs with the MinProtocol floor set below
general_error alert added (117)A new alert value dashboards will not recognize
Extension length bounds correctedParser-level only, no configuration change
”master secret” renamed “main secret”Field and log names, not the wire

The KeyShare rule is the one with operational teeth here. ML-KEM keygen costs more than an X25519 keygen, which is the pressure that makes caching a key share look attractive on a busy fleet. RFC 9846 §4.3.8 says clients and servers MUST NOT reuse a key share for multiple connections, and, because RFC 8446 permitted reuse, that receiving implementations MUST permit reuse by a peer. The requirement is deliberately one-sided: tighten your own behavior, tolerate a peer that has not.

The combiner security argument that makes X25519MLKEM768 IND-CCA-secure (Bindel, Brendel, Fischlin, Goncalves, and Stebila 2019 (Bindel et al., 2019), under the DHKEM packaging for X25519 (Barnes et al., 2022)) is inherited from Ch 27 and not repeated here.

The runnable package is at solutions/ch28-tls-migration/.

Classify each component on the path against this version-minimum table before touching server config:

Component classLibrary / productEarliest versionSource
Server TLS stackOpenSSL3.5.0 (April 2025)(OpenSSL Project, 2025)
Server TLS stacknginx (via OpenSSL 3.5+)through the OpenSSL backend(nginx, Inc., 2025)
Client runtimeGo crypto/tlsGo 1.24 (February 2025)(Valsorda & Shoemaker, 2025)
Web browserChromium / ChromeChrome 131 (November 2024)(Adrian et al., 2024)
Web browserFirefox (Desktop, TCP)Firefox 132 (October 2024)(Mozilla Networking Team, 2024)
Web browserFirefox (Desktop, HTTP/3)Firefox 135 (February 2025)(Mozilla Networking Team, 2025)
Platform TLSiOS, iPadOS, macOSOS 26 family(Apple, 2025)

Chrome 124 (April 2024) shipped an earlier non-standard group, X25519Kyber768Draft00, which Chrome 131 replaced with the IANA-registered X25519MLKEM768 group (Adrian et al., 2024; Kwiatkowski et al., 2026). That group was specified by draft-ietf-tls-ecdhe-mlkem throughout its deployment and was published on the Standards Track as RFC 10024 in August 2026. The registry entry carried the same name and codepoint across publication, so no deployed configuration changed. IANA marks X25519MLKEM768 (codepoint 0x11EC) as Recommended = Y. The NIST-curve hybrids SecP256r1MLKEM768 (0x11EB) and SecP384r1MLKEM1024 (0x11ED) remain Recommended = N, which reflects IANA / standards-process status, not a cryptographic deprecation signal. RFC 10024 Section 7.4 obsoletes the Draft00 codepoints outright, and the registry now carries X25519Kyber768Draft00 (25497) and SecP256r1Kyber768Draft00 (25498) as OBSOLETE with Recommended = D, so a deployment still advertising either should remove it.

# Block 1: pedagogical slice of fleet_assessment.classify (stdlib only).
LIBRARY_MINIMUMS = {
"openssl": (3, 5, 0),
"go-crypto-tls": (1, 24, 0),
}
def parse_version(value):
return tuple(int(p) for p in value.split("."))
def classify(inventory):
rows = []
for component, library, version in inventory:
parsed = parse_version(version)
minimum = LIBRARY_MINIMUMS.get(library)
if minimum is None:
rows.append((component, library, version, "blocked"))
elif parsed >= minimum:
rows.append((component, library, version, "ready"))
else:
rows.append((component, library, version, "gated"))
return rows
inventory = [
("edge-lb", "openssl", "3.5.2"),
("app-server", "openssl", "3.3.0"),
("backend-peer", "go-crypto-tls", "1.24.1"),
("sidecar", "envoy", "1.30.0"),
]
for row in classify(inventory):
print(" | ".join(str(x) for x in row))
# ==> edge-lb | openssl | 3.5.2 | ready
# ==> app-server | openssl | 3.3.0 | gated
# ==> backend-peer | go-crypto-tls | 1.24.1 | ready
# ==> sidecar | envoy | 1.30.0 | blocked

Every Python block this chapter prints is also a standalone file in the companion repository, under chapter-code/ch28/, one file per block. Appendix C covers the clone and the environment they run on.

The gated row is the app server on OpenSSL 3.3, which needs a runtime upgrade to OpenSSL 3.5.0 or newer. The blocked row is the Envoy sidecar at the version this deployment runs. The classifier has no verified X25519MLKEM768 support path for it as of April 2026. A gated row is a runtime-upgrade task; a blocked row needs either a vendor-supplied upgrade path or an architectural change that routes post-quantum traffic around the component. Production inventory should prefer feature detection over version strings where possible: run the deployed binary, inspect the loaded TLS library, and verify that X25519MLKEM768 is actually accepted and negotiated rather than relying on a version comparison alone. The inline parse_version does not handle build suffixes such as 3.5.0-fips or 3.5.0+vendor, distro backports, or BoringSSL-derived stacks; the full classifier in the package treats those as additional inputs.

Fleet capability snapshot for tls_endpoint_api. A status table with three columns. The first lists five components: the client browser, the edge load balancer, the app server, the backend mTLS peer, and the sidecar. The two status columns beside it show TLS 1.3 support (ready on every row) and X25519MLKEM768 support (ready on browser, edge, and backend; gated on the app server; blocked on the sidecar). A legend below names the three states: ready, gated (upgrade runtime), blocked (no matrix entry). Component (version observed) TLS 1.3 X25519MLKEM768 client browser (Chrome 131) ready ready edge load balancer (nginx + OpenSSL 3.5.2) ready ready app server (OpenSSL 3.3.0) ready gated backend mTLS peer (Go 1.24.1) ready ready sidecar (Envoy 1.30.0) ready blocked Legend ready gated (upgrade runtime) blocked (no matrix entry)
Figure 28.1. Fleet capability snapshot for the tls_endpoint_api path at the point Block 1 was run. Gated rows need a runtime upgrade; blocked rows need a vendor-supplied upgrade path or an architectural change that routes post-quantum traffic around the component.

An OpenSSL 3.5+ server configuration advertises X25519MLKEM768 as the first-preference group and keeps classical X25519 and secp256r1 as fallbacks for older clients (OpenSSL Project, 2025). The Groups= directive accepts a colon-separated list in preference order:

# OpenSSL 3.5+ system-wide config: enforce TLS 1.3, prefer the PQ hybrid.
[system_default_sect]
MinProtocol = TLSv1.3
Groups = X25519MLKEM768:X25519:secp256r1

An nginx server block backed by OpenSSL 3.5+ uses ssl_ecdh_curve with the same list shape, and nginx delegates the actual group selection to the OpenSSL backend (nginx, Inc., 2025):

# nginx server block: X25519MLKEM768 first, classical groups as fallback.
server {
listen 443 ssl;
ssl_protocols TLSv1.3;
ssl_ecdh_curve X25519MLKEM768:X25519:secp256r1;
}

A correctly configured server that prefers the hybrid should negotiate X25519MLKEM768 with clients that offer it. Clients that do not offer the hybrid land on X25519 or secp256r1 under TLS 1.3 graceful-fallback semantics. Conforming clients fall back cleanly, but the canary exists to catch misconfiguration, middlebox intolerance, oversized ClientHello flights, and implementation bugs. The three-group list is the rollout configuration. The steady-state configuration, for an endpoint that requires post-quantum protection once the client mix supports it, drops the classical fallbacks and advertises X25519MLKEM768 alone. The reasoning is in the cryptanalysis section.

# Block 2: pedagogical slice of lint_nginx_groups (stdlib only).
import re
HYBRID = "X25519MLKEM768"
def lint_nginx(config_text):
for i, raw in enumerate(config_text.splitlines(), start=1):
line = raw.strip()
if not line or line.startswith("#"):
continue
m = re.match(r"ssl_ecdh_curve\s+(.+)$", line)
if m:
groups = [g.strip() for g in m.group(1).rstrip(";").split(":")]
if HYBRID not in groups:
return [("blocker", "hybrid-missing", i)]
return []
raise ValueError("no ssl_ecdh_curve directive")
good = "server {\n ssl_ecdh_curve X25519MLKEM768:X25519:secp256r1;\n}"
bad = "server {\n ssl_ecdh_curve X25519:secp256r1;\n}"
print("good:", lint_nginx(good))
print("bad:", lint_nginx(bad))
# ==> good: []
# ==> bad: [('blocker', 'hybrid-missing', 2)]

The full package lint also covers the OpenSSL Groups= form, flags classical groups that appear before the hybrid (hybrid-not-first-preference), and flags duplicate codepoints (duplicate-codepoint).

The rollout follows a five-phase sequence: shadow (capture ClientHello offers in logs without changing server behavior), 1 percent canary (enable X25519MLKEM768 on one percent of traffic), 10 percent, 50 percent, 100 percent. At each phase the operator watches three signals:

SignalExpected valueRollback trigger
Handshake-success rate by NamedGroup≥99.9 percent on X25519MLKEM768sustained drop below 99.9 percent on any five-minute window
ML-KEM validation failure rate0 percent on controlled clients, a low background on server vantagesustained nonzero rate on previously stable traffic
P99 handshake latency on the groupbaseline + keyshare delta (measured)sustained rise above the pre-agreed P99 budget
Rollout phases and monitoring signals. Five phase bars labeled shadow, 1 percent, 10 percent, 50 percent, and 100 percent span the horizontal axis. A green handshake-success-rate line sits at the top and stays near 99.95 percent across all phases. A dashed red 99.9 percent rollback threshold sits just under it. A blue P99 handshake-latency line sits in the lower half and moves slightly upward as more traffic uses the hybrid keyshare. The vertical axis is qualitative, labelled only higher and lower; the two lines are drawn schematically and their separation is not to scale. Rollout phases and monitoring signals shadow 1% 10% 50% 100% higher lower success rate 99.9% threshold P99 latency Latency rises slightly as more traffic uses the hybrid; success rate stays above threshold.
Figure 28.2. Progressive rollout phases with the two monitoring signals. The dashed red line marks the 99.9 percent handshake-success-rate rollback threshold. The blue P99 handshake-latency line rises as more traffic uses the hybrid keyshare.

A per-window NamedGroup rollup over the TLS connection log gives the hybrid adoption share, which is not the table’s first signal. Block 3 ingests a synthetic log (timestamps in seconds and IANA codepoints 0x001D for X25519 and 0x11EC for X25519MLKEM768 (IANA, 2026)) and emits per-window totals and the hybrid percentage. Cloudflare reported at the end of October 2025 that over half of human-initiated traffic on its network used post-quantum key agreement (Westerbaan, 2025). A healthy canary shows the hybrid percentage climbing toward the fraction of capable clients in the traffic mix. The handshake-success rate the rollback trigger is written against needs a denominator this log does not carry: attempted handshakes per group per window, counting the ones that fail before any group is negotiated.

The second signal comes from the client and server validation checks in RFC 10024 §4.2 (Kwiatkowski et al., 2026). A server MUST run the FIPS 203 Section 7.2 encapsulation-key check on the client’s key and abort with illegal_parameter if it fails (National Institute of Standards and Technology, 2024). A client MUST abort with illegal_parameter on a ciphertext-length mismatch and with internal_error on any other decapsulation failure. The alert a monitored client emits therefore narrows the diagnosis on its own. On controlled clients this rate is zero by construction; on a server vantage point it stays near zero across stable traffic, with a low background from internet scans, malformed clients, and middlebox corruption. A sustained nonzero step on previously stable traffic is the rollback trigger, with diagnosis spanning implementation bugs, active attack, fuzzing or scanning, telemetry-pipeline regressions, and middlebox corruption.

# Block 3: pedagogical slice of rollup (stdlib only).
from collections import Counter
MONITORED = {"0x001D": "X25519", "0x11EC": "X25519MLKEM768"}
def rollup(records, window_seconds):
if window_seconds <= 0:
raise ValueError("window_seconds must be positive")
sorted_records = sorted(records)
origin = sorted_records[0][0]
last = sorted_records[-1][0]
out = []
cursor = origin
i = 0
n = len(sorted_records)
while cursor <= last:
end = cursor + window_seconds
counts = Counter()
total = 0
while i < n and sorted_records[i][0] < end:
_, code = sorted_records[i]
total += 1
if code in MONITORED:
counts[code] += 1
i += 1
out.append((cursor, total, dict(counts)))
cursor = end
return out
records = [
(0, "0x11EC"), (30, "0x11EC"), (60, "0x001D"),
(300, "0x11EC"), (330, "0x11EC"), (360, "0x11EC"),
]
for start, total, counts in rollup(records, window_seconds=300):
pct = 100.0 * counts.get("0x11EC", 0) / total if total else 0.0
print(f"t={start}s total={total} X25519MLKEM768={pct:.1f}%")
# ==> t=0s total=3 X25519MLKEM768=66.7%
# ==> t=300s total=3 X25519MLKEM768=100.0%

The rollup is the operator’s primary health signal. A sudden drop in the hybrid percentage on previously stable traffic is either a client-population shift (normal variance, worth logging) or a silent server-side configuration regression (rollback territory). The IANA registry lists five codepoints on the post-quantum rollout path: 0x001D (X25519), 0x0017 (secp256r1), 0x11EB (SecP256r1MLKEM768), 0x11EC (X25519MLKEM768), and 0x11ED (SecP384r1MLKEM1024) (IANA, 2026). The runnable package tracks all five; the inline covers only the two codepoints in the synthetic log.

Rollback is the inverse of the forward operation: remove X25519MLKEM768 from the Groups= or ssl_ecdh_curve list, redeploy the server configuration, and the next full TLS handshake falls back to X25519. No key material changes. No session is invalidated: TLS 1.3 sessions use ephemeral keys, and existing resumption tickets still validate. The only visible effect is that the NamedGroup rollup for new full handshakes shifts back toward classical X25519.

Treat resumed sessions separately in monitoring. A resumed TLS 1.3 connection inherits from the original full handshake through the PSK (RFC 9846 §2.2 (Rescorla, 2026)), and what it adds depends on the mode. Under psk_ke there is no new key exchange, and 0-RTT early data is protected by the PSK alone, so neither is a fresh X25519MLKEM768 negotiation. Under psk_dhe_ke, which OpenSSL uses unless SSL_OP_ALLOW_NO_DHE_KEX is set, the resumption negotiates a group and performs a fresh key exchange, which may be the hybrid. A dashboard that counts every resumption as post-quantum therefore overstates coverage. Count a resumption only when its own negotiated group is 0x11EC, and never count early data. During a post-quantum-required cutover, log full handshakes and resumptions separately, set ticket lifetimes deliberately, and rotate ticket keys (or flush outstanding tickets) when a clean policy boundary is required.

Cryptanalysis: negotiation downgrade and the HNDL scope

Section titled “Cryptanalysis: negotiation downgrade and the HNDL scope”

The combiner-level attack surface is covered in Ch 27’s cryptanalysis section. KDF weakness, shared-state randomness failure, and combiner implementation bugs all apply to any X25519MLKEM768 deployment and are not repeated here. The chapter-specific surface is at the group-list level.

A server that advertises X25519MLKEM768 alongside classical X25519 to a client that offers both should select the hybrid when its preference policy puts it first, but group-selection policy is implementation-specific (RFC 9846 §4.3.8 (Rescorla, 2026)). In OpenSSL and nginx-on-OpenSSL deployments the Groups= / ssl_ecdh_curve list order represents server preference and interacts with the OpenSSL server-preference options. Configure the hybrid first and verify the selected group under the deployed configuration. Go crypto/tls from Go 1.24 enables X25519MLKEM768 by default but explicitly ignores the order of Config.CurvePreferences and applies an internal preference order regardless of configuration (The Go Authors, 2025; Valsorda & Shoemaker, 2025). Operators relying on Go-side preference cannot tune the order through the standard library. BoringSSL and rustls each carry their own preference policy and should be verified the same way. The operator-facing test is the same in every stack: complete a representative handshake and confirm the server-selected group.

TLS 1.3 transcript binding defends against an on-path tamperer through two mechanisms. The server’s CertificateVerify signs the transcript hash computed over the ClientHello bytes as the server received them (RFC 9846 §4.5.2 (Rescorla, 2026)). The Finished MAC on both sides is keyed from transcript-derived secrets (RFC 9846 §4.5.3 (Rescorla, 2026)). A client whose ClientHello bytes were modified in flight computes a transcript hash that disagrees with the server’s. CertificateVerify or Finished fails at the client, and the client aborts. A pure on-path MITM that strips X25519MLKEM768 from a ClientHello therefore cannot complete a downgraded handshake.

The failure mode transcript binding does not prevent is a policy bypass. A server that advertises X25519MLKEM768 as an optional group alongside classical X25519 will cleanly negotiate classical with any client that never offered the hybrid, including an adversary that impersonates a legacy classical-only client. No tampering of a capable client’s handshake is required, both sides’ transcripts agree, and there is nothing for CertificateVerify or Finished to catch. This is standard backward-compatibility behavior for the staged rollout, and it is an organizational-policy risk only on endpoints that have committed to post-quantum protection. FREAK (2015, export-grade 512-bit RSA) and Logjam (2015, export-grade 512-bit Diffie-Hellman) are the historical analogues at the TLS 1.2 ciphersuite level.

The operational conclusion is that an endpoint requiring post-quantum protection must enforce the hybrid as a hard requirement, not as an optional alternative. Enforcement is a server-side stance. Chrome, Firefox, Go crypto/tls, and OpenSSL all advertise classical groups alongside the hybrid in their default supported_groups list, so a server that picks a classical group negotiates cleanly with no client-visible error. A server that requires the hybrid must reduce its advertised group list to X25519MLKEM768 only and reject handshakes from classical-only clients. The rollout configuration from the earlier server-configuration subsection is a staged posture; the steady state on a post-quantum-required endpoint is hybrid-only.

The threat X25519MLKEM768 defends against is harvest now, decrypt later, applied to session confidentiality. An adversary who records ciphertext today and gains access to a large-scale quantum computer later cannot recover the session key, because doing so requires breaking ML-KEM, and the combiner argument from Ch 27 says breaking X25519 alone is not enough. TLS 1.3 sessions use ephemeral key exchange, so later compromise of the server’s certificate private key does not retroactively decrypt recorded sessions on its own. The rollout does not defend against an active attacker who can authenticate as the server today, against endpoint compromise during the session, against leakage of TLS secrets (key-log files, debug exports), or against compromised session-ticket or PSK material. It does not defend against a forged certificate chain that enables live MITM either. Certificate-chain migration, including signature-scheme updates across the CA chain, is covered in Ch 29.

Four dimensions capture the operational cost of a X25519MLKEM768 deployment against a pure-classical TLS 1.3 baseline:

DimensionX25519 baselineHybridDelta
ClientHello keyshare32 B1216 B+1184 B (Kwiatkowski et al., 2026)
ServerHello keyshare32 B1120 B+1088 B (Kwiatkowski et al., 2026)
Server CPU / handshakeX25519 onlyX25519 + ML-KEM-768 encapstens of microseconds, from Kyber768 (Avanzi et al., 2021)
Round-trip count1-RTT1-RTTunchanged

Bandwidth: the keyshare totals in the table come from the Ch 27 wire-format breakdown (1184-byte ML-KEM encapsulation key plus 32-byte X25519 public key in the ClientHello, 1088-byte ML-KEM ciphertext plus 32-byte X25519 ephemeral in the ServerHello) (Kwiatkowski et al., 2026). A long-lived TLS 1.3 connection amortizes the cost over the session. A short-lived connection fleet (mobile cellular handoff, IoT heartbeat, high-latency satellite) pays the cost once per handshake, so the per-percentile latency delta is worth measuring against the deployment’s target.

CPU: the Kyber round-3 specification, the direct antecedent of FIPS 203, reports Kyber768 enc at 67,624 cycles for its AVX2 implementation on one core of a 3.492 GHz Intel Core i7-4770K (Table 2), which is about 19 microseconds (Avanzi et al., 2021). That is a measurement of Kyber768 and not of ML-KEM-768: FIPS 203 publishes no cycle counts, so the submission’s figure stands in for the standardized scheme rather than measuring it. That benchmark part is a 2013 Haswell, so a current server core does better; either way the cost is the order of the ECDHE-ECDSA signing the server already pays per handshake. For a typical HTTPS endpoint serving keep-alive connections, server CPU is not the binding constraint; keyshare bandwidth and the segment count in the initial handshake flight are.

Latency: the handshake round-trip count does not change. What changes is the size of the client’s first flight. Measured with openssl s_client -tls1_3 on OpenSSL 3.6.2 and counted as TLS record bytes, the 5-byte record header included, a ClientHello offering only X25519 is 215 bytes on the wire. Offering X25519MLKEM768 it is 1393 bytes, and 1443 bytes once a 15-character server name, a two-entry ALPN list, and X25519 as a second offered group sit alongside the hybrid share. A client that also sends a key share for that second group, so that a server preferring X25519 needs no HelloRetryRequest, adds 36 bytes and reaches 1479. The budget those numbers meet depends on the path. A 1500-byte maximum transmission unit carries 1460 bytes of TCP payload per segment over IPv4 with no options and 1440 over IPv6, the fixed headers being 20 bytes for IPv4, 40 for IPv6, and 20 for TCP, and any option in either header comes out of the payload (Borman, 2012). So the 1443-byte ClientHello fits one IPv4 segment with 17 bytes to spare and does not fit one IPv6 segment, and the 1479-byte one fits neither. That margin, rather than the microseconds of keygen, is the deployment hazard. The ClientHello historically fit in a single packet, so middleboxes and load balancers came to assume it always would, and post-quantum key shares are what first pushed real client populations past the boundary (Westerbaan, 2025). Measure the P99 handshake-time delta on the affected traffic before and after enabling the hybrid.

Compatibility: a client that does not offer X25519MLKEM768 silently lands on X25519 under TLS 1.3 group negotiation. A server can ship the hybrid with graceful fallback to the classical group, and no conforming client fails the negotiation. The breakage that remains is the class the canary phases exist to catch: misconfiguration, middlebox intolerance, and implementation bugs. TLS 1.2 has no standard X25519MLKEM768 group. NIST SP 800-227 (final, September 2025) gives approved procedures for key establishment using FIPS 203 ML-KEM (National Institute of Standards and Technology, 2025). Any endpoint that requires post-quantum protection must enforce TLS 1.3 minimum.

Where Chapter 28 ends and Chapter 29 picks up

Section titled “Where Chapter 28 ends and Chapter 29 picks up”

This chapter moved one endpoint’s key exchange and stopped there. The group list changed, the keyshare grew, the NamedGroup rollup told the operator when to advance, and the whole operation stayed reversible by deleting one name from a configuration file. Nothing in it touched a key that outlives a session: TLS 1.3 key exchange is ephemeral, so a rolled-back hybrid leaves no artifact behind.

The certificate does outlive the session, and that is Chapter 29. It takes the jwt_signing touchpoint from the same Ch 25 inventory, under a 2033 deadline rather than this chapter’s 2031. What it migrates is signatures rather than key agreement: the CA hierarchy, the JWKS keys and the tokens they sign, and the code-signing pipeline. The composite construction Ch 27 derived is the transition primitive there, which is where id-MLDSA65-Ed25519-SHA512 stops being a profile and becomes a chain-wide cost.

The asymmetry between the two operations is what carries forward. A hybrid key exchange can be rolled back in a redeploy because nothing signed under it survives. A signature migration cannot: every certificate issued under the new chain stays in the field for its full validity, and a bootloader signed in 2026 may still be verified in 2040. Chapter 29 spends most of its length on that consequence, from deprecation and compatibility windows down to the choice between stateful LMS and XMSS for firmware and stateless SLH-DSA for commercial software signing.

  1. Configure a local openssl s_server on OpenSSL 3.5+ to advertise X25519MLKEM768, and verify with openssl s_client -groups X25519MLKEM768 that the handshake completes and selects the hybrid. Report the server-selected group from the handshake output.

  2. Run the named-group rollup from solutions/ch28-tls-migration over a 24-hour CSV fixture that you generate yourself (monotonic timestamps, mixed codepoints 0x001D and 0x11EC). Identify the hour with the largest percentage-point jump in the hybrid fraction and state whether the jump is consistent with a canary flip (server configuration change) or a traffic-mix shift (new client population entering the hour).

  3. Extend classify in the tls_migration package under solutions/ch28-tls-migration to add an unsupported fourth status, for a library known to have no PQC support at any version, as distinct from blocked, which records only that the classifier holds no entry for that library. The caller should pass an unsupported_libraries set alongside the library-minimum matrix. Update the Chapter 28 test suite to cover the new status.

  4. Write a one-page rollback runbook for the failure mode “handshake-success-rate on X25519MLKEM768 drops below 99.9 percent on the 10 percent canary for any five-minute window”. The runbook should name the trigger, the action (remove X25519MLKEM768 from the group list and redeploy), the recovery condition (success rate returns above 99.9 percent on the classical groups for three consecutive five-minute windows), and the post-mortem template.

  5. Pick one of Ch 26’s six architectural areas where the X25519MLKEM768 rollout is the direct action, and one where it is adjacent but not the action. Refer to the specific Ch 26 subsection and explain why.

Worked solutions and editorial notes for these exercises are in Appendix D, Chapter 28. A separate track, for rebuilding rather than reading: the package exercises/ch28-tls-migration has every function the chapter teaches replaced by a stub. Run PQC_IMPL=exercises pytest tests/ch28 to grade your version against the suite that proves the reference one.

Adrian, D., Benjamin, D., Beck, B., & O’Brien, D. (2024). A new path for Kyber on the web. security.googleblog.com/2024/09/a-new-path-for-kyber-on-web.html. https://security.googleblog.com/2024/09/a-new-path-for-kyber-on-web.html
Apple. (2025). Prepare your network for quantum-secure encryption in TLS. https://support.apple.com/en-us/122756
Avanzi, R., Bos, J., Ducas, L., Kiltz, E., Lepoint, T., Lyubashevsky, V., Schanck, J. M., Schwabe, P., Seiler, G., & Stehlé, D. (2021). CRYSTALS-Kyber Algorithm Specifications and Supporting Documentation (Version 3.02). NIST Post-Quantum Cryptography Project, Round 3 submission package. https://pq-crystals.org/kyber/data/kyber-specification-round3-20210804.pdf
Barnes, R., Bhargavan, K., Lipp, B., & Wood, C. A. (2022). Hybrid Public Key Encryption. IETF RFC 9180. https://doi.org/10.17487/RFC9180
Bindel, N., Brendel, J., Fischlin, M., Goncalves, B., & Stebila, D. (2019). Hybrid Key Encapsulation Mechanisms and Authenticated Key Exchange. Post-Quantum Cryptography (PQCrypto 2019), 11505, 206–226. https://doi.org/10.1007/978-3-030-25510-7_12
Borman, D. (2012). TCP Options and Maximum Segment Size (MSS). IETF RFC 6691. https://doi.org/10.17487/RFC6691
IANA. (2026). Transport Layer Security (TLS) Parameters: Supported Groups. IANA registry tls-parameters-8 (Supported Groups). https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8
Kwiatkowski, K., Kampanakis, P., Westerbaan, B., & Stebila, D. (2026). Post-Quantum Traditional (PQ/T) Hybrid Key Agreement Mechanisms for TLS 1.3. RFC 10024. https://doi.org/10.17487/RFC10024
Mozilla Networking Team. (2024). Let mlkem768x25519 support for desktop TLS connections ride the trains. Bugzilla bug 1919097. https://bugzilla.mozilla.org/show_bug.cgi?id=1919097
Mozilla Networking Team. (2025). Add post-quantum key agreement X25519MLKEM768 for HTTP/3. Firefox 135.0 release notes, firefox.com/firefox/135.0/releasenotes/. https://www.mozilla.org/en-US/firefox/135.0/releasenotes/
National Institute of Standards and Technology. (2024). FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard. Federal Information Processing Standards Publication. https://doi.org/10.6028/NIST.FIPS.203
National Institute of Standards and Technology. (2025). Recommendations for Key-Encapsulation Mechanisms. NIST Special Publication 800-227. https://doi.org/10.6028/NIST.SP.800-227
nginx, Inc. (2025). Post-quantum TLS with nginx and OpenSSL 3.5. blog.nginx.org/blog/pqc-nginx. https://blog.nginx.org/blog/pqc-nginx
OpenSSL Project. (2025). OpenSSL 3.5.0 release announcement. https://openssl-library.org/post/2025-04-08-openssl-35-final-release/
Rescorla, E. (2018). The Transport Layer Security (TLS) Protocol Version 1.3. RFC 8446. https://doi.org/10.17487/RFC8446
Rescorla, E. (2026). The Transport Layer Security (TLS) Protocol Version 1.3. RFC 9846. https://doi.org/10.17487/RFC9846
The Go Authors. (2025). Go 1.24 Release Notes: crypto/tls. go.dev/doc/go1.24. https://go.dev/doc/go1.24
UK National Cyber Security Centre. (2025). Timelines for migration to post-quantum cryptography. NCSC guidance. https://www.ncsc.gov.uk/guidance/pqc-migration-timelines
Valsorda, F., & Shoemaker, R. (2025). crypto/tls: enable X25519MLKEM768 by default. golang.org/issue/69985. https://go.dev/issue/69985
Westerbaan, B. (2025). State of the post-quantum Internet in 2025. The Cloudflare Blog. https://blog.cloudflare.com/pq-2025/

Last updated: