TLS Handshake — Step by Step
TLS handshake negotiates a cipher suite, verifies the server's identity via certificates, and derives ephemeral session keys — all in one round trip with TLS 1.3.
The Problem
Every time a client connects to a server over HTTPS, they must agree on encryption parameters, verify identity, and establish shared keys — all without an attacker being able to intercept or forge the exchange. The TLS handshake solves this in milliseconds, but getting it wrong means the encrypted traffic might as well be plaintext.
Mental Model
Like two spies meeting — they verify identity with credentials, agree on a secret code, then communicate in that code. If anyone eavesdrops on the code negotiation, they still cannot decode the messages because the shared secret was never sent over the wire.
Architecture Diagram
How It Works
Every HTTPS connection starts with a TLS handshake. Before a single byte of application data flows, client and server must accomplish three things: agree on cryptographic parameters, verify each other's identity, and derive shared encryption keys. The elegance of TLS is that it does all of this over an untrusted network where anyone can watch the packets.
TLS 1.2 Handshake (2-RTT)
The TLS 1.2 handshake requires two full round trips before the client can send application data. Here is what happens at each step:
Flight 1 — ClientHello: The client sends a ClientHello message containing its maximum supported TLS version, a list of cipher suites it supports (ordered by preference), a 32-byte random nonce, and optionally extensions like SNI (Server Name Indication) that tells the server which hostname the client wants. SNI is critical for servers hosting multiple domains on the same IP address.
Flight 2 — ServerHello + Certificate: The server responds with its chosen TLS version and cipher suite, its own 32-byte random nonce, and its X.509 certificate chain. If using ECDHE key exchange, the server also sends a ServerKeyExchange message with its ephemeral public key, signed with its long-term private key.
Flight 3 — ClientKeyExchange: The client verifies the server's certificate chain up to a trusted root CA. If valid, it sends its own ephemeral public key in a ClientKeyExchange message. Both sides now independently compute the premaster secret using ECDHE. From the premaster secret and both random nonces, they derive the symmetric encryption keys. The client sends ChangeCipherSpec (switching to encrypted mode) and a Finished message containing a hash of the entire handshake transcript.
Flight 4 — Server Finished: The server sends its own ChangeCipherSpec and Finished message. Both sides verify the other's Finished hash matches their local computation. If it matches, the handshake is complete, and encrypted application data flows.
# Watch a TLS 1.2 handshake in detail
openssl s_client -connect example.com:443 -tls1_2 -state -debug
# See just the negotiated parameters
openssl s_client -connect example.com:443 -tls1_2 2>/dev/null | grep -E "Protocol|Cipher|Session-ID"
TLS 1.3 Handshake (1-RTT)
TLS 1.3, defined in RFC 8446, fundamentally restructured the handshake to require only one round trip. The key insight: instead of waiting for the server to choose the key exchange parameters, the client guesses what the server will choose and sends key shares for its preferred groups in the ClientHello.
Flight 1 — ClientHello + Key Shares: The client sends supported cipher suites (TLS 1.3 only allows five), supported key exchange groups (like x25519 or P-256), and actual ECDHE key shares for each group. This is speculative — the client is betting the server will accept one of these groups.
Flight 2 — ServerHello + Encrypted Extensions + Finished: The server picks a cipher suite and key share group, sends its own key share, and both sides immediately derive handshake keys. Everything after ServerHello is encrypted — including the certificate and Finished message. The server sends its Finished in this same flight.
Flight 3 — Client Finished: The client verifies the certificate, sends its Finished, and can immediately send application data. Total: 1 round trip.
# Verify TLS 1.3 is negotiated
curl -v https://example.com 2>&1 | grep "TLSv1.3"
# Test with specific TLS 1.3 cipher
openssl s_client -connect example.com:443 -tls1_3 -ciphersuites TLS_AES_256_GCM_SHA384
What TLS 1.3 Removed
TLS 1.3 was not just an optimization — it was a security cleanup. Several features were removed entirely:
- RSA key exchange — No forward secrecy. If the server's private key is compromised later, all past traffic can be decrypted. Only ECDHE and DHE are allowed.
- CBC-mode ciphers — Vulnerable to padding oracle attacks (POODLE, Lucky13). Only AEAD ciphers (AES-GCM, ChaCha20-Poly1305) are allowed.
- Static DH — Only ephemeral Diffie-Hellman is allowed, ensuring every session has unique keys.
- Compression — Removed due to the CRIME attack.
- Renegotiation — Simplified to post-handshake authentication.
0-RTT Resumption
TLS 1.3 supports 0-RTT (Zero Round Trip Time) resumption for returning clients. After a successful handshake, the server issues a session ticket. On the next connection, the client can send encrypted application data alongside the ClientHello, eliminating even the single round trip.
The catch: 0-RTT data is replayable. An attacker who captures the ClientHello + 0-RTT data can replay it. This means 0-RTT should never be used for non-idempotent requests (POST, DELETE). Most implementations restrict it to GET requests only.
# Check if a server supports 0-RTT
openssl s_client -connect example.com:443 -tls1_3 -sess_out /tmp/sess.pem
openssl s_client -connect example.com:443 -tls1_3 -sess_in /tmp/sess.pem -early_data /tmp/req.txt
Forward Secrecy and Cipher Suites
Forward secrecy is the property that ensures compromising the server's long-term private key does not compromise past session keys. This is achieved through ephemeral key exchange — each session generates a fresh Diffie-Hellman key pair, and the shared secret is derived from these ephemeral keys.
In TLS 1.2, operators had to explicitly choose ECDHE cipher suites to get forward secrecy:
# Good — forward secrecy
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
# Bad — no forward secrecy
TLS_RSA_WITH_AES_256_GCM_SHA384
In TLS 1.3, forward secrecy is mandatory. There are only five cipher suites, and all use ephemeral key exchange:
| Cipher Suite | Key Exchange | Encryption | Use Case |
|---|---|---|---|
| TLS_AES_128_GCM_SHA256 | ECDHE | AES-128-GCM | General purpose, fastest |
| TLS_AES_256_GCM_SHA384 | ECDHE | AES-256-GCM | Higher security margin |
| TLS_CHACHA20_POLY1305_SHA256 | ECDHE | ChaCha20-Poly1305 | Mobile/ARM devices without AES-NI |
ECDHE Key Exchange In Brief
Elliptic Curve Diffie-Hellman Ephemeral works like this: both client and server independently generate a private key and compute a public key on the elliptic curve (commonly x25519). They exchange public keys and each side combines their private key with the other's public key to arrive at the same shared secret. An eavesdropper sees both public keys but cannot compute the shared secret — that is the discrete logarithm problem on elliptic curves.
Session Resumption
Establishing new TLS connections is expensive — the handshake involves public key cryptography, certificate verification, and multiple network round trips. Session resumption lets returning clients skip most of this.
TLS 1.2 offered two mechanisms:
- Session IDs — Server stores session state and sends an ID to the client. On reconnection, client sends the ID, server looks up the state. Problem: server must maintain session cache.
- Session Tickets — Server encrypts the session state and sends it to the client. On reconnection, client sends the ticket back. Server decrypts it. No server-side state needed.
TLS 1.3 uses only PSK (Pre-Shared Key) with session tickets. The ticket includes a PSK identity that the server can use to resume the session. Combined with 0-RTT, this can eliminate the handshake round trip entirely for returning clients.
Debugging TLS in Production
When TLS goes wrong, the error messages are often cryptic. Here is a systematic approach:
# Step 1: Check what the server actually presents
openssl s_client -connect the-server.com:443 -servername the-server.com </dev/null 2>&1 | \
openssl x509 -noout -subject -issuer -dates
# Step 2: Verify the full certificate chain
openssl s_client -connect the-server.com:443 -showcerts 2>/dev/null | \
openssl verify -verbose
# Step 3: Check for protocol/cipher mismatches
openssl s_client -connect the-server.com:443 -tls1_3 2>&1 | grep "Protocol\|Cipher"
openssl s_client -connect the-server.com:443 -tls1_2 2>&1 | grep "Protocol\|Cipher"
# Step 4: Enumerate all supported ciphers
nmap --script ssl-enum-ciphers -p 443 the-server.com
# Step 5: Test with SSL Labs (web-based, thorough)
# https://www.ssllabs.com/ssltest/
Common TLS failure modes and their causes:
| Error | Likely Cause |
|---|---|
SSL_ERROR_HANDSHAKE_FAILURE_ALERT | No common cipher suite between client and server |
CERTIFICATE_VERIFY_FAILED | Expired cert, wrong hostname, or missing intermediate cert |
TLSV1_ALERT_PROTOCOL_VERSION | Server does not support the client's requested TLS version |
SSL_ERROR_SYSCALL | Network-level issue — firewall, timeout, or RST during handshake |
The most common production issue is a missing intermediate certificate. The server must send the full chain (leaf + intermediates). Browsers often cache intermediates and work fine, but API clients, curl, and mobile apps fail. Always test with openssl s_client, not just a browser.
Key Points
- •TLS 1.3 reduced the handshake from 2 round trips to 1, cutting connection setup latency in half.
- •Forward secrecy means a compromised server private key cannot decrypt past sessions — each session uses ephemeral keys.
- •TLS 1.3 removed RSA key exchange entirely. Only ECDHE-based cipher suites are allowed.
- •0-RTT resumption in TLS 1.3 allows sending application data with the first flight, but is vulnerable to replay attacks.
- •The cipher suite determines everything: key exchange algorithm, bulk encryption, and MAC — a bad choice means the connection is insecure.
Key Components
| Component | Role |
|---|---|
| ClientHello | Client sends supported TLS versions, cipher suites, and a random nonce to the server |
| ServerHello | Server selects the TLS version and cipher suite, sends its random nonce |
| Certificate Exchange | Server sends its X.509 certificate chain for the client to verify identity |
| Key Exchange (ECDHE) | Both sides contribute to a shared secret using ephemeral Diffie-Hellman, providing forward secrecy |
| Finished Messages | Both sides send a hash of the entire handshake transcript, encrypted with the new keys, to verify integrity |
When to Use
TLS is mandatory for any production traffic carrying sensitive data. Use TLS 1.3 whenever both endpoints support it. Fall back to TLS 1.2 with ECDHE cipher suites for legacy clients. Never use TLS 1.0/1.1 or SSL 3.0.
Tool Comparison
| Tool | Type | Best For | Scale |
|---|---|---|---|
| OpenSSL | Open Source | Industry-standard TLS library with the most features and widest compatibility | Small-Enterprise |
| BoringSSL | Open Source | Google's hardened fork optimized for Chrome and Android, smaller attack surface | Enterprise |
| LibreSSL | Open Source | OpenBSD's security-focused fork with cleaner codebase, fewer CVEs | Small-Enterprise |
| GnuTLS | Open Source | LGPL-licensed alternative when OpenSSL's license is incompatible | Small-Enterprise |
Debug Checklist
- Run openssl s_client -connect host:443 to see the full handshake, certificate chain, and negotiated cipher.
- Check cipher suites with nmap --script ssl-enum-ciphers -p 443 host to identify weak or deprecated ciphers.
- Verify the certificate chain with openssl verify -CAfile ca-bundle.crt server.crt.
- Test TLS 1.3 specifically: openssl s_client -connect host:443 -tls1_3.
- Use curl -v https://host to see the TLS version and cipher in the connection output.
Common Mistakes
- Still supporting TLS 1.0 or 1.1 in production. These are deprecated and have known vulnerabilities.
- Allowing CBC-mode cipher suites that are vulnerable to padding oracle attacks like POODLE and Lucky13.
- Not configuring forward secrecy. Using RSA key exchange means a stolen private key decrypts all historical traffic.
- Ignoring certificate chain errors during development and then shipping that code to production with verify disabled.
- Enabling 0-RTT resumption without understanding the replay attack surface — never use it for non-idempotent requests.
Real World Usage
- •Every HTTPS website in the world performs a TLS handshake before exchanging any data.
- •AWS ALB terminates TLS at the edge, performing millions of handshakes per second using custom hardware acceleration.
- •Cloudflare uses TLS 1.3 by default and pioneered 0-RTT support, reducing latency for returning visitors.
- •Google Chrome drove TLS 1.3 adoption by marking TLS 1.0/1.1 sites as insecure starting in 2020.
- •Netflix uses TLS 1.3 for all streaming traffic, benefiting from the reduced handshake latency on mobile networks.