Two handshakes on one wire: upgrading TLS crypto without touching the endpoints
My last post ended on a claim: a component in the path can speak post-quantum to one side and classical to the other, upgrading a connection without either endpoint changing. That sounds like it shouldn't be possible — TLS is end-to-end encrypted and tamper-evident. The trick is that there is no single connection. There are two, stitched together.
You can't edit a TLS handshake in flight
TLS binds itself together on purpose. Every handshake message feeds a running transcript hash, and the final Finished message is a MAC over that transcript — change one byte of the key exchange and the MAC fails and the handshake dies. Every record of application data is then sealed with an AEAD tag under keys derived from that handshake. So you cannot sit in the middle and "rewrite X25519 to X25519MLKEM768" on a live connection. The math won't let you. To change the crypto, you have to terminate the connection and start another.
Two endpoints, one agent
A TLS splice is exactly that: two complete, independent TLS sessions with the agent in the middle, acting as a full endpoint on both sides.
client ⇄ [ agent ] ⇄ server
session A session B
classical X25519 X25519MLKEM768
To the client, the agent is the server. It runs a full server-side handshake, negotiating whatever the client supports — say, classical X25519 over TLS 1.2. To the server, the agent is the client. It runs a separate client-side handshake, negotiating whatever the server supports — say, X25519MLKEM768 over TLS 1.3.
Two handshakes, two key schedules, two sets of traffic keys. Application data is decrypted out of session A and re-encrypted into session B, record by record. The client thinks it reached the server directly; the server thinks the client did. Neither changed a line of code.
The certificate is the whole game
There's a catch that makes or breaks this: to be "the server" to the client, the agent has to present a certificate for the server's name that the client will accept — and TLS exists precisely to stop a man in the middle from doing that. So a splice only works with explicit, operator-established trust:
Outbound (upgrading connections your clients make): the agent mints a leaf certificate for the requested name on the fly, signed by a CA your clients already trust. Operator-provisioned — not a forged public cert.
Inbound (you operate the server): you hand the agent the real server certificate and key. No minting; it presents the genuine cert.
Either way the trust is granted deliberately. Without it, the client correctly refuses — which is the system working as designed.
Why it's not "just a proxy"
A TCP proxy moves bytes blind. A TLS splice is a full TLS endpoint twice over: it parses and rebuilds every handshake message, runs the key schedule, handles the AEAD, and reconciles two negotiated parameter sets that can differ in version, cipher, key-exchange group, and extensions. The interesting engineering is in the mismatch — a client offering TLS 1.2 with classical crypto on one side, a server wanting TLS 1.3 with ML-KEM on the other. The agent has to hand each side a handshake it finds completely coherent while bridging the gap between them.
And it has to be exact. Rebuild the upstream ClientHello so its offered groups don't match the keys you generated, or get the transcript hash wrong by a single byte, and the Finished MAC fails and the handshake collapses. There is no "mostly right" in a TLS handshake.
One connection at a time
This is the mechanism under "migrate without touching the endpoints." Whether it runs inline in the kernel data path or as a userspace proxy, the shape is the same: terminate, negotiate the best each side can do, translate between them. A mixed fleet — modern clients, legacy origins, appliances you can't upgrade — becomes something you migrate connection by connection, on your schedule, instead of all at once or not at all.
That's the difference between post-quantum migration as a multi-year rip-and-replace and as an engineering problem you solve at the wire. If you want to see what that looks like on your own traffic, that's where I start.
← All posts