How we got curl to speak post-quantum TLS without changing curl
Somewhere, someone may be recording your encrypted traffic today. They cannot read it. They are betting that in some years a quantum computer will be able to break the key exchange that protected it, and then they can. This is called harvest now, decrypt later (HNDL), and it means the deadline for post-quantum cryptography is not the day quantum computers arrive. For any data that must stay confidential for years, the deadline has already passed.
The fix exists and is standardised. NIST published ML-KEM as FIPS 203 in 2024, and TLS 1.3 can use it today in a *hybrid* key exchange called X25519MLKEM768: a classical X25519 exchange and an ML-KEM-768 exchange combined, so the session key is safe unless both are broken.
Browsers and the large CDNs already use it. The long tail does not: internal services, older clients, command-line tools, service-to-service links, appliances. On the server we were testing from, the system OpenSSL could not even name the group:
$ openssl s_client -connect example.com:443 -groups X25519MLKEM768
Call to SSL_CONF_cmd(-groups, X25519MLKEM768) failed
Upgrading every one of those is a multi-year programme. We wanted something that works this afternoon.
## The idea: move the upgrade into a proxy
If the application cannot do hybrid key exchange, put something next to it that can. We built a small proxy, CipherFlow, with two modes:
- **Client mode** listens on localhost. The application connects to it in its own TLS, and the proxy carries the bytes to the destination inside TLS 1.3 with X25519MLKEM768.
- **Server mode** sits in front of a service, terminates the hybrid TLS and forwards to the service locally.
It works at the TCP layer, so it does not care what protocol is inside.
## The test
Cloudflare runs a public endpoint that reports which key exchange a connection used. We pointed the proxy at it:
```bash
docker run -d — name cipherflow-demo -p 127.0.0.1:8080:8080 \
-e CIPHERFLOW_MODE=client \
-e CIPHERFLOW_LISTEN=:8080 \
-e CIPHERFLOW_TARGET=pq.cloudflareresearch.com:443 \
quaisecurity/cipherflow
curl -s -H “Host: pq.cloudflareresearch.com” http://127.0.0.1:8080/cdn-cgi/trace
```
Part of what Cloudflare sent back:
```
uag=curl/8.5.0
tls=TLSv1.3
kex=X25519MLKEM768
```
The user agent is an ordinary curl. The key exchange is post-quantum. curl was not rebuilt, reconfigured or told anything; it made a plain HTTP request to localhost.
The proxy’s own log recorded the same session independently:
```json
Get Vikalp Nagori’s stories in your inbox
Join Medium for free to get updates from this writer.
{“event”:”session”,”mode”:”client”,”target”:”pq.cloudflareresearch.com:443",”tls”:”TLSv1.3",”group”:”X25519MLKEM768",”pq”:true,”policy”:”2026–10–10.1"}
```
Two parties that do not trust each other’s word, agreeing on what was negotiated.
## No silent fallback
A hybrid-capable client that quietly falls back to classical key exchange gives you a false sense of safety. In its default policy the proxy offers and accepts *only* the hybrid group. In server mode, a classical-only client is refused at the handshake:
```
$ cipherflow probe -kex X25519 127.0.0.1:8443
REFUSED 127.0.0.1:8443 did not complete a TLS 1.3 handshake offering only X25519
```
## Crypto agility, because this will change again
X25519MLKEM768 will not be the last algorithm change. So the permitted groups live in a policy file, not in the binary:
```json
{
“version”: “2026–10–10.2”,
“profiles”: {
“hybrid-only”: { “key_exchange”: [“X25519MLKEM768”] }
}
}
```
Edit the file and the running proxy picks it up within seconds, without dropping connections. Before applying a policy it performs a real handshake for every group listed; if any group cannot be negotiated by this build, or the file is malformed, the policy is rejected and the previous one stays in force. In our test we changed the policy three times in ninety seconds on a public gateway, once with a deliberately invalid group, with no restart. Every session line carries the policy version it ran under.
## What this does not solve
- **Authentication is still classical.** Certificates use ECDSA or RSA. That is acceptable for HNDL, because forging a certificate requires a quantum computer at the time of the connection, and recorded traffic does not help. Post-quantum signatures in TLS are the next step.
- **It is not FIPS validated.** The cryptography is the Go standard library’s `crypto/tls` and `crypto/mlkem`.
- **The hop between the proxy and the application is local.** Bind the application to localhost.
- **TCP only**, and we have not published performance numbers yet.
## Try it
Check whether any server negotiates hybrid key exchange:
```bash
docker run — rm quaisecurity/cipherflow probe yourdomain.com:443
```
CipherFlow Community Edition is free to use. The image is on Docker Hub at `quaisecurity/cipherflow`, and the documentation is at https://github.com/QuAi-Security-Labs/cipherflow. It is a release candidate: tell us where it breaks.
*Amit Bhonsle is a co-founder of QuAi Security Labs, which builds post-quantum and AI security products.*




