9.17. Sockets criptografados e TLS¶
Tudo o que foi abordado até aqui move bytes de forma aberta. Qualquer dispositivo no caminho entre a câmera e o servidor – o roteador doméstico, o provedor de acesso à internet, um ponto de acesso malicioso em uma cafeteria – pode, em princípio, ler ou modificar o que passa por ali. Para a maior parte do tráfego da internet isso não é aceitável. A solução padrão é envolver a conexão em uma camada de criptografia: TLS, o protocolo Transport Layer Security. O ícone de cadeado “HTTPS” em um navegador é o TLS rodando sobre TCP, e esse mesmo envoltório é o que torna “seguro” qualquer outro protocolo da internet. O módulo ssl da câmera é o que envolve um socket em TLS.
9.17.1. O que o TLS acrescenta, e com o que a câmera vem de fábrica¶
O TLS fica entre o TCP e a aplicação – a aplicação escreve bytes em um socket envolto em TLS, o TLS os criptografa e entrega o resultado ao TCP, e o processo é invertido do outro lado. Em sua forma completa, o TLS oferece três garantias além do TCP puro:
Confidencialidade. Bisbilhoteiros no caminho não conseguem ler o que os dois extremos estão trocando.
Integridade. Qualquer modificação do tráfego em trânsito é detectada; a conexão é interrompida em vez de entregar dados adulterados.
Autenticação. O servidor prova que é o servidor que diz ser, e não um impostor (e, opcionalmente, o cliente também prova quem ele é).
As duas primeiras vêm da própria criptografia. A terceira precisa de certificados em pelo menos um dos lados, além de algo previamente confiável para verificar esses certificados. A câmera OpenMV vem sem nenhum armazenamento de certificados embutido: uma câmera recém-gravada não confia em nenhuma autoridade certificadora, não possui um certificado de servidor próprio, e o modo de verificação padrão (ssl.CERT_NONE) não confronta o certificado do par com nada. Portanto, de fábrica, o TLS na câmera oferece as duas primeiras garantias – criptografia contra espionagem e adulteração por um observador passivo – mas não a terceira.
9.17.2. Criptografando uma conexão de saída¶
O uso mais simples é envolver uma conexão TCP de saída. O fluxo é: abrir um socket TCP normal, entregá-lo a ssl.wrap_socket() e, em seguida, ler e escrever através do socket envolto exatamente como você faria com o socket puro:
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 envoltório executa o handshake TLS; depois disso, cada byte que passa por s.send é criptografado na saída e cada byte vindo de s.recv foi criptografado no fio. Nenhum certificado foi configurado, nenhuma âncora de confiança foi fornecida – o TLS simplesmente negocia uma chave de sessão efêmera com qualquer servidor que responda e a utiliza.
O handshake TLS que ssl.wrap_socket() executa. Ele fica sobre a conexão TCP já aberta da figura anterior; uma vez que ambos os lados tenham enviado Finished, o restante da conversa é criptografado em ambas as direções.¶
Aviso
Isto é apenas criptografia, não TLS autenticado. A câmera fala com segurança com o que quer que tenha respondido na outra ponta da conexão TCP. Se um intermediário (man-in-the-middle) redirecionar a conexão para um servidor que ele controla e esse servidor apresentar qualquer certificado, o handshake ainda assim terá sucesso e a câmera acabará falando com segurança com o atacante. Use este modo apenas quando um intermediário não fizer parte do modelo de ameaças – uma rede local fechada, um ambiente de desenvolvimento, a câmera falando com um serviço rodando no mesmo hardware – e não quando estiver acessando a internet pública.
Para autenticação real – a câmera verificando um servidor público, a câmera atuando como um servidor TLS, ou TLS mútuo – você precisa trazer certificados para o dispositivo. A história completa está em Trabalhando com certificados TLS.
O mesmo envoltório funciona para tráfego TCP de entrada, selecionando o protocolo de servidor e passando server_side=True para ssl.wrap_socket(). O aviso acima ainda se aplica: sem um certificado próprio, a câmera não consegue provar quem é para o cliente, e um cliente curioso veria uma falha de handshake por “no certificate” na maioria das pilhas TLS. O fluxo de trabalho de certificados do lado de produção é o que desbloqueia rodar a câmera como um servidor TLS de forma útil.
9.17.3. Com asyncio¶
O capítulo sobre asyncio mostrou asyncio.open_connection() para clientes TCP puros. A mesma chamada aceita uma palavra-chave ssl=True que envolve a conexão em TLS, novamente sem nenhuma configuração de certificado:
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 trás de uma conexão TLS tem o mesmo formato de uma conexão TCP pura – apenas a configuração difere. A mesma ressalva sobre autenticação se aplica: ssl=True sozinho oferece apenas criptografia, não verificação.
9.17.4. DTLS – TLS sobre UDP¶
O TLS discutido até aqui roda sobre o TCP. O protocolo paralelo para UDP é o DTLS (Datagram TLS), e o módulo ssl da câmera o suporta da mesma maneira. Enquanto o TLS transforma uma conexão TCP em um fluxo de bytes criptografado, o DTLS transforma um socket UDP em um fluxo de datagramas criptografados, entregues individualmente – de modo que as propriedades de perda / fora de ordem / sem controle de fluxo do UDP, vistas em UDP – envie um pacote e torça pelo melhor, todas se mantêm, agora com os bytes dentro de cada datagrama criptografados.
O envoltório se parece com o caso do TLS, apenas com um socket SOCK_DGRAM e as constantes do 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() em um socket UDP não abre uma conexão – apenas memoriza um destino padrão para que as chamadas subsequentes a send() / recv() não tenham que repeti-lo. O DTLS precisa desse destino fixo para executar seu handshake.)
O handshake tem o mesmo formato do diagrama TLS acima; a diferença é que cada mensagem do handshake é, ela própria, um datagrama UDP, e qualquer um dos lados retentará em caso de perda.
Nota
Perder pacotes quebra a criptografia? Não. Cada pacote DTLS carrega um número de sequência, e a criptografia usa esse número para produzir uma saída diferente para cada pacote – de modo que a mesma entrada nunca é criptografada para os mesmos bytes duas vezes, e qualquer pacote pode ser descriptografado sozinho, sem que o anterior tenha chegado. Pacotes perdidos ou fora de ordem não dessincronizam os dois lados. (O próprio handshake é a única parte que precisa chegar de forma confiável, e o DTLS cuida disso com sua própria retransmissão.)
O mesmo aviso de “apenas criptografia, sem certificados” de cima se aplica: um handshake DTLS contra um par CERT_NONE criptografa o tráfego mas não verifica quem é o outro lado. O fluxo de trabalho completo do DTLS – certificados, o cookie anti-spoofing do lado do servidor, como esta é a mesma superfície que o TLS exceto pelas constantes do protocolo – é abordado junto com o material sobre TLS em Trabalhando com certificados TLS.
A versão com asyncio usa o mesmo padrão de UDP não bloqueante de Sockets com asyncio. Faça o handshake de forma síncrona logo no início, coloque o socket em modo não bloqueante e então faça o 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 ponto em que esta corrotina bloqueia o event loop; depois disso, cada s.send / s.recv retorna imediatamente (ou lança OSError), e o await asyncio.sleep_ms mantém o restante do programa rodando.
9.17.5. Indo além¶
Tudo que vai além do TLS apenas com criptografia – verificar o certificado de um servidor HTTPS público, rodar a câmera como um servidor TLS autenticado, TLS mútuo entre a câmera e um back-end, escolher chaves e tipos de chaves, lidar com a expiração de certificados – está em Trabalhando com certificados TLS. Essa seção aborda como gerar certificados autoassinados para testes locais, como obter certificados assinados por uma CA para produção, como colocá-los na câmera no formato correto (DER), como verificar um servidor público quando a câmera é o cliente, como pensar na proteção de chaves em um dispositivo que um atacante pode desmontar, e como planejar para o dia em que o certificado expira.
Para a referência completa da API ssl – versões de TLS suportadas, conjuntos de cifras e opções de contexto – consulte ssl — Módulo SSL/TLS.