The TLS handshake

Two strangers shouting across a crowded room agree on a secret nobody else can reconstruct, prove who they are, then whisper privately.

The idea

When your browser opens a plain TCP connection to a server, anyone on the path can read every byte. TLS turns that exposed pipe into a private one, out in the open, with no secret arranged in advance.

The two sides first agree on a key using a public exchange that an eavesdropper can watch but not reproduce. The server then proves its identity with a certificate signed by a trusted authority. Only after both happen does any real data flow — now encrypted and tamper-evident. In TLS 1.3 the whole dance takes a single round trip.

See it work

Client your browser shared secret ✓ Server example.com shared secret ✓ channel: plaintext
Press play, or step through the handshake one message at a time.

How it works

The magic is ECDHE — ephemeral elliptic-curve Diffie–Hellman. Each side rolls a fresh random private number and computes a matching public point. They swap only the public points in the clear. Because of the curve's math, each side can combine its own private with the other's public and land on the exact same point — yet an eavesdropper holding both public points cannot. The shared point is never transmitted; it is independently re-derived on both ends.

Because the private keys are thrown away after the connection (ephemeral), recording the traffic and stealing the server's long-term key later still won't decrypt it. That property is forward secrecy. The certificate and CertificateVerify are a separate job: they prove the server actually holds the private key for the name you dialled.

# Simplified ECDHE — conceptually correct, not real curve code.

# 1. Each side picks a fresh ephemeral private scalar (a secret number)
client_priv = random_scalar()
server_priv = random_scalar()

# 2. Derive the matching public point and send ONLY this on the wire
client_pub = scalar_mult(client_priv, BASE_POINT)   # in ClientHello
server_pub = scalar_mult(server_priv, BASE_POINT)   # in ServerHello

# 3. Each side combines its own private with the other's public.
#    Curve math guarantees both reach the SAME point — never sent.
shared_c = scalar_mult(client_priv, server_pub)
shared_s = scalar_mult(server_priv, client_pub)
assert shared_c == shared_s          # identical on both ends

# 4. Stretch that shared point into real keys, bound to the
#    transcript so a tampered handshake yields different keys.
keys = HKDF(shared_c, transcript_hash(client_hello, server_hello))
# -> separate keys per direction, fed to an AEAD cipher (e.g.
#    AES-GCM) that gives confidentiality AND integrity at once.

Cost / trade-offs

ChoiceBenefitCost / risk
TLS 1.3 full handshake (1-RTT) Encrypted app data after one round trip; key share rides in the first messages Still one network round trip of latency before app data
TLS 1.2 full handshake (2-RTT) Widely deployed, well understood Extra round trip; many legacy suites lack forward secrecy
0-RTT resumption Send app data in the very first flight — zero added latency Early data is replayable; only safe for idempotent requests
Asymmetric crypto at connect Enables key agreement + signature without a pre-shared secret CPU-heavy; one-time cost paid per new handshake
Symmetric AEAD afterward Cheap, fast per-record encryption + integrity for all app data Useless without the handshake that bootstrapped its keys
Session resumption (PSK ticket) Skips certificate + signature work on reconnect Ticket lifetime + storage; replay surface if paired with 0-RTT

Watch out for

Worked example

You type https://example.com. After the TCP connection opens, your browser sends a ClientHello: "I speak TLS 1.3, here are the cipher suites I support, and here is my ephemeral public key share." The server replies with one flight — a ServerHello carrying its own key share, then (now encrypted) its Certificate for example.com, a CertificateVerify signature over the handshake transcript proving it holds that cert's private key, and a Finished MAC over everything said so far.

Both sides ran ECDHE the moment the two key shares met, so the certificate and the rest arrived encrypted. Your browser checks the chain up to a trusted root, confirms the name matches, verifies the signature and the server's Finished, then sends its own Finished. One round trip in, the channel is confidential, integrity-protected, and authenticated — your GET / goes out encrypted.

Check yourself

1. The client and server swap their public key shares over a channel an attacker can fully read. Why can't that attacker compute the same shared secret?

2. What does the server's CertificateVerify message actually prove?