RouteHardenHire us
Back to Networking Fundamentals

TLS 1.3 handshake byte by byte

TLS 1.3 from first principles: ClientHello, key agreement, key schedule, certificate authentication, 0-RTT replay caveats, and what the wire still leaks.

TLS 1.3 is the protocol that finally got rid of TLS's accumulated mistakes. Twenty years of security research, downgrade attacks, padding-oracle bugs, and slow-handshake critiques produced a redesign that's measurably simpler than its predecessor. The handshake completes in 1 RTT instead of 2, almost everything after the ServerHello is encrypted, forward secrecy is mandatory, and the cipher-suite catalog shrank from 300+ to 5. This module walks through a TLS 1.3 handshake message by message, explains how the key schedule turns shared randomness into traffic-encryption keys, and covers what the protocol still doesn't hide.

Prerequisites

  • Module 1.7 — TCP at the wire level. TLS rides on TCP (or QUIC, which embeds TLS); understanding the underlying byte-stream context is needed.
  • Module 1.9 — DNS, name resolution end to end. The DNS-layer SNI and HTTPS-record interactions show up here.
  • Module 1.10 — HTTP/1.1, /2, /3 evolution. ALPN selects the application protocol inside TLS; HTTP/3 takes a different route via QUIC's integrated TLS.

Learning objectives

By the end of this module you should be able to:

  1. Walk through a TLS 1.3 handshake from ClientHello to Finished and explain what each message contributes to security and to performance.
  2. Explain ephemeral (EC)DH key exchange, certificate authentication, and key-schedule derivation at the engineering level — without hand-waving the math but without re-proving it either.
  3. Distinguish 1-RTT, 0-RTT, and session-resumption modes, including the replay caveats that make 0-RTT a limited tool rather than a universal performance upgrade.
  4. Inspect a real TLS 1.3 handshake with openssl s_client and relate command-line output to the protocol state machine.
  5. Articulate what TLS 1.3 doesn't hide — SNI, ALPN, timing, packet sizes — and which countermeasures (ECH, padding) address what.

Why TLS 1.3 looks simpler than TLS 1.2

TLS 1.2 (and earlier) accumulated baggage: dozens of cipher suites, multiple key-exchange algorithms with subtly different security properties, an alert system that leaked information, optional features that turned into downgrade attacks, and a 2-RTT handshake that felt slower every year as page-load budgets shrank.

TLS 1.3 (RFC 8446, 2018) addressed all of those by deletion as much as by addition. The high-level changes:

  • Cipher-suite catalog shrunk to 5. All AEAD-only, all forward-secret. No RC4, no MD5, no static RSA, no DES.
  • Key exchange is always ephemeral. Static-RSA key exchange — where the server's long-term private key encrypts the premaster secret — is gone. Every TLS 1.3 connection uses (EC)DH for fresh forward secrecy.
  • Almost everything after ServerHello is encrypted. Certificate, CertificateVerify, Finished, Application Data. Only ClientHello and the early portion of ServerHello travel in cleartext.
  • 1-RTT for first contact. TLS 1.2 needed 2 RTT before sending application data. TLS 1.3 sends application data in the second flight.
  • 0-RTT optional. A returning client with a saved pre-shared key can send application data in the first flight, with one significant caveat (replay) we'll cover.
  • Key schedule formalized. A clean HKDF-based derivation tree replaces the ad-hoc PRF mess of earlier versions.
  • Renegotiation removed. The "renegotiate within an established connection" feature, source of many bugs and downgrade attacks, was deleted entirely.

The net effect: the spec is shorter than its predecessors, the implementation is smaller, and the security analysis is more tractable.

Record layer vs handshake layer

TLS has two conceptual layers, and confusion about them causes most novice debugging failures.

The record layer is the framing of bytes on the wire. Every TLS message — handshake or application data — is wrapped in a record header: 5 bytes containing record type (0x16 for handshake, 0x17 for application data, 0x14 for change-cipher-spec, 0x15 for alert), TLS version, and record length. Records are independent of TCP segmentation; one record might span multiple TCP segments, or one TCP segment might contain multiple records.

