14.4.5. Verificare un server pubblico (camera come client)

Tutto quanto detto nella pagina precedente sul fatto che un client «possieda già la root» è vero per browser, telefoni e PC, ma non è vero per la camera. Il modulo ssl di MicroPython non include alcun trust store integrato: una camera appena flashata non si fida di alcuna CA, e l’impostazione predefinita (ssl.CERT_NONE) non verifica nulla ed è completamente esposta a un attacco man-in-the-middle. Per questo, quando la camera è il client che si connette verso un server TLS pubblico (una API HTTPS, un broker MQTT, …) e si vuole che verifichi davvero quel server, occorre fornire da soli l’ancora di fiducia.

I meccanismi sono gli stessi dell’esempio di client con certificato self-signed in Certificati autofirmati; l’unica differenza è che il file caricato è un vero certificato CA invece del certificato del peer:

  1. Ottieni il certificato CA che ancora la catena del server. «Ancorare» significa il certificato che si trova in cima (o vicino alla cima) alla catena del server e che scegli come punto di partenza della fiducia. Un server TLS invia il proprio certificato leaf e di solito gli intermedi; non invia mai la propria root. Devi procurarti tu stesso quell’ancora di fiducia, in modo indipendente dal server: fidarsi semplicemente di ciò che un server fornisce vanificherebbe l’intero scopo della verifica.

    Per prima cosa scopri quale CA ha effettivamente emesso il certificato del server. Ad esempio, per openmv.io

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

    Il blocco Certificate chain elenca ogni certificato con il suo soggetto (s:) e il suo emittente (i:); le versioni più recenti di OpenSSL stampano anche le righe a: (tipo di chiave) e v: (validità) che qui puoi ignorare:

    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
    

    La voce 0 è il leaf (openmv.io), emesso dall’intermedio E8. La voce 1 è quell’intermedio, emesso dalla root ISRG Root X1. L’emittente (i:) della voce più in alto indica la root, qui ISRG Root X1. (L’intermedio è E8 invece dei R10 / R11 che potresti aver visto altrove perché openmv.io usa un certificato ECDSA; Let’s Encrypt firma i leaf ECDSA con i suoi intermedi della serie E e i leaf RSA con quelli della serie R. Entrambi si concatenano a ISRG Root X1.)

    OpenSSL stampa anche le righe depth= e può riportare la root con Verification: OK. Questo accade solo perché il tuo PC si fida già di ISRG Root X1: il server non l’ha inviata (un server non invia mai la propria root), e nemmeno la camera, priva di trust store, la possiederà. È esattamente per questo che devi fornirla tu.

    Scarica quella root dalle root pubblicate dalla CA stessa. Let’s Encrypt cataloga tutte le proprie nella pagina dei certificati di Let’s Encrypt; il file diretto per ISRG Root X1 è isrgrootx1.pem (lo offrono anche già codificato come isrgrootx1.der). Le altre CA pubblicano le proprie su una pagina analoga «root certificates» / «repository»; l’insieme pubblico canonico è il programma CA di Mozilla (CCADB). Conferma di aver scaricato il file giusto confrontandone l’impronta con il valore pubblicato dalla CA (aggiungi -inform DER se hai scaricato il .der):

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

    Se preferisci non gestire una root, puoi invece copiare l”intermedio direttamente dall’output di -showcerts (il secondo blocco -----BEGIN CERTIFICATE-----), fidarti di quello e accettare di doverlo aggiornare ogni volta che la CA ruota l’intermedio, cosa molto più frequente rispetto alla root (vedi il compromesso più sotto).

  2. Convertilo in DER, esattamente come prima:

    openssl x509 -in isrgrootx1.pem -outform DER -out ca.der
    
  3. Copia ca.der sulla camera (filesystem o ROMFS) e caricalo come ancora di fiducia:

    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 è qui obbligatorio: pilota l’SNI ed è il nome confrontato con il subjectAltName del certificato del server.

Suggerimento

Scorciatoia per il caso comune. Let’s Encrypt è la CA pubblica più utilizzata, e attualmente sia i suoi certificati RSA sia quelli ECDSA si concatenano a ISRG Root X1 (come mostra l’esempio openmv.io sopra). Se i server con cui la tua camera comunica usano Let’s Encrypt, puoi saltare del tutto l’ispezione: basta mettere isrgrootx1.der sulla camera e caricarlo con load_verify_locations.

Questo non fa funzionare il TLS verso qualsiasi sito. Un server il cui certificato proviene da una CA diversa (DigiCert, Google Trust Services, Amazon, Sectigo, …) fallirà comunque la verifica e, poiché la camera si fida di un singolo certificato DER per ssl.SSLContext, non puoi raggruppare ogni root come fa un browser. In caso di dubbio, identifica la CA effettiva del server come mostrato sopra e fidati di quella root.

Di quale certificato fidarsi è un compromesso:

  • La root (consigliata). Ha vita lunga, spesso decenni, quindi ca.der cambia raramente. Richiede che il server invii il proprio intermedio affinché mbedTLS possa costruire il percorso leaf → intermedio → la tua root attendibile; praticamente ogni server pubblico configurato correttamente lo fa.

  • L’intermedio. Funziona anch’esso e continua a funzionare anche se un server omette l’intermedio, ma gli intermedi vengono ruotati molto più spesso delle root, quindi dovrai aggiornare ca.der con maggiore frequenza.

  • Il leaf stesso (certificate pinning). È il più stringente, ma il leaf cambia a ogni rinnovo, circa ogni 90 giorni per Let’s Encrypt, quindi ha senso solo quando controlli anche il server e puoi distribuire il nuovo pin a ogni camera in sincronia. È esattamente ciò che fa l’esempio di client con certificato self-signed.

Nota

ssl.SSLContext.load_verify_locations() accetta un singolo certificato CA codificato in DER, quindi la camera si fida esattamente di un’ancora per volta. Per raggiungere server appartenenti a CA diverse, usa un ssl.SSLContext separato per ciascuna ancora. E poiché quel certificato prima o poi scadrà o verrà ruotato dalla CA, trattalo come qualsiasi altro certificato sul dispositivo.