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:

  1. 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.io

    openssl s_client -connect openmv.io:443 -showcerts < /dev/null
    

    Blocul Certificate chain listează fiecare certificat cu subiectul său (s:) și emitentul (i:); versiunile mai noi de OpenSSL afișează și liniile a: (tipul cheii) și v: (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 intermediarul E8. Intrarea 1 este acel intermediar, emis de rădăcina ISRG Root X1. Emitentul (i:) al intrării de cel mai sus identifică rădăcina – aici ISRG Root X1. (Intermediarul este E8 și nu R10 / R11 pe care poate le-ai văzut în altă parte, deoarece openmv.io folosește un certificat ECDSA; Let’s Encrypt semnează leaf-urile ECDSA cu intermediarii săi din seria E și leaf-urile RSA cu cei din seria R. Ambele se leagă în lanț până la ISRG Root X1.)

    OpenSSL afișează și liniile depth= și poate raporta rădăcina cu Verification: OK. Asta se întâmplă doar pentru că PC-ul tău are deja încredere în ISRG 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 DER dacă ai descărcat fișierul .der):

    openssl x509 -in isrgrootx1.pem -noout -subject -fingerprint -sha256
    

    Dacă 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).

  2. Convertește-l în DER, exact ca înainte:

    openssl x509 -in isrgrootx1.pem -outform DER -out ca.der
    
  3. Copiază ca.der pe 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_hostname este obligatoriu aici: acesta determină SNI și este numele verificat față de subjectAltName din 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.der se 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.der mai 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.