Before a browser sends a single byte of your request, it and the server run a negotiation: agree on a cipher suite, agree on a shared secret neither one ever transmits, and prove the server's identity — all in one round trip under TLS 1.3, two under the TLS 1.2 that's finally aging out. The mechanism doing the work is (EC)DHE key exchange wrapped in a fixed message sequence: ClientHello, ServerHello, Certificate, Finished. Everything after the second message is already encrypted. That's the whole handshake — the rest of this is what's actually inside each of those messages and why the sequence is shaped the way it is.
What actually goes over the wire
Byte traces of a TLS handshake are surprisingly rare online, so here's a real one — captured locally with openssl s_client -tls1_3 -msg against a local test server, so the CN is localhost rather than a real domain, but the message sequence and structure are identical to any TLS 1.3 connection you'll open today:
>>> TLS 1.3, Handshake, ClientHello
...cipher_suites: 13 02 13 03 13 01 (AES-256-GCM, ChaCha20-Poly1305, AES-128-GCM)
...key_share: group 001d (x25519), 32-byte ephemeral public key
...supported_versions: 03 04 (TLS 1.3)
<<< TLS 1.3, Handshake, ServerHello
...cipher_suites: 13 02 (server picked AES-256-GCM-SHA384)
...key_share: group 001d (x25519), 32-byte ephemeral public key
--- everything below here is encrypted ---
<<< TLS 1.3, Handshake, EncryptedExtensions
<<< TLS 1.3, Handshake, Certificate
<<< TLS 1.3, Handshake, CertificateVerify
<<< TLS 1.3, Handshake, Finished
>>> TLS 1.3, Handshake, Finished
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384Two things worth noticing directly in that trace. First, the client doesn't wait to be told what key exchange group to use — it guesses, sending a fresh x25519 public key in the very first message alongside a list of groups it also supports (supported_groups includes P-256, P-384, P-521, and a few finite-field Diffie-Hellman groups). If the server agrees with the guess, which it almost always does, the whole exchange finishes in one round trip. Second, the ServerHello is the last plaintext message — EncryptedExtensions onward is already protected by keys both sides just derived from that x25519 exchange, before either side has proven anything about identity.
That ordering is deliberate. Once both sides have their shared secret, encrypting the certificate and its chain costs nothing extra and closes off passive eavesdropping on who you're connecting to — a real, if minor, privacy leak that TLS 1.2 had (its certificate was sent in the clear).
One round trip, not two
TLS 1.2 negotiated the cipher suite and the key exchange group as separate steps, which meant a full round trip just to agree on parameters before the actual key exchange could start:
| TLS 1.2 | TLS 1.3 | |
|---|---|---|
| Cipher/group negotiation | Separate round trip (ClientHello → ServerHello, then a second exchange for key material) | Client guesses the group and sends its key share in ClientHello |
| Round trips before app data | 2-RTT | 1-RTT (0-RTT possible on resumption) |
| Static RSA key exchange | Allowed | Removed entirely — every cipher suite requires (EC)DHE |
| Certificate visibility | Sent in cleartext | Encrypted, sent after the key exchange completes |
| Renegotiation | Supported (and the source of several CVEs) | Removed; KeyUpdate replaces it for rotating traffic keys mid-connection |
The client's guess-and-go approach is why 1-RTT works: instead of asking "which group do you support?" and waiting for an answer, the client just sends a key share for whatever it thinks the server will pick — x25519 today, essentially always — and only falls back to a second round trip (HelloRetryRequest) if the server disagrees. On the open internet that fallback is rare enough that 1-RTT is the practical default, not a best case.
Losing a full round trip matters more than it sounds like: on a connection with 150ms of latency to the server, that's 150ms shaved off every new connection before the first byte of your response even starts — no code change, just the protocol version. It's also why TLS 1.3 added a 0-RTT resumption mode for repeat visits: a returning client can send encrypted application data in its very first flight, using a pre-shared key left over from an earlier session, with no round trip spent waiting at all. That speed comes at a real cost, covered below.
Why RSA key exchange had to go
The strikethrough on "static RSA key exchange" in that table isn't a stylistic call — RFC 8446 forbids it outright, and the reason is forward secrecy. In the deleted RSA mode, the client picked the shared secret itself, encrypted it with the server's long-lived RSA public key, and sent it over. That's a shared secret protected by exactly one key, forever: if that RSA private key is ever compromised — stolen, subpoenaed, or cracked years later — every past session recorded off the wire becomes readable retroactively, because the same key that authenticates the server also protected the secret.
(EC)DHE avoids that by generating a brand-new key pair per connection purely for the key exchange, discarded the moment the handshake ends. The certificate's long-lived key still proves identity — via the CertificateVerify signature in the trace above — but it never touches the shared secret directly, so compromising it later doesn't unlock anything already recorded. That's the entire point of the x25519 exchange in the trace: a fresh key pair, used once, with the certificate playing prosecutor rather than participant. The math behind why elliptic-curve exchange won that job over RSA — smaller keys, cheaper per-connection cost — is its own topic, covered in our RSA vs ECC piece; the SSL/TLS certificate fields breakdown covers what's actually inside that Certificate message once you decode it.
Common mistakes
Assuming a fresh handshake happens on every request
A TLS connection, once established, stays open and reused for many HTTP requests (HTTP/1.1 keep-alive, or a single HTTP/2 or HTTP/3 connection multiplexing dozens of requests) — the handshake traced above runs once per connection, not once per request. And even a brand-new connection often skips the full handshake: NewSessionTicket messages issued after the first handshake let a returning client resume with a pre-shared key instead of redoing the (EC)DHE exchange, which is where 0-RTT comes from.
Treating 0-RTT early data as equivalent to normal application data
0-RTT data is sent before the handshake's Finished messages complete, using a key derived from the previous session's secret rather than a fresh exchange — which means it has no forward secrecy for that specific data, and worse, a network attacker who captures a 0-RTT flight can replay it verbatim and the server has no built-in way to tell the replay from the original. RFC 8446 is explicit that 0-RTT should only ever carry idempotent requests (a GET is fine; a POST that charges a credit card is not) unless the application layer adds its own replay defense. Plenty of production 0-RTT deployments have shipped this wrong by simply enabling it and not auditing which requests can arrive twice.
Confusing "the handshake completed" with "the connection is safe end-to-end"
If a CDN or load balancer terminates TLS at the edge, the handshake you can observe from a browser proves the browser is talking securely to that edge — it says nothing about whether the hop from edge to origin is also encrypted. That's a separate, often-skipped configuration decision, and it's a common gap: a service can pass every external TLS check while shipping plaintext internally.
FAQ
Does a weak cipher suite in the ClientHello mean the connection is insecure?
Only if the server actually selects it. The client lists what it supports (often including some deliberately conservative options for legacy compatibility) but the server picks; a server hardened to reject weak suites is protected regardless of what the client offers. If you're auditing a server's own configuration rather than a client's, our TLS/SSL config snippet grader checks exactly this — cipher suite strength, forward secrecy, and protocol version — against your actual ssl_ciphers/SSLCipherSuite directives.
Why did my connection do a HelloRetryRequest and add a round trip?
The client guessed a key-exchange group the server doesn't support (or doesn't prefer) — a client that only offers, say, secp256r1 against a server configured to require x25519 triggers exactly this. It's not an error, just the fallback path RFC 8446 defines for when the 1-RTT guess misses; the handshake still completes, just at TLS 1.2's two-round-trip cost instead of one.
Where does the certificate's public key actually get used, if not for the shared secret?
Purely for authentication: the server signs a hash of the entire handshake transcript so far with its certificate's private key (the CertificateVerify message in the trace), and the client verifies that signature against the public key in the certificate. That's what proves the server holding the certificate is the one actually on the other end of this specific connection — a completely separate job from the x25519 exchange that produced the shared secret those messages are encrypted with.
Cover photo by Brett Sayles on Pexels.
