14.4.5. Een openbare server verifiëren (camera als client)

Alles op de vorige pagina over een client die “de root al heeft” geldt voor browsers, telefoons en pc’s – het geldt niet voor de camera. De ssl van MicroPython wordt geleverd zonder ingebouwde trust store: een vers geflashte camera vertrouwt helemaal geen enkele CA, en de standaardinstelling (ssl.CERT_NONE) verifieert niets en staat wijd open voor een man-in-the-middle. Dus wanneer de camera de client is die uitgaand verbinding maakt met een openbare TLS-server (een HTTPS-API, een MQTT-broker, …) en je wilt dat hij die server echt verifieert, moet je het trust anchor zelf aanleveren.

De werking is hetzelfde als bij het zelfondertekende clientvoorbeeld op Zelfondertekende certificaten; het enige verschil is dat het bestand dat je laadt een echt CA-certificaat is in plaats van het eigen certificaat van de peer:

  1. Verkrijg het CA-certificaat dat de keten van de server verankert. “Verankert” betekent het certificaat boven aan (of nabij de top van) de keten van de server dat je kiest als je startpunt van vertrouwen. Een TLS-server stuurt zijn leaf en doorgaans zijn intermediate(s); hij stuurt nooit zijn root. Je moet dat trust anchor zelf verkrijgen en onafhankelijk van de server – simpelweg vertrouwen op wat een server je aanreikt, zou het hele doel van verificatie tenietdoen.

    Zoek eerst uit welke CA het certificaat van de server daadwerkelijk heeft uitgegeven. Bijvoorbeeld tegen openmv.io

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

    Het Certificate chain-blok somt elk certificaat op met zijn subject (s:) en issuer (i:); nieuwere OpenSSL drukt ook a: (sleuteltype) en v: (geldigheid) regels af die je hier kunt negeren:

    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
    

    Item 0 is de leaf (openmv.io), uitgegeven door de intermediate E8. Item 1 is die intermediate, uitgegeven door de root ISRG Root X1. De issuer (i:) van het bovenste item benoemt de root – hier ISRG Root X1. (De intermediate is E8 in plaats van de R10 / R11 die je elders mogelijk hebt gezien, omdat openmv.io een ECDSA-certificaat gebruikt; Let’s Encrypt ondertekent ECDSA-leaves met zijn E-serie intermediates en RSA-leaves met zijn R-serie. Beide leiden naar ISRG Root X1.)

    OpenSSL drukt ook depth= regels af en kan de root rapporteren met Verification: OK. Dat gebeurt alleen omdat jouw pc ISRG Root X1 al vertrouwt – de server heeft hem niet gestuurd (een server stuurt nooit zijn root), en de camera, die geen trust store heeft, zal hem ook niet hebben. Dat is precies waarom je hem moet aanleveren.

    Download die root van de eigen gepubliceerde roots van de CA. Let’s Encrypt catalogiseert al die van hen op de Let’s Encrypt-certificatenpagina; het directe bestand voor ISRG Root X1 is isrgrootx1.pem (ze bieden het ook vooraf gecodeerd aan als isrgrootx1.der). Andere CA’s publiceren die van hen op een vergelijkbare “root certificates” / “repository”-pagina; de canonieke openbare set is het Mozilla CA-programma (CCADB). Bevestig dat je het juiste bestand hebt opgehaald door de fingerprint te vergelijken met de waarde die de CA publiceert (voeg -inform DER toe als je de .der hebt gedownload):

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

    Als je liever geen root bijhoudt, kun je in plaats daarvan de intermediate rechtstreeks uit de -showcerts-uitvoer kopiëren (het tweede -----BEGIN CERTIFICATE------blok), die vertrouwen, en accepteren dat je die moet verversen telkens wanneer de CA de intermediate roteert – veel vaker dan de root (zie de afweging hieronder).

  2. Converteer hem naar DER, precies zoals eerder:

    openssl x509 -in isrgrootx1.pem -outform DER -out ca.der
    
  3. Kopieer ca.der naar de camera (bestandssysteem of ROMFS) en laad hem als het trust anchor:

    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 is hier vereist: het stuurt SNI aan en is de naam die wordt gecontroleerd tegen de subjectAltName van het servercertificaat.

Tip

Snelkoppeling voor het gangbare geval. Let’s Encrypt is de meest gebruikte openbare CA, en zowel de RSA- als de ECDSA-certificaten leiden momenteel naar ISRG Root X1 (zoals het openmv.io-voorbeeld hierboven laat zien). Als de servers waarmee je camera praat Let’s Encrypt gebruiken, kun je de inspectie volledig overslaan: zet gewoon isrgrootx1.der op de camera en pas er load_verify_locations op toe.

Dit zorgt er niet voor dat TLS naar elke site werkt. Een server waarvan het certificaat van een andere CA komt (DigiCert, Google Trust Services, Amazon, Sectigo, …) zal de verificatie nog steeds niet doorstaan, en omdat de camera één enkel DER-certificaat per ssl.SSLContext vertrouwt, kun je niet elke root bundelen zoals een browser dat doet. Bij twijfel: identificeer de werkelijke CA van de server zoals hierboven getoond en vertrouw die root.

Welk certificaat je vertrouwt is een afweging:

  • De root (aanbevolen). Lang geldig – vaak decennia – dus ca.der verandert zelden. Het vereist dat de server zijn intermediate stuurt zodat mbedTLS het pad leaf → intermediate → jouw vertrouwde root kan opbouwen; vrijwel elke correct geconfigureerde openbare server doet dat.

  • De intermediate. Werkt ook, en blijft werken zelfs als een server de intermediate weglaat, maar intermediates worden veel vaker geroteerd dan roots, dus je zult ca.der vaker moeten verversen.

  • De leaf zelf (certificate pinning). Het strakst, maar de leaf verandert bij elke verlenging – ongeveer elke 90 dagen voor Let’s Encrypt – dus dit heeft alleen zin wanneer je ook de server beheert en de nieuwe pin gelijktijdig naar elke camera kunt pushen. Dit is precies wat het zelfondertekende clientvoorbeeld doet.

Notitie

ssl.SSLContext.load_verify_locations() neemt één enkel DER-gecodeerd CA-certificaat, dus de camera vertrouwt precies één anchor tegelijk. Om servers onder verschillende CA’s te bereiken, gebruik je een aparte ssl.SSLContext per anchor. En omdat dat certificaat zelf uiteindelijk zal verlopen of door de CA wordt geroteerd, behandel je het zoals elk ander certificaat op het apparaat.