TLS 1.3 Handshake

On this page

TLS 1.3 places the material required for key negotiation in the ClientHello in advance, allowing a common full handshake to reduce one round of waiting; at the same time, it phases the protection of handshake traffic and application traffic. It still requires identity verification, historical negotiation, and key derivation, and 0-RTT has different security attributes from normal application data.

As of September 2026, the TLS 1.3 updated specification is RFC 9846, released in July 2026, which replaces RFC 8446. This article uses the updated specification as the entry point and retains the correspondence with common names from older materials. ECH was published as RFC 9849 in March 2026 and is no longer just a draft. RFC 9846, RFC 9849

QuestionCore Understanding
Why does the common handshake require only one RTTThe client provides a suitable key_share in advance
Why does HelloRetryRequest occurConditions such as shared material and cookies need to be supplemented
What key is used before certificate encryptionDerive handshake keys first after ServerHello
Does TLS 1.3 guarantee forward secrecyMust distinguish between asymmetric negotiation, PSK-only, and early data
Why can't the three layers of HKDF be concatenated directly via ExtractThere are derived steps between layers, and each branch has tags and transcripts
Does ECH hide all access behaviorHides the inner ClientHello, but metadata still has boundaries

Version, Cipher Suites, and Authentication Algorithms Negotiated Separately

TLS 1.3 version selection looks at supported_versions; the compatible field legacy_version uses 0x0303, so seeing this field does not directly indicate that the final negotiated version is TLS 1.2. The selected_version in ServerHello and the full handshake context are the basis for judgment.

TLS_AES_128_GCM_SHA256 only expresses the record AEAD and the hash used for HKDF/transcript, unlike TLS 1.2 suite names which also bind key exchange and authentication categories. key_share/supported_groups and signature_algorithms express other constraints separately; you cannot infer that the certificate must be RSA or key exchange must be X25519 from an AES suite name.

TLS 1.3 removes old mechanisms such as static RSA key exchange and CBC record protection; RSA signatures do not disappear entirely. The five basic suites in the specification do not mean that the registration space will forever only have five values. TLS 1.2 and TLS 1.3 suites cannot be used cross-wise. RFC 9846: Negotiation and Suites

Four Key Actions in One Round Trip

Common full handshake with certificate authentication + ECDHE: negotiate secrets first, then authenticate history Client Server ClientHello: supported_versions, key_share, etc. ServerHello: selected version, suite, and key_share Encrypted: EncryptedExtensions, Certificate, CertificateVerify, Finished Encrypted: Finished; then send data using application keys ClientHello/ServerHello baselines are still visible; Finished and Application Data use traffic keys from different phases.

The client provides supported groups and the public key share for the selected group in ClientHello; the server selects compatible material and returns its own share. In common ECDHE modes, both parties can calculate the shared secret at this point, derive handshake traffic keys, and thus subsequent messages such as Certificate can be encrypted.

"Already able to decrypt" does not mean "already authenticated the other party." The client still needs to verify the certificate service identity, the CertificateVerify signature, and Finished; the server confirms the corresponding handshake status after receiving the client's Finished. In normal server authentication scenarios, the client can send Finished and subsequent application data after the first RTT, but they use keys from different phases.

The "one RTT" here refers to TLS handshake semantics, excluding TCP connection establishment, DNS, or business response. TLS 1.2 handshake messages were not all plaintext either; its Finished used negotiated record protection. TLS 1.3's advantage is protecting more handshake content earlier, rather than jumping from "completely unprotected" in one step.

HelloRetryRequest: Normal Retry When Material is Needed

supported_groups lists supported groups, and key_share only carries actual material for some of them; if the server needs a group that is not carried but is supported by both parties, it can send an HRR, and may also require a cookie. The client constructs a second ClientHello according to the constraints, adding one round trip.

HRR is a ServerHello structure with a special Random value and cannot be treated as a normal ServerHello just by looking at the message type. The transcript uses a synthesized message_hash to include the hash of the first ClientHello and cannot be simply discarded. Implementations must also check for prohibited second HRRs and illegal parameter changes. RFC 9846: HelloRetryRequest

Whether an HRR occurs depends on deployment group policies and client material; you cannot use "X25519 almost always guesses correctly" as a permanent fact. The updated specification uses more general asymmetric key establishment terminology for related mechanisms and cannot define all future key_share as bare public keys on elliptic curves.

What Do CertificateVerify and Finished Prove?

CertificateVerify signs the specified context and transcript hash with the private key corresponding to the certificate, binding identity authentication to this handshake. The signature input also contains fixed padding and role context strings; it is not just signing a piece of ClientHello text arbitrarily.

Finished derives finished_key from the handshake traffic secret of that direction and authenticates the transcript hash with HMAC. Under SHA256 suites, verify_data is 32 B, and under SHA384 it is 48 B; you cannot copy the common 12 B from TLS 1.2. Certificate verification, proof of private key possession, and shared key confirmation complement each other; missing any one cannot be compensated for by "TLS can decrypt." RFC 9846: Authentication Messages

Compatibility mode may show dummy CCS, but TLS 1.3 does not use TLS 1.2-style CCS to switch real encryption states. Packet captures should understand records according to handshake phases and traffic secrets, rather than treating every CCS as a key switch event.

HKDF: Extract Mixes Secrets, Expand Separates Purposes

HKDF-Extract gets PRK from salt and input keying material; HKDF-Expand then derives output using info and length. TLS's HKDF-Expand-Label encodes length, a label with a tls13 prefix, and context in info. Labels are protocol bytes and cannot be changed due to natural language variable renaming. RFC 5869: HKDF

Key Schedule: Two derived steps cannot be omitted Early Secret = Extract(0, PSK or zero value) Early traffic keys are derived from this phase Handshake Secret = Extract(derived₁, shared secret) Derive client/server handshake traffic secrets Main Secret = Extract(derived₂, zero value) Derive application traffic secrets, exporter, and resumption secrets Derive-Secret(Early Secret, "derived", empty_messages) Derive-Secret(Handshake Secret, "derived", empty_messages) The figure shows the common path mixing asymmetric shared secrets; traffic secrets then derive keys, IVs, etc., for each direction using labeled Expand.

Let Derive-Secret(S, label, messages) denote derivation using the transcript hash label. The common asymmetric negotiation path is:

  1. Early = Extract(zeros, PSK), where the input is filled with zeros to the hash length if PSK is not used.
  2. derived₁ = Derive-Secret(Early, "derived", empty_messages).
  3. Handshake = Extract(derived₁, shared_secret).
  4. derived₂ = Derive-Secret(Handshake, "derived", empty_messages).
  5. Main = Extract(derived₂, zeros).

Both Extracts require deriving a derived secret first; omitting either changes subsequent secrets and traffic keys. Main Secret was called Master Secret in RFC 8446 materials; the updated specification changes the terminology but retains labels required for compatibility. RFC 9846: Key Schedule

Handshake traffic secret is bound to the ServerHello transcript, initial application traffic secret is bound to the server Finished, and resumption-related secrets are also bound to the client Finished. Not every Expand mixes in "all messages at this moment"; branches like key/IV/finished have their own context rules and cannot be uniformly replaced.

Record Nonce and Overhead

Application traffic secret derives key and IV; TLS 1.3 record nonce is obtained by XORing the IV for that direction with the padded record sequence number, unlike TLS 1.2 GCM which sends an additional 8 B explicit nonce online. After key update, sequence numbers are managed according to the new state, and the same key/nonce must not be reused.

Example of AES-GCM without extra padding: 1000 B content plus 1 B inner content type, 16 B tag, and 5 B outer header totals 1022 B TLS data. The inner real type is encrypted, and the outer protected record uses the corresponding compatible type; this does not hide packet length and timing.

Why can the same secret derive keys for different directions?

Direction switching should only change traffic and its sub-branches, not the upstream Handshake Secret; modifying the transcript is the same. Checking "omit derived" changes the salt for Extract, affecting all subsequent values starting from Handshake. Read the dependency graph through change boundaries.

Preparing the visual
Why can the same secret derive keys for different directions?

Labels and context are derivation inputs, not explanatory text. c/s branches share the parent secret but separate purposes; omitting derived produces different results in earlier Extract steps.

PSK Resumption and 0-RTT are Two Different Things

Resumption uses existing PSK state to establish a new connection; it can choose normal 1-RTT, or attempt early data when appropriate. The ticket field in NewSessionTicket is a resumption identifier/encapsulation recognizable by the server, not sending PSK in plaintext; the client derives the corresponding PSK based on the resumption secret and ticket nonce, etc.

The PSK identity in ClientHello specifies candidates, binder proves the client holds the relevant PSK, and binds to specified parts of ClientHello. It does not authenticate new server messages not yet received, nor does it automatically make early data non-replayable.

ModeNew Asymmetric Shared SecretForward Secrecy Boundary
Certificate + Temporary Asymmetric NegotiationYesDepends on algorithm and secret destruction conditions
PSK + New Asymmetric NegotiationYesNormal subsequent traffic can be protected by the new secret
PSK-only (psk_ke)NoCannot claim forward secrecy equivalent to new key exchange
0-RTT early dataNot yet mixed with server's new shareRelies mainly on PSK, does not have equivalent forward secrecy

Therefore, "TLS 1.3 only allows ECDHE and forces forward secrecy for all data" is incorrect. The dhe in psk_dhe_ke name is a historical naming in the updated specification, covering the corresponding asymmetric key establishment mode. RFC 9846: PSK Modes

Acceptance and Replay of Early Data

The server can reject early data while continuing the handshake. The client must distinguish between "handshake success" and "early operation accepted," and decide how to resend based on application semantics. max_early_data_size constrains the acceptable scale and cannot be used as a business deduplication mechanism.

Attackers may replay valid early data; the server needs to handle this in combination with ticket usage, time windows, anti-replay state, and application design; cross-node caching may also make one-time use difficult to guarantee. HTTP method names are not security proofs; GET with side effects is still not suitable for casual early execution. 0-RTT reduces waiting before sending, but does not mean receiving business results in zero time. RFC 9846: 0-RTT and Anti-Replay

KeyUpdate: Switch Direction Keys, Do Not Re-authenticate Identity

After the connection is established, KeyUpdate can be used to advance application traffic secrets, then protect records in that direction according to the new key/IV, and require the peer to respond to the update according to rules. It does not re-send certificates, does not mean re-executing asymmetric key exchange, and does not automatically restore all security for currently leaked secrets.

Closed TLS renegotiation, client authentication after handshake, and KeyUpdate are different mechanisms. If the application needs to refresh authorization status, it should design it within its own lifecycle and cannot treat "switching traffic key" as user permissions being re-verified.

ECH: Protect Inner ClientHello, Do Not Eliminate All Metadata

RFC 9849 defines inner and outer ClientHello: real sensitive parameters are placed in the protected inner layer, and the outer layer provides public information for frontend processing; the server holding the corresponding private key decrypts the inner layer. It protects not just the bare SNI field, but also requires handling acceptance confirmation, configuration expiration, and failure retries. RFC 9849: ECH

The client needs to obtain ECHConfigList first. The ech parameter in DNS SVCB/HTTPS is one standard bootstrapping method; the authenticity and privacy of configuration acquisition are also part of complete deployment; it is not guaranteed that ECH is enabled just because the domain has an HTTPS record. RFC 9848: DNS Bootstrapped ECH

ECH combined with encrypted DNS reduces direct exposure of names, but destination IP, outer public name, traffic size, time, and server logs are still visible. A dedicated IP may also directly imply the site, so it cannot promise that "which website the user visited is completely hidden."

Local HKDF Verification and Derivation Comparison

import hashlib
import hmac
import struct

def extract(salt,ikm):
    return hmac.digest(salt,ikm,'sha256')
def expand(prk,info,length):
    previous,result = b'',b''
    for counter in range(1,(length+31)//32+1):
        previous = hmac.digest(prk,previous+info+bytes([counter]),'sha256')
        result += previous
    return result[:length]
def expand_label(secret,label,context,length):
    label = b'tls13 '+label
    info = struct.pack('!H',length)+bytes([len(label)])+label+bytes([len(context)])+context
    return expand(secret,info,length)
def derive(secret,label,messages):
    return expand_label(secret,label,hashlib.sha256(messages).digest(),32)

# RFC 5869 Appendix A.1, SHA256 test vector.
ikm = bytes([11])*22
prk = extract(bytes.fromhex('000102030405060708090a0b0c'),ikm)
assert prk.hex()=='077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5'
assert expand(prk,bytes.fromhex('f0f1f2f3f4f5f6f7f8f9'),42).hex()==(
    '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf'
    '34007208d5b887185865')
zero = bytes(32)
early = extract(zero,zero)
# Early secret without PSK in RFC 8448.
assert early.hex()=='33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a'
shared = bytes(range(32)) # Teaching input, not a real key exchange result.
handshake = extract(derive(early,b'derived',b''),shared)
assert handshake != extract(early,shared)
main = extract(derive(handshake,b'derived',b''),zero)
client = derive(handshake,b'c hs traffic',b'hello transcript')
server = derive(handshake,b's hs traffic',b'hello transcript')
assert client!=server
assert len(expand_label(client,b'key',b'',16))==16
assert len(expand_label(client,b'iv',b'',12))==12
assert len(main)==32 and 1000+1+16+5==1022
print('HKDF standard vector, early secret, derived steps, and record overhead checks passed')

This example verifies HKDF primitives and some derivation relationships; it does not implement full transcript, certificate authentication, real ECDHE/ECH, or TLS interoperability. Public test values are not production secrets. RFC 8448: Handshake Test Materials

Continue Reading