From ClientHello to Application Data —
The Protocol That Secures the Internet
ECDHE AEAD X.509 RFC 8446
Transport Layer Security is the most widely deployed cryptographic protocol on Earth. Every HTTPS connection, every email transmission, every API call depends on it.
Chrome reports over 95% of pages
loaded over HTTPS as of 2025
Cloudflare alone handles billions
of connections daily
Down from 2-RTT in TLS 1.2 —
faster, simpler, more secure
Two decades of protocol refinement, driven by real-world attacks and performance demands.
| Version | Year | RFC | Key Changes | Status |
|---|---|---|---|---|
| SSL 3.0 | 1996 | RFC 6101 | First practical deployment | Deprecated (POODLE) |
| TLS 1.0 | 1999 | RFC 2246 | SSL renamed, minor fixes | Deprecated (BEAST) |
| TLS 1.1 | 2006 | RFC 4346 | Explicit IV for CBC | Deprecated |
| TLS 1.2 | 2008 | RFC 5246 | AEAD ciphers, SHA-256, configurable PRF | Legacy (still ~20%) |
| TLS 1.3 | 2018 | RFC 8446 | 1-RTT, ECDHE-only, AEAD-only, encrypted handshake | Current standard |
The most important design decision in TLS 1.3 was what to delete. Every removed feature had been the source of real-world attacks.
RSA key exchange No forward secrecy
CBC mode ciphers BEAST, Lucky13, POODLE
RC4 Statistical biases
SHA-1 Collision attacks (SHAttered)
3DES Sweet32 birthday attack
MD5 Broken since 2004
Renegotiation Triple Handshake attack
Compression CRIME/BREACH attacks
Static DH/ECDH No forward secrecy
ChangeCipherSpec Simplified state machine
Custom DHE groups Logjam attack vector
Export ciphers FREAK/Logjam
TLS 1.2 had 37+ cipher suites with complex negotiation. TLS 1.3 has exactly 5, all AEAD-only. Key exchange and authentication are negotiated separately.
| Cipher Suite | AEAD | HKDF Hash | Key Size | Notes |
|---|---|---|---|---|
| TLS_AES_128_GCM_SHA256 | AES-128-GCM | SHA-256 | 128-bit | Most common; mandatory to implement |
| TLS_AES_256_GCM_SHA384 | AES-256-GCM | SHA-384 | 256-bit | Higher security margin |
| TLS_CHACHA20_POLY1305_SHA256 | ChaCha20-Poly1305 | SHA-256 | 256-bit | Fast on devices without AES-NI |
| TLS_AES_128_CCM_SHA256 | AES-128-CCM | SHA-256 | 128-bit | IoT / constrained environments |
| TLS_AES_128_CCM_8_SHA256 | AES-128-CCM-8 | SHA-256 | 128-bit | 8-byte tag; IoT only |
TLS 1.3 completes the handshake in a single round trip. The client sends key shares speculatively, enabling encrypted data after just one flight.
The ClientHello is the only unencrypted message from the client. It must contain enough information for the server to complete key exchange in a single round trip.
ClientHello {
legacy_version: 0x0303 // TLS 1.2 for middlebox compat
random: 32 bytes // Client random nonce
legacy_session_id: 32 bytes // Compatibility mode
cipher_suites: [0x1301, 0x1302, 0x1303]
legacy_compression: [0x00] // Null only
extensions: {
supported_versions: [0x0304] // TLS 1.3
key_share: X25519 pubkey (32 bytes)
P-256 pubkey (65 bytes)
signature_algorithms: [ed25519, ecdsa_secp256r1_sha256,
rsa_pss_rsae_sha256, ...]
server_name (SNI): "example.com"
psk_key_exchange_modes: [psk_dhe_ke]
}
}
The server selects one cipher suite, one key share group, and sends its ephemeral public key. Everything after ServerHello is encrypted.
Sent in plaintext (last unencrypted server message):
ServerHello {
legacy_version: 0x0303
random: 32 bytes
cipher_suite: 0x1301
(AES_128_GCM_SHA256)
extensions: {
supported_versions: 0x0304
key_share: X25519 pubkey
}
}
First encrypted message — uses handshake traffic keys:
{EncryptedExtensions} {
server_name: ack
max_fragment_length: ...
alpn: "h2"
// No key-exchange or
// auth-related extensions
// (those stay in ServerHello)
}
The core of TLS 1.3 security — ephemeral Elliptic Curve Diffie-Hellman. Connects directly to Module 02 (ECC).
Field: GF(2255 - 19)
Key size: 32 bytes public, 32 bytes private
Speed: ~50,000 ops/sec on modern CPU
Properties: Constant-time by design, no invalid-curve attacks, Montgomery ladder
Preferred ~70% of connections
Field: GF(p) where p = 2256 - 2224 + ...
Key size: 65 bytes public (uncompressed)
Speed: ~20,000 ops/sec on modern CPU
Properties: NIST standard, HSM support, FIPS validated
NIST Compliance required
shared_secret = ECDH(my_private, peer_public). The result is a 32-byte value fed into the HKDF key schedule. New ephemeral keys are generated for every single connection — guaranteeing forward secrecy.
TLS 1.3 derives all keys through a structured HKDF pipeline. Two primitives from Module 05 (Hashing): HKDF-Extract (combine entropy) and HKDF-Expand (derive keys).
After key exchange, the server proves its identity with three encrypted messages. All sent under handshake traffic keys.
X.509 certificate chain sent to client. Contains server's public key, signed by a Certificate Authority. Encoded as a sequence of CertificateEntry structures with optional per-certificate extensions (OCSP, SCT).
Identity
Digital signature over the handshake transcript hash using the server's private key. Proves the server possesses the private key matching the certificate. Uses signature algorithms from Module 06.
Proof of possession
HMAC over the entire handshake transcript using a key derived from the handshake secret. Provides key confirmation and transcript integrity — proves the handshake was not tampered with.
Integrity
Hash(Handshake Context || messages). This binds the server's identity to the specific key exchange that occurred — preventing mismatch attacks where an attacker substitutes a different server's response.
The client verifies the server's Certificate, CertificateVerify, and Finished. Then it sends its own Finished message to complete the handshake.
Application data can flow immediately
after client sends Finished
ECDHE + signature verification
dominates CPU time
Early → Handshake → Application
each with separate traffic keys
Returning clients can send encrypted application data in the very first flight — zero round trips before data. But this comes with significant security trade-offs.
TLS 1.3 supports two PSK modes for resumption and external PSK use cases.
Key derived solely from the pre-shared key. No ephemeral key exchange is performed.
Forward secrecy: None — if the PSK is compromised, all sessions using it are exposed.
Use case: IoT devices with pre-provisioned keys, constrained environments where ECDHE is too expensive.
No forward secrecy Constrained only
PSK combined with ephemeral ECDHE exchange. Both the PSK and ECDHE shared secret feed into HKDF-Extract.
Forward secrecy: Yes — even if PSK is later compromised, past sessions are safe.
Use case: Normal TLS 1.3 session resumption. This is the recommended mode.
Forward secrecy Recommended
After the handshake completes, the server sends NewSessionTicket messages enabling future resumption without server-side state.
NewSessionTicket {
ticket_lifetime: 7200 // Max 604800s (7 days)
ticket_age_add: 0x4a2bc8e1 // Obfuscate ticket age
ticket_nonce: 0x0000000000000001 // Unique per ticket
ticket: <encrypted blob> // Server-encrypted state
extensions: {
early_data: max_early_data_size: 16384
}
}
The ticket is an encrypted container holding the resumption secret, cipher suite, and expiry. The server encrypts with a ticket encryption key (STEK) known only to the server cluster — no session database needed.
The client reports obfuscated_age = (age_ms + ticket_age_add) mod 2^32. This prevents observers from correlating ticket reuse across connections — a privacy improvement over TLS 1.2 session IDs.
TLS 1.3 carries X.509 certificate chains with inline extensions for revocation checking and certificate transparency.
Server sends its leaf certificate plus intermediates. The client validates the chain to a trusted root CA. Chain is typically 2-3 certificates, ~3-5 KB total.
Identity binding
Server includes a signed OCSP response in the CertificateEntry extension. Client verifies revocation status without contacting the CA — reduces latency and improves privacy.
Revocation
Signed Certificate Timestamps prove the certificate was logged in a public Certificate Transparency log. Detects misissued certificates. Required by Chrome since 2018.
Transparency
compress_certificate extension. This reduces certificate data by ~60%, saving ~2-3 KB per handshake — significant at scale.
TLS 1.3 defines specific signature algorithms for CertificateVerify. Connecting to Module 06 (Digital Signatures).
| Scheme | Algorithm | Hash | Key Size | Notes |
|---|---|---|---|---|
| ecdsa_secp256r1_sha256 | ECDSA over P-256 | SHA-256 | 256-bit | Most widely deployed |
| ecdsa_secp384r1_sha384 | ECDSA over P-384 | SHA-384 | 384-bit | Higher security level |
| ed25519 | EdDSA / Curve25519 | SHA-512 | 256-bit | Deterministic, fast; from Module 06 |
| ed448 | EdDSA / Ed448 | SHAKE256 | 448-bit | ~224-bit security |
| rsa_pss_rsae_sha256 | RSA-PSS (RSAE) | SHA-256 | 2048+ bit | PSS padding mandatory |
| rsa_pss_rsae_sha384 | RSA-PSS (RSAE) | SHA-384 | 2048+ bit | RSA-PSS only; no PKCS#1 v1.5 |
| rsa_pss_pss_sha256 | RSA-PSS (PSS key) | SHA-256 | 2048+ bit | Key restricted to PSS |
All application data (and handshake messages after ServerHello) is encrypted using the AEAD cipher from the negotiated suite. Connects to Module 03 (AES-GCM) and Module 04 (ChaCha20-Poly1305).
TLSCiphertext {
opaque_type: 0x17 (application_data) // Always 0x17!
legacy_version: 0x0303 // Always TLS 1.2
length: uint16 // Encrypted payload length
encrypted_record:
AEAD-Encrypt(
key: write_key,
nonce: write_iv XOR sequence_number,
aad: TLSCiphertext header (5 bytes),
plaintext: content || ContentType || zeros
)
}
The real content type (handshake, alert, application_data) is encrypted inside the record. The outer header always says 0x17. This prevents traffic analysis that distinguishes handshake from data.
Each record uses nonce = write_iv XOR padded_sequence_number. The sequence number is implicit (not sent on wire), incrementing from 0. This guarantees nonce uniqueness without transmitting it.
The most pragmatic hack in TLS 1.3: it disguises itself as TLS 1.2 on the wire to avoid being broken by enterprise middleboxes.
Enterprise firewalls, load balancers, and inspection devices parse TLS headers. When TLS 1.3 was first deployed, many middleboxes:
• Dropped connections with unknown version numbers
• Expected ChangeCipherSpec messages
• Failed on missing session IDs
• Caused ~3-8% connection failures in field trials
TLS 1.3 includes deliberate lies for compatibility:
• legacy_version = 0x0303 (claims TLS 1.2)
• Sends a dummy ChangeCipherSpec message
• Includes a random legacy_session_id
• Real version negotiated in supported_versions extension
TLS 1.3 is the most formally analyzed cryptographic protocol in history. Multiple independent verification efforts proved its security properties.
Tamarin Prover — Symbolic analysis of the handshake state machine. Proved authentication, secrecy, and forward secrecy properties.
ProVerif — Automated Dolev-Yao model verification. Confirmed no protocol-level attacks on the 1-RTT and 0-RTT modes.
miTLS (F*) — Verified reference implementation in F* language. Crypto proofs composed with code correctness proofs.
BEAST CBC IV predictability → no CBC
CRIME Compression oracle → no compression
Lucky13 CBC padding oracle → no CBC
POODLE SSL 3.0 downgrade → no fallback
FREAK Export cipher downgrade → no export
Logjam Weak DH groups → no custom groups
ROBOT RSA padding oracle → no RSA KE
Triple HS Renegotiation → no renegotiation
44 4F 57 4E 47 52 44 01 = "DOWNGRD\x01") when negotiating TLS 1.2. If a client sees this sentinel while expecting 1.3, it detects a downgrade attack and aborts.
A side-by-side comparison of the two protocol generations.
Quantum computers threaten ECDHE key exchange. The transition to post-quantum TLS is already underway. Connecting to Module 07 (Post-Quantum Cryptography).
Combines classical ECDHE with a PQ KEM in a single key_share. The shared secret is derived from both — secure even if one scheme is broken.
shared_secret = HKDF(ECDHE_secret || ML-KEM_secret)
Chrome since M131
X25519MLKEM768 — X25519 + ML-KEM-768 hybrid. Now the most common PQ key exchange on the web.
ClientHello size: Grows from ~300 bytes to ~1100 bytes (ML-KEM public key is 1184 bytes for level 3).
FIPS 203
A TLS 1.3 handshake as captured in a packet trace. Note how everything after ServerHello appears as opaque Application Data.
Frame 1: ClientHello (227 bytes)
16 03 01 00 e0 # Record: Handshake, TLS 1.0 (compat!)
01 00 00 dc # ClientHello, length 220
03 03 # legacy_version: TLS 1.2
[32 bytes random]
20 [32 bytes session_id] # Compatibility mode
00 06 13 01 13 02 13 03 # 3 cipher suites
01 00 # Compression: null
00 91 # Extensions length
00 2b 00 03 02 03 04 # supported_versions: TLS 1.3
00 33 00 47 00 45 # key_share: X25519
00 1d 00 20 [32 bytes pubkey]
00 0d 00 14 ... # signature_algorithms
00 00 00 0f ... # server_name: example.com
Frame 2: ServerHello (155 bytes)
16 03 03 00 97 # Record: Handshake, TLS 1.2 (compat!)
02 00 00 93 # ServerHello
03 03 [32 bytes random]
20 [32 bytes session_id] # Echo client's session_id
13 01 # Selected: TLS_AES_128_GCM_SHA256
00 2b 00 02 03 04 # supported_versions: TLS 1.3
00 33 00 24 00 1d 00 20 # key_share: X25519
[32 bytes server pubkey]
Frame 3: Change Cipher Spec (6 bytes) # Fake! Compatibility mode
14 03 03 00 01 01
Frame 4-7: Application Data (opaque) # All encrypted from here
17 03 03 [len] [encrypted EncryptedExtensions]
17 03 03 [len] [encrypted Certificate]
17 03 03 [len] [encrypted CertificateVerify]
17 03 03 [len] [encrypted Finished]
Major implementations with different design philosophies and security profiles.
| Library | Language | 0-RTT | PQ Hybrid | Notes |
|---|---|---|---|---|
| OpenSSL 3.x | C | Yes | Yes (3.5+) | Most widely deployed; provider architecture |
| BoringSSL | C++ | Yes | Yes | Google's fork; Chrome, Android; first PQ deploy |
| rustls | Rust | Yes | Yes | Memory-safe; no C code; ~10K LoC |
| Go crypto/tls | Go | Yes | Yes | Standard library; simple API |
| mbedTLS | C | Yes | Partial | Embedded/IoT focus; ARM-maintained |
| wolfSSL | C | Yes | Yes | Embedded + FIPS 140-3 certified |
| NSS | C | Yes | Yes | Mozilla/Firefox; PKCS#11 based |
| s2n-tls | C | Yes | Yes | AWS; minimal code footprint |
HKDF details · Finished computation · Key update
HKDF (HMAC-based Key Derivation Function, RFC 5869) is the engine behind TLS 1.3's key schedule. Two operations, each a single HMAC call.
PRK = HMAC-Hash(salt, IKM)
// Extracts entropy from input
// keying material (IKM) into a
// fixed-length pseudorandom key
// salt = previous Derive-Secret
// IKM = PSK or ECDHE secret
HKDF-Expand-Label(Secret, Label,
Context, Length)
= HKDF-Expand(Secret,
HkdfLabel, Length)
HkdfLabel = Length ||
"tls13 " || Label ||
Hash(Context)
Derive-Secret(Secret, Label, Messages)
= HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length)
// Example: deriving handshake traffic secret
client_handshake_traffic_secret =
Derive-Secret(Handshake Secret, "c hs traffic", ClientHello...ServerHello)
finished_key = HKDF-Expand-Label(
base_key, // {client,server}_handshake_traffic_secret
"finished", "", Hash.length
)
verify_data = HMAC(finished_key, Transcript-Hash(
Handshake Context ... last message before Finished
))
The Finished message is a MAC over the entire transcript, keyed with a value derived from the handshake secret. It provides key confirmation — both parties prove they computed the same keys.
application_traffic_secret_N+1 =
HKDF-Expand-Label(application_traffic_secret_N,
"traffic upd", "", Hash.length)
// New write key and IV derived from new secret
// Triggered via KeyUpdate message (encrypted)
// Provides key rotation without new handshake
Key update allows rekeying application traffic keys without a new handshake. This limits the amount of data encrypted under a single key, mitigating potential nonce-reuse issues.
RFC 8446 — TLS 1.3 Protocol
RFC 8447 — IANA Registry Updates for TLS
RFC 8448 — Example TLS 1.3 Handshake Traces
RFC 8449 — Record Size Limit Extension
RFC 8879 — Certificate Compression
RFC 5869 — HKDF (HMAC-based KDF)
RFC 9001 — QUIC uses TLS 1.3
RFC 8998 — ShangMi Cipher Suites for TLS 1.3
draft-ietf-tls-hybrid-design — Hybrid Key Exchange
FIPS 203 — ML-KEM (Kyber)
Chrome: X25519MLKEM768 deployment
Cloudflare: PQ key exchange telemetry
Cremers et al., "A Comprehensive Symbolic Analysis of TLS 1.3" (CCS 2017)
Bhargavan et al., "miTLS: Verified Reference Implementation" (S&P 2017)
Dowling & Paterson, "A Cryptographic Analysis of TLS 1.3" (JoC 2021)
Delignat-Lavaud et al., "Implementing TLS with Verified Cryptographic Security" (S&P 2014)
OpenSSL — openssl.org
BoringSSL — boringssl.googlesource.com
rustls — github.com/rustls/rustls
s2n-tls — github.com/aws/s2n-tls
wolfSSL — wolfssl.com
Protocol design — TLS 1.3 achieves 1-RTT handshakes by removing legacy features, mandating ECDHE forward secrecy, and encrypting the handshake after ServerHello. Only 5 AEAD-only cipher suites remain.
Key exchange (Module 02) — Ephemeral ECDHE with X25519 or P-256 provides the shared secret. Post-quantum hybrid key exchange with ML-KEM is actively deploying.
Symmetric encryption (Modules 03/04) — AES-128-GCM, AES-256-GCM, and ChaCha20-Poly1305 protect all application data via AEAD. Content type hiding prevents traffic analysis.
Hash functions (Module 05) — HKDF-Extract and HKDF-Expand (built on HMAC-SHA256/384) drive the three-stage key schedule from Early Secret through Master Secret.
Digital signatures (Module 06) — RSA-PSS, Ed25519, and ECDSA authenticate servers in CertificateVerify. PKCS#1 v1.5 is banned for handshake signatures.