TLS 1.3 vs TLS 1.2 — What Changed
TLS 1.3 (RFC 8446, 2018) is a near-complete rewrite of TLS 1.2. The goals were explicit: remove everything unsafe, reduce latency, and simplify the specification. The result is a cleaner, faster, and significantly more secure protocol.
Features Removed in TLS 1.3
| Removed Feature | Why It Was Dangerous |
|---|---|
| RSA key exchange | Static RSA key — compromise of server key decrypts all past sessions (no forward secrecy) |
| DHE_EXPORT (weak Diffie-Hellman) | FREAK, Logjam attacks — downgrade to 512-bit |
| CBC cipher suites | BEAST, Lucky13, POODLE attacks on CBC padding |
| RC4 | Biases in RC4 keystream — practical ciphertext recovery |
| MD5 and SHA-1 in signatures | Collision attacks (SHAttered, 2017) |
| Compression | CRIME attack — payload compression leaks secrets via size oracle |
| Renegotiation | Renegotiation injection attacks (CVE-2009-3555) |
| Session resumption via session IDs | Server-side state, less efficient than tickets |
The list of removed features reads like a history of TLS vulnerabilities. TLS 1.3's security comes largely from *what it does not do*.
Forward Secrecy — Mandatory in TLS 1.3
Every TLS 1.3 handshake uses Ephemeral Diffie-Hellman (ECDHE or DHE) for key exchange. Fresh key material is generated per-connection and discarded afterward. An attacker who records encrypted traffic today and later compromises the server's private key gains nothing — the session keys are gone.
TLS 1.2 with RSA key exchange had no forward secrecy: the client encrypted the pre-master secret with the server's RSA public key. Compromise the private key and you decrypt every past session.
The 1-RTT Handshake
TLS 1.2 Handshake (2 RTTs)
Client Server
│── ClientHello ─────────────────▶│ RTT 0 start
│ (cipher suites, random) │
│◀── ServerHello ─────────────────│ RTT 0 end
│◀── Certificate ─────────────────│
│◀── ServerKeyExchange ───────────│
│◀── ServerHelloDone ─────────────│
│── ClientKeyExchange ────────────▶│ RTT 1 start
│── ChangeCipherSpec ─────────────▶│
│── Finished ─────────────────────▶│
│◀── ChangeCipherSpec ────────────│ RTT 1 end
│◀── Finished ────────────────────│
│── HTTP Request ─────────────────▶│ RTT 2 start
TLS 1.2 required 2 full round-trips before the first application byte. On a 100ms RTT connection, that is 200ms of pure handshake latency.
TLS 1.3 Handshake (1 RTT)
Client Server
│── ClientHello ─────────────────▶│ RTT 0 start
│ (supported versions, key share)│
│◀── ServerHello ─────────────────│
│◀── {EncryptedExtensions} ───────│
│◀── {Certificate} ───────────────│ Encrypted! (no MITM fingerprinting)
│◀── {CertificateVerify} ─────────│
│◀── {Finished} ──────────────────│ RTT 0 end
│── {Finished} ───────────────────▶│ RTT 1 start
│── [HTTP Request] ───────────────▶│ Sent immediately with Finished
│◀── [HTTP Response] ─────────────│ RTT 1 end
In TLS 1.3, the client sends its key share in the ClientHello, the server responds with its key share in ServerHello, and both sides derive session keys immediately — allowing the server to encrypt the Certificate and Finished messages without waiting for another round-trip. The client sends its application data alongside the Finished message, saving one full RTT.
The Key Share in ClientHello
TLS 1.3 requires the client to guess which key exchange group the server supports and include a key share for that group in the ClientHello:
ClientHello extensions:
supported_versions: [TLS 1.3, TLS 1.2] ← negotiate version
supported_groups: [X25519, P-256, P-384]
key_share:
- group: X25519
key_exchange: <32 bytes of ephemeral public key>
If the server supports X25519 (which almost all do), it uses the provided key share immediately. If not, it sends a HelloRetryRequest asking the client to resend with a different group — adding one RTT. Clients learn the server's preferred group and remember it for future connections.
0-RTT Early Data
How 0-RTT Works
After a successful TLS 1.3 connection, the server can issue a session ticket encrypted with a server-side key. On the next connection, the client presents this ticket and sends application data *before* receiving any server response:
Client Server
│── ClientHello ─────────────────▶│
│ + early_data extension │
│ + pre_shared_key (ticket) │
│── [HTTP GET /api/users] ─────────▶│ 0-RTT data, no round-trip!
│◀── ServerHello ─────────────────│
│◀── {EncryptedExtensions} ───────│
│◀── {Finished} ──────────────────│
│◀── [HTTP 200 OK] ───────────────│ Response arrives immediately
The 0-RTT data is encrypted with the early traffic secret derived from the PSK — not from a fresh key exchange. This means 0-RTT data lacks forward secrecy relative to the PSK.
The Replay Attack Problem
0-RTT is vulnerable to replay attacks: an attacker who intercepts the 0-RTT ClientHello packet can resend it to the server, causing the server to process the same request twice. For a GET /prices this is harmless; for a POST /api/transfer this is catastrophic.
TLS 1.3 mitigations:
- Single-use tickets: Server marks tickets as used (requires shared state across load-balanced servers — expensive)
- Time-bound tickets: Include a
ticket_agefield; server rejects requests where the age is outside expected bounds - Application-level replay protection: Idempotency keys in request headers
The RFC recommendation: use 0-RTT only for idempotent requests (GET, HEAD) or requests with application-level replay protection.
Cipher Suites
TLS 1.3 Cipher Suite Simplification
TLS 1.2 had hundreds of cipher suites combining key exchange, authentication, bulk cipher, and MAC algorithms. This created a negotiation space so complex that misconfiguration was common. TLS 1.3 separates concerns:
- Key exchange is always ECDHE or DHE (mandatory forward secrecy)
- Authentication is always certificate-based or PSK
- The cipher suite specifies only: bulk cipher + hash for HKDF
TLS 1.3 defines exactly five cipher suites:
TLS_AES_128_GCM_SHA256 ← Default, recommended for all TLS 1.3
TLS_AES_256_GCM_SHA384 ← High-security variant
TLS_CHACHA20_POLY1305_SHA256 ← Mobile-friendly (no AES-NI needed)
TLS_AES_128_CCM_SHA256 ← IoT/constrained devices
TLS_AES_128_CCM_8_SHA256 ← IoT with shorter authentication tag
All three main cipher suites use AEAD (Authenticated Encryption with Additional Data) — they simultaneously encrypt and authenticate the data, making padding oracle attacks (a major source of TLS 1.2 vulnerabilities) impossible by design.
ChaCha20-Poly1305 — For Devices Without AES Hardware
AES-GCM is extremely fast on processors with AES-NI instructions (every modern desktop/server CPU). But older mobile phones and IoT devices lack AES-NI — software AES is ~10x slower and vulnerable to timing side-channels.
ChaCha20-Poly1305 is designed to be fast in pure software without hardware instructions, while maintaining constant-time operation (no timing attacks). TLS 1.3 clients typically advertise both and let servers without AES-NI pick ChaCha20-Poly1305.
Deployment — Certificate Requirements and Middlebox Issues
Certificate Requirements
TLS 1.3 does not fundamentally change certificate requirements — RSA and ECDSA certificates both work. However, the industry has moved toward:
Recommended certificate configuration:
Algorithm: ECDSA P-256 (smaller, faster than RSA-2048)
Minimum RSA: 2048 bits (4096 bits for sensitive applications)
Key usage: TLS Web Server Authentication (1.3.6.1.5.5.7.3.1)
SANs: Include both apex and www (plus all relevant subdomains)
Validity: 90 days (Let's Encrypt default) to 398 days (CA/Browser maximum)
# Check TLS version and cipher suite negotiated:
openssl s_client -connect example.com:443 -tls1_3 2>&1 | grep -E 'Protocol|Cipher'
# Protocol : TLSv1.3
# Cipher : TLS_AES_256_GCM_SHA384
# Using curl:
curl -v --tlsv1.3 https://example.com 2>&1 | grep TLS
Downgrade Protection
TLS 1.3 protects against downgrade attacks where a MITM strips TLS 1.3 support from the ClientHello to force a weaker TLS 1.2 negotiation. The server embeds a downgrade sentinel in the ServerRandom field:
If negotiated TLS 1.2 (but client offered 1.3):
ServerRandom[24:32] = 44 4F 57 4E 47 52 44 01 ← 'DOWNGRD' + 0x01
TLS 1.3 clients check for this sentinel and abort if present —
a legitimate server that actually supports TLS 1.3 would never set it.
Middlebox Compatibility
Many corporate TLS inspection boxes ("SSL bump" proxies) broke on initial TLS 1.3 because they expected TLS 1.2 handshake patterns. TLS 1.3 includes a legacy session ID field and a legacy_session_id_echo in ServerHello that mimic TLS 1.2 patterns, helping TLS 1.3 pass through buggy middleboxes.
The key_share extension is placed after the standard extensions in ClientHello for the same reason — middleboxes that forward up to a fixed extension offset see familiar TLS 1.2 structure before the new TLS 1.3 fields.
# Nginx TLS configuration — TLS 1.3 only:
server {
listen 443 ssl;
ssl_protocols TLSv1.3; # TLS 1.3 only (drop 1.2 for new deployments)
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
# Enable 0-RTT early data (use only for idempotent endpoints):
ssl_early_data on;
add_header Early-Data $tls1_3_early_data; # Log if request used 0-RTT
}
When to Still Support TLS 1.2
Pure TLS 1.3 deployments are appropriate for:
- Public APIs with modern client requirements
- Internal microservices (you control the clients)
- New applications with no legacy client base
Continue supporting TLS 1.2 alongside TLS 1.3 for:
- Consumer-facing services (some users on old Android/iOS)
- Enterprise clients with corporate proxy requirements
- Any service where a 0.1% client compatibility loss is unacceptable
The practical recommendation: configure ssl_protocols TLSv1.2 TLSv1.3 and let clients negotiate the best version they support. Remove TLS 1.2 only when telemetry confirms no clients use it.