Nevím, jestli stejný, nicméně podobný problém jsem již viděl.
curl občas při "regulérním" požadavku GET na https server (v našem případě Apache) DOSTAL (nevymyslel si) od serveru HTTP 400 Bad request. V logu bylo informace o tom, že jméno v SNI neodpovídalo hlavičce Host: HTTP požadavku.
A vysvětlení: curl s nss knihovnou má zřejmě nějaký SSL session pool (cachuje si SSL sessions zřejmě podle cílové IP adresy). No a když komunikuje se dvěma různými jmény, která mají stejnou IP adresu (třeba CNAMEs), tak se může stát, že pro spojení se druhým serverem použije TLSv1 session, která byla původně iniciována pro první server.
První session byla vytvořena tak, že v TLSv1 požadavku šlo SNI: server1 a v hlavičce HTTP Host: server1. Takovou SSL session si ovšem cachuje i server a uloží si, že pro danou session SNI=server1.
Druhý požadavek jde na server2, na stejnou IP. curl/nss si z poolu vybere předchozí SSL session. Posílá požadavek na server, SNI je nyní server2 a Host: server2. Apache je v SSL handshaku SNI nepotvrdí (RFC 4366, poslední odstavec 3.1; a tedy SNI je v rámci dané session server1 i nadále) a pokračuje na úrovni HTTP návratovou hodnotou 400, protože SNI je server1 a Host: server2.
Problém je hezky popsán následujícím demo skriptem (bez curlu).
rm /tmp/sslsession
(
echo GET /neco.html HTTP/1.1
echo Host: server1
echo
echo
sleep 5
) | openssl s_client -no_ticket -host server1 -port 443 -sess_out /tmp/sslsession -servername server1
openssl sess_id -inform pem -in /tmp/sslsession -text -noout
(
echo GET /necojineho.html HTTP/1.1
echo Host: server2
echo
echo
sleep 5
) | openssl s_client -no_ticket -host server2 -port 443 -sess_in /tmp/sslsession -servername server2
Budu rád, když mi napíšete, jestli se jedná o stejný problém.
Díky.
P.S. Děkuji všem kolegům, kteří se na odhalení tohoto problému podíleli. I za hádky zda "regulérní" je opravdu regulérní nebo ne, myslím, že nás to všechny docela obohatilo (mne určitě, už vím co je to SNI 8) ;-)
U mně se to s curl+NSS děje i když se explicitně zavře connection a curl ji skutečne zavře (na SNI jsem si díval jako na první, ten často způsobuje podobné "bážky"). Ve wiresharku je vidět dva connectiony a v nový TLS handshake má správné SNI. Host headery jsou taky správně.
Zkoušel jsem i vypnout SSL session cache, nemá to na to vliv (CURLOPT_SSL_SESSIONID_CACHE).
Napsal jsem krátký PoC, který to reprodukuje (pythoní zdroják a CACert certifikáty): https://dl.dropbox.com/u/53317596/curl_nss_test.tar.gz
Podobné je, že oba wiki.vorratsdatenspeicherung.de i www.vorratsdatenspeicherung.de mají stejnou IP (je to uděláno přes CNAME). Zvláštní je, že pokud se vymění NSS za openssl, změní se 'Server' response header u toho HTTP 400. Ostatní servery, které se chovali stejně, vykazovali podobnou změnu v Server header, jenom tam bylo vidět navíc, že je to Varnish.
> Ve wiresharku je vidět dva connectiony a v nový TLS handshake má správné SNI. Host headery jsou taky správně.
Je SNI potvrzeno v ServerHello druhého požadavku (viz můj odkaz na poslední odstavec RFC4366 3.1 v prvním komentáři)? Je v druhém požadavku znovu použita daná SSL session? Můžu vidět PCAP? Mailem jsem napsal více, celý obrázek se mi už pomalu skládá.
Demo skript pro 200,400 pro openssl: http://tmp.janik.cz/scr_demoSNIbug.DE
Tak jsem konečně odhalil důvod. Chyba zdá se je na straně klienta i servera.
Než se uzavře connection, posílá se alert message. V TLS protokolu by se mělo invalidovat session ID. Klient (NSS) by ho neměl posílat a server by ho neměl přijmout (sekce 7.2 u RFC 2246 a 5246 pro TLS 1.0, resp. 1.2).
Nějaká předřazená cache nebo SSL load balancer vidí stejné TLS session ID, nedívá se na SNI a pravděpodobně připojí vnitřní HTTP tunely na nějaké existující interní cachované spojení. To má nejspíš někde poznamenáno ke kterému virtualhostu patřilo a nelíbí se mu nový Host header.
Ještě detail: pokud se u curl+NSS vypne validace, nepoužijou se ty session ID u nového spojení a funguje to (když se ty zmiňované SSL_VERIFYHOST a SSL_VERIFYPEER nastaví na 0). Naopak, u zapnuté validace curl+NSS ignoruje nastavení CURLOPT_SSL_SESSIONID_CACHE a posíla session ID stejně.
Ad alert: ssl session invalidace by se měla provést pouze pokud AlertLevel je větší nebo roven fatal. IMO jsou close_notify zasílány normálně jako warning. Jinak by to ani nemohlo fungovat.
SSL_VERIFYPEER jsem vysvětlil v mailu:
Už chápu i souvislost s tvrzením, že SSL_VERIFYPEER=0 pomůže nebo také curl bez nss pomůže.
curl/nss.c totiž obsahuje:
/* do not use SSL cache if we are not going to verify peer */
ssl_no_cache = (data->set.ssl.verifypeer) ? PR_FALSE : PR_TRUE;
if(SSL_OptionSet(model, SSL_NO_CACHE, ssl_no_cache) != SECSuccess)
goto error;
a tedy pokud je verifikace peera vypnuta, není použita ani ssl cache. Tedy nová SSL session, tedy nemůže dojít k SNI!=Host.
Jen doplním, že u TLS 1.2 to bude "naopak" - server bude muset udělat full handshake na novou session a nesmí potvrzovat server_name. Vývojáři z toho budou mít radost, ale dává to větší smysl. Z RFC 6066 konec sekce 3:
A server that implements this extension MUST NOT accept the request
to resume the session if the server_name extension contains a
different name. Instead, it proceeds with a full handshake to
establish a new session. When resuming a session, the server MUST
NOT include a server_name extension in the server hello.
Šílená API, tak to naprosto souhlasím, už jsem si u linuxových knihoven musel zvyknout. Hlavně mám rád knihovny "all-in-one". Člověk si napíše projekt na pár řádků a k tomu musí nalinkovat několik desítek megabajtů knihoven, které aplikace využije pro požadovanou funkčnost asi z 1%.
Mimochodem, nemáte někdo náhodou po ruce zdroják (v C/C++) na jednoduchý výpočet DSA (Digital Signing Algorithm). Potřebuju jen samotné core toho algoritmu. Nechci OpenSSL ani libopenssl ani nic podobného. Stačí mi, když to bude umět vygenerovat pár klíčů, podepsat text, a ověřit podpis. Nechci certifikáty, nechci řetězce certifikátů, nechci ověřovat doménu, nechci ověřovat DNS, nechci standardní formát klíčů, nechci přístup do repozitáře klíčů a potřebuju to maximálně portabilní.
Implementovat svépomocí nějaký šifrovací algoritmus nebo protokol obyčejně není dobrý nápad, protože je to o několik řádů těžší než použít šílená API. Třeba DSA se může velmi jednoduše položit na chybě v generátoru náhodných čísel, který musí být při každém podpisu být "bezchybný".
Dále jsou pak náročně ošetřitelné věci jako chybové nebo časové postranné kanály (u RSA se proti timing side-channels typicky používá blinding - násobení náhodným číslem a jeho inverzem). Obecně "textbook verze" šifrovacích primitiv jsou zranitelné na spoustu známých útoků.
Jeden pokus o rozumnou knihovnu s rozumným API je Bernsteinova NaCl (http://nacl.cr.yp.to/), která má za cíl kromě rozumných API implementovat i různé ochrany třeba proti side channels - například odstranění časování závislé od cachování procesoru a predikcí skoků. Těžko ale říct jak moc bude do budoucna podporována.
Zrovna probíhá jedna diskuse "Just how bad is OpenSSL?" (http://lists.randombit.net/pipermail/cryptography/2012-October/003373.html). Je to zajímavé, ale celkem dlouhé. Ve zkratce pár pozorování: největší problém OpenSSL je chybějící dokumentace. Má sice celkem hnusný kód, ale zároveň ten kód prošel nejvíce code review a celkově se v něm moc závažných chyb tak často nevyskytuje.
Ad "Certificate policies se totiž můžou chovat „kvantově“: mějme certifikát s rozšířením certificate policies, které je označené jako critical. Je klidně možné, že ze dvou TLS klientů jeden daný certifikát nepřijme a druhý ano – přitom u obou to může být korektní chování. Viděl jsem to i naživo u Chrome a Firefoxu, jednalo se o korejskou CA (certifikáty mezitím obměnili, ale mám je uložené). Trik je v tom, že rozšíření je označeno jako „critical“. To značí, že pokud mu klient rozumí, pak se podle něj musí chovat a pokud mu nerozumí, musí ohlásit chybu validace."
A jak by se to mělo chovat jinak? Když je rozšíření kritické, je správně, že klient ohlásí chybu a odmítne pokračovat, pokud ho nepodporuje. Představte si třeba, že CA má v kritickém rozšíření napsáno, že smí vydávat certifikáty jen pro .cz doménu, nebo třeba jen .nejaka-firma.cz -- kdyby prohlížeč toto rozšíření ignoroval, protože mu nerozumí, byl by to velký průšvih, protože by nějaká CA důvěryhodná pro danou (sub)doménu mohla podepisovat certifikáty komukoli.
Na omezení DNS jmen třeba na .cz nebo nejaka-firma.cz slouží X.509 extension name constraints, která je mnohem jednodušší než certificate policies. Velké browsery name constraints podporují.
Vyhození chyby na nerozeznanou extension označenou jako critical je samozřejmě správně. Jak se má chovat TLS klient, pokud označená critical není a má špatnou ASN.1 syntax, už definováno není. OpenSSL je třeba dost permisivní pokud se zaměňujou některé věci jako typy řetězců (např. BMPString nebo UTF8String místo vyžadovaného IA5String).
Peter Gutmann "ve svém stylu" komentuje name constraints:
https://groups.google.com/group/mozilla.dev.security.policy/tree/browse_frm/month/2011-11/9a9a02cc001c697f?rnum=31&_done=%2Fgroup%2Fmozilla.dev.security.policy%2Fbrowse_frm%2Fmonth%2F2011-11%3F
HARICA měla nějaké testovací servery s name constraints, ale zdá se, že už nejsou v provozu:
https://groups.google.com/forum/#!msg/mozilla.dev.security.policy/5v-yMBII6Mw/EB1QEytGne8J
Ad "Vyhození chyby na nerozeznanou extension označenou jako critical je samozřejmě správně."
V tom případě moc nechápu, na co si daný odstavec stěžuje a jaký je jeho smysl.
Ad "Jak se má chovat TLS klient, pokud označená critical není a má špatnou ASN.1 syntax, už definováno není."
Definováno to je:
"A certificate-using system MUST reject the certificate if it encounters a critical extension it does not recognize or a critical extension that contains information that it cannot process. A non-critical extension MAY be ignored if it is not recognized, but MUST be processed if it is recognized."
Pokud je tam špatný datový typ, tak to buď "nerozpoznáme" (a pak to můžeme ignorovat) nebo to rozpoznáme a zpracujeme a součástí toho zpracování je zotavení se z chyb nebo konverze typu případně zamítnutí... každopádně je to v kompetenci dané aplikace.
Není na tom nic záhadného ani nepředvídatelného. Prostě když posílám nekritické rozšíření, které druhá strana nemusí podporovat nebo ho posílám špatně zakódované, musím počítat s tím, že se možná nezpracuje.
Nejde vysloveně o "critical", ta část je zrovna celkem jednoduchá.
Neexistuje ale jediný TLS klient, který by plně nebo alespoň korektně implementoval RFC 5280. Jenom k path validation je další RFC 4158, které si opět různé implementace vykládájí různě.
MS CryptoAPI dělá třeba "AIA chasing" kdy si stáhne odněkud certifikáty CA uvedené v Authority Information Access, nahradí nimi původní certifikáty v řetězci - a tam třeba můžou klidně být úplně jiné extensions.
Hledání cesty v grafu při validaci certifikátů může klidně vrátit pro jeden certificate chain poslaný od serveru třeba 16 různých možných cest (některé jsou důsledkem toho AIA chasing, jiné důsledkem cross-certification, možných příčin je dost).
Dovolím si tvrdit, že neexistuje nikdo, kdo dokáže předvídat a ošetřit všechny případy v kódu a to ani kdyby TLS klienti implementovali RFC přesně (což se nikdy nestane).
Příklad jedné z policy: OID 1.2.410.200004.5.1.1.14 (http://www.oid-info.com/cgi-bin/display?oid=1.2.410.200004.5.1.1.14&action=display)
Co má ta policy znamenat, je napsáno někde v Certificate Practice Statement. Který bude nejspíš jenom korejsky (není vůbec výjimkou, že CPS nejsou v angličtině). Jedno CPS do angličtiny přeloženo mají, ale není to to správné: http://www.rootca.or.kr/rca/cps.html
Jeden z těch korejských certifikátů (zde zrovna policy není critical), ale chci na tom ukázat "speciální" případ: http://pastebin.com/yNxr9vu4
CRL URI má jenom ldap:// schému - nenašel jsem zatím běžně používaného TLS klienta, který by CRL přístup přes LDAP implementoval. OCSP v Authority Information Access není. Je tam jenom CA issuers, které je opět přístupné jenom LDAPem.
Prakticky je tento certifikát nerevokovatelný: OCSP nepodporuje a CRL si žádný klient nestáhne.