How-to guide · Labs and advanced

How to test post-quantum hybrid TLS

In one sentence

You will know whether your servers and network negotiate the X25519MLKEM768 hybrid key exchange, and how to read, force and troubleshoot it with OpenSSL 3.5.

Task
Labs and advanced
Platforms
  • Linux
  • macOS
  • Windows
Who it is for
Level
Expert · 6 steps
On this page
  1. Before you start
  2. Steps
  3. 1. Check that your OpenSSL can speak it
  4. 2. Test a server that is known to support it
  5. 3. Test your own servers with the default settings
  6. 4. Force a hybrid-only handshake
  7. 5. Measure the bigger ClientHello
  8. 6. Turn it on where you terminate TLS
  9. Check it worked
  10. If something goes wrong
  11. Why this works
  12. Go deeper

Before you start

  • OpenSSL 3.5 or later as a test client. Older versions do not implement ML-KEM. Check with openssl version; we used the openssl package of Alpine Linux 3.24 (OpenSSL 3.5.9).
  • The host names of the servers, load balancers and CDNs you want to test, and a machine inside your network to test from.
  • Optionally OpenSSL 3.5 on a server, to try the server side.

Why this matters: an attacker can record encrypted traffic today and decrypt it once a large quantum computer exists. Hybrid key exchange stops that, because the session key depends on both a classical exchange and ML-KEM. X25519MLKEM768 hybrid post-quantum key exchange is on by default in Chrome, Firefox and Apple's operating systems (since late 2024 in Chrome 131 and Firefox 132), so your visitors already offer it. The question is whether your side accepts it, and whether anything in between breaks.

Steps

1. Check that your OpenSSL can speak it

openssl version
openssl list -tls-groups | tr ':' '\n' | grep -i mlkem
OpenSSL 3.5.9 29 Sep 2026 (Library: OpenSSL 3.5.9 29 Sep 2026)
MLKEM512
MLKEM768
MLKEM1024
SecP256r1MLKEM768
X25519MLKEM768
SecP384r1MLKEM1024

X25519MLKEM768, the group browsers use, combines X25519 with ML-KEM-768 and is defined in RFC 10024. With OpenSSL 3.2, the same commands fail with list: Unknown option: -tls-groups, and asking for the group gives group 'X25519MLKEM768' cannot be set. Those are client errors: upgrade the test client before you draw conclusions about a server.

2. Test a server that is known to support it

Start with a reference, so you know what success looks like:

openssl s_client -connect www.cloudflare.com:443 -servername www.cloudflare.com \
  -verify_hostname www.cloudflare.com -brief </dev/null
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=www.cloudflare.com
Hash used: SHA256
Signature type: ecdsa_secp256r1_sha256
Verification: OK
Verified peername: www.cloudflare.com
Negotiated TLS1.3 group: X25519MLKEM768

Two lines matter. Negotiated TLS1.3 group: X25519MLKEM768 means the key exchange was hybrid. Signature type: ecdsa_secp256r1_sha256 shows the certificate is still classical, which is expected: Publicly trusted TLS certificates still use classical signatures such as RSA and ECDSA; post-quantum signatures are not yet allowed in them. www.google.com gave the same result on 20 October 2026.

3. Test your own servers with the default settings

OpenSSL 3.5 offers key shares for both X25519MLKEM768 and X25519 by default, just as browsers do, so a plain connection shows what the server prefers:

openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -brief </dev/null
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_128_GCM_SHA256
...
Peer Temp Key: X25519, 253 bits

Peer Temp Key: X25519 means the server chose the classical group. The connection is still secure today, but it is not protected against later quantum decryption.

4. Force a hybrid-only handshake

To prove support rather than preference, offer nothing else:

openssl s_client -connect www.example.com:443 -servername www.example.com -groups X25519MLKEM768 -brief </dev/null

A server that supports it answers as in step 2. A server that does not ends the handshake. Against letsencrypt.org:

…:error:0A000410:SSL routines:ssl3_read_bytes:ssl/tls alert handshake failure:…:SSL alert number 40

Alert 40 (handshake_failure) here means “no group in common”. Only use hybrid-only settings for tests; real clients must keep a classical fallback.

5. Measure the bigger ClientHello

The hybrid key share is 1,216 bytes instead of 32, so the first message of the handshake grows. -msg prints its length in hexadecimal:

openssl s_client -connect www.cloudflare.com:443 -servername www.cloudflare.com -msg </dev/null 2>&1 | grep -m1 ClientHello
openssl s_client -connect www.cloudflare.com:443 -servername www.cloudflare.com -groups X25519 -msg </dev/null 2>&1 | grep -m1 ClientHello
>>> TLS 1.3, Handshake [length 060a], ClientHello
>>> TLS 1.3, Handshake [length 0138], ClientHello

That is 1,546 bytes with the default hybrid offer against 312 bytes with X25519 alone. A ClientHello that no longer fits in one network packet is where old firewalls, TLS-inspection proxies and load balancers fail. Run steps 3 and 4 from inside your network, through each proxy and load balancer path (-proxy host:port sends s_client through an HTTP proxy), not only from the internet.

6. Turn it on where you terminate TLS

Servers built with OpenSSL 3.5 or later usually accept and prefer the hybrid group with their default settings, because OpenSSL 3.5’s default group list puts the hybrid groups first, unless a configured group list leaves it out. In nginx and Apache, put it first in the list, as in How to configure TLS securely:

ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1;

You can see the server side work without touching production. OpenSSL 3.5’s own test server, with any test certificate:

openssl s_server -accept 127.0.0.1:4443 -cert server.crt -key server.key -www &
openssl s_client -connect 127.0.0.1:4443 -servername localhost -CAfile ca.crt -brief </dev/null
Protocol version: TLSv1.3
Negotiated TLS1.3 group: X25519MLKEM768

The same client against our lab nginx, built with OpenSSL 3.0, reported Peer Temp Key: X25519, 253 bits: the server’s library decides. CDNs and managed load balancers have their own settings; check their documentation.

Check it worked

From inside and outside your network, for each public name:

openssl s_client -connect www.example.com:443 -servername www.example.com -groups X25519MLKEM768 -brief </dev/null 2>&1 | grep -E 'Negotiated|alert'

Success prints Negotiated TLS1.3 group: X25519MLKEM768. In a browser, the developer tools’ security view shows the key exchange of the current connection; a current Chrome or Firefox should show X25519MLKEM768 for the same site.

If something goes wrong

  • group 'X25519MLKEM768' cannot be set or Unknown option: -tls-groups: your test client is older than OpenSSL 3.5.
  • alert handshake failure (40) only with -groups X25519MLKEM768: the server has no hybrid support. Upgrade its TLS library, or the load balancer or CDN in front of it.
  • The default handshake shows X25519 although the server is new: a configured group list leaves the hybrid group out, or a device in front terminates TLS with an older library.
  • Hybrid handshakes hang or reset from inside the network, but work from outside: a middlebox mishandles the large ClientHello. Update or reconfigure it; disabling the hybrid group on clients only hides the problem.
  • Older clients fail after you enable it on a server: they should simply not offer the group. If they break, report it as a client bug and keep X25519 in the server’s list as the fallback.

Why this works

In TLS 1.3, the client sends key shares for the groups it prefers and the server picks one it supports (the TLS 1.3 handshake). A hybrid group runs an X25519 exchange and an ML-KEM encapsulation side by side and feeds both secrets into the key schedule, so an attacker must break both (post-quantum cryptography). Because the certificate only authenticates the handshake, it can stay classical for now; the key exchange is what protects recorded traffic.

Go deeper

Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.

Last reviewed Edition 0.9