# The TLS 1.3 Handshake: What Those Milliseconds Buy

> One round trip now negotiates keys, proves identity, and encrypts everything after the first message. What actually happens in a TLS 1.3 handshake, why it's structurally safer than 1.2, and where the remaining sharp edges live.

URL: https://harshith.in/blog/tls-handshake-mechanics/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: security, networking, systems

Every HTTPS connection begins with a negotiation that has to solve an
absurd-sounding problem: two parties who've never met, over a wire an
attacker fully controls, must agree on a secret key *and* verify who
they're talking to — in one round trip, because milliseconds are money.
TLS 1.3 (RFC 8446, 2018) is the cleanest answer cryptography has shipped,
mostly because of what it *removed*. Knowing its moving parts is the
difference between debugging certificate errors by ritual and by reading.

## One Round Trip, Three Jobs

```text
  ClientHello ──▶   supported versions/ciphers, SNI,
                    + ECDHE key share (a guess, sent upfront)
              ◀── ServerHello (chosen cipher, server's key share)
                  ── everything below already encrypted ──
              ◀── EncryptedExtensions, Certificate,
                  CertificateVerify, Finished
  Finished    ──▶   application data flows
```

**Job 1 — key agreement** is ephemeral Diffie-Hellman (X25519 in practice):
each side contributes a fresh keypair, both derive the same shared secret,
an eavesdropper recording everything learns nothing. The 1.3 trick for
speed: the client sends its key share *speculatively* in the first flight,
gambling the server will accept X25519 (it will), collapsing 1.2's two
round trips into one. Note what's gone: RSA key transport — 1.2's mode
where the client encrypted the secret to the server's long-term key —
which meant one leaked server key retroactively decrypted every recorded
session. Ephemeral-only means **forward secrecy is mandatory**, and an
entire class of both attacks and passive-decryption middleboxes died with
it (enterprises now run explicit MITM proxies instead — at least the
model is honest).

**Job 2 — authentication** rides the certificate chain: the server sends
its cert plus intermediates; the client builds a path to a trusted root,
checks names (SNI in, SAN out — CN has been dead for years), validity
windows, and signatures; then — the step people forget exists —
`CertificateVerify`: the server *signs the handshake transcript* with the
certificate's private key, proving it actually holds it. Without that, a
certificate is just a public document anyone could replay.

**Job 3 — integrity of the negotiation itself**: both `Finished` messages
MAC the entire transcript, so a middlebox stripping strong ciphers from
the ClientHello (the downgrade attacks that plagued 1.2 — FREAK, Logjam)
breaks the handshake. 1.3 also encrypts everything after the ServerHello,
so certificates travel encrypted — network observers get SNI (until ECH
lands widely) and little else.

## Resumption, and the 0-RTT Footgun

Reconnecting clients get session tickets (PSKs) and skip certificate
work — cheap and uncontroversial. The controversial gift is **0-RTT**:
sending application data *in the first flight*, encrypted under the
resumed key, before any server response. Free latency — and structurally
**replayable**: an attacker can capture the flight and deliver it again;
no handshake-completion proof protects it. Safe only for idempotent
requests, which is why serious deployments restrict it (CDNs allow GETs
without bodies and reject the rest with 425 Too Early) and why "we enabled
0-RTT globally" belongs in a risk review, not a performance PR.

## Where It Actually Breaks

Production TLS failures cluster in unglamorous places, none of them the
cryptography: **incomplete chains** (server sends leaf without
intermediate — works in browsers that cache/fetch intermediates, fails in
curl/openssl/your language runtime; the source of "works in Chrome,
fails in prod" forever; `openssl s_client -connect host:443 -servername
host` shows exactly what's sent), **expiry** (monitor `-enddate`
programmatically; outages by calendar are self-inflicted), **name
mismatches** (SNI-less clients hitting multi-cert servers get the default
cert), and **trust-store drift** (containers FROM scratch, old CA bundles
— ask the box, not the spec: `curl -v` names the failing step). The
debugging kit is three commands deep: `openssl s_client` (handshake
transcript + chain), `openssl x509 -text` (read the cert), and your
load balancer's config, because TLS terminates there and the backend hop
is a separate security decision people forget to make.

## Takeaways

- 1.3 = one round trip: speculative ECDHE share, chain + transcript
  signature for identity, transcript MAC against downgrade.
- Forward secrecy is mandatory now — key compromise no longer decrypts
  history; passive-decrypt middleboxes are extinct by design.
- CertificateVerify is why possession is proven, not just presented;
  everything after ServerHello travels encrypted.
- 0-RTT data is replayable by construction — idempotent requests only,
  enforced, or leave it off.
- Real-world failures: chains, expiry, SNI/SAN, trust stores — all
  legible via openssl s_client before anyone blames the crypto.
