# Hashe, HMAC a podpisy

Hash je nejpoužívanější kryptografický nástroj a zároveň ten, kolem kterého je nejvíc omylů. Není to šifrování, nedá se vrátit zpátky a **sám o sobě nic nezabezpečuje**. Přesto je uvnitř každého podpisu, každého certifikátu, každého Git commitu a každé uložené databáze hesel.

Tahle stránka vysvětluje, co hash umí, co neumí, a jak se z něj postaví ověření integrity, ukládání hesel a elektronický podpis.

Navazuje na [stavební kameny](Sifrovani-zaklady) a [veřejný a soukromý klíč](Verejny-a-soukromy-klic).

## Co hash je

Funkce, která z libovolně velkého vstupu udělá **otisk pevné délky**:

```bash
echo -n "ahoj" | sha256sum
# 3f3b08eca62c21d76256e6e1d0b8bf99f4efbe376f64335b72f4163a8fc50dba

echo -n "ahoj." | sha256sum
# d16b42563ade4f7005d06bd65e1505a2ec3353f6b24acf5285b5c378101b2587
```

Jedna tečka navíc a otisk nemá s předchozím nic společného. Vstup může mít bajt nebo terabajt, výstup má vždycky 32 bajtů.

Vlastnosti, které od kryptografického hashe chceš:

**Determinismus.** Stejný vstup vždycky stejný výstup, na jakémkoliv stroji.

**Odolnost proti nalezení vzoru.** Z otisku nejde vypočítat vstup. Nejde to ani „s klíčem", protože žádný klíč neexistuje — informace se ztratila.

**Odolnost proti druhému vzoru.** Když máš konkrétní soubor, nikdo nedokáže vyrobit **jiný** soubor se stejným otiskem.

**Odolnost proti kolizím.** Nikdo nedokáže vyrobit **libovolnou dvojici** různých souborů se stejným otiskem. Tohle je nejtěžší požadavek a padá jako první.

**Lavinový efekt.** Změna jednoho bitu vstupu změní zhruba polovinu bitů výstupu.

### Kolize existují vždycky

Vstupů je nekonečně mnoho, výstupů konečně. Dva různé soubory se stejným otiskem tedy nutně **existují** — jde jen o to, aby je nikdo nedokázal najít.

U 256bitového hashe je díky narozeninovému paradoxu očekávaný počet pokusů 2¹²⁸. To je stejná bariéra jako u AES-128, tedy fyzikálně nedosažitelná. Proto se říká, že **SHA-256 poskytuje 128bitovou odolnost proti kolizím**, i když má 256 bitů výstupu.

## Které hashe používat

| Funkce | Stav | Kde |
|---|---|---|
| **SHA-256, SHA-512** | výchozí volba | TLS, certifikáty, balíčky, všude |
| **SHA-512/256** | SHA-512 zkrácené na 256 bitů | rychlejší na 64bitech, imunní vůči prodloužení |
| **SHA-3 / SHAKE** | v pořádku, jiná konstrukce | pojistka, kdyby padla rodina SHA-2 |
| **BLAKE2, BLAKE3** | v pořádku a velmi rychlé | `age`, WireGuard, `b3sum`, zálohovací nástroje |
| **SHA-1** | **prolomený** | jen jako identifikátor, ne jako ochrana |
| **MD5** | **prolomený úplně** | leda kontrola překlepu v přenosu |
| **CRC32** | není kryptografický | kontrola chyb linky, nic víc |

Praktické doporučení: **SHA-256, pokud nemáš důvod na něco jiného.** BLAKE3, když počítáš otisky terabajtů a záleží ti na rychlosti.

### Jak přesně padly MD5 a SHA-1

Stojí za to vědět, protože z toho plyne, co ještě smíš a co už ne.

**MD5** — kolize se hledají od roku 2004 a dnes je to otázka sekund. V roce 2012 toho využil malware **Flame**: podvrhl si podpisový certifikát Microsoftu a tvářil se jako legitimní aktualizace Windows.

**SHA-1** — v roce 2017 ukázal Google útok **SHAttered**: dva různé PDF soubory se stejným otiskem. V roce 2020 přišel útok s **volitelnou předponou** za zhruba 45 tisíc dolarů na pronajatém výpočetním výkonu, což je částka v dosahu kohokoliv, komu na tom záleží. To je nebezpečnější varianta — umožňuje vyrobit kolizi ke **konkrétnímu smysluplnému dokumentu**.

Co z toho plyne: **kolize neznamená, že jde otisk zvrátit.** Když má někdo tvůj SHA-1 otisk, pořád z něj tvůj soubor nedostane. Ale nemůže už platit tvrzení „shodný otisk = shodný soubor", a přesně to podpisy potřebují.

Proto Git, který SHA-1 používá jako identifikátor commitů, není „prolomený" — jen ho nemůžeš použít jako důkaz, že historii nikdo nezměnil. Na to je podpis commitů, případně repozitáře se SHA-256.

## Past: prodloužení zprávy

Tohle je vlastnost, která překvapí i zkušené lidi, a stojí za ní nejběžnější domácí implementace ověřování API.

SHA-256 (a SHA-1, a MD5) staví na konstrukci Merkle–Damgård: data se zpracovávají po blocích a **výstupem je vnitřní stav funkce**. Když tedy znáš otisk, znáš i stav, ve kterém funkce skončila — a můžeš od něj pokračovat.

Důsledek: kdo zná `SHA256(tajemství ‖ zpráva)` a délku tajemství, **umí spočítat platný otisk pro `zpráva ‖ cokoliv`**, aniž by to tajemství znal.

```
mám:     SHA256(klíč ‖ "amount=10")          ← podepsané, důvěryhodné
vyrobím: SHA256(klíč ‖ "amount=10" ‖ pad ‖ "&amount=10000")   ← taky platné
```

Tomuhle se říká *length extension attack* a je to důvod, proč **vlastní podepisování ve tvaru `hash(tajemství + data)` je chyba**. Řešením je HMAC.

Imunní jsou SHA-512/256, SHA-3, BLAKE2 a BLAKE3 — ty vnitřní stav nezveřejňují.

## HMAC

Standardní způsob, jak z hashe udělat ověření s klíčem. Není to hash s klíčem přilepeným zepředu, je to **dvojité hashování s dvěma odvozenými klíči**:

```
HMAC(K, m) = H( (K ⊕ opad) ‖ H( (K ⊕ ipad) ‖ m ) )
```

Ta vnější vrstva zabrání prodloužení zprávy, protože útočník nevidí stav vnitřního hashe.

```bash
# podepsat
echo -n "amount=10" | openssl dgst -sha256 -hmac "tajny-klic"

# v Pythonu, včetně bezpečného porovnání
python3 - <<'EOF'
import hmac, hashlib
sig = hmac.new(b"tajny-klic", b"amount=10", hashlib.sha256).hexdigest()
print(sig, hmac.compare_digest(sig, sig))
EOF
```

**Porovnávej v konstantním čase.** `==` skončí na prvním odlišném bajtu a rozdíl v době odpovědi prozradí, kolik znaků sedí. Útočník pak nehádá celý podpis, ale bajt po bajtu. Na to jsou `hmac.compare_digest`, `subtle.ConstantTimeCompare`, `sodium_memcmp`.

Kde HMAC potkáš: webhooky (GitHub, Stripe), JWT s algoritmem `HS256`, podepsané URL, ověřování relací, starší TLS 1.2, uvnitř HKDF.

Rozdíl proti podpisu je zásadní: **HMAC ověří jen ten, kdo zná klíč — a ten ho zároveň umí vyrobit.** Nedokazuje tedy, která ze dvou stran zprávu vytvořila. To umí jen podpis.

## Hesla: úplně jiná disciplína

Na hesla se **nikdy nepoužívá běžný hash.** Ani SHA-256, ani BLAKE3, ani HMAC.

Důvod je rychlost. Grafická karta spočítá miliardy SHA-256 za sekundu. Když ti unikne databáze, útočník projede všechna hesla ze slovníku i všechny krátké kombinace během chvíle. To, co je u ověřování integrity výhoda, je tady fatální.

Potřebuješ funkci, která je **úmyslně pomalá a paměťově náročná**:

| Funkce | Doporučené nastavení | Poznámka |
|---|---|---|
| **Argon2id** | 19 MiB paměti, 2 průchody, 1 vlákno | dnešní doporučení, odolná proti GPU i ASIC |
| **scrypt** | N=2¹⁷, r=8, p=1 | starší, ale paměťově náročná |
| **bcrypt** | cost 12 | **omezení 72 bajtů**, delší heslo se uřízne |
| **PBKDF2-HMAC-SHA256** | 600 000 iterací | jen když to vyžaduje certifikace; na GPU se počítá dobře |

**Sůl.** Ke každému heslu náhodná hodnota, uložená vedle otisku. Bez ní má stejné heslo stejný otisk, takže útočník rozezná uživatele se stejným heslem a hlavně může použít předpočítanou tabulku pro všechny naráz. Sůl není tajná, jen unikátní. Všechny čtyři funkce výše ji řeší samy.

**Pepř.** Tajná hodnota mimo databázi, třeba v proměnné prostředí nebo v HSM. Přimíchá se ke každému heslu. Únik samotné databáze pak k hádání nestačí. Volitelné, ale levné.

Praktické: **tohle si nepiš sám.** Použij, co má framework — `password_hash()` v PHP, `argon2-cffi` v Pythonu, `bcrypt` v Node. Ve svém homelabu tuhle úlohu nejspíš vůbec nemáš, protože přihlašování řeší [Pocket ID nebo Authelia](Autentizace-pred-sluzbami).

### Ještě lepší je hesla neukládat

Nejbezpečnější databáze hesel je ta, kterou nemáš. Passkey (WebAuthn) nahrazuje heslo dvojicí klíčů: server má veřejný, tvoje zařízení soukromý. **Nedá se uhodnout, nedá se odposlechnout a nedá se vyfishovat**, protože prohlížeč podepíše i doménu, na které jsi. Podvržený web dostane podpis, který jeho vlastní adresu odmítá.

## KDF: odvození klíčů

Nezaměňuj s hashováním hesel, i když to vypadá podobně.

**HKDF** — pro vstupy, které už jsou náhodné (výsledek [Diffie-Hellmana](Diffie-Hellman), obsah `/dev/urandom`). Nemá důvod být pomalá. Skládá se ze dvou kroků: *extract* z nerovnoměrného vstupu vytáhne rovnoměrný klíč, *expand* z něj vyrobí libovolný počet pojmenovaných klíčů.

```
klíč_pro_klienta = HKDF(tajemství, "client traffic")
klíč_pro_server  = HKDF(tajemství, "server traffic")
```

Ten popisek je důležitý: díky němu jsou oba klíče různé, přestože vznikly ze stejného tajemství. Kdyby byly stejné, šlo by pakety přehrávat zpátky odesílateli. Přesně takhle staví klíče TLS 1.3 i WireGuard.

**Argon2id a spol.** — pro hesla. Musí být pomalá, protože vstup je předvídatelný.

Chyba, která se dělá: použít HKDF na heslo. Pak je odvozený klíč tak silný jako to heslo, což je málo.

## Digitální podpis

Kombinace hashe a asymetrické kryptografie.

```
podepsání:  otisk = H(dokument)  →  podpis = Sign(soukromý_klíč, otisk)
ověření:    otisk = H(dokument)  →  Verify(veřejný_klíč, otisk, podpis)
```

**Podepisuje se otisk, ne dokument.** Ze dvou důvodů: asymetrické operace jsou pomalé a u RSA se do klíče vejde jen pár set bajtů. Zároveň z toho plyne důležitá věc — **podpis není silnější než použitý hash**. Podpis nad SHA-1 je dnes bezcenný bez ohledu na sílu klíče, protože stačí vyrobit jiný dokument se stejným otiskem.

### Schémata

**Ed25519** — dnešní volba. Podpis 64 bajtů, klíč 32 bajtů, deterministické (nepotřebuje při podepisování náhodnost), rychlé, bez okrajových případů.

**ECDSA** (P-256, P-384) — rozšířené v certifikátech. Funguje, ale **vyžaduje pro každý podpis unikátní náhodné číslo** a jeho zopakování znamená okamžitou ztrátu soukromého klíče. Viz [náhodnost a entropie](Nahodnost-a-entropie).

**RSA-PSS** — moderní zarovnání pro RSA, s důkazem bezpečnosti. Používej, kde to protistrana umí.

**RSA PKCS#1 v1.5** — staré zarovnání. U podpisů zatím drží, u šifrování je zdroj opakovaných útoků. Nové věci na něm nestav.

**DSA** — mrtvé. OpenSSH ho odstranil úplně.

### Co podpis dokazuje a co ne

**Dokazuje:** dokument vytvořil někdo, kdo má soukromý klíč, a od té doby se nezměnil ani o bit.

**Nedokazuje:** že ten někdo je ten, koho máš na mysli. Tohle je vazba, kterou musí řešit něco dalšího — [certifikát od CA](Certifikaty-a-Lets-Encrypt), zápis v `known_hosts`, klíč zadaný do konfigurace, osobní předání.

**Nedokazuje ani:** kdy vznikl. Systémový čas se dá přenastavit. Na to jsou časová razítka od nezávislé autority.

### Kontrolní součet není podpis

Klasická chyba u stahování softwaru:

```
software.tar.gz         ← stažené
software.tar.gz.sha256  ← z téhož serveru
```

Kdo podstrčil první soubor, podstrčil i druhý. **Otisk ze stejného zdroje jako data nedokazuje nic** kromě toho, že se přenos nepokazil.

Smysl to dává ve dvou případech: otisk je na **jiném** místě (README v gitu, jiná doména), nebo je k souboru **podpis** ověřitelný veřejným klíčem, který už máš z dřívějška.

```bash
# tohle ověřuje jen přenos
sha256sum -c software.tar.gz.sha256

# tohle ověřuje původ
gpg --verify software.tar.gz.asc software.tar.gz
ssh-keygen -Y verify -f allowed_signers -I autor@example.cz -n file \
    -s software.tar.gz.sig < software.tar.gz
```

## Kde v síti narazíš na hash

| Místo | Co to dělá |
|---|---|
| **Certifikát** | CA podepisuje otisk certifikátu, `sha256WithRSAEncryption` |
| **TLS handshake** | otisk celého průběhu se podepisuje → [ochrana proti podvržení](TLS-handshake) |
| **SSH host key** | otisk v `known_hosts`, `SHA256:...` při prvním připojení |
| **APT / DNF** | podpis seznamu balíčků, otisk každého balíčku |
| **Git** | identifikátor commitu, podpis commitu |
| **Docker** | `sha256:...` u obrazu — jediný spolehlivý identifikátor, na rozdíl od značky |
| **HSTS preload, CT logy** | Merkleovy stromy nad otisky |
| **DNSSEC, DANE, SSHFP** | otisky klíčů v DNS |
| **Fail2ban, CrowdSec** | ne, tam žádný hash není — jen ať to nehledáš |

Ten řádek s Dockerem stojí za zdůraznění. `nginx:latest` ukazuje pokaždé na něco jiného. `nginx@sha256:abc...` ukazuje navždy na tentýž obsah — a přesně tohle chceš v produkci a v `docker-compose.yml`, který má fungovat i za rok.

## Praktické příkazy

```bash
# otisk souboru
sha256sum soubor
openssl dgst -sha256 soubor
b3sum soubor                       # BLAKE3, výrazně rychlejší na velkých datech

# otisk řetězce — pozor na -n, jinak počítáš i odřádkování
echo -n "text" | sha256sum

# ověření stažených souborů podle seznamu
sha256sum -c SHA256SUMS 2>&1 | grep -v OK

# otisk veřejného klíče v certifikátu (pro pinning)
openssl x509 -in cert.pem -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary | base64

# otisk SSH klíče
ssh-keygen -lf ~/.ssh/id_ed25519.pub
ssh-keyscan -t ed25519 server 2>/dev/null | ssh-keygen -lf -
```

## Časté omyly

**„Hash se dá dešifrovat."** Nedá. Stránky, které to nabízejí, mají tabulku předpočítaných otisků běžných hesel. Proto se solí.

**„Dvojitý hash je bezpečnější."** `sha256(sha256(x))` nepřidá prakticky nic a u hesel to situaci nezachrání — pořád je to miliarda pokusů za sekundu.

**„MD5 stačí, když jde jen o kontrolu."** Na kontrolu chyby v přenosu ano. Jakmile v úvaze figuruje útočník, ne.

**„Máme hesla hashovaná, takže je únik databáze v pohodě."** Záleží čím. SHA-256 bez soli je prakticky totéž jako otevřený text. Argon2id s rozumným nastavením je skutečná ochrana.

**„Sůl musí být tajná."** Nemusí, ukládá se vedle otisku. Tajný je pepř, a ten je nepovinný navíc.

**„Podepsané = důvěryhodné."** Podepsané znamená „vím, kdo to vyrobil". Jestli je to ten správný někdo, je jiná otázka. Podepsaný malware je pořád malware.

## Kam dál

- **[Náhodnost a entropie](Nahodnost-a-entropie)** — proč ECDSA přijde o klíč
- **[Veřejný a soukromý klíč](Verejny-a-soukromy-klic)** — druhá půlka podpisu
- **[TLS handshake krok za krokem](TLS-handshake)** — otisk průběhu jako ochrana proti podvržení
- **[Autentizace před službami](Autentizace-pred-sluzbami)** — hesla v praxi, když je řeší někdo jiný
- **[Postkvantová kryptografie](Postkvantova-kryptografie)** — hashů se netýká skoro vůbec
