TLS Handshake
TLS Session Resumption
Read aloud is unavailable in this browser.
Optimizing Connection Establishment: Session Resumption
The TLS handshake, while cryptographically elegant, comes with a cost: multiple network round-trips and expensive asymmetric cryptographic operations. For a client that connects to the same server repeatedly—as browsers do constantly—repeating the full handshake each time is wasteful.
Session resumption allows parties to skip the expensive parts of the handshake by reusing cryptographic state from a previous connection. When successful, resumption:
- Reduces latency: Fewer round-trips (TLS 1.3: 0-RTT possible)
- Saves computation: No asymmetric crypto operations (certificate validation, DH)
- Maintains security: Derives fresh keys from resumed secrets
This page explores TLS resumption mechanisms: the legacy approaches (session IDs and session tickets), TLS 1.3's unified PSK-based resumption, and the cutting-edge 0-RTT early data feature with its important security caveats.
This page provides a comprehensive examination of TLS session resumption. You will understand session ID resumption, session tickets, TLS 1.3 PSK resumption, 0-RTT early data, anti-replay mechanisms, and the security tradeoffs in resumption strategies.
The Cost of Full Handshakes
To understand why resumption matters, we must quantify what full handshakes cost.
Network Latency:
Each round-trip adds network delay. For a user with 50ms latency:
- TLS 1.2 full handshake: 2 round-trips = 100ms before application data
- TLS 1.3 full handshake: 1 round-trip = 50ms before application data
- TLS 1.3 resumption: 1 round-trip = 50ms (same as full)
- TLS 1.3 0-RTT: 0 round-trips = 0ms additional latency
Computational Cost:
Asymmetric operations dominate handshake CPU time:
| Operation | Time (2048-bit RSA) | Time (P-256 ECDSA) |
|---|---|---|
| RSA signature | ~1.5 ms | — |
| RSA verification | ~0.05 ms | — |
| ECDSA signature | — | ~0.15 ms |
| ECDSA verification | — | ~0.35 ms |
| ECDHE (P-256) | ~0.5 ms | ~0.5 ms |
| Certificate chain validation | ~2-10 ms | ~2-10 ms |
A typical full handshake: ~5-15ms of CPU time per connection.
Real-World Impact:
Consider a CDN edge server handling 100,000 connections per second:
- Full handshakes: 500-1500 CPU-seconds of crypto per second of real time
- Resumed handshakes: ~50 CPU-seconds (10x reduction)
Or consider a mobile device on 4G (100ms latency):
| Handshake Type | Time to First Byte | User Experience |
|---|---|---|
| TLS 1.2 full | TCP + 200ms TLS + HTTP = 400ms+ | Noticeable delay |
| TLS 1.3 full | TCP + 100ms TLS + HTTP = 300ms+ | Slight delay |
| TLS 1.3 0-RTT | TCP + 0ms TLS + HTTP = 200ms | Feels instant |
For interactive applications, 100-200ms savings fundamentally change perceived responsiveness.
Why Resumption Works:
Both parties can cache session secrets from a previous handshake. On reconnection:
- Client identifies cached session (ID, ticket, or PSK identity)
- Server verifies it also has the session state
- Both parties derive new keys from the cached secret
- Full crypto handshake is skipped
Fresh keys for each connection provide forward secrecy within the resumption window.
High-traffic servers target resumption rates of 50-80%+. Achieving this requires proper session caching, ticket encryption key management, and understanding client connection patterns. Monitoring resumption rate is essential for TLS performance optimization.
Session ID Resumption (TLS 1.2 Legacy)
The original TLS resumption mechanism uses session IDs—opaque identifiers assigned by the server.
How Session ID Resumption Works:
Initial Full Handshake:
- Client sends ClientHello with empty session_id
- Server generates session_id and sends in ServerHello
- Full handshake completes (certificates, key exchange, etc.)
- Server stores {session_id → master_secret, cipher suite, etc.}
- Client caches {session_id, server, master_secret, cipher suite, etc.}
Abbreviated Handshake (Resumption):
- Client sends ClientHello with cached session_id
- Server finds session_id in its session cache
- Server sends ServerHello with same session_id (confirms resumption)
- Both skip Certificate, ServerKeyExchange, ClientKeyExchange
- Both send Finished (encrypted with keys derived from cached master_secret + new randoms)
Abbreviated Handshake Properties:
- 1 round-trip instead of 2 (TLS 1.2)
- No certificates transmitted — saves bandwidth
- No asymmetric crypto — only symmetric operations
- Fresh keys derived — new randoms ensure different session keys
Limitations of Session ID Resumption:
-
Server-side state: Server must store session data for every client. At scale, this means large distributed session caches.
-
Load balancing complexity: If a client reconnects to a different server, the session cache isn't available (unless shared via Redis, memcached, etc.).
-
Memory pressure: Session entries consume memory. Servers must balance cache size vs. resumption rate.
-
No forward secrecy relative to session cache: If the session cache is compromised, all resumed sessions are at risk.
Cache Configuration:
# Nginx session cache configuration
ssl_session_cache shared:SSL:50m; # 50 MB shared cache
ssl_session_timeout 1d; # Sessions valid for 24 hours
50MB can store roughly 200,000 sessions—sufficient for most deployments.
TLS 1.3 doesn't use session ID resumption—it's replaced by PSK-based resumption with session tickets. However, TLS 1.3 sends a legacy_session_id for compatibility with middleboxes expecting it. The field is present but semantically meaningless.
Session Tickets: Stateless Resumption
Session tickets (RFC 5077) solve the server-side state problem by encrypting session state and sending it to the client.
How Session Tickets Work:
The server possesses a ticket encryption key (STEK) known only to itself (or shared among cluster members).
After Full Handshake:
-
Server creates session ticket containing:
- Master secret
- Cipher suite
- Session validity period
- Any other needed state
-
Server encrypts ticket with STEK (AES-GCM or similar)
-
Server sends NewSessionTicket message to client
-
Client caches the encrypted ticket (opaque blob to client)
Resumption with Ticket:
- Client sends ClientHello with session_ticket extension containing cached ticket
- Server decrypts ticket using STEK, recovers master secret
- Abbreviated handshake proceeds as with session ID resumption
Server becomes stateless — no session cache needed. The client stores the state (encrypted).
- •No server-side session cache required
- •Any server with STEK can resume the session
- •Simplifies load balancing (no sticky sessions)
- •Reduces server memory requirements
- •Scales to millions of clients trivially
- •STEK is a high-value target (decrypts all tickets)
- •STEK must be rotated regularly
- •STEK compromise breaks forward secrecy
- •No revocation mechanism (unlike session IDs)
- •Ticket lifetime = forward secrecy window
Ticket Encryption Key (STEK) Management:
STEK management is critical:
STEK lifecycle:
1. Generate new STEK with cryptographically secure random
2. Distribute to all servers in cluster
3. Use for ticket encryption for period T (e.g., 24 hours)
4. Retain for ticket decryption for period 2T (accept old tickets)
5. Securely delete after 2T
Forward Secrecy Implications:
Session tickets create a forward secrecy window:
Forward secrecy window = STEK lifetime + ticket lifetime
If an attacker records traffic and later obtains a STEK (before deletion), they can:
- Decrypt any session tickets encrypted with that STEK
- Recover master secrets from those tickets
- Derive session keys for those connections
- Decrypt recorded application data
Best Practices:
- Rotate STEK frequently — Every 1-24 hours
- Use ephemeral ECDHE with tickets — New DHE per connection preserves PFS (TLS 1.3 default)
- Delete old STEKs securely — Memory zeroing, HSM key destruction
- Monitor ticket usage — Detect anomalies suggesting STEK compromise
- Consider disabling tickets — If forward secrecy is paramount (high-security environments)
Many TLS implementations don't rotate STEKs automatically—they're static from server start. This is a significant forward secrecy weakness. If the STEK is compromised at any point, all sessions since server restart are vulnerable. Always configure STEK rotation.
TLS 1.3 PSK-Based Resumption
TLS 1.3 unifies resumption under the Pre-Shared Key (PSK) mechanism. Session tickets become a way to establish PSKs.
PSK in TLS 1.3:
A pre-shared key (PSK) is a secret known to both client and server before the handshake. PSKs can come from:
- Session resumption — Derived from a previous connection (most common)
- Out-of-band provisioning — Configured manually (enterprise, IoT)
NewSessionTicket Message (TLS 1.3):
After the handshake completes, the server sends one or more NewSessionTicket messages:
struct {
uint32 ticket_lifetime; // Seconds until expiration
uint32 ticket_age_add; // Obfuscation for age
opaque ticket_nonce<0..255>; // For PSK derivation
opaque ticket<1..2^16-1>; // The encrypted session state
Extension extensions<0..2^16-2>;
} NewSessionTicket;
Deriving the PSK:
PSK = HKDF-Expand-Label(
resumption_master_secret,
"resumption",
ticket_nonce,
Hash.length
)
Each ticket has a unique nonce, so each produces a different PSK—even from the same connection.
PSK Modes:
TLS 1.3 defines two PSK modes, communicated via the psk_key_exchange_modes extension:
1. psk_ke (PSK-only):
- Keys derived solely from the PSK
- No ephemeral key exchange
- Susceptible to session ticket decryption attacks (no forward secrecy vs ticket compromise)
- Fastest option
2. psk_dhe_ke (PSK + DHE):
- PSK provides authentication and base key material
- Fresh ECDHE provides forward secrecy
- Even if PSK/ticket is compromised, session keys are safe
- Recommended default
Binder Verification:
To prevent offline dictionary attacks on PSKs, TLS 1.3 includes a binder:
binder = HMAC(binder_key, Transcript-Hash(Truncated-ClientHello))
The binder proves the client knows the PSK without revealing it. The server verifies before proceeding.
Ticket Age Obfuscation:
To prevent correlation of connections:
obfuscated_ticket_age = (current_time - ticket_issue_time) + ticket_age_add
The ticket_age_add is random, known only to server (stored in ticket). This prevents outsiders from correlating the timing of resumption connections.
The psk_dhe_ke mode adds only ~0.3ms of ECDHE computation but provides full forward secrecy. There's no reason to use psk_ke except in extremely constrained environments or for 0-RTT initial data. Modern TLS libraries default to psk_dhe_ke.
0-RTT Early Data: The Ultimate Optimization
TLS 1.3 introduces 0-RTT (zero round-trip time) resumption—sending application data in the first flight without waiting for server response. This eliminates TLS latency entirely for resumed connections.
How 0-RTT Works:
- Client has PSK and early_traffic_secret from previous connection
- Client sends ClientHello with PSK identity
- Client immediately sends early data (encrypted with early_traffic_secret)
- Server receives ClientHello, validates PSK, derives early_traffic_secret
- Server can process early data before sending ServerHello
The Timing Advantage:
Standard 1-RTT resumption:
C→S: ClientHello
S→C: ServerHello, Finished
C→S: Finished
C→S: Request (Application Data) ← First app data after 1 RTT
S→C: Response
0-RTT resumption:
C→S: ClientHello + Early Data (Request) ← App data in first flight!
S→C: ServerHello, Finished + Response ← Response can be immediate
C→S: Finished
For a 100ms latency connection, 0-RTT saves 100ms—often half the total request time.
The Replay Problem:
0-RTT has a fundamental security limitation: early data is replayable.
In standard TLS:
- Server's random value is unique per connection
- Keys incorporate the random, making replays impossible
In 0-RTT:
- Early data is encrypted before receiving ServerHello
- No server randomness included in early_traffic_secret
- An attacker who captures the ClientHello + early data can replay it
Attack Scenario:
- Attacker captures: ClientHello + early data (e.g., POST /transfer?amount=1000)
- Attacker replays the exact bytes to the server
- Server decrypts successfully (same PSK, same keys)
- Server executes the request again
Mitigations:
-
Application-level idempotency: Only send 0-RTT data for idempotent requests (GET, HEAD). Never for POST, PUT, DELETE.
-
Server-side anti-replay:
- Single-use session tickets
- Client IP + ticket database (prevents replay from different IPs)
- Time-window checks (reject old ClientHellos)
-
0-RTT rejection: Server can refuse early data and require 1-RTT (safe fallback)
| Property | 0-RTT Early Data | 1-RTT Data |
|---|---|---|
| Confidentiality | ✅ Encrypted with early_traffic_secret | ✅ Encrypted with traffic_secret |
| Integrity | ✅ AEAD authenticated | ✅ AEAD authenticated |
| Forward Secrecy | ⚠️ Only vs PSK compromise, not vs ticket key | ✅ Full (with psk_dhe_ke) |
| Replay Protection | ❌ Replayable without server-side measures | ✅ Guaranteed unique (fresh ServerHello) |
| Latency | ✅ Zero additional RTT | Standard 1 RTT |
Never send state-changing requests as 0-RTT data without server-side replay protection. An attacker replaying 'transfer $10,000' or 'vote for candidate X' can have disastrous consequences. Use 0-RTT only for truly idempotent requests or with robust anti-replay mechanisms.
Anti-Replay Mechanisms for 0-RTT
Servers accepting 0-RTT must implement anti-replay protection. TLS 1.3 specifies two approaches.
1. Single-Use Tickets:
Each session ticket is marked as used after the first resumption:
1. Client sends 0-RTT with ticket T
2. Server checks: Is T in used-tickets database?
- Yes: Reject 0-RTT, proceed with 1-RTT
- No: Accept 0-RTT, add T to used-tickets database
Challenges:
- Requires synchronized database across all servers
- Database grows large (all recent tickets)
- Timing: replays within the synchronization window may succeed
2. Client Hello Recording:
Track unique ClientHello fingerprints:
1. Compute fingerprint F = Hash(ClientHello)
2. Check if F in recent-fingerprints set
- Yes: Reject 0-RTT (duplicate)
- No: Accept 0-RTT, add F to set
3. Expire old fingerprints after time window
Window-Based Approach:
Combine timestamp checking with bounded storage:
- Reject ClientHellos with ticket_age > max_early_data_age
- Within the window, track fingerprints (Bloom filter possible)
- Expire entries after max_early_data_age
This bounds storage: only need to remember recent connections.
Implementation Strategies:
Bloom Filter:
- Space-efficient probabilistic set membership
- False positives (reject valid 0-RTT) but no false negatives
- Acceptable tradeoff: wrongly rejecting 0-RTT just falls back to 1-RTT
Distributed Cache (Redis/Memcached):
- Store ticket IDs or ClientHello hashes
- TTL set to max_early_data_age
- Latency: adds database lookup to 0-RTT acceptance
- Complexity: another infrastructure component
IP-Bound Tickets:
Embed client IP in the session ticket (encrypted):
- At ticket issuance, store client_ip in ticket
- At resumption, compare current IP with ticket IP
- If different, reject 0-RTT (attacker has different IP)
Limitations: Legitimate IP changes (mobile roaming, NAT) cause 0-RTT rejection.
Practical Server Configuration:
# Nginx: conservative 0-RTT settings
ssl_early_data on;
ssl_session_tickets on;
ssl_session_timeout 1h; # Short timeout limits replay window
# Ensure upstream knows about 0-RTT
proxy_set_header Early-Data $ssl_early_data;
The Early-Data header tells the application whether the request came via 0-RTT, allowing it to defer state changes until confirmation.
RFC 8470 defines the 'Early-Data' header. Proxies should set it to '1' for 0-RTT requests. The origin server can then choose to process the request immediately (if idempotent) or wait for the handshake to complete (for state-changing operations). This gives applications fine-grained replay control.
Resumption Best Practices
Implementing resumption correctly requires balancing performance, security, and operational concerns.
- •Enable session tickets for stateless resumption — Simplifies load balancing and scaling. Session IDs require shared state.
- •Rotate ticket keys regularly — Every 1-24 hours depending on security requirements. Automate rotation.
- •Use short ticket lifetimes — 1-24 hours limits the forward secrecy window. Don't exceed 7 days.
- •Prefer psk_dhe_ke mode — Adds minimal overhead but provides full forward secrecy for resumed sessions.
- •Be conservative with 0-RTT — Enable only after careful analysis. Consider rejecting 0-RTT for sensitive endpoints.
- •Implement anti-replay for 0-RTT — Single-use tickets, ClientHello recording, or coordinated databases.
- •Monitor resumption rates — Track full vs. resumed handshakes. Low resumption may indicate configuration issues.
- •Test across server rotation — Ensure all servers can decrypt tickets. Plan key synchronization during deploys.
Ticket Key Rotation Pattern:
// Key rotation timeline
t=0h: Generate key K1, encrypt tickets with K1
t=12h: Generate key K2, decrypt K1+K2, encrypt with K2
t=24h: Generate key K3, decrypt K2+K3, encrypt with K3, delete K1
...
Overlapping decrypt windows ensure smooth transitions without breaking resumption.
When to Disable Tickets:
- Maximum forward secrecy — When protecting against future key compromise is paramount
- Extreme replay sensitivity — When even briefly accepting 0-RTT is unacceptable
- Simple deployments — When session ID caching is sufficient and simpler
Configuration Example (High-Security):
# Maximum forward secrecy configuration
ssl_session_tickets off; # Disable tickets
ssl_session_cache shared:SSL:10m; # Session ID cache
ssl_session_timeout 5m; # Short timeout
This trades resumption efficiency for stronger forward secrecy—appropriate for extremely sensitive applications.
Use TLS logging to measure resumption rate. In OpenSSL, enable session logging. Look for 'Reused' in connection logs. Aim for 50%+ resumption rate for return visitors. If lower, investigate ticket lifetime, client caching behavior, and server clock synchronization.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.