ťařina
Obsah Soubory
markdown

TLS-handshake.md

17 kB 327 řádků Změněno Zobrazit na GitHubu Stáhnout
markdown
# TLS handshake krok za krokem [TLS a HTTPS prakticky](TLS-a-HTTPS) ř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](Diffie-Hellman), [podpisy](Hashe-a-podpisy) a [certifikáty](Verejny-a-soukromy-klic#certifikát-podepsané-tvrzení-o-klíči). ## Co má handshake zvládnout Za jedno až dvě kolo tam a zpět musí padnout čtyři rozhodnutí: 1. **Kterou verzi a které algoritmy** budeme používat — a to bez toho, aby to šlo po cestě zmanipulovat.2. **Na jakém klíči se domluvíme** — s [dopředným utajením](Diffie-Hellman#dopředné-utajení).3. **Kdo je server** — certifikát a hlavně **důkaz, že k němu má soukromý klíč**.4. **Ž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. ```mermaidsequenceDiagram    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](TLS-a-HTTPS#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](#ech-poslední-otevřený-údaj). ### 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](HTTP-QUIC-a-WebSocket) || `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](Diffie-Hellman). 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ů](TLS-a-HTTPS#ř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](Diffie-Hellman#bez-ověření-je-to-k-ničemu). **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**: 1. **Řetěz vede ke kořeni, kterému věřím.** Podpisy sedí až k certifikátu v [úložišti důvěry](Vlastni-certifikacni-autorita).2. **Jméno souhlasí.** Doména z SNI je v `subjectAltName`. `CN` se dnes ignoruje.3. **Platnost.** `notBefore` až `notAfter`, podle **hodin klienta** — proto rozbité datum v zařízení rozbije HTTPS.4. **Podpis v `CertificateVerify` sedí** s veřejným klíčem z certifikátu. Případně ještě odvolání, viz [dál](#odvolání-certifikátu). ### 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                                   │ deriveDH 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á**: ```mermaidsequenceDiagram    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í](Diffie-Hellman#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](Hashe-a-podpisy#jak-přesně-padly-md5-a-sha-1) || komprese | CRIME || renegociace | složitost a útoky || vlastní DH parametry | [Logjam](Diffie-Hellman#parametry-a-jak-se-to-lámalo-v-praxi) || 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_SHA256TLS_AES_256_GCM_SHA384TLS_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í](TLS-a-HTTPS#verze-a-šifry). 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](HTTP-QUIC-a-WebSocket#na-co-si-dát-pozor-u-0-rtt). ## 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](Certifikaty-a-Lets-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](TLS-a-HTTPS#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. ```bashdig +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 ```bash# 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 neopenssl s_client -connect example.cz:443 -tls1_2 </dev/nullopenssl s_client -connect example.cz:443 -tls1_3 </dev/null # vynuť konkrétní skupinu — ověříš podporu postkvantové výměnyopenssl s_client -connect example.cz:443 -groups X25519MLKEM768 </dev/null # celý řetěz včetně mezilehlýchopenssl s_client -connect example.cz:443 -showcerts </dev/null # OCSP odpověď přiložená serveremopenssl s_client -connect example.cz:443 -status </dev/null | grep -A5 "OCSP Response" # vyjednaný protokol aplikaceopenssl 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žkuVerify return code: 0 (ok)   ← cokoliv jiného je problémNegotiated TLS1.3 group: X25519MLKEM768New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384``` ### Komplexní kontrola ```bashtestssl.sh https://example.cz      # nejdůkladnější, běží lokálněsslyze --regular example.cz:443``` Nebo online [ssllabs.com/ssltest](https://www.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: ```bashSSLKEYLOGFILE=/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](TLS-a-HTTPS#řetěz-certifikátů) || `self signed certificate in certificate chain` | [vlastní CA](Vlastni-certifikacni-autorita), 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](Vlastni-certifikacni-autorita) || `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](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](TLS-a-HTTPS)** — co nastavit na serveru- **[Certifikáty a Let's Encrypt](Certifikaty-a-Lets-Encrypt)** — jak certifikát získat- **[Diffie-Hellman](Diffie-Hellman)** — jádro celé výměny- **[Hashe, HMAC a podpisy](Hashe-a-podpisy)** — co se v handshaku podepisuje- **[HTTP, QUIC a WebSocket](HTTP-QUIC-a-WebSocket)** — totéž v QUIC podání- **[Postkvantová kryptografie](Postkvantova-kryptografie)** — proč tam vidíš `X25519MLKEM768`