14.4.5. Verificar um servidor público (câmara como cliente)

Tudo o que foi dito na página anterior sobre o cliente «já ter a raiz» é verdade para browsers, telemóveis e PCs – não é verdade para a câmara. O ssl do MicroPython não inclui nenhum repositório de confiança embutido: uma câmara recém-instalada não confia em nenhuma CA, e a predefinição (ssl.CERT_NONE) não verifica nada e fica completamente exposta a ataques man-in-the-middle. Por isso, quando a câmara é o cliente que se liga a um servidor TLS público (uma API HTTPS, um broker MQTT, …) e pretende verificar verdadeiramente esse servidor, tem de fornecer a âncora de confiança você mesmo.

O procedimento é o mesmo que o exemplo de cliente com certificado autoassinado em Certificados autoassinados; a única diferença é que o ficheiro carregado é um certificado de CA real em vez do certificado do próprio par:

  1. Obtenha o certificado de CA que ancora a cadeia do servidor. «Ancora» significa o certificado no topo (ou perto do topo) da cadeia do servidor que escolhe como ponto de partida de confiança. Um servidor TLS envia o seu certificado folha e normalmente os intermédios; nunca envia a raiz. Tem de obter essa âncora de confiança por si próprio e independentemente do servidor – confiar simplesmente no que o servidor apresenta anularia todo o propósito da verificação.

    Primeiro, descubra qual é a CA que emitiu o certificado do servidor. Por exemplo, para openmv.io

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

    O bloco Certificate chain lista cada certificado com o seu sujeito (s:) e emissor (i:); versões mais recentes do OpenSSL também imprimem linhas a: (tipo de chave) e v: (validade) que podem ser ignoradas aqui:

    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
    

    A entrada 0 é a folha (openmv.io), emitida pelo intermédio E8. A entrada 1 é esse intermédio, emitido pela raiz ISRG Root X1. O emissor (i:) da entrada mais acima indica a raiz – neste caso ISRG Root X1. (O intermédio é E8 em vez de R10 / R11 que pode ter visto noutros contextos porque openmv.io usa um certificado ECDSA; a Let’s Encrypt assina folhas ECDSA com os seus intermédios da série E e folhas RSA com os da série R. Ambas as cadeias chegam a ISRG Root X1.)

    O OpenSSL também imprime linhas depth= e pode reportar a raiz com Verification: OK. Isso acontece apenas porque o seu PC já confia em ISRG Root X1 – o servidor não a enviou (um servidor nunca envia a sua raiz), e a câmara, sem repositório de confiança, também não a terá. É precisamente por isso que tem de a fornecer.

    Transfira essa raiz a partir das raízes publicadas pela própria CA. A Let’s Encrypt cataloga todas as suas na página de certificados da Let’s Encrypt; o ficheiro direto para ISRG Root X1 é isrgrootx1.pem (também disponível pré-codificado como isrgrootx1.der). Outras CAs publicam as suas numa página semelhante de «certificados raiz» / «repositório»; o conjunto público canónico é o programa Mozilla CA (CCADB). Confirme que transferiu o ficheiro correto comparando a sua impressão digital com o valor publicado pela CA (acrescente -inform DER se transferiu o .der):

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

    Se preferir não monitorizar uma raiz, pode em alternativa copiar o intermédio diretamente da saída do -showcerts (o segundo bloco -----BEGIN CERTIFICATE-----), confiar nele e aceitar que terá de o renovar sempre que a CA rodar o intermédio – muito mais frequentemente do que a raiz (ver o compromisso abaixo).

  2. Converta para DER, exatamente como antes:

    openssl x509 -in isrgrootx1.pem -outform DER -out ca.der
    
  3. Copie ca.der para a câmara (sistema de ficheiros ou ROMFS) e carregue-o como âncora de confiança:

    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 é obrigatório aqui: ativa o SNI e é o nome verificado contra o subjectAltName do certificado do servidor.

Dica

Atalho para o caso comum. A Let’s Encrypt é a CA pública mais utilizada, e os seus certificados RSA e ECDSA encadeiam atualmente para ISRG Root X1 (como o exemplo openmv.io acima demonstra). Se os servidores com que a câmara comunica usam Let’s Encrypt, pode saltar a inspeção por completo: basta colocar isrgrootx1.der na câmara e usar load_verify_locations com esse ficheiro.

Isto não faz com que o TLS funcione para todos os sites. Um servidor cujo certificado provém de uma CA diferente (DigiCert, Google Trust Services, Amazon, Sectigo, …) continuará a falhar a verificação e, como a câmara confia num único certificado DER por ssl.SSLContext, não é possível incluir todas as raízes à semelhança de um browser. Em caso de dúvida, identifique a CA real do servidor como mostrado acima e confie nessa raiz.

A escolha do certificado em que confia é um compromisso:

  • A raiz (recomendada). De longa duração – frequentemente décadas – pelo que ca.der raramente muda. Exige que o servidor envie o seu intermédio para que o mbedTLS possa construir o caminho folha → intermédio → raiz de confiança; praticamente todos os servidores públicos corretamente configurados fazem isso.

  • O intermédio. Também funciona e continua a funcionar mesmo que um servidor omita o intermédio, mas os intermédios são rodados muito mais frequentemente do que as raízes, pelo que terá de atualizar ca.der com maior frequência.

  • A própria folha (fixação de certificado). A opção mais restrita, mas a folha muda em cada renovação – aproximadamente a cada 90 dias para a Let’s Encrypt – pelo que só faz sentido quando controla também o servidor e pode distribuir o novo pin a todas as câmaras em simultâneo. É exatamente o que o exemplo de cliente com certificado autoassinado faz.

Nota

ssl.SSLContext.load_verify_locations() aceita um único certificado de CA codificado em DER, pelo que a câmara confia em exatamente uma âncora de cada vez. Para aceder a servidores sob CAs diferentes, utilize um ssl.SSLContext separado por âncora. E como esse certificado acabará por expirar ou ser rodado pela CA, trate-o como qualquer outro certificado no dispositivo.