TLS Handshake
TLS 1.2 (2 RTT) vs 1.3 (1 RTT) vs 1.3 resume (0 RTT).
This interactive explanation is built for system design interview prep: step through TLS Handshake, watch the internal state change, and connect the concept to real distributed-system trade-offs.
Overview
TLS is the cryptographic layer that turns a plain TCP socket into a confidential, authenticated, integrity-protected channel. Modern web traffic runs over TLS 1.3, which is simpler, faster, and more secure than the 1.2 it replaces. A 1.3 handshake takes one RTT for a new connection and zero RTTs for a resumed session, versus two RTTs for 1.2. The handshake proves the server's identity via a certificate chain rooted at a CA the client trusts, agrees on a shared symmetric key via ephemeral Diffie-Hellman, and negotiates a cipher suite that defines the AEAD algorithm (AES-GCM or ChaCha20-Poly1305) for bulk encryption. Once the handshake completes, every record is encrypted and authenticated. Forward secrecy — guaranteed by ephemeral DH — means that even if the server's long-term key is stolen later, past sessions cannot be decrypted. Getting TLS right is the foundation of every secure web API; getting it wrong means leaking credentials in plaintext.
How it works
A TLS 1.3 handshake starts with the client sending ClientHello containing its supported cipher suites, a list of supported groups (for key exchange), a key_share with its ephemeral DH public key, and the server_name extension (SNI) so a server hosting multiple domains can pick the right cert. The server replies with ServerHello picking the cipher and group, its own key_share, its certificate chain, a CertificateVerify signed with the cert's private key, and Finished. The client verifies the certificate chain up to a trusted root, verifies the signature, derives the symmetric keys from the combined DH secret using HKDF, and sends its Finished. Application data starts flowing on the next packet — 1-RTT total. A resumed session (0-RTT) can piggyback application data on the ClientHello using a pre-shared key ticket from a previous session; this is fast but replayable, so only idempotent requests should use it. Cipher suites in 1.3 are fixed to a small AEAD set: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256. Certificate validation checks the hostname against the cert's Subject Alternative Name, the chain back to a trusted CA, expiry, and revocation (via OCSP stapling to avoid a separate round-trip). Java exposes TLS through SSLContext and SSLEngine (or the higher-level HttpClient), which negotiate transparently when the system truststore has the right roots.
Implementation
public class TlsContextFactory {
public static SSLContext tls13() throws Exception {
SSLContext ctx = SSLContext.getInstance("TLSv1.3");
// Default truststore (cacerts) + no client cert for server auth only.
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
ctx.init(null, tmf.getTrustManagers(), new SecureRandom());
return ctx;
}
/** Enable only TLS 1.3 and AEAD ciphers on a socket. */
public static void harden(SSLSocket s) {
s.setEnabledProtocols(new String[]{"TLSv1.3"});
s.setEnabledCipherSuites(new String[]{
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256"
});
SSLParameters p = s.getSSLParameters();
p.setEndpointIdentificationAlgorithm("HTTPS"); // enforce hostname verification
s.setSSLParameters(p);
}
}
public class TlsHttpClient {
public static String getHttps(String url) throws Exception {
HttpClient client = HttpClient.newBuilder()
.sslContext(TlsContextFactory.tls13())
.connectTimeout(Duration.ofSeconds(5))
.build();
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.header("User-Agent", "tls-demo/1.0")
.GET()
.build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("negotiated protocol: " + resp.version());
return resp.body();
}
public static void main(String[] args) throws Exception {
String body = getHttps("https://example.com/");
System.out.println(body.substring(0, Math.min(200, body.length())));
}
}
Complexity
- TLS 1.3 new connection:
1 RTT - TLS 1.3 resumed (0-RTT):
0 RTT - TLS 1.2 new connection:
2 RTT - AES-GCM per-record overhead:
~16 B auth tag + 5 B header - cert chain verify:
~1 ms CPU
Key design decisions & trade-offs
- TLS 1.3 vs 1.2 — Chosen: 1.3 everywhere possible. Half the RTT, stronger defaults, fewer insecure ciphers. 1.2 only for legacy clients.
- 0-RTT data — Chosen: Only for idempotent requests. 0-RTT is replayable; non-idempotent writes can be replayed by an attacker.
- OCSP stapling vs live OCSP — Chosen: Stapling. Removes the privacy-leaking third-party round trip during handshake.
Common pitfalls
- Disabling hostname verification to 'make it work' — allows MITM
- Pinning a certificate instead of a public key — fails on CA rotation
- Running old TLS 1.0/1.1 for legacy clients — deprecated and insecure
- Missing SNI on multi-tenant hosts — server returns the wrong cert
- Private key on disk with world-readable permissions
Interview follow-ups
- Add HSTS and HSTS preload
- Automate certs with ACME / Let's Encrypt
- Enable mutual TLS (mTLS) for service-to-service auth
- Rotate session ticket keys frequently to limit 0-RTT replay window
Recommended reading
- Alex Petrov, Database Internals — storage engines and distributed systems internals.
- Martin Kleppmann, Designing Data-Intensive Applications (DDIA) — data models, replication, partitioning, consistency.
- The System Design Primer — high-level design building blocks.
- Foundational networking + web-security references (TCP/IP, TLS 1.3, OWASP Top 10).