TLS handshake
TLS handshake krok za krokem
TLS a HTTPS prakticky řeší to, co potřebuješ nastavit. Tahle stránka řeší, co si ty dvě strany doopravdy říkají — protože jakmile to jednou uvidíš, přestanou být chybové hlášky náhodné a openssl s_client se stane užitečným nástrojem místo záhadného výpisu.
Předpokládá Diffie-Hellmana, podpisy a certifikáty.
Co má handshake zvládnout
Za jedno až dvě kolo tam a zpět musí padnout čtyři rozhodnutí:
- Kterou verzi a které algoritmy budeme používat — a to bez toho, aby to šlo po cestě zmanipulovat.
- Na jakém klíči se domluvíme — s dopředným utajením.
- Kdo je server — certifikát a hlavně důkaz, že k němu má soukromý klíč.
- Že se po cestě nic nezměnilo — ověření celého dosavadního průběhu.
Rozdíl mezi TLS 1.2 a 1.3 je hlavně v tom, kolik kol to stojí a kolik toho jde vidět zvenku.
TLS 1.3
RFC 8446, rok 2018. Dnešní norma a jediné, co má smysl nasazovat.
sequenceDiagram
participant K as Klient
participant S as Server
K->>S: ClientHello
Note right of K: verze, náhoda, seznam šifer,<br/>SNI, ALPN, key_share (X25519), sig algs
S->>K: ServerHello
Note left of S: vybraná šifra, náhoda, key_share
Note over K,S: obě strany mají klíče — dál je vše šifrované
S->>K: {EncryptedExtensions}
S->>K: {Certificate}
S->>K: {CertificateVerify}
S->>K: {Finished}
K->>K: ověří řetěz, jméno, podpis
K->>S: {Finished}
K->>S: {GET / HTTP/1.1}
S->>K: {NewSessionTicket}
Složené závorky znamenají šifrováno. Certifikát jde přes drát zašifrovaný — to je jedna z největších změn oproti 1.2 a znamená, že odposlech už z certifikátu nepozná, na jaký web jdeš. Jméno v SNI ale zůstává otevřené, takže je to jen půlka výhry. Tu druhou půlku má vyřešit ECH, viz níže.
ClientHello
Všechno podstatné je v prvním paketu. Klient neposílá otázku, posílá nabídku i s prvním tahem:
| Položka | K čemu |
|---|---|
supported_versions |
„umím 1.3". Pole verze o řádek výš zůstalo kvůli starým krabicím zamrzlé na hodnotě pro 1.2. |
random |
32 bajtů náhody, aby dvě spojení nikdy nevyšla stejně |
| seznam šifer | u 1.3 jen pět možností |
server_name (SNI) |
na kterou doménu jdu — nešifrovaně |
application_layer_protocol_negotiation |
h2, http/1.1 — viz HTTP a QUIC |
supported_groups |
které křivky umím |
key_share |
rovnou veřejná část mého dočasného klíče |
signature_algorithms |
jakými podpisy mě smíš přesvědčit |
To key_share je celý trik zrychlení. Klient hádá dopředu, kterou skupinu server zvolí (prakticky vždycky X25519 nebo hybrid s ML-KEM) a rovnou přiloží svou půlku Diffie-Hellmana. Server odpoví svou půlkou a klíč je hotový po jednom kole.
Když klient hádá špatně, přijde HelloRetryRequest s určením správné skupiny a stojí to jedno kolo navíc. Proto se výchozí nastavení nemá bez důvodu měnit.
ServerHello a co následuje
Server vybere šifru a skupinu, pošle svou část key_share a od téhle chvíle je všechno šifrované. Ještě ne klíči pro data — nejdřív klíči pro handshake, které se odvodí právě z DH výsledku.
EncryptedExtensions — zbytek dohody, který už nemusí být vidět (ALPN, velikost záznamů).
Certificate — řetěz certifikátů bez kořene.
CertificateVerify — tohle je ta část, kterou většina výkladů vynechá, a přitom je nejdůležitější. Server podepíše otisk celého dosavadního průběhu soukromým klíčem, který patří k certifikátu.
Bez tohohle by certifikát nedokazoval nic — je to veřejný soubor, kdokoliv si ho může stáhnout a poslat dál. CertificateVerify je důkaz, že protistrana má odpovídající soukromý klíč. To je ten okamžik, kdy se z výměny klíčů stane ověřené spojení a kdy padá útok prostředníka.
Finished — HMAC přes otisk celého průběhu, klíčem z právě dohodnutého tajemství. Ověří, že obě strany viděly totožnou posloupnost zpráv. Kdyby útočník cokoliv změnil nebo umazal — třeba vyškrtl silnější šifru ze seznamu — otisky se rozejdou a spojení skončí.
Ověření na straně klienta
Server prohlásil, kdo je. Klient teď musí ověřit čtyři věci a selhání kterékoliv znamená konec:
- Řetěz vede ke kořeni, kterému věřím. Podpisy sedí až k certifikátu v úložišti důvěry.
- Jméno souhlasí. Doména z SNI je v
subjectAltName.CNse dnes ignoruje. - Platnost.
notBeforeažnotAfter, podle hodin klienta — proto rozbité datum v zařízení rozbije HTTPS. - Podpis v
CertificateVerifysedí s veřejným klíčem z certifikátu.
Případně ještě odvolání, viz dál.
Klíčový rozvrh
Klíče nevzniknou přímo z DH výsledku, ale postupným odvozováním pomocí HKDF:
PSK (nebo nuly) ──extract──► Early Secret
│ derive
DH tajemství ──extract──► Handshake Secret
├──► klíč klienta pro handshake
├──► klíč serveru pro handshake
│ derive
──extract──► Master Secret
├──► klíč klienta pro data
├──► klíč serveru pro data
├──► resumption secret (pro tikety)
└──► exporter secret
Proč tolik kroků:
Každý směr má vlastní klíč. Kdyby byly stejné, šlo by pakety přehrát zpátky odesílateli.
Handshake a data mají vlastní klíče. Únik jednoho neohrozí druhé.
Do každého odvození vstupuje otisk dosavadního průběhu. Klíč tedy závisí i na tom, co si strany řekly — nejen na DH výsledku. To dělá manipulaci s handshakem neproveditelnou.
Klíče se dají za běhu obměnit (KeyUpdate), aniž by se navazovalo nové spojení.
Ochrana proti podvržení verze
Elegantní drobnost. Když server umí 1.3, ale klient se hlásí ke starší verzi, server do posledních osmi bajtů svého random vloží pevnou značku — bajty DOWNGRD a číslo.
Klient, který 1.3 umí, tuhle značku hledá. Když ji najde v situaci, kdy měl dostat 1.3, ví, že se do komunikace vložil někdo, kdo mu ukradl podporu nové verze, a spojení ukončí. Značka je uvnitř random, který je krytý podpisem, takže se nedá odstranit.
TLS 1.2 pro srovnání
Pořád se s ním potkáš u starších zařízení. Rozdíl je hlavně v tom, že se nejdřív mluví a teprve pak počítá:
sequenceDiagram
participant K as Klient
participant S as Server
K->>S: ClientHello
S->>K: ServerHello
S->>K: Certificate
S->>K: ServerKeyExchange (DH parametry + podpis)
S->>K: ServerHelloDone
K->>S: ClientKeyExchange (moje DH část)
K->>S: ChangeCipherSpec
K->>S: {Finished}
S->>K: ChangeCipherSpec
S->>K: {Finished}
K->>S: {GET / HTTP/1.1}
Dvě kola místo jednoho a certifikát letí otevřeně.
Zásadnější je ale to, co TLS 1.3 vyhodilo:
| Vyhozeno | Proč |
|---|---|
| RSA key transport | žádné dopředné utajení |
| statické DH | totéž |
| CBC režimy | padding oracle, Lucky13, POODLE |
| RC4, 3DES, DES, export šifry | prolomené nebo slabé |
| SHA-1 a MD5 v podpisech | kolize |
| komprese | CRIME |
| renegociace | složitost a útoky |
| vlastní DH parametry | Logjam |
| vyjednávání „custom" všeho | plocha pro útok |
Je to seznam dvaceti let útoků. TLS 1.3 nepřidalo tolik nového, jako spíš odstranilo možnost udělat chybu — v tom je jeho hlavní přínos.
Názvy šifer
V TLS 1.2 popisuje název čtyři věci:
ECDHE - RSA - AES128 - GCM - SHA256
│ │ │ │ │
│ │ │ │ └── hash pro odvození klíčů
│ │ │ └───────── režim
│ │ └───────────────── šifra
│ └──────────────────────── čím se server podepisuje
└─────────────────────────────── výměna klíčů
V TLS 1.3 jich popisuje jen dvě, protože výměna klíčů i typ podpisu se vyjednávají zvlášť:
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
To je celý výběr (plus dvě varianty s CCM pro úsporná zařízení). Nedá se zvolit nic špatného, což je záměr. Konfigurace typu ssl_ciphers se u čistého 1.3 tedy nemá co ladit — a když ji v návodu vidíš, je z doby 1.2.
Obnovení relace
Plný handshake stojí kolo tam a zpět plus asymetrické operace. Pro druhý požadavek na tentýž web je to zbytečné.
TLS 1.3 to řeší tiketem. Server po handshake pošle NewSessionTicket — zašifrovanou hroudu, které rozumí jen on sám. Klient ji při dalším spojení přiloží do ClientHello jako předsdílený klíč a handshake se zkrátí.
Tady je ale past, která se týká přímo tvé konfigurace nginxu:
Tiket šifruje server dlouhodobým klíčem. Když se ten klíč nerotuje a unikne, dá se dešifrovat všechen provoz obnovených relací. Dopředné utajení je pryč, přestože jsi poctivě používal ECDHE.
Odtud to ssl_session_tickets off; v doporučeném nastavení. Buď tikety vypni a nech jen serverovou keš, nebo klíče pravidelně rotuj (ssl_session_ticket_key s výměnou souborů).
Obnovení se dá udělat i s dopředným utajením — režim psk_dhe_ke přidá k tiketu ještě čerstvý Diffie-Hellman. Prohlížeče to používají, ale závisí to na nastavení serveru.
0-RTT
Klient může při obnovení poslat data rovnou v prvním paketu, ještě než dostane odpověď. Nulová latence, což je lákavé.
Tahle data se dají přehrát. Kdo je odposlechne, může je poslat znovu a server je zpracuje podruhé. Nemá je jak rozlišit, protože nemá stav.
Pravidlo je jednoduché: tudy smí jít jen požadavky, u kterých opakování nevadí. Zapnout ssl_early_data on; před aplikací, která mění stav na GET (a takových je víc, než by mělo být), je bezpečnostní chyba, ne ladění výkonu.
Totéž platí pro QUIC, viz HTTP a QUIC.
Odvolání certifikátu
Certifikát platí do data v něm. Když unikne soukromý klíč dřív, potřebuješ ho zneplatnit — a tohle je v celém PKI nejslabší místo.
CRL — seznam odvolaných certifikátů ke stažení. Roste, je zastaralý, prohlížeče ho většinou nestahují.
OCSP — dotaz na CA, jestli konkrétní certifikát platí. Prozrazuje CA, které weby navštěvuješ, přidává latenci a při výpadku CA se stejně musí propustit. Kvůli tomu ho prohlížeče v praxi přestaly používat a Let's Encrypt ho v roce 2025 ukončil úplně.
OCSP stapling — server si odpověď vyzvedne sám a přiloží ji k handshake. Řeší latenci i soukromí. S koncem OCSP u Let's Encrypt ztrácí význam a ssl_stapling on; v konfiguraci je dnes u certifikátů od nich zbytečný řádek.
Co reálně funguje: prohlížeče si vozí vlastní agregované seznamy (CRLSets v Chrome, CRLite ve Firefoxu) a hlavně se zkracuje platnost certifikátů. Devadesátidenní certifikát od Let's Encrypt představuje devadesátidenní okno zneužití místo tříletého, a limity se dál posouvají dolů — proto se dnes ruční obnova certifikátů stává nepoužitelnou a automatizace povinností. Viz certifikáty a Let's Encrypt.
Poctivé shrnutí: odvolání v praxi téměř nefunguje. Chraň soukromý klíč a spoléhej na krátkou platnost.
ECH: poslední otevřený údaj
TLS 1.3 skrylo certifikát, ale SNI zůstalo otevřené. Kdo poslouchá linku, pořád vidí seznam domén, na které chodíš.
Encrypted Client Hello to řeší tak, že se celé ClientHello zašifruje veřejným klíčem, který server publikuje v DNS záznamu typu HTTPS. Vnějšek nese jen zástupné jméno sdílené celým poskytovatelem.
dig +short example.com HTTPS
Praktický stav: funguje to hlavně u velkých CDN, které mají dost domén na to, aby se v nich jednotlivec ztratil. V homelabu ti to nepomůže — když je za tvým jménem jedna IP adresa, prozradí tě adresa sama. Navíc to celé stojí a padá na tom, jestli DNS dotaz nikdo nevidí, takže bez DoH nemá smysl vůbec.
Jak se na to dívat
openssl s_client
# základ — verze, šifra, řetěz, ověření
openssl s_client -connect example.cz:443 -servername example.cz </dev/null
# co server umí a co ne
openssl s_client -connect example.cz:443 -tls1_2 </dev/null
openssl s_client -connect example.cz:443 -tls1_3 </dev/null
# vynuť konkrétní skupinu — ověříš podporu postkvantové výměny
openssl s_client -connect example.cz:443 -groups X25519MLKEM768 </dev/null
# celý řetěz včetně mezilehlých
openssl s_client -connect example.cz:443 -showcerts </dev/null
# OCSP odpověď přiložená serverem
openssl s_client -connect example.cz:443 -status </dev/null | grep -A5 "OCSP Response"
# vyjednaný protokol aplikace
openssl s_client -connect example.cz:443 -alpn h2 </dev/null 2>&1 | grep ALPN
Co číst ve výstupu:
Certificate chain ← musí mít víc než jednu položku
Verify return code: 0 (ok) ← cokoliv jiného je problém
Negotiated TLS1.3 group: X25519MLKEM768
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Komplexní kontrola
testssl.sh https://example.cz # nejdůkladnější, běží lokálně
sslyze --regular example.cz:443
Nebo online ssllabs.com/ssltest, který navíc ukáže, jak se spojí konkrétní staří klienti.
Rozšifrovat vlastní provoz ve Wiresharku
Nejlepší způsob, jak si tuhle stránku prohlédnout naživo. Prohlížeč umí zapisovat klíče relací do souboru:
SSLKEYLOGFILE=/tmp/tls-keys.log firefox
Ve Wiresharku pak Preferences → Protocols → TLS → (Pre)-Master-Secret log filename nastav na ten soubor. Zachycený provoz se rozbalí včetně HTTP obsahu.
Funguje to i pro curl (--ssl-key-logfile) a pro programy nad OpenSSL a NSS. Ten soubor je klíč od tvého provozu, takže po ladění smaž.
Časté chyby a co znamenají
| Hláška | Skutečná příčina |
|---|---|
unable to get local issuer certificate |
chybí mezilehlý certifikát — nejčastější chyba vůbec |
self signed certificate in certificate chain |
vlastní CA, kterou klient nezná |
certificate has expired |
neproběhla obnova — nebo má klient špatný čas |
Hostname mismatch |
jméno není v subjectAltName |
no shared cipher |
server nabízí jen to, co klient neumí (nebo naopak) |
wrong version number |
mluvíš TLS na port, kde poslouchá otevřené HTTP |
tlsv1 alert protocol version |
klient chce TLS 1.0/1.1, server je má vypnuté — a má |
sslv3 alert handshake failure |
obvykle chybějící klientský certifikát u mTLS |
bad record mac hned po handshake |
nesouhlasí klíče — obvykle vadný prostředník nebo hardwarová akcelerace |
funguje v prohlížeči, ne v curl |
chybí mezilehlý — prohlížeč si ho domyslel z keše |
Co si odnést
Certifikát nešifruje. Šifruje Diffie-Hellman. Certifikát jen říká, s kým.
CertificateVerify je ten důkaz. Vlastnictví certifikátu nic neznamená, vlastnictví klíče ano.
Finished chrání celý průběh. Proto nejde vnutit slabší šifru.
TLS 1.3 je hlavně o tom, co v něm není. Míň možností, míň chyb.
Obnovení relace umí zrušit dopředné utajení. Rotuj klíče tiketů, nebo je vypni.
0-RTT se dá přehrát. Nezapínej to bezmyšlenkovitě.
Odvolání nefunguje. Krátká platnost ano.
Kam dál
- TLS a HTTPS prakticky — co nastavit na serveru
- Certifikáty a Let's Encrypt — jak certifikát získat
- Diffie-Hellman — jádro celé výměny
- Hashe, HMAC a podpisy — co se v handshaku podepisuje
- HTTP, QUIC a WebSocket — totéž v QUIC podání
- Postkvantová kryptografie — proč tam vidíš
X25519MLKEM768