<!-- https://pkizerotohero.com/guides/post-quantum-tls-test/ | Markdown mirror of the HTML page | PKI Zero to Hero, by Duyutech BV -->

> Test post-quantum hybrid TLS (X25519MLKEM768) with OpenSSL 3.5: check servers, force hybrid-only handshakes, measure ClientHello size and fix failures.

Source: https://pkizerotohero.com/guides/post-quantum-tls-test/ · Last reviewed: 2026-10-20

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](https://pkizerotohero.com/guides/#task-advanced)

Platforms

- Linux
- macOS
- Windows

Who it is for

- [IT ops](https://pkizerotohero.com/start/#itops)
- [Expert](https://pkizerotohero.com/start/#expert)
- [Developer](https://pkizerotohero.com/start/#developer)

Level

Expert · 6 steps

## 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](https://pkizerotohero.com/handbook/glossary/#hybrid-key-exchange) stops that, because the session key depends on both a classical exchange and [ML-KEM](https://pkizerotohero.com/handbook/glossary/#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

```bash
openssl version
openssl list -tls-groups | tr ':' '\n' | grep -i mlkem
```

```text
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:

```bash
openssl s_client -connect www.cloudflare.com:443 -servername www.cloudflare.com \
  -verify_hostname www.cloudflare.com -brief </dev/null
```

```text
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](http://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:

```bash
openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -brief </dev/null
```

```text
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:

```bash
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:

```text
…: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:

```bash
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
```

```text
>>> 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](https://pkizerotohero.com/guides/tls-configuration/):

```text
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:

```bash
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
```

```text
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:

```bash
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](https://pkizerotohero.com/handbook/tls-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](https://pkizerotohero.com/handbook/post-quantum/)). Because the certificate only authenticates the handshake, it can stay classical for now; the [key exchange](https://pkizerotohero.com/handbook/key-exchange/) is what protects recorded traffic.

## Go deeper

- [RFC 10024](https://www.rfc-editor.org/rfc/rfc10024): the hybrid groups for TLS 1.3, including X25519MLKEM768 and its key-share sizes.
- [RFC 9954](https://www.rfc-editor.org/rfc/rfc9954): the general design of hybrid key exchange in TLS 1.3.
- [NIST FIPS 203](https://csrc.nist.gov/pubs/fips/203/final): the ML-KEM standard.
- [OpenSSL 3.5 release notes](https://openssl-library.org/news/openssl-3.5-notes/index.html): ML-KEM support and the new default key shares.
- [OpenSSL: openssl-s\_client](https://docs.openssl.org/master/man1/openssl-s_client/): `-groups`, `-brief`, `-msg` and `-proxy`.

**Early edition.** Checked against the standards; an independent expert review is in progress. Found a mistake? [Tell us](https://pkizerotohero.com/contact/?topic=content-error&page=/guides/post-quantum-tls-test/).

Last reviewed 20 October 2026Edition 0.9

## Keep going

### Learn why it works

- [Chapter 26: Post-quantum cryptography](https://pkizerotohero.com/handbook/post-quantum/)
- [Chapter 15: The TLS 1.3 handshake](https://pkizerotohero.com/handbook/tls-handshake/)
- [Chapter 4: Key exchange and hybrid encryption](https://pkizerotohero.com/handbook/key-exchange/)

### Topics

- [TLS and HTTPS](https://pkizerotohero.com/topics/tls/)
- [Post-quantum cryptography](https://pkizerotohero.com/topics/post-quantum/)

## More guides: Labs and advanced

- [How to run a small private CA with step-ca (lab)](https://pkizerotohero.com/guides/private-ca-step-ca/)
- [How to build a test CA with OpenSSL (lab only)](https://pkizerotohero.com/guides/openssl-test-ca/)
- [How to sign code with signtool and cosign](https://pkizerotohero.com/guides/code-signing-signtool-cosign/)
- [How to use SSH certificates instead of known\_hosts](https://pkizerotohero.com/guides/ssh-certificates/)