The handshake layer is the message sequence that establishes the connection: ClientHello, ServerHello, certificate, etc. Handshake messages are typed and have their own internal length fields. Each handshake message is wrapped in one or more records.

The point: when you're reading a packet capture, the layer your parser is at matters. Wireshark's TLS dissector handles both — it parses records, stitches them across TCP segments if needed, then interprets the handshake messages within. openssl s_client -msg shows handshake messages, not records.

After the handshake, application data is record-framed too. An HTTPS connection sending HTTP/2 frames is sending: TCP carrying TLS records carrying HTTP/2 frames. Each layer's framing is independent.

ClientHello anatomy

The ClientHello starts the handshake. It's the first TLS-layer byte the server sees. It's also the only part of the handshake that's unencrypted, which makes it the part with the most fingerprintable information.

Its structure:

  • Version. Says "TLS 1.2" for backwards-compatibility reasons. The actual TLS version is signaled in the supported_versions extension. This is one of the cleanups: clients that lie about their version (older deployments, or attackers) couldn't get a usable connection.

  • Random. 32 bytes of client randomness, used in the key schedule.

  • Legacy session ID. A short opaque value. Mostly empty in TLS 1.3 — a vestige of TLS 1.2's session-resumption mechanism, kept for compatibility with middleboxes that expected it.

  • Cipher suites. A list of cipher-suite identifiers the client supports. In TLS 1.3 this is one of: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_CCM_SHA256, TLS_AES_128_CCM_8_SHA256. Almost everyone supports the first three; CCM is mostly for embedded.

  • Compression methods. The list null only — TLS 1.2's compression option was disabled after the CRIME attack. TLS 1.3 doesn't have it.

  • Extensions. Where the actual modern functionality lives. The ones to know:

    • server_name (SNI). The hostname the client is connecting to. Lets one IP serve multiple domains. This is the most-discussed metadata leak in TLS — see ECH below.
    • supported_versions. The list of TLS versions the client supports, with TLS 1.3 marked. This is what actually signals "I want TLS 1.3."
    • supported_groups. The (EC)DH groups the client supports: x25519, secp256r1, secp384r1, secp521r1, ffdhe2048-ffdhe8192. Modern clients prefer x25519 for performance.
    • key_share. The client's actual ephemeral public key for one or more groups. Critical: the client speculatively offers a key for the group it expects the server to pick. If guessed right, the handshake completes in 1 RTT. If wrong, the server replies with HelloRetryRequest asking for a different group, and the client retries — adding 1 RTT.
    • signature_algorithms. Which signature schemes the client will accept on the server's certificate.
    • alpn. The Application-Layer Protocol Negotiation list — h3, h2, http/1.1. The server picks one; that's what the application protocol becomes after the handshake.
    • psk_key_exchange_modes and pre_shared_key. For session resumption (more later).
    • early_data. Set if the client wants to send 0-RTT application data with this ClientHello.
    • encrypted_client_hello (ECH). If supported and configured, contains an encrypted "inner" ClientHello whose true contents (including the real SNI) are hidden from network observers.

The ClientHello is also the source of TLS fingerprinting (JA3, JA4) that adversaries use to identify clients. The order and exact contents of cipher suites, supported_groups, signature_algorithms, and the extensions list, taken together, form a fingerprint that often uniquely identifies a client implementation. This is one reason private-network protocols like Reality (covered in Xray Reality vs WireGuard) work hard to mimic the ClientHello of a popular browser exactly.

ServerHello and key agreement

The server picks one cipher suite, one signature scheme, and one key share group from the client's offers. It generates its own ephemeral key for the chosen group, computes the shared secret with the client's offered key, and sends back:

ServerHello:
  random           32 bytes
  legacy_session_id_echo
  cipher_suite     (chosen)
  legacy_compression  (null)
  extensions:
    supported_versions  (TLS 1.3)
    key_share           (server's ephemeral public key)
    pre_shared_key      (only if resuming with PSK)

The instant the client receives ServerHello, it can compute the same shared secret using the server's public key. Both sides now hold an identical handshake secret derived from the (EC)DH exchange. From that point on, everything else in the handshake is encrypted using keys derived from the handshake secret.

The handshake secret is the moment the wire becomes opaque. Before ServerHello — and the ClientHello itself — anything along the path can read what's being sent. After ServerHello — Certificate, CertificateVerify, Finished, and all application data — only the two endpoints can read.

This is the cryptographic content of "TLS 1.3 encrypts more of the handshake than TLS 1.2 did." TLS 1.2 sent the certificate in cleartext. TLS 1.3 sends it encrypted under the handshake traffic secret.

EncryptedExtensions, Certificate, CertificateVerify, Finished

The server's first encrypted message is EncryptedExtensions, which carries server-side extensions that don't belong in the cleartext ServerHello: ALPN selection, supported_groups acknowledgment, server_name confirmation. This is mostly housekeeping.

Then comes Certificate: the server's certificate chain. Each certificate is a standard X.509 certificate; the chain ends at a root the client trusts. The client validates the chain against its trust store before accepting the server's identity.

Then CertificateVerify: a digital signature, made with the server's private key, over the entire transcript so far. This proves to the client that the server actually possesses the private key corresponding to the certificate it sent — not just that someone copied the certificate. Without CertificateVerify, an attacker could replay the server's certificate from any past handshake; with it, the attacker would also need the private key, which they shouldn't have.

CertificateVerify uses one of the signature algorithms the client offered in signature_algorithms. Common choices: ECDSA over P-256, RSA-PSS, Ed25519.

Finally Finished: a HMAC over the entire transcript (now including the server's handshake messages), keyed by a value derived from the handshake secret. This message proves the server processed the same transcript the client did — no MITM has been substituting messages — and that both sides agree on the final handshake state.

The client now has everything it needs: a confidential channel via the shared (EC)DH secret, an authenticated identity via the certificate and CertificateVerify, and integrity over the whole handshake via Finished. The client sends its own Finished message (also HMAC over the transcript), proving the same in the other direction.

After both Finished messages, the connection switches to the application traffic secrets (a different set of keys derived from the handshake secret), and application data flows.

The key schedule in human language

TLS 1.3's key schedule is a series of HKDF-based derivations turning the (EC)DH shared secret and the handshake transcript into traffic keys. Conceptually, it's a tree:

                        (EC)DH shared secret
                              |
                          HKDF-Extract
                              |
                       Handshake Secret
                              |
                          HKDF-Expand
                              |
                +-------------+-------------+
                |                           |
        Client Handshake             Server Handshake
        Traffic Secret                 Traffic Secret
                |                           |
              ...                         ...
                              |
                          HKDF-Extract
                              |
                        Master Secret
                              |
                          HKDF-Expand
                              |
                +-------------+-------------+
                |                           |
        Client Application           Server Application
        Traffic Secret                 Traffic Secret
                |                           |
            (used for encrypting all post-handshake application data)

HKDF (HMAC-based Key Derivation Function, RFC 5869) is two operations:

  • Extract. Mixes input key material with a salt to produce a uniform pseudo-random key. Used at the start of each phase to absorb the (EC)DH secret and "reset" entropy.
  • Expand. Generates one or more output keys of arbitrary length from the uniform key. Used to derive the actual traffic-encryption keys.

The operational consequence: each phase has a separate set of keys. Compromising the application traffic secret doesn't reveal the handshake traffic secret. A new (EC)DH key per connection means each connection has its own set of keys; compromise of one connection doesn't compromise others.

This is forward secrecy in modern TLS: even if the server's long-term private key (the certificate signing key) is compromised tomorrow, today's recorded handshake can't be decrypted because the (EC)DH ephemeral keys were destroyed at the end of the connection. Mandatory forward secrecy is one of TLS 1.3's biggest security wins.

Session resumption and PSKs

A client that has connected before doesn't need to do the full handshake again. The server, at the end of the previous handshake, can send a NewSessionTicket containing an opaque blob that the server can later use to recover the session keys. The client stores this blob.

On reconnection, the client sends the blob in the pre_shared_key extension. The server recognizes it, recovers the keys, and the connection resumes — typically in 1 RTT, sometimes 0-RTT if early_data is offered.

Two flavors:

  • External PSK — keys provisioned out-of-band between client and server. Useful for closed environments where you don't want certificates.
  • Resumption PSK — keys derived from a previous handshake, packaged in a ticket.

Resumption PSKs are what most consumer scenarios use: the browser saves tickets after each TLS handshake, and presents one on reconnection. The server's storage burden is minimal because the ticket can be self-contained — encrypted with the server's session-ticket key — meaning the server doesn't need a session database.

PSK-only handshakes (no certificate) are valid in TLS 1.3 and are how 0-RTT data transmission works.

0-RTT early data

A returning client with a resumption PSK can send the request with the ClientHello. The server processes the early data using keys derived from the PSK, before completing its own ServerHello and Finished. The application has already started before the cryptographic handshake is finished.

The performance win is real on long-RTT paths: the client saves a full RTT on reconnections.

The security caveat is also real: early data is replayable. Because it's encrypted with keys derived purely from the PSK (no fresh randomness from the server), an attacker who captured an early-data flight can replay it later and the server will accept it as new traffic. The attacker won't decrypt the response — they don't have the keys — but they can cause the same request to be processed again.

For idempotent requests (GET /pricing.html), this is fine. For state-changing requests (POST /transfer-money), it's not. The TLS spec mandates that servers either:

  • Refuse early data for state-changing endpoints, or
  • Maintain anti-replay state (a record of recently-seen early-data nonces) to detect replays.

In practice, web servers treat 0-RTT as opt-in per route. Most routes that are unsafe to replay don't accept early data; routes that are safe (static resources, idempotent reads) do. The HTTP/2 and HTTP/3 specs both require servers to validate early-data eligibility per request.

The simplest mental model: 0-RTT is a performance feature for safe-to-replay requests only. If you don't know whether your request is safe to replay, don't use 0-RTT.

What the wire still leaks

TLS 1.3 protects payload confidentiality and authenticity. It doesn't hide:

SNI. The hostname in the ClientHello travels in cleartext. Network observers can see which domain you're connecting to even when payload is encrypted. Encrypted Client Hello (ECH, RFC 9180) addresses this: the real ClientHello — including the real SNI — is encrypted under a key the server publishes via DNS HTTPS records. The visible "outer" ClientHello has a generic SNI ("frontend.example.com"). Deployment is partial in 2026 — Cloudflare and Apple have rolled it out, others are slower.

ALPN. The application protocol selection (h3, h2, http/1.1) is in the cleartext ClientHello and ServerHello. ECH protects this too when deployed.

Timing and packet sizes. TLS 1.3 records can be padded to obscure exact payload sizes, but few stacks do this aggressively. A request to a login page produces different-sized responses than a request to a 404 page; an observer who knows the site can fingerprint behavior even without seeing the bodies. Application-aware padding (or sending fixed-size chunks) is the only structural defense, and it's expensive in bandwidth.

Connection metadata. Source IP, destination IP, port, packet timestamps. None of those are TLS's responsibility. Anonymity-preserving protocols (Tor, mix networks) layer additional protection on top of TLS for exactly this reason.

The lock icon in your browser means "the bytes between you and the server you reached are confidential and authenticated." It does not mean "no one can see what site you visited" or "no one can tell what you did once you got there." That's a bigger problem with bigger answers.

Hands-on exercise

Exercise 1 — Inspect a live TLS 1.3 handshake

openssl s_client -connect example.com:443 -servername example.com -tls1_3 -msg

Look at the output. The lines starting with >>> are messages sent by the client; <<< are messages received from the server. Find:

  • The ClientHello (very long — has all the extensions).
  • The ServerHello (much shorter — just the chosen cipher suite, key share, and a couple of extensions).
  • The encrypted extensions, certificate, certificate verify, and finished — these may show up as record-level "Application Data" because they're encrypted under the handshake key, and openssl s_client's default presentation hides their inner structure.
  • The negotiated parameters: Protocol : TLSv1.3, Cipher : TLS_AES_128_GCM_SHA256, Server certificate followed by the chain.

After the handshake, you can type plain HTTP requests:

GET / HTTP/1.1
Host: example.com

(Two newlines.)

You'll see the response. The transport is encrypted; the HTTP semantics ride on top, just like Module 1.10 covered.

Stretch: add -key /tmp/keys.txt -keylogfile /tmp/keys.txt (recent OpenSSL) and use Wireshark to decrypt the captured traffic. Wireshark imports the keylog file under Edit → Preferences → Protocols → TLS → Pre-Master Secret log filename.

Exercise 2 — ALPN and HTTP version selection

# What ALPN does the server announce for HTTP?
openssl s_client -connect routeharden.com:443 -servername routeharden.com \
  -alpn h2,http/1.1 -tls1_3 < /dev/null 2>&1 | grep "ALPN protocol"

# Confirm the application protocol curl actually uses.
curl -sI --http2 https://routeharden.com 2>&1 | head -3

The first command negotiates TLS with ALPN offering both h2 and http/1.1; the server picks the application protocol it prefers. The second confirms that curl and the server actually use HTTP/2 over TLS once the connection is up.

The interaction between TLS and HTTP versions is happening exactly here: TLS picks the application protocol via ALPN; HTTP semantics begin afterward. The version is decided in the TLS layer, not by HTTP itself.

Common misconceptions

"TLS encrypts everything." TLS encrypts payload after the ServerHello. The ClientHello is in cleartext, including the SNI (the hostname you're connecting to). ECH addresses this when deployed; without ECH, network observers know which domain you're connecting to.

"The certificate creates the encryption." The certificate authenticates the peer's identity. The (EC)DH key exchange creates the encryption keys. Compromising the certificate's private key in TLS 1.3 doesn't decrypt past sessions because the (EC)DH ephemeral keys are destroyed at the end of the connection (forward secrecy).

"0-RTT is a free speedup." Early data is replayable. An attacker can capture and replay the encrypted bytes; the server has to either refuse early data per route or implement anti-replay machinery. 0-RTT is fine for safe-to-replay GETs, dangerous for state-changing operations.

"TLS 1.3 removed public-key crypto from the handshake." It removed static RSA key exchange (where the server's long-term key encrypted the premaster secret). TLS 1.3 still uses public-key cryptography intensively — for ephemeral (EC)DH key agreement and for the server's certificate-based authentication. It's just that long-term keys never travel as session encryption keys.

"If the lock icon is green, the site is trustworthy in every sense." TLS authenticates that you're connected to the holder of the certificate, and only that. It says nothing about whether the application is well-built, whether the operator is honest, or whether your request is going to a legitimate business. Trust still requires a separate set of judgments.

Further reading

  1. RFC 8446 — TLS 1.3. The protocol specification. Worth reading once for the algorithm definitions and the security rationale appendix.
  2. RFC 5869 — HKDF. The key-derivation primitive that powers the entire key schedule.
  3. RFC 9180 — Encrypted Client Hello. The mechanism for hiding SNI and other ClientHello metadata. Deployment is partial.
  4. RFC 9001 — Using TLS to Secure QUIC. How TLS 1.3 is integrated into QUIC for HTTP/3. Different framing, same cryptographic core.
  5. RFC 8773 — TLS 1.3 Extension for Certificate-Based Authentication with an External Pre-Shared Key. PSK + cert authentication for cases where neither alone suffices.
  6. Eric Rescorla, The Transport Layer Security (TLS) Protocol Version 1.3, Real World Crypto talks 2017–2018. Slides and recordings exist; the protocol's chief author walks through the design choices in plain English.
  7. TLS 1.3 by David Wong (book). Modern crypto textbook with a substantial TLS 1.3 chapter, intended for engineers.

The next module — NAT traversal and the end-to-end principle — moves from in-line confidentiality to a different kind of address-translation challenge: how applications find and connect to each other across NATs that broke the "every host has a unique address" promise.