14.4.5. Verificarea unui server public (camera ca client)¶
Tot ce s-a spus pe pagina anterioară despre un client care „are deja rădăcina” este valabil pentru browsere, telefoane și PC-uri – nu este valabil pentru cameră. Modulul ssl din MicroPython nu include niciun magazin de încredere încorporat: o cameră proaspăt programată nu are încredere în nicio autoritate de certificare (CA), iar valoarea implicită (ssl.CERT_NONE) nu verifică nimic și este complet expusă la un atac de tip man-in-the-middle. Așadar, atunci când camera este clientul care se conectează către un server TLS public (un API HTTPS, un broker MQTT, …) și vrei ca aceasta să verifice cu adevărat acel server, trebuie să furnizezi tu însuți ancora de încredere.
Mecanismul este identic cu cel din exemplul de client autosemnat de pe Certificate auto-semnate; singura diferență este că fișierul pe care îl încarci este un certificat CA real în loc de certificatul propriu al partenerului:
Obține certificatul CA care ancorează lanțul serverului. „Ancorează” înseamnă certificatul aflat la (sau aproape de) vârful lanțului serverului pe care îl alegi ca punct de plecare al încrederii. Un server TLS își trimite certificatul de tip leaf și de obicei intermediarul (intermediarii); nu își trimite niciodată rădăcina. Trebuie să obții tu însuți acea ancoră de încredere, independent de server – a avea pur și simplu încredere în orice îți oferă un server ar anula întregul scop al verificării.
Mai întâi află ce CA a emis de fapt certificatul serverului. De exemplu, pentru
openmv.ioopenssl s_client -connect openmv.io:443 -showcerts < /dev/nullBlocul
Certificate chainlistează fiecare certificat cu subiectul său (s:) și emitentul (i:); versiunile mai noi de OpenSSL afișează și liniilea:(tipul cheii) șiv:(valabilitatea), pe care le poți ignora aici:Certificate chain 0 s:CN=openmv.io i:C=US, O=Let's Encrypt, CN=E8 1 s:C=US, O=Let's Encrypt, CN=E8 i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
Intrarea 0 este leaf-ul (
openmv.io), emis de intermediarulE8. Intrarea 1 este acel intermediar, emis de rădăcinaISRG Root X1. Emitentul (i:) al intrării de cel mai sus identifică rădăcina – aiciISRG Root X1. (Intermediarul esteE8și nuR10/R11pe care poate le-ai văzut în altă parte, deoareceopenmv.iofolosește un certificat ECDSA; Let’s Encrypt semnează leaf-urile ECDSA cu intermediarii săi din seriaEși leaf-urile RSA cu cei din seriaR. Ambele se leagă în lanț până laISRG Root X1.)OpenSSL afișează și liniile
depth=și poate raporta rădăcina cuVerification: OK. Asta se întâmplă doar pentru că PC-ul tău are deja încredere înISRG Root X1– serverul nu a trimis-o (un server nu își trimite niciodată rădăcina), iar camera, neavând niciun magazin de încredere, nu o va avea nici ea. Exact de aceea trebuie să o furnizezi.Descarcă acea rădăcină din rădăcinile publicate chiar de CA. Let’s Encrypt le catalogează pe toate ale sale pe pagina cu certificate Let’s Encrypt; fișierul direct pentru ISRG Root X1 este isrgrootx1.pem (îl oferă și preconvertit ca isrgrootx1.der). Alte CA-uri își publică rădăcinile pe o pagină similară de tip „root certificates” / „repository”; setul public canonic este programul Mozilla CA (CCADB). Confirmă că ai descărcat fișierul corect comparând amprenta sa cu valoarea publicată de CA (adaugă
-inform DERdacă ai descărcat fișierul.der):openssl x509 -in isrgrootx1.pem -noout -subject -fingerprint -sha256Dacă preferi să nu urmărești o rădăcină, poți în schimb să copiezi intermediarul direct din ieșirea
-showcerts(al doilea bloc-----BEGIN CERTIFICATE-----), să ai încredere în el și să accepți că trebuie să-l reîmprospătezi de fiecare dată când CA-ul rotește intermediarul – mult mai des decât rădăcina (vezi compromisul de mai jos).Convertește-l în DER, exact ca înainte:
openssl x509 -in isrgrootx1.pem -outform DER -out ca.derCopiază
ca.derpe cameră (sistemul de fișiere sau ROMFS) și încarcă-l ca ancoră de încredere:import socket import ssl import ntptime ntptime.settime() # validity check needs the clock addr = socket.getaddrinfo("api.example.com", 443)[0][-1] sock = socket.socket() sock.connect(addr) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.verify_mode = ssl.CERT_REQUIRED ctx.load_verify_locations(cafile="ca.der") ssock = ctx.wrap_socket(sock, server_hostname="api.example.com")
server_hostnameeste obligatoriu aici: acesta determină SNI și este numele verificat față desubjectAltNamedin certificatul serverului.
Sfat
Scurtătură pentru cazul obișnuit. Let’s Encrypt este cel mai folosit CA public, iar în prezent atât certificatele sale RSA cât și cele ECDSA se leagă în lanț până la ISRG Root X1 (așa cum arată exemplul openmv.io de mai sus). Dacă serverele cu care vorbește camera ta folosesc Let’s Encrypt, poți sări complet peste inspecție: pune doar isrgrootx1.der pe cameră și fă load_verify_locations pe el.
Acest lucru nu face TLS să funcționeze către orice site. Un server al cărui certificat provine de la un alt CA (DigiCert, Google Trust Services, Amazon, Sectigo, …) tot va eșua la verificare și, deoarece camera are încredere într-un singur certificat DER per ssl.SSLContext, nu poți grupa toate rădăcinile așa cum face un browser. În caz de îndoială, identifică CA-ul real al serverului așa cum s-a arătat mai sus și ai încredere în acea rădăcină.
Ce certificat alegi să consideri de încredere este un compromis:
Rădăcina (recomandat). Are durată lungă de viață – adesea zeci de ani – așa că
ca.derse schimbă rar. Necesită ca serverul să-și trimită intermediarul, pentru ca mbedTLS să poată construi calea leaf → intermediar → rădăcina ta de încredere; practic orice server public configurat corect face acest lucru.Intermediarul. Funcționează de asemenea și continuă să funcționeze chiar dacă un server omite intermediarul, dar intermediarii sunt rotiți mult mai des decât rădăcinile, așa că va trebui să reîmprospătezi
ca.dermai frecvent.Leaf-ul însuși (certificate pinning). Cea mai strictă variantă, dar leaf-ul se schimbă la fiecare reînnoire – aproximativ la fiecare 90 de zile pentru Let’s Encrypt – așa că aceasta are sens doar atunci când controlezi și serverul și poți împinge noul pin către fiecare cameră în mod sincronizat. Exact asta face exemplul de client autosemnat.
Notă
ssl.SSLContext.load_verify_locations() acceptă un singur certificat CA codificat în DER, așa că la un moment dat camera are încredere în exact o singură ancoră. Pentru a ajunge la servere aflate sub CA-uri diferite, folosește câte un ssl.SSLContext separat pentru fiecare ancoră. Și pentru că acel certificat va expira el însuși la un moment dat sau va fi rotit de CA, tratează-l ca pe orice alt certificat de pe dispozitiv.