Why TLS 1.3 Handshakes Are Faster (And What Changed Under the Hood)

Featured image for "Why TLS 1.3 Handshakes Are Faster (And What Changed Under the Hood)"

Written by

in

The 1-RTT handshake and what changed

TLS 1.2 vs TLS 1.3 handshake sequence comparison

TLS 1.3 (RFC 8446, finalized August 2018) isn’t a tuning pass on TLS 1.2. It’s a different protocol with the same name. The headline feature is a 1-RTT handshake instead of 2-RTT, but what I find more interesting is why that became possible: the designers deleted static RSA key exchange, deleted renegotiation, and made forward secrecy mandatory. This post walks through what changed on the wire.

TLS 1.2’s 2-RTT problem

A full TLS 1.2 handshake looks like this:

Client                                               Server
  ClientHello  (cipher suites, supported groups, random) -->
                                              <-- ServerHello (chosen cipher)
                                              <-- Certificate
                                              <-- ServerKeyExchange (if DHE/ECDHE)
                                              <-- ServerHelloDone
  ClientKeyExchange -->
  [ChangeCipherSpec] -->
  Finished -->
                                              <-- [ChangeCipherSpec]
                                              <-- Finished
  Application Data <-->

The client sends a hello, the server replies with everything needed to pin down the cipher and key exchange, and only then can the client respond with its key material and a Finished message. Application data doesn’t move until after that second round trip. On a connection with 80ms RTT to a distant CDN edge, that’s 160ms spent on negotiation before any application data moves.

The root cause is ordering: the client has no idea which key exchange group or cipher the server will pick until ServerHello shows up, so it can’t send a key share in the first flight. So it has to wait.

TLS 1.3’s fix: guess the group instead of negotiating it

TLS 1.3 collapses the handshake by having the client guess which (EC)DHE group the server prefers and send a key share speculatively, right in the ClientHello:

Client                                               Server
  ClientHello
    + key_share (e.g. x25519 public value)
    + supported_versions, signature_algorithms -->
                                              <-- ServerHello
                                                    + key_share
                                              <-- {EncryptedExtensions}
                                              <-- {Certificate}
                                              <-- {CertificateVerify}
                                              <-- {Finished}
  {Finished} -->
  Application Data <-->                       <-- Application Data

Everything in {} is encrypted. The server derives handshake traffic keys from its own ephemeral share plus the client’s key_share immediately after ClientHello arrives, so even the certificate exchange is now confidential. That closes a leak 1.2 had for years: certificates went over the wire in cleartext, so a passive eavesdropper could see which server you were talking to. Because the client already sent a key share up front, the server can finish the key agreement and start pushing encrypted application data in its very first response. Once the client has ServerHello and Finished, it can start sending data too, with no extra round trip to acknowledge anything. That’s the 1-RTT.

If the client guesses wrong (rare these days since x25519 is close to universal), the server sends a HelloRetryRequest and you’re back to something like 2-RTT. That’s the exception, though, not what happens on a normal connection.

You can watch this happen directly:

$ curl -v --tlsv1.3 https://example.com 2>&1 | grep -i 'TLS\|SSL connection'
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CV (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384

Notice there’s no ServerKeyExchange or ClientKeyExchange message type anymore. Key exchange data rides inside the Hello messages as an extension (key_share) instead of being its own standalone handshake message.

Static RSA key exchange is gone

TLS 1.2 allowed TLS_RSA_WITH_* cipher suites, where the client just encrypts a premaster secret directly with the server’s RSA public key. There’s no Diffie-Hellman anywhere. It was fast (one asymmetric operation, no DHE math) but bad for confidentiality: if the server’s private key is ever compromised, subpoenaed, or pulled out via something like Heartbleed, every past session a passive adversary ever recorded becomes decryptable after the fact. There’s no forward secrecy, because one static key protects every session’s secret.

RFC 8446 removes static RSA key exchange entirely. TLS 1.3 only supports (EC)DHE-based exchange, plus PSK for resumption, which I’ll get to below. Every handshake negotiates a fresh ephemeral key pair, so compromising the server’s long-term signing key lets an attacker impersonate the server from that point forward, but it buys them nothing against traffic captured earlier. You can no longer misconfigure forward secrecy away with a cipher-suite setting, because the protocol always provides it.

Renegotiation is gone too. TLS 1.2’s renegotiation, re-running the handshake mid-connection to rotate keys or ask for a client cert, was the root cause of CVE-2009-3555, a plaintext-injection bug that needed the RFC 5746 patch as a partial band-aid. TLS 1.3 replaces that general-purpose “redo the whole handshake” mechanism with two narrower ones: post-handshake authentication (a CertificateRequest sent after the handshake, for mTLS-style client cert checks) and a dedicated KeyUpdate message for rotating keys.

0-RTT resumption trades replay protection for speed

For repeat connections, TLS 1.3 offers 0-RTT via PSK resumption. The client reuses a session ticket from an earlier connection and sends encrypted application data in the very first flight, before the server has said a word back:

Client                                               Server
  ClientHello + early_data + psk extension
  (encrypted with early_data_key)
  Application Data (0-RTT) -->
                                              <-- ServerHello, ... Finished
  Application Data <-->

That’s zero round trips of latency before data moves, which helps a mobile client reconnecting to a server it already knows. The catch is that 0-RTT data has no replay protection. The client sends it before the handshake finishes, specifically before the server’s Finished message, which is the thing that actually proves liveness and ties the exchange together. A network-positioned attacker can just capture that first flight and replay it verbatim. At that layer the server can’t tell a replay from a legitimate retransmission.

RFC 8446 Section 8 doesn’t try to fix this cryptographically. It leaves the problem to the application: only use 0-RTT for idempotent requests (safe GETs, not POST /transfer-funds), and let servers implement their own anti-replay measures, such as single-use tickets or bounded replay caches. Those measures give back some of the latency win in exchange for safety. I’ve seen teams enable 0-RTT, forget the tradeoff, and get surprised when a retry storm double-charges something. Most production setups (NGINX, HAProxy, Cloudflare) either disable 0-RTT outside of static content or require the application to opt in per request.

Checking your own setup

If you’re terminating TLS yourself, not sitting behind a CDN, confirm your stack is actually negotiating 1.3 and not quietly falling back to 1.2 for compatibility reasons that stopped mattering years ago. Run openssl s_client -connect host:443 -tls1_3 and check for Protocol: TLSv1.3 in the output.

Drop any RSA key-exchange cipher suites still sitting in your 1.2 fallback config. Those are the forward secrecy hole TLS 1.3 was designed to close.

If you turn on 0-RTT, go audit which endpoints can actually receive early data. Most servers let you scope this. Default to none until you’ve confirmed an endpoint is idempotent rather than assuming it.

TLS 1.3 is more than TLS 1.2 with a speed boost. Static RSA key exchange is gone, renegotiation is replaced by narrower mechanisms, and 0-RTT gives up replay protection for latency. Know which guarantees you’re giving up before you turn it on.

Comments

Leave a Reply