9.17. Sockets cifrados y TLS

Todo lo cubierto hasta aquí mueve bytes en claro. Cualquier dispositivo en la ruta entre la cámara y el servidor – el router del hogar, el proveedor de servicios de internet, un punto de acceso malicioso en una cafetería – puede, en principio, leer o modificar lo que pasa a través de él. Para la mayoría del tráfico de internet eso no es aceptable. La solución estándar es envolver la conexión en una capa de cifrado: TLS, el protocolo Transport Layer Security. El icono del candado «HTTPS» en un navegador es TLS ejecutándose sobre TCP, y ese mismo envoltorio es lo que hace «seguro» a cualquier otro protocolo de internet. El módulo ssl de la cámara es lo que envuelve un socket en TLS.

9.17.1. Qué añade TLS, y qué incluye la cámara de fábrica

TLS se sitúa entre TCP y la aplicación – la aplicación escribe bytes en un socket envuelto en TLS, TLS los cifra y entrega el resultado a TCP, y el proceso se invierte en el otro extremo. En su forma completa, TLS ofrece tres garantías por encima de TCP en claro:

  • Confidencialidad. Quien escuche en la ruta no puede leer lo que intercambian los dos extremos.

  • Integridad. Cualquier modificación del tráfico en tránsito se detecta; la conexión se rompe en lugar de entregar datos manipulados.

  • Autenticación. El servidor demuestra que es el servidor nombrado, no un impostor (y, opcionalmente, el cliente demuestra quién es él también).

Las dos primeras provienen del propio cifrado. La tercera necesita certificados en al menos uno de los lados, más algo previamente confiable contra lo que verificar esos certificados. La cámara OpenMV se entrega sin ningún almacén de certificados integrado: una cámara recién flasheada no confía en ninguna autoridad de certificación, no tiene un certificado de servidor propio, y el modo de verificación predeterminado (ssl.CERT_NONE) no comprueba el certificado del par contra nada. Así que, de fábrica, TLS en la cámara te da las dos primeras garantías – cifrado contra escuchas y manipulaciones por parte de un observador pasivo – pero no la tercera.

9.17.2. Cifrar una conexión saliente

El uso más sencillo es envolver una conexión TCP saliente. El flujo es: abrir un socket TCP normal, entregarlo a ssl.wrap_socket(), y luego leer y escribir a través del socket envuelto exactamente igual que lo harías con el socket en claro:

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()

El envoltorio realiza el handshake TLS; después, cada byte que pasa por s.send se cifra al salir y cada byte de s.recv viajó cifrado por el cable. No se configuró ningún certificado, no se suministró ningún ancla de confianza – TLS simplemente negocia una clave de sesión efímera con el servidor que responda y la usa.

Un diagrama con dos columnas etiquetadas como "client" y "server". Una línea horizontal discontinua cerca de la parte superior está etiquetada como "TCP connection already open". Debajo de ella, tres flechas muestran el handshake TLS: "ClientHello" del cliente al servidor, "ServerHello + certificate + key share" de vuelta, y "Finished" hacia adelante de nuevo. Una segunda línea horizontal discontinua más abajo está etiquetada como "TLS session open -- everything after this is encrypted". Dos gruesas flechas bidireccionales debajo de ella transportan "encrypted data".

El handshake TLS que ejecuta ssl.wrap_socket(). Se sitúa encima de la conexión TCP ya abierta de la figura anterior; una vez que ambos lados han enviado Finished, el resto de la conversación se cifra en ambas direcciones.

Advertencia

Esto es solo cifrado, no TLS autenticado. La cámara habla de forma segura con lo que sea que respondió en el otro extremo de la conexión TCP. Si un atacante intermediario (man-in-the-middle) redirige la conexión a un servidor que controla y ese servidor presenta cualquier certificado, el handshake aún tiene éxito y la cámara termina hablando de forma segura con el atacante. Usa este modo solo cuando un atacante intermediario no forme parte del modelo de amenazas – una red local cerrada, un entorno de desarrollo, la cámara hablando con un servicio que se ejecuta en el mismo hardware – y no cuando se conecta a la internet pública.

Para una autenticación real – la cámara verificando un servidor público, la cámara actuando como servidor TLS, o TLS mutuo – necesitas llevar certificados al dispositivo. La historia completa está en Trabajar con certificados TLS.

El mismo envoltorio funciona para el tráfico TCP entrante, seleccionando el protocolo de servidor y pasando server_side=True a ssl.wrap_socket(). La advertencia anterior sigue siendo válida: sin un certificado propio, la cámara no puede demostrar quién es ante el cliente, y un cliente curioso vería un fallo de handshake de «no certificate» en la mayoría de las pilas TLS. El flujo de trabajo de certificados del lado de producción es lo que desbloquea ejecutar la cámara como servidor TLS de forma útil.

9.17.3. Con asyncio

El capítulo de asyncio mostró asyncio.open_connection() para clientes TCP en claro. La misma llamada acepta una palabra clave ssl=True que envuelve la conexión en TLS, de nuevo sin ninguna configuración 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())

El par lector/escritor detrás de una conexión TLS tiene la misma forma que el de una conexión TCP en claro – solo difiere la configuración. La misma salvedad sobre la autenticación se aplica: ssl=True por sí solo da únicamente cifrado, no verificación.

9.17.4. DTLS – TLS sobre UDP

TLS, tal como se ha descrito hasta ahora, viaja encima de TCP. El protocolo paralelo para UDP es DTLS (Datagram TLS), y el módulo ssl de la cámara lo admite del mismo modo. Donde TLS convierte una conexión TCP en un flujo de bytes cifrado, DTLS convierte un socket UDP en un flujo de datagramas cifrados y entregados individualmente – de modo que las propiedades de pérdida / desorden / sin control de flujo de UDP de UDP – envía un paquete y cruza los dedos se mantienen, ahora con los bytes dentro de cada datagrama cifrados.

El envoltorio se ve igual que en el caso de TLS, solo que con un socket SOCK_DGRAM y las 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()

(Llamar a connect() en un socket UDP no abre una conexión – simplemente recuerda un destino predeterminado para que las llamadas posteriores send() / recv() no tengan que repetirlo. DTLS necesita ese destino fijo contra el que ejecutar su handshake.)

El handshake tiene la misma forma que el diagrama TLS de arriba; la diferencia es que cada mensaje del handshake es en sí mismo un datagrama UDP, y cualquiera de los dos lados reintentará en caso de pérdida.

Nota

¿Perder paquetes rompe el cifrado? No. Cada paquete DTLS lleva un número de secuencia, y el cifrado usa ese número para producir una salida diferente por cada paquete – de modo que la misma entrada nunca se cifra a los mismos bytes dos veces, y cualquier paquete se puede descifrar por sí solo sin que el anterior haya llegado. Los paquetes perdidos o desordenados no desincronizan a los dos lados. (El propio handshake es la única parte que tiene que llegar de forma fiable, y DTLS se encarga de eso con su propia retransmisión.)

La misma advertencia de solo-cifrado-sin-certificados de arriba se aplica: un handshake DTLS contra un par CERT_NONE cifra el tráfico pero no verifica quién es el otro lado. El flujo de trabajo completo de DTLS – certificados, la cookie anti-spoofing del lado del servidor, cómo esta es la misma superficie que TLS salvo por las constantes de protocolo – se cubre junto al material de TLS en Trabajar con certificados TLS.

La versión con asyncio usa el mismo patrón de UDP no bloqueante de Sockets con asyncio. Realiza el handshake de forma síncrona al principio, cambia el socket a no bloqueante, y luego sondea dentro de una corrutina:

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)

El handshake es el único punto donde esta corrutina bloquea el bucle de eventos; después de eso, cada s.send / s.recv retorna de inmediato (o lanza OSError), y el await asyncio.sleep_ms mantiene en marcha el resto del programa.

9.17.5. Ir más allá

Todo lo que va más allá de TLS de solo cifrado – verificar el certificado de un servidor HTTPS público, ejecutar la cámara como un servidor TLS autenticado, TLS mutuo entre la cámara y un back-end, elegir claves y tipos de clave, lidiar con la caducidad de certificados – está en Trabajar con certificados TLS. Esa sección cubre cómo generar certificados autofirmados para pruebas locales, cómo obtener certificados firmados por una CA para producción, cómo llevarlos a la cámara en el formato correcto (DER), cómo verificar un servidor público cuando la cámara es el cliente, cómo pensar en la protección de claves en un dispositivo que un atacante podría desmontar, y cómo planificar para el día en que el certificado caduque.

Para la referencia completa de la API ssl – versiones de TLS admitidas, conjuntos de cifrado y opciones de contexto – consulta ssl — Módulo SSL/TLS.