14.4.5. Verificando um servidor público (câmera como cliente)

Tudo o que foi dito na página anterior sobre um cliente “já possuir a raiz” é verdade para navegadores, telefones e PCs – mas não é verdade para a câmera. O módulo ssl do MicroPython não vem com nenhum armazenamento de confiança embutido: uma câmera recém-gravada não confia em nenhuma CA, e o padrão (ssl.CERT_NONE) não verifica nada e fica totalmente exposto a um ataque man-in-the-middle. Portanto, quando a câmera é o cliente que se conecta a um servidor TLS público (uma API HTTPS, um broker MQTT, …) e você quer que ela realmente verifique esse servidor, você mesmo precisa fornecer a âncora de confiança.

A mecânica é a mesma do exemplo de cliente autoassinado em Certificados autoassinados; a única diferença é que o arquivo que você carrega é um certificado de CA real em vez do próprio certificado do par:

  1. Obtenha o certificado de CA que ancora a cadeia do servidor. “Ancorar” significa o certificado no (ou perto do) topo da cadeia do servidor que você escolhe como seu ponto de partida da confiança. Um servidor TLS envia seu certificado folha e geralmente seu(s) intermediário(s); ele nunca envia sua raiz. Você mesmo precisa obter essa âncora de confiança e independentemente do servidor – simplesmente confiar no que quer que um servidor lhe entregue anularia todo o propósito da verificação.

    Primeiro descubra qual CA realmente emitiu o certificado do servidor. Por exemplo, contra openmv.io

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

    O bloco Certificate chain lista cada certificado com seu sujeito (s:) e emissor (i:); o OpenSSL mais recente também imprime linhas a: (tipo de chave) e v: (validade) que você pode ignorar 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 intermediário E8. A entrada 1 é esse intermediário, emitido pela raiz ISRG Root X1. O emissor (i:) da entrada mais ao topo nomeia a raiz – aqui ISRG Root X1. (O intermediário é E8 em vez do R10 / R11 que você pode ter visto em outros lugares porque openmv.io usa um certificado ECDSA; a Let’s Encrypt assina folhas ECDSA com seus intermediários da série E e folhas RSA com os da série R. Ambos encadeiam até ISRG Root X1.)

    O OpenSSL também imprime linhas depth= e pode relatar 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 sua raiz), e a câmera, não tendo armazenamento de confiança, também não a terá. É exatamente por isso que você precisa fornecê-la.

    Baixe 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 arquivo direto para a ISRG Root X1 é isrgrootx1.pem (eles também a oferecem pré-codificada como isrgrootx1.der). Outras CAs publicam as suas em uma página similar de “root certificates” / “repository”; o conjunto público canônico é o programa de CAs da Mozilla (CCADB). Confirme que você baixou o arquivo correto comparando sua impressão digital com o valor que a CA publica (adicione -inform DER se você baixou o .der):

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

    Se você preferir não acompanhar uma raiz, pode em vez disso copiar o intermediário diretamente da saída de -showcerts (o segundo bloco -----BEGIN CERTIFICATE-----), confiar nele e aceitar que precisará atualizá-lo sempre que a CA rotacionar o intermediário – muito mais frequentemente do que a raiz (veja o trade-off abaixo).

  2. Converta-o para DER, exatamente como antes:

    openssl x509 -in isrgrootx1.pem -outform DER -out ca.der
    
  3. Copie ca.der para a câmera (sistema de arquivos ou ROMFS) e carregue-o como a â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: ele orienta 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 amplamente utilizada, e tanto seus certificados RSA quanto ECDSA encadeiam atualmente até a ISRG Root X1 (como mostra o exemplo de openmv.io acima). Se os servidores com os quais sua câmera conversa usam a Let’s Encrypt, você pode pular a inspeção inteiramente: basta colocar isrgrootx1.der na câmera e fazer load_verify_locations dele.

Isso não faz o TLS funcionar com todos os sites. Um servidor cujo certificado venha de uma CA diferente (DigiCert, Google Trust Services, Amazon, Sectigo, …) ainda falhará na verificação, e como a câmera confia em um único certificado DER por ssl.SSLContext, você não pode agrupar todas as raízes da forma que um navegador faz. Em caso de dúvida, identifique a CA real do servidor conforme mostrado acima e confie nessa raiz.

Em qual certificado você confia é um trade-off:

  • A raiz (recomendado). Longa duração – frequentemente décadas – então ca.der raramente muda. Ela exige que o servidor envie seu intermediário para que o mbedTLS possa construir o caminho folha → intermediário → sua raiz confiável; praticamente todo servidor público corretamente configurado faz isso.

  • O intermediário. Também funciona, e continua funcionando mesmo que um servidor omita o intermediário, mas os intermediários são rotacionados com muito mais frequência do que as raízes, então você terá que atualizar ca.der com mais frequência.

  • A própria folha (certificate pinning). O mais restrito, mas a folha muda a cada renovação – aproximadamente a cada 90 dias para a Let’s Encrypt – então isso só faz sentido quando você também controla o servidor e pode enviar o novo pin para todas as câmeras em sincronia. É exatamente isso que o exemplo de cliente autoassinado faz.

Nota

ssl.SSLContext.load_verify_locations() aceita um único certificado de CA codificado em DER, então a câmera confia em exatamente uma âncora por vez. Para alcançar servidores sob CAs diferentes, use um ssl.SSLContext separado por âncora. E como esse próprio certificado eventualmente expirará ou será rotacionado pela CA, trate-o como qualquer outro certificado no dispositivo.