TLS 1.3 (RFC 8446) is what secures most HTTPS traffic today. The handshake is compact — one round trip in the common case — and the protocol eliminates an entire class of vulnerabilities present in TLS 1.2. This post walks through the mechanics: what each message contains, what cryptographic properties it establishes, and why.
What the Handshake Must Accomplish
Before any application data flows, TLS must establish:
- Confidentiality — session keys derived from a secret unknown to passive observers
- Forward secrecy — compromise of long-term keys doesn’t expose past sessions
- Authentication — at least the server is who it claims to be
- Integrity — messages can’t be tampered with in transit
TLS 1.3 achieves all four in one round trip under normal conditions.
The Full Handshake (1-RTT)
Client Server
│ │
│──── ClientHello ─────────────────────────────▶│
│ (supported cipher suites, key shares, │
│ supported groups, SNI extension) │
│ │
│◀─── ServerHello ─────────────────────────────│
│ (selected cipher suite, key share) │
│◀─── {EncryptedExtensions} ───────────────────│
│◀─── {Certificate} ────────────────────────────│
│◀─── {CertificateVerify} ──────────────────────│
│◀─── {Finished} ────────────────────────────── │
│ │
│──── {Finished} ──────────────────────────────▶│
│ │
│════ Application Data ═════════════════════════│
Curly braces {} denote encrypted messages. Encryption starts immediately after ServerHello.
ClientHello
The client sends:
- Supported cipher suites — in TLS 1.3, the list is short:
TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256. Cipher suites in 1.3 only specify AEAD algorithm + hash — key exchange is handled separately. - Supported groups — named elliptic curves or finite-field DH groups the client can use for key exchange:
x25519,secp256r1,secp384r1. - Key shares — one or more DH public keys for the client’s predicted group choices. If the server agrees, a full key exchange happens this round trip without additional negotiation.
- SNI — the target hostname, used for virtual hosting and certificate selection. This is plaintext in the handshake; ECH addresses this.
- Session ticket — if resuming a previous session (0-RTT), the client includes this.
ServerHello and Key Derivation
The server selects a cipher suite and group, generates its own DH key pair, and responds with its public key share. Both sides now have all inputs for key derivation.
For x25519 (the most common group in practice):
client_private, client_public = X25519.keygen()
server_private, server_public = X25519.keygen()
shared_secret = X25519(client_private, server_public)
= X25519(server_private, client_public)
# Same value, computed independently
The shared_secret is fed into the TLS 1.3 key schedule — an HKDF-based construction that derives multiple keys:
HKDF-Extract(salt=0, IKM=0) → early_secret
HKDF-Extract(salt=derived_secret, IKM=shared_secret) → handshake_secret
HKDF-Expand-Label(handshake_secret, "c hs traffic", ...) → client_handshake_traffic_secret
HKDF-Expand-Label(handshake_secret, "s hs traffic", ...) → server_handshake_traffic_secret
Separate keys for client and server, separate keys for handshake and application data, all derived from the ephemeral DH shared secret. The server’s long-term private key is not used in key derivation — it’s only used to authenticate.
This is what provides forward secrecy: each session uses a fresh ephemeral DH key pair. Compromising the server’s RSA/ECDSA private key later doesn’t reveal session keys, because those were derived from ephemeral keys that are discarded after the session.
Server Authentication
After ServerHello, the server sends (encrypted):
Certificate — the server’s certificate chain. The leaf certificate contains the server’s public key (RSA or ECDSA) and its binding to a domain name, signed by a CA.
CertificateVerify — a signature over a hash of the entire handshake transcript so far, using the server’s private key:
signature = Sign(server_private_key,
SHA256("TLS 1.3, server CertificateVerify\0" || transcript_hash))
This proves the server possesses the private key corresponding to the certificate’s public key, and that the handshake messages haven’t been tampered with. The transcript includes the ClientHello, so the signature also binds the server identity to this specific handshake.
Finished — an HMAC over the handshake transcript using the server_handshake_traffic_secret. Confirms the server’s key derivation matches the client’s.
Client Finished
The client verifies the certificate chain against trusted CAs, checks the domain name, verifies CertificateVerify, checks Finished, then sends its own Finished message:
client_finished = HMAC(client_handshake_traffic_secret, transcript_hash)
After this, both sides derive application traffic keys from master_secret and begin sending application data.
0-RTT Resumption
TLS 1.3 supports 0-RTT data: the client can send application data in its first flight, before the handshake completes, using a pre-shared key from a prior session.
Client Server
│──── ClientHello ────────────────▶│
│──── {0-RTT Application Data} ───▶│
│ │
│◀─── ServerHello ─────────────────│
│◀─── ... (rest of handshake) ─────│
The trade-off: 0-RTT data has no forward secrecy (it uses a key derived from the previous session’s resumption secret) and is replay-vulnerable — a network attacker can replay 0-RTT packets to a different server instance. Applications must treat 0-RTT data as potentially replayed. It’s appropriate for idempotent requests (GET, read-only APIs), not state-mutating operations.
What TLS 1.3 Removed from 1.2
TLS 1.3 eliminated:
- RSA key exchange — no more static RSA key transport, which had no forward secrecy
- CBC cipher suites — eliminated BEAST, POODLE, Lucky13 attack surfaces
- RC4, 3DES, MD5, SHA-1 — removed entirely
- Renegotiation — removed the protocol feature that enabled multiple attack classes
- Compression — removed after CRIME
The result is a protocol with a dramatically smaller attack surface. The cipher suite list collapsed from dozens of options (many weak) to five, all AEAD.
Certificate Transparency
Since 2018, Chrome requires all publicly-trusted TLS certificates to be logged to CT logs — append-only, publicly auditable records. CAs submit certificates to logs and include signed timestamps (SCTs) in the certificate or TLS extension. Clients can verify SCTs to confirm a certificate was logged before being used.
CT doesn’t prevent certificate misissuance — it makes it detectable. Domain owners can monitor CT logs for unauthorized certificates:
# Query crt.sh for certificates issued to your domain
curl "https://crt.sh/?q=example.com&output=json" | jq '.[].name_value'
Summary
TLS 1.3 establishes confidentiality and forward secrecy via ephemeral DH key exchange, authentication via certificate + CertificateVerify signature, and integrity via AEAD ciphers and Finished MACs. The handshake takes one round trip. The cryptographic design is clean — no weak ciphers, no static key transport, no unauthenticated legacy paths. Understanding it is prerequisite to reasoning about certificate pinning, mTLS, HSTS, and the ECH work that’s closing the SNI privacy gap.