The TLS Handshake
Before the first encrypted byte, client and server negotiate a version and cipher suite, the server proves its identity with a certificate and a signature, and both derive the same session key from an ephemeral key exchange — one round trip in TLS 1.3, two in TLS 1.2, on top of TCP’s own.
The problem
api.example.com — using as few round trips as possible, because each one costs a full RTT before the application can send anything.Progressive depth
The same mechanism at different altitudes — start where you are.
The client says what it can do, the server picks and proves its identity with a certificate, both sides derive the same secret, and everything after that is encrypted. One extra round trip before the first request.
The shape of the handshake
The handshake is a negotiation followed by a proof. The client opens with a Client Hello: the highest TLS version it supports, a list of cipher suites in preference order, the hostname it wants (SNI, so a server hosting many sites can pick the right certificate), the application protocols it can speak (ALPN: h2, http/1.1, h3), and — in TLS 1.3 — a key share: a fresh ephemeral ECDHE public value, sent speculatively before it knows what the server will pick.
The server answers with a Server Hello choosing the version and suite, plus its own key share. At that moment both sides can compute the same shared secret, and in TLS 1.3 everything after the Server Hello is already encrypted with keys derived from it. The server then sends its Certificate chain, a CertificateVerify — a signature over the handshake transcript made with the certificate’s private key, which is what proves the server holds that key — and Finished, a MAC over the transcript that proves the negotiation was not tampered with.
The client validates the chain against its trust store (see Certificates and the Chain of Trust), checks the hostname, verifies the signature, verifies Finished, and sends its own Finished. From then on both sides exchange application data in encrypted records. Client authentication (mTLS) adds a CertificateRequest and a client certificate in the same flight; it is common between services, rare between browsers and websites.
Versions and cipher suites change the details
Everything above is the TLS 1.3 flow; the exact messages depend on the version and the negotiated suite, and it is a mistake to describe "the" TLS handshake without saying which. In TLS 1.2 the client does not send a key share up front. It sends Client Hello, waits for Server Hello + Certificate + ServerKeyExchange (the server’s ECDHE value, signed) + ServerHelloDone, then sends ClientKeyExchange + ChangeCipherSpec + Finished, and only after the server’s ChangeCipherSpec + Finished can it send application data. That is two round trips before the first request byte, versus one for 1.3.
TLS 1.2 also allowed suites where the key exchange was RSA — the client encrypted a random premaster secret with the server’s public key. No ephemeral key means no forward secrecy: a stolen server key decrypts every recorded session. TLS 1.3 removed static RSA, removed every suite without AEAD (CBC-mode, RC4), removed renegotiation and compression, and cut the suite list to five names of the form TLS_AES_128_GCM_SHA256 — the key exchange and signature are negotiated separately via extensions rather than baked into the suite name.
Cipher suite strings therefore read differently by version. A 1.2 suite ECDHE-RSA-AES128-GCM-SHA256 names key exchange, authentication, cipher and hash; a 1.3 suite TLS_CHACHA20_POLY1305_SHA256 names only cipher and hash. When you see TLS_RSA_WITH_AES_256_CBC_SHA in a scan, that is a 1.2-era suite with no forward secrecy and a non-AEAD cipher — a finding, not a curiosity.
| Version | Round trips before application data | Forward secrecy | Encrypted handshake | Resumption |
|---|---|---|---|---|
| TLS 1.2 | 2 RTT | Only with (EC)DHE suites; RSA key exchange has none | No — certificate sent in the clear | Session IDs or tickets; 1-RTT abbreviated handshake, no 0-RTT |
| TLS 1.3 | 1 RTT | Always (ephemeral key share mandatory) | Yes — everything after Server Hello | PSK via NewSessionTicket; 1-RTT, optionally 0-RTT early data |
Resumption, 0-RTT and its replay caveat
A full handshake costs an RTT and the asymmetric operations; a client returning to the same server should not pay again. After a 1.3 handshake the server sends one or more `NewSessionTicket` messages carrying an opaque, server-encrypted ticket. The client presents it as a pre-shared key in its next Client Hello and skips certificate transmission and verification; the handshake is still 1 RTT, and with psk_dhe_ke mode a fresh ECDHE exchange still happens so forward secrecy holds. TLS 1.2 had session IDs (server-side cache) and session tickets (RFC 5077) with an abbreviated 1-RTT handshake.
TLS 1.3 goes one step further: with a PSK the client may send 0-RTT early data — application bytes in the very first flight, encrypted under a key derived from the ticket, before any server response. The saving is an entire RTT for the first request. The cost is that early data is replayable: an attacker who captured the first flight can resend it and the server will decrypt and process it again, because nothing from the server has yet contributed freshness. 0-RTT is therefore safe only for requests that are idempotent by construction. Browsers and CDNs restrict it to GET without a body, and a well-configured server either rejects early data for non-idempotent paths or is engineered so that replaying them is harmless.
Tickets have a lifetime (hours to a week) and are bound to the server’s ticket-encryption key; rotating that key invalidates outstanding tickets, and a fleet behind a load balancer must share it or resumption silently fails and every connection pays a full handshake — visible as a jump in handshake CPU and latency after a deploy.
- Full 1.3 handshake: 1 RTT + one signature verification + one ECDHE. Resumed: 1 RTT, no certificate, optional ECDHE. 0-RTT: request in the first flight, replayable.
- HTTP/3 uses the same TLS 1.3 machinery and the same 0-RTT rules — see HTTP/3 and QUIC.
Early-Data: 1is the header a proxy adds so an origin can refuse to act on a replayable request (HTTP 425 Too Early).
What it costs on top of TCP
TLS runs over an established TCP connection, so its RTTs add to the The Three-Way Handshake. On a cold connection to a server 100 ms away: 1 RTT for SYN/SYN-ACK, then 1 RTT for TLS 1.3 (2 for 1.2), then 1 RTT for request and first byte of response — roughly 300 ms before any application data arrives with 1.3, 400 ms with 1.2, before a single byte of the page has been transferred. Cross-continent RTTs of 150–250 ms turn that into over half a second, which is the whole motivation for Keep-Alive and Connection Reuse and Connection Pooling (do the handshake once) and for QUIC (fold the transport and TLS handshakes into one RTT).
The CPU cost lands almost entirely in the handshake: a P-256 ECDHE plus an RSA-2048 signature verification is on the order of 100–300 µs on a modern core; an ECDSA P-256 server certificate makes the server’s signing side roughly ten times cheaper than RSA, which is why large deployments prefer ECDSA certificates. Once keys are established, AES-GCM with hardware support encrypts at several gigabytes per second per core — bulk encryption is rarely the bottleneck; handshake rate under a connection storm frequently is.
CONNECTED(00000003)
depth=2 C = US, O = DigiCert Inc, CN = DigiCert Global Root G2
verify return:1
depth=1 C = US, O = DigiCert Inc, CN = DigiCert Global G2 TLS RSA SHA256 2020 CA1
verify return:1
depth=0 C = US, ST = California, O = Internet Corporation for Assigned Names and Numbers, CN = www.example.org
verify return:1
---
Certificate chain
0 s:CN = www.example.org
i:CN = DigiCert Global G2 TLS RSA SHA256 2020 CA1
1 s:CN = DigiCert Global G2 TLS RSA SHA256 2020 CA1
i:CN = DigiCert Global Root G2
---
SSL handshake has read 4512 bytes and written 393 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
Protocol: TLSv1.3
Verify return code: 0 (ok)
---
Post-Handshake New Session Ticket arrived:
Resumption PSK: ...
TLS session ticket lifetime hint: 86400 (seconds)Key points
- Client Hello offers versions, suites, SNI, ALPN and (in 1.3) an ephemeral key share; Server Hello picks and replies with its own share, and from then on the handshake is encrypted.
- The certificate proves the binding of hostname to public key;
CertificateVerifyproves the server holds the matching private key; Finished proves the transcript was not altered. - TLS 1.3 is 1 RTT, TLS 1.2 is 2 RTT; the exact messages depend on the version and cipher suite.
- Forward secrecy comes from ephemeral ECDHE; TLS 1.3 makes it mandatory by removing static RSA key exchange.
- Resumption via tickets/PSK skips certificate work; 0-RTT early data saves a full RTT but is replayable and must be limited to idempotent requests.
- TLS RTTs stack on top of TCP’s: a cold HTTPS request is ≥ 3 RTTs to first byte with 1.3, which is why connection reuse matters so much.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why does the client send a key share before it knows what the server supports?
To save a round trip. TLS 1.3 clients guess the server will accept X25519 or P-256 and send a share for it speculatively; if the server wants a different group it replies with HelloRetryRequest and the handshake costs one extra RTT — a rare path in practice.
▸Why is the certificate encrypted in TLS 1.3 but not in 1.2?
Because in 1.3 the key exchange completes at the Server Hello, so handshake keys exist before the certificate is sent. Hiding the certificate from passive observers reduces what the path learns about who you are talking to; SNI in the Client Hello still leaks the hostname unless ECH is used.
▸Why is 0-RTT data replayable when the rest of the handshake is not?
Anti-replay needs a value the attacker cannot predict contributed by the receiver — the server’s random or key share. In the first flight the server has contributed nothing yet, so the only defence is the application: idempotent requests only, or server-side single-use ticket tracking.
▸Why does resumption still do an ECDHE exchange?
A PSK alone means a leaked ticket key decrypts every resumed session. Mixing in a fresh ephemeral exchange (psk_dhe_ke) keeps forward secrecy at the cost of one cheap key agreement.
TLS handshake
- ClientHello versions[1.3,1.2] suites[AES_128_GCM_SHA256, CHACHA20_POLY1305] SNI=engineer-atlas.dev key_share(x25519)▶
- ◀ServerHello suite=TLS_AES_128_GCM_SHA256 key_share(x25519)
- ◀{EncryptedExtensions}
- ◀{Certificate} leaf + intermediate
- ◀{CertificateVerify} sig(transcript) with the leaf private key
- ◀{Finished} HMAC(transcript)
- client validates the chain (hostname, dates, signatures up to a trusted root), verifies CertificateVerify, derives application traffic keys
- {Finished}▶
- [GET / HTTP/1.1] application data▶
- ◀[200 OK] application data
How it fails
What the failure looks like from inside real software.
SSL_ERROR_HANDSHAKE_FAILURE_ALERT/handshake failure: no cipher suite or version in common — a client pinned to TLS 1.0 or a server configured with only 1.3 suites the client library does not know.- Every request pays a full handshake: keep-alive is disabled or ticket keys are not shared across the fleet, so resumption never succeeds; handshake CPU and p50 latency both climb.
- 0-RTT replay: a
POSTaccepted as early data is replayed by an attacker and processed twice; the fix is refusing non-idempotent early data (HTTP 425) or disabling 0-RTT. - Wrong certificate served because SNI was not sent — a client connecting by IP or an old library gets the server’s default certificate and fails hostname validation.
- Handshake timeouts under load: the server’s accept queue fills with connections waiting for the CPU-bound signature step; symptoms are
ETIMEDOUTduring connect rather than during the request. - A middlebox that cannot parse a large Client Hello (post-quantum key share, ECH) drops it silently; the connection hangs at the first flight and works only with a smaller Hello.