9.17. Sockets encriptados e TLS

Tudo o que foi abordado até aqui move bytes em texto claro. Qualquer dispositivo no caminho entre a câmara e o servidor – o router doméstico, o fornecedor de serviços de internet, um ponto de acesso malicioso numa cafetaria – pode, em princípio, ler ou modificar o que passa por ele. Para a maior parte do tráfego internet isso não é aceitável. A solução padrão é envolver a ligação numa camada de encriptação: TLS, o protocolo Transport Layer Security. O ícone de cadeado «HTTPS» no browser é TLS a correr sobre TCP, e o mesmo envolvimento é o que torna qualquer outro protocolo internet «seguro». O módulo ssl da câmara é o que envolve um socket em TLS.

9.17.1. O que o TLS acrescenta, e o que a câmara inclui

O TLS fica entre o TCP e a aplicação – a aplicação escreve bytes num socket encapsulado por TLS, o TLS encripta-os e entrega o resultado ao TCP, e o processo inverte-se do outro lado. Na sua forma completa, o TLS oferece três garantias sobre o TCP simples:

  • Confidencialidade. Espiões no caminho não conseguem ler o que os dois endpoints estão a trocar.

  • Integridade. Qualquer modificação do tráfego em trânsito é detetada; a ligação quebra em vez de entregar dados adulterados.

  • Autenticação. O servidor prova que é o servidor indicado, e não um impostor (e, opcionalmente, o cliente também prova quem ele é).

As duas primeiras derivam da própria encriptação. A terceira precisa de certificados em pelo menos um dos lados, mais algo pré-confiável para validar esses certificados. A câmara OpenMV é fornecida sem nenhum repositório de certificados integrado: uma câmara recém-instalada não confia em nenhuma autoridade de certificação, não tem certificado de servidor próprio, e o modo de verificação predefinido (ssl.CERT_NONE) não verifica o certificado do par com nada. Portanto, por defeito, o TLS na câmara fornece as duas primeiras garantias – encriptação contra escutas e adulteração por um observador passivo – mas não a terceira.

9.17.2. Encriptar uma ligação de saída

O uso mais simples é envolver uma ligação TCP de saída. O fluxo é: abrir um socket TCP normal, passá-lo a ssl.wrap_socket(), depois ler e escrever através do socket encapsulado exactamente como faria com o simples:

import socket
import ssl

addr = socket.getaddrinfo("example.com", 443)[0][-1]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(addr)

s = ssl.wrap_socket(sock)

s.send(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
print(s.recv(4096))
s.close()

O encapsulamento realiza o handshake TLS; depois disso, cada byte enviado via s.send é encriptado na saída e cada byte recebido via s.recv estava encriptado na rede. Não foram configurados certificados, não foi fornecida nenhuma âncora de confiança – o TLS simplesmente negoceia uma chave de sessão efémera com o servidor que responder e utiliza-a.

A diagram with two columns labelled "client" and "server". A dashed horizontal line near the top is labelled "TCP connection already open". Below it, three arrows show the TLS handshake: "ClientHello" from client to server, "ServerHello + certificate + key share" back, and "Finished" forward again. A second dashed horizontal line below is labelled "TLS session open -- everything after this is encrypted". Two thick bidirectional arrows below it carry "encrypted data".

O handshake TLS que ssl.wrap_socket() executa. Assenta sobre a ligação TCP já aberta da figura anterior; assim que ambos os lados enviaram Finished, o resto da conversa fica encriptado em ambas as direções.

Aviso

Isto é apenas encriptação, não TLS autenticado. A câmara comunica de forma segura com o que quer que tenha respondido no outro extremo da ligação TCP. Se um homem-no-meio redirecionar a ligação para um servidor que controla e esse servidor apresentar qualquer certificado, o handshake continua a ter sucesso e a câmara acaba a comunicar de forma segura com o atacante. Use este modo apenas quando um homem-no-meio não faz parte do modelo de ameaça – uma rede local fechada, um ambiente de desenvolvimento, a câmara a comunicar com um serviço a correr no mesmo hardware – não quando se liga à internet pública.

Para autenticação real – a câmara a verificar um servidor público, a câmara a funcionar como servidor TLS, ou TLS mútuo – é necessário trazer certificados para o dispositivo. A história completa está em Trabalhar com certificados TLS.

O mesmo encapsulamento funciona para tráfego TCP de entrada, selecionando o protocolo de servidor e passando server_side=True a ssl.wrap_socket(). O aviso acima ainda se aplica: sem um certificado próprio, a câmara não consegue provar quem é ao cliente, e um cliente curioso verá uma falha de handshake «sem certificado» na maioria das pilhas TLS. O fluxo de trabalho de certificados do lado de produção é o que permite executar a câmara como servidor TLS de forma útil.

9.17.3. Com asyncio

O capítulo sobre asyncio mostrou asyncio.open_connection() para clientes TCP simples. A mesma chamada aceita uma palavra-chave ssl=True que encapsula a ligação em TLS, novamente sem qualquer configuração de certificados:

import asyncio

async def main():
    reader, writer = await asyncio.open_connection(
        "example.com", 443, ssl=True,
    )
    writer.write(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
    await writer.drain()
    print(await reader.read(4096))
    writer.close()
    await writer.wait_closed()

asyncio.run(main())

O par leitor/escritor por detrás de uma ligação TLS tem a mesma forma que para uma ligação TCP simples – apenas a configuração difere. O mesmo aviso sobre autenticação aplica-se: ssl=True por si só fornece apenas encriptação, não verificação.

9.17.4. DTLS – TLS sobre UDP

O TLS tal como foi discutido até agora assenta sobre TCP. O protocolo paralelo para UDP é DTLS (Datagram TLS), e o módulo ssl da câmara suporta-o da mesma forma. Onde o TLS transforma uma ligação TCP numa stream de bytes encriptados, o DTLS transforma um socket UDP numa stream de datagramas encriptados entregues individualmente – por isso as propriedades de perda / fora de ordem / sem controlo de fluxo do UDP de UDP – envia um pacote e torce para o melhor mantêm-se, com os bytes dentro de cada datagrama agora encriptados.

O encapsulamento tem o mesmo aspecto que o caso TLS, apenas com um socket SOCK_DGRAM e as constantes de protocolo DTLS:

import socket
import ssl

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(socket.getaddrinfo("example.com", 4433)[0][-1])

ctx = ssl.SSLContext(ssl.PROTOCOL_DTLS_CLIENT)
s = ctx.wrap_socket(sock)

s.send(b"ping")
print(s.recv(64))
s.close()

(Chamar connect() num socket UDP não abre uma ligação – apenas memoriza um destino predefinido para que as chamadas subsequentes send() / recv() não tenham de o repetir. O DTLS precisa desse destino fixo para executar o seu handshake.)

O handshake tem a mesma forma que o diagrama TLS acima; a diferença é que cada mensagem do handshake é em si um datagrama UDP, e qualquer lado repetirá em caso de perda.

Nota

A perda de pacotes quebra a encriptação? Não. Cada pacote DTLS carrega um número de sequência, e a encriptação usa esse número para produzir saída diferente para cada pacote – portanto a mesma entrada nunca encripta para os mesmos bytes duas vezes, e qualquer pacote pode ser desencriptado por si só sem que o anterior tenha chegado. Pacotes perdidos ou fora de ordem não dessincronizam os dois lados. (O handshake em si é a única parte que tem de chegar de forma fiável, e o DTLS trata disso com a sua própria retransmissão.)

O mesmo aviso de encriptação-apenas-sem-certificados acima aplica-se: um handshake DTLS contra um par CERT_NONE encripta o tráfego mas não verifica quem está do outro lado. O fluxo de trabalho DTLS completo – certificados, o cookie anti-spoofing do lado do servidor, como esta é a mesma superfície que o TLS exceto pelas constantes de protocolo – é abordado juntamente com o material TLS em Trabalhar com certificados TLS.

A versão asyncio usa o mesmo padrão UDP não-bloqueante de Sockets com asyncio. Realize o handshake de forma síncrona no início, mude o socket para não-bloqueante, depois faça polling dentro de uma corrotina:

import asyncio
import socket
import ssl

async def dtls_ping(target_addr, period_ms):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.connect(target_addr)

    # Handshake while still blocking, then switch to async polling.
    ctx = ssl.SSLContext(ssl.PROTOCOL_DTLS_CLIENT)
    s = ctx.wrap_socket(sock)
    s.setblocking(False)

    while True:
        try:
            s.send(b"ping")
        except OSError:
            pass
        await asyncio.sleep_ms(period_ms)

O handshake é o único lugar onde esta corrotina bloqueia o ciclo de eventos; depois disso, cada s.send / s.recv retorna imediatamente (ou lança OSError), e o await asyncio.sleep_ms mantém o resto do programa a correr.

9.17.5. Indo mais longe

Tudo mais do que TLS apenas com encriptação – verificar o certificado de um servidor HTTPS público, executar a câmara como servidor TLS autenticado, TLS mútuo entre a câmara e um back-end, escolher chaves e tipos de chave, lidar com expiração de certificados – está em Trabalhar com certificados TLS. Essa secção aborda como gerar certificados autoassinados para testes locais, como obter certificados assinados por CA para produção, como os colocar na câmara no formato correto (DER), como verificar um servidor público quando a câmara é o cliente, como pensar na proteção de chaves num dispositivo que um atacante pode desmontar, e como planear para o dia em que o certificado expira.

Para a referência completa da API ssl – versões TLS suportadas, conjuntos de cifras e opções de contexto – consulte ssl — módulo SSL/TLS.