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
Question
Core Understanding
Why does the common handshake require only one RTT
The client provides a suitable key_share in advance
Why does HelloRetryRequest occur
Conditions such as shared material and cookies need to be supplemented
What key is used before certificate encryption
Derive handshake keys first after ServerHello
Does TLS 1.3 guarantee forward secrecy
Must distinguish between asymmetric negotiation, PSK-only, and early data
Why can't the three layers of HKDF be concatenated directly via Extract
There are derived steps between layers, and each branch has tags and transcripts
Does ECH hide all access behavior
Hides 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
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 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
Let Derive-Secret(S, label, messages) denote derivation using the transcript hash label. The common asymmetric negotiation path is:
Early = Extract(zeros, PSK), where the input is filled with zeros to the hash length if PSK is not used.
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.
{"id":"tls13-hkdf-labels","title":"Why can the same secret derive keys for different directions?","summary":"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.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"role\"><button data-mode=\"c\" data-i18n=\"client\"></button><button data-mode=\"s\" data-i18n=\"server\"></button></div><label class=\"form-field\"><span data-i18n=\"message\"></span><input type=\"text\" id=\"message\" value=\"hello transcript\" maxlength=\"24\" autocomplete=\"off\" spellcheck=\"false\"></label><label class=\"check\"><input type=\"checkbox\" id=\"skip\"><span data-i18n=\"skip\"></span></label><section class=\"panel\"><h3 data-i18n=\"outputs\"></h3><div id=\"outputs\"></div></section><p id=\"encoding\" class=\"message\"></p><p id=\"status\" class=\"status\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#outputs .tr{grid-template-columns:1fr 1.4fr}#outputs .tr>*{white-space:pre-line}.tr>*{min-height:4em;font-family:var(--viz-font-ui)}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}const enc=s=>new TextEncoder().encode(s),cat=(...xs)=>Uint8Array.from(xs.flatMap(x=>[...x])),hex=x=>[...x].map(n=>n.toString(16).padStart(2,'0')).join('');async function sha(x){return new Uint8Array(await crypto.subtle.digest('SHA-256',x))}async function hmac(k,x){const key=await crypto.subtle.importKey('raw',k,{name:'HMAC',hash:'SHA-256'},false,['sign']);return new Uint8Array(await crypto.subtle.sign('HMAC',key,x))}async function prf(secret,label,seed,n){const s=cat(enc(label),seed);let a=s,out=new Uint8Array;while(out.length<n){a=await hmac(secret,a);out=cat(out,await hmac(secret,cat(a,s)))}return out.slice(0,n)}async function expand(secret,info,n){let prev=new Uint8Array,out=new Uint8Array;for(let i=1;out.length<n;i++){prev=await hmac(secret,cat(prev,info,[i]));out=cat(out,prev)}return out.slice(0,n)}async function label(secret,name,ctx,n){const l=enc('tls13 '+name),info=cat([n>>8,n&255,l.length],l,[ctx.length],ctx);return expand(secret,info,n)}async function derive(secret,name,messages){return label(secret,name,await sha(messages),32)}let role='c',epoch=0;async function draw(){const e=++epoch;$('#status').dataset.ready='false';set('status',t('busy'));const zero=new Uint8Array(32),early=await hmac(zero,zero),salt=$('#skip').checked?early:await derive(early,'derived',new Uint8Array),hs=await hmac(salt,Uint8Array.from({length:32},(_,i)=>i)),traffic=await derive(hs,role+' hs traffic',enc($('#message').value)),key=await label(traffic,'key',new Uint8Array,16);if(e!==epoch)return;table('outputs',['Early','Extract salt','Handshake','Traffic','Key'].map((name,i)=>[name,hex([early,salt,hs,traffic,key][i].slice(0,8)).match(/.{8}/g).join(String.fromCharCode(10))]));set('encoding','Label = tls13 '+role+' hs traffic\\nLength = 32; Context = SHA256(transcript)\\nkey: Length = 16; Context = empty');$('#outputs').dataset.hex=JSON.stringify([early,salt,hs,traffic,key].map(hex));set('status',t('ready'));$('#status').dataset.ready='true';pressed(role)}table('outputs',['Early','Extract salt','Handshake','Traffic','Key'].map(x=>[x,'00000000'+String.fromCharCode(10)+'00000000']));document.querySelectorAll('input').forEach(e=>e.oninput=draw);document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{role=b.dataset.mode;draw()});draw();","audio":false,"strings":{"busy":"Calculating","client":"c hs traffic","heading":"Why can the same secret derive keys for different directions?","intro":"Change labels or transcript to view HKDF encoding and actual output; or compare with the error path of omitting derived.","message":"transcript teaching bytes","outputs":"First 8 B of derivation result","ready":"Recalculated","reset":"Reset","role":"Direction Label","scope":"Actual SHA256/HMAC HKDF, no PSK, shared=00…1f, transcript is teaching string. Only calculate up to handshake traffic secret and 16 B key, do not execute full TLS authentication.","scopeLabel":"Model Scope and Conventions","server":"s hs traffic","skip":"Intentionally omit derived (error)","status":"Calculation Status"}}
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.
Mode
New Asymmetric Shared Secret
Forward Secrecy Boundary
Certificate + Temporary Asymmetric Negotiation
Yes
Depends on algorithm and secret destruction conditions
PSK + New Asymmetric Negotiation
Yes
Normal subsequent traffic can be protected by the new secret
PSK-only (psk_ke)
No
Cannot claim forward secrecy equivalent to new key exchange
0-RTT early data
Not yet mixed with server's new share
Relies 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
importhashlibimporthmacimportstructdefextract(salt,ikm):returnhmac.digest(salt,ikm,'sha256')defexpand(prk,info,length):previous,result=b'',b''forcounterinrange(1,(length+31)//32+1):previous=hmac.digest(prk,previous+info+bytes([counter]),'sha256')result+=previousreturnresult[:length]defexpand_label(secret,label,context,length):label=b'tls13 '+labelinfo=struct.pack('!H',length)+bytes([len(label)])+label+bytes([len(context)])+contextreturnexpand(secret,info,length)defderive(secret,label,messages):returnexpand_label(secret,label,hashlib.sha256(messages).digest(),32)# RFC 5869 Appendix A.1, SHA256 test vector.
ikm=bytes([11])*22prk=extract(bytes.fromhex('000102030405060708090a0b0c'),ikm)assertprk.hex()=='077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5'assertexpand(prk,bytes.fromhex('f0f1f2f3f4f5f6f7f8f9'),42).hex()==('3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf''34007208d5b887185865')zero=bytes(32)early=extract(zero,zero)# Early secret without PSK in RFC 8448.
assertearly.hex()=='33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a'shared=bytes(range(32))# Teaching input, not a real key exchange result.
handshake=extract(derive(early,b'derived',b''),shared)asserthandshake!=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')assertclient!=serverassertlen(expand_label(client,b'key',b'',16))==16assertlen(expand_label(client,b'iv',b'',12))==12assertlen(main)==32and1000+1+16+5==1022print('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