Como desarrollador de la biblioteca
PyGOST (primitivas criptográficas GOST en Python puro), a menudo recibo preguntas sobre cómo implementar la mensajería segura más simple en mi rodilla. Muchos consideran que la criptografía aplicada es algo bastante simple, y una llamada .encrypt () a un cifrado de bloque será suficiente para enviar de forma segura a través de un canal de comunicación. Otros creen que la criptografía aplicada es el destino de unos pocos, y es aceptable que compañías ricas como Telegram con olimpiadas matemáticas
no puedan implementar un protocolo seguro.
Todo esto me llevó a escribir este artículo para mostrar que la implementación de protocolos criptográficos y mensajería instantánea segura no es una tarea tan difícil. Sin embargo, inventar sus propios protocolos de autenticación y acuerdos clave no merece la pena.
El artículo se escribirá de mensajería instantánea
cifrada punto a punto ,
amigo a amigo , de
extremo a extremo con autenticación
SIGMA-I y protocolo de acuerdo de clave (basado en el cual se implementa
IPsec IKE ), utilizando exclusivamente la biblioteca PyGOST de algoritmos criptográficos GOST y Codificación ASN.1 de mensajes con la biblioteca
PyDERASN (sobre la cual ya
escribí antes ). Requisito previo: debe ser tan simple que se pueda escribir desde cero en una noche (o día de trabajo), de lo contrario, ya no es un programa simple. Probablemente tenga errores, dificultades innecesarias, deficiencias, además este es mi primer programa que usa la biblioteca asyncio.
Design IM
Para comenzar, debe comprender cómo se verá nuestro IM. Para simplificar, deje que sea una red de igual a igual, sin ningún descubrimiento de participantes. Le indicaremos personalmente a qué dirección: el puerto para conectarse para comunicarse con el interlocutor.
Entiendo que en este momento, la suposición de la disponibilidad de comunicación directa entre dos computadoras arbitrarias es una limitación significativa de la aplicabilidad de IM en la práctica. Pero mientras más desarrolladores implementen todo tipo de muletas transversales NAT, más tiempo permaneceremos en Internet IPv4, con una probabilidad deprimente de comunicación entre computadoras arbitrarias. Bueno, ¿cuánto puede soportar la falta de IPv6 en casa y en el trabajo?
Tendremos una red de amigo a amigo: todos los posibles interlocutores deben conocerse de antemano. En primer lugar, simplifica enormemente todo: presentarse, encontrar o no encontrar un nombre / clave, desconectado o continuar trabajando, conociendo al interlocutor. En segundo lugar, en el caso general, es seguro y excluye muchos ataques.
La interfaz de mensajería instantánea estará cerca de las soluciones clásicas de
proyectos sin éxito , que realmente me gustan por su minimalismo y filosofía Unix-way. Un programa de mensajería instantánea para cada interlocutor crea un directorio con tres sockets de dominio Unix:
- en - los mensajes enviados al interlocutor se registran en él;
- fuera: los mensajes recibidos del interlocutor se leen;
- estado: al leerlo, descubriremos si el interlocutor está conectado ahora, la dirección / puerto de conexión.
Además, se crea un zócalo de conexión, escribiendo en qué puerto host, iniciamos una conexión a un interlocutor remoto.
| - alicia
El | | - en
El | | - fuera
El | `- estado
| - bob
El | | - en
El | | - fuera
El | `- estado
`- conn
Este enfoque le permite realizar implementaciones independientes del transporte de mensajería instantánea y la interfaz de usuario, ya que no hay amigos para el gusto y el color, no complacerá a todos. Usando
tmux y / o
multitail , puede obtener una interfaz de múltiples ventanas con resaltado de sintaxis. Y con
rlwrap, puede obtener una cadena compatible con GNU Readline para ingresar mensajes.
De hecho, los proyectos suckless usan archivos FIFO. Personalmente, no podía entender cómo en asyncio trabajar con archivos de manera competitiva sin un sustrato hecho a mano de hilos seleccionados (he estado usando el lenguaje
Go para esas cosas durante mucho tiempo). Por lo tanto, decidí sobrevivir con los sockets de dominio de Unix. Desafortunadamente, esto hace que sea imposible hacer echo 2001: 470: dead :: babe 6666> conn.
Resolví este problema usando
socat : echo 2001: 470: dead :: babe 6666 | socat - UNIX-CONNECT: conn, socat READLINE UNIX-CONNECT: alice / in.
Protocolo inicial inseguro
TCP se utiliza como transporte: garantiza la entrega y su pedido. UDP no garantiza ni lo uno ni lo otro (lo que sería útil cuando se aplica la criptografía), y el soporte
SCTP en Python está
listo para usar.
Desafortunadamente, en TCP no hay un concepto de mensaje, sino solo un flujo de bytes. Por lo tanto, es necesario crear un formato para los mensajes para que puedan compartirse entre ellos en esta secuencia. Podemos aceptar usar el carácter de avance de línea. Para empezar, es adecuado, sin embargo, cuando comenzamos a encriptar nuestros mensajes, este símbolo puede aparecer en cualquier parte del texto cifrado. Por lo tanto, los protocolos son populares en las redes y envían primero la longitud del mensaje en bytes. Por ejemplo, en Python, fuera de la caja hay xdrlib, que le permite trabajar con un formato
XDR similar.
No vamos a trabajar de manera correcta y eficiente con la lectura de TCP: simplificamos el código. Leemos los datos del socket en un bucle sin fin hasta que decodificamos el mensaje completo. También puede usar JSON con XML como formato para este enfoque. Pero cuando se agrega la criptografía, entonces los datos deberán firmarse y autenticarse, y esto requerirá una representación idéntica byte por byte de objetos, que JSON / XML no proporciona (los volcados pueden variar).
XDR es adecuado para tal tarea, sin embargo, elijo ASN.1 con codificación DER y biblioteca
PyDERASN , ya que tendremos a mano objetos de alto nivel, que a menudo son más agradables y convenientes para trabajar. A diferencia de bencode sin
esquema ,
MessagePack o
CBOR , ASN.1 validará automáticamente los datos con un esquema codificado.
El mensaje recibido será Msg: ya sea un texto MsgText (con un campo de texto hasta el momento) o un mensaje de saludo MsgHandshake (en el que se transmite el nombre del interlocutor). Ahora parece demasiado complicado, pero es un desafío para el futuro.
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│MsgHandshake (IdA) │
│───────────────── >> │
│ │
│MsgHandshake (IdB) │
│ <─────────────────│
│ │
│ MsgText () │
│───────────────── >> │
│ │
│ MsgText () │
│ <─────────────────│
│ │
IM sin criptografía
Como dije, para todas las operaciones con sockets se utilizará la biblioteca asincio. Declare lo que esperamos en el lanzamiento:
parser = argparse.ArgumentParser(description="GOSTIM") parser.add_argument( "--our-name", required=True, help="Our peer name", ) parser.add_argument( "--their-names", required=True, help="Their peer names, comma-separated", ) parser.add_argument( "--bind", default="::1", help="Address to listen on", ) parser.add_argument( "--port", type=int, default=6666, help="Port to listen on", ) args = parser.parse_args() OUR_NAME = UTF8String(args.our_name) THEIR_NAMES = set(args.their_names.split(","))
Establezca su propio nombre (--nuestro nombre alice). Una coma enumera todos los interlocutores esperados (--sus nombres bob, eve). Para cada uno de los interlocutores, se crea un directorio con sockets Unix, así como una rutina para cada estado de entrada, salida, entrada:
for peer_name in THEIR_NAMES: makedirs(peer_name, mode=0o700, exist_ok=True) out_queue = asyncio.Queue() OUT_QUEUES[peer_name] = out_queue asyncio.ensure_future(asyncio.start_unix_server( partial(unixsock_out_processor, out_queue=out_queue), path.join(peer_name, "out"), )) in_queue = asyncio.Queue() IN_QUEUES[peer_name] = in_queue asyncio.ensure_future(asyncio.start_unix_server( partial(unixsock_in_processor, in_queue=in_queue), path.join(peer_name, "in"), )) asyncio.ensure_future(asyncio.start_unix_server( partial(unixsock_state_processor, peer_name=peer_name), path.join(peer_name, "state"), )) asyncio.ensure_future(asyncio.start_unix_server(unixsock_conn_processor, "conn"))
Los mensajes del socket de entrada del usuario se envían a la cola IN_QUEUES:
async def unixsock_in_processor(reader, writer, in_queue: asyncio.Queue) -> None: while True: text = await reader.read(MaxTextLen) if text == b"": break await in_queue.put(text.decode("utf-8"))
Los mensajes de los interlocutores se envían a la cola OUT_QUEUES, desde donde se escriben los datos en el socket de salida:
async def unixsock_out_processor(reader, writer, out_queue: asyncio.Queue) -> None: while True: text = await out_queue.get() writer.write(("[%s] %s" % (datetime.now(), text)).encode("utf-8")) await writer.drain()
Al leer desde el socket de estado, el programa busca en el diccionario PEER_ALIVE la dirección del interlocutor. Si todavía no hay conexión con el interlocutor, se escribe una línea vacía.
async def unixsock_state_processor(reader, writer, peer_name: str) -> None: peer_writer = PEER_ALIVES.get(peer_name) writer.write( b"" if peer_writer is None else (" ".join([ str(i) for i in peer_writer.get_extra_info("peername")[:2] ]).encode("utf-8") + b"\n") ) await writer.drain() writer.close()
Cuando se escribe una dirección en el zócalo de conexión, se inicia la función "iniciador" de la conexión:
async def unixsock_conn_processor(reader, writer) -> None: data = await reader.read(256) writer.close() host, port = data.decode("utf-8").split(" ") await initiator(host=host, port=int(port))
Considera al iniciador. Primero, obviamente abre una conexión con el host / puerto especificado y envía un mensaje de saludo con su nombre:
130 async def initiator(host, port): 131 _id = repr((host, port)) 132 logging.info("%s: dialing", _id) 133 reader, writer = await asyncio.open_connection(host, port) 134
Luego espera una respuesta desde el lado remoto. Intenta decodificar la respuesta recibida de acuerdo con el esquema Msg ASN.1. Suponemos que todo el mensaje será enviado por un segmento TCP y lo recibiremos atómicamente cuando se llame a .read (). Verificamos que recibimos exactamente el mensaje de apretón de manos.
141
Verificamos que conocemos el nombre de la persona con la que estamos hablando. Si no, entonces rompa la conexión. Verificamos si ya hemos establecido una conexión con él (el interlocutor nuevamente dio la orden de conectarse con nosotros) y la cerramos. Las cadenas de Python con el texto del mensaje se colocan en la cola IN_QUEUES, pero hay un valor especial None, que indica msg_sender a la rutina para que deje de funcionar para que se olvide de su escritor relacionado con la conexión TCP obsoleta.
159 msg_handshake = msg.value 160 peer_name = str(msg_handshake["peerName"]) 161 if peer_name not in THEIR_NAMES: 162 logging.warning("unknown peer name: %s", peer_name) 163 writer.close() 164 return 165 logging.info("%s: session established: %s", _id, peer_name) 166
msg_sender acepta mensajes salientes (en cola desde un socket de entrada), los serializa en un mensaje MsgText y los envía a través de una conexión TCP. Puede interrumpirse en cualquier momento, claramente lo estamos interceptando.
async def msg_sender(peer_name: str, writer) -> None: in_queue = IN_QUEUES[peer_name] while True: text = await in_queue.get() if text is None: break writer.write(Msg(("text", MsgText(( ("text", UTF8String(text)), )))).encode()) try: await writer.drain() except ConnectionResetError: del PEER_ALIVES[peer_name] return logging.info("%s: sent %d characters message", peer_name, len(text))
Al final, el iniciador entra en un ciclo interminable de lectura de mensajes desde el socket. Comprueba si se trata de un mensaje de texto y coloca en OUT_QUEUES la cola desde la que se enviarán a la salida del interlocutor correspondiente. ¿Por qué no puedes simplemente hacer .read () y decodificar el mensaje? Porque es posible que varios mensajes del usuario se agreguen en el búfer del sistema operativo y se envíen por un segmento TCP. Podemos decodificar el primero, y luego parte del siguiente puede permanecer en el búfer. En cualquier emergencia, cerramos la conexión TCP y detenemos la rutina de msg_sender (enviando None a la cola OUT_QUEUES).
174 buf = b"" 175
Volvamos al código principal. Después de crear todas las rutinas, al momento de iniciar el programa, iniciamos el servidor TCP. Para cada conexión establecida, crea una rutina de respuesta.
logging.basicConfig( level=logging.INFO, format="%(levelname)s %(asctime)s: %(funcName)s: %(message)s", ) loop = asyncio.get_event_loop() server = loop.run_until_complete(asyncio.start_server(responder, args.bind, args.port)) logging.info("Listening on: %s", server.sockets[0].getsockname()) loop.run_forever()
El respondedor es similar al iniciador y refleja las mismas acciones, pero un ciclo interminable de lectura de mensajes comienza de inmediato, por simplicidad. Ahora el protocolo de protocolo de enlace envía un mensaje desde cada lado, pero en el futuro, habrá dos del iniciador de la conexión, después de lo cual se pueden enviar mensajes de texto de inmediato.
72 async def responder(reader, writer): 73 _id = writer.get_extra_info("peername") 74 logging.info("%s: connected", _id) 75 buf = b"" 76 msg_expected = "handshake" 77 peer_name = None 78 while True: 79
Protocolo seguro
Ha llegado el momento de asegurar nuestra comunicación. ¿Qué queremos decir con seguridad y qué queremos?
- confidencialidad de los mensajes transmitidos;
- autenticidad e integridad de los mensajes transmitidos: su cambio debe ser detectado;
- protección contra ataques de repetición: se debe detectar el hecho de que los mensajes se han perdido o vuelto a intentar (y decidimos desconectarnos);
- identificación y autenticación de interlocutores mediante claves públicas controladas previamente: ya hemos decidido anteriormente que estamos creando una red de amigo a amigo. Solo después de la autenticación entenderemos con quién nos estamos comunicando;
- La presencia de propiedades perfectas de confidencialidad directa (PFS): el compromiso de nuestra clave de firma de larga duración no debe conducir a la posibilidad de leer toda la correspondencia anterior. Grabar el tráfico interceptado se vuelve inútil;
- validez / validez de mensajes (transporte y apretones de manos) solo dentro de la misma sesión TCP La inserción de mensajes correctamente firmados / autenticados de otra sesión (incluso con el mismo interlocutor) no debería ser posible;
- el observador pasivo no debería ver identificadores de usuario, claves públicas transmitidas de larga duración, ni hashes de ellos. Algún tipo de anonimato de un observador pasivo.
Sorprendentemente, casi todos quieren tener este mínimo en cualquier protocolo de protocolo de enlace, y muy pocos de los anteriores se realizan en última instancia para los protocolos locales. Así que ahora no inventaremos cosas nuevas. Definitivamente recomendaría usar el
marco Noise para construir protocolos, pero escojamos algo más simple.
Los más populares son dos protocolos:
- TLS es un protocolo complejo con una larga historia de errores, escuelas, vulnerabilidades, poca reflexión, complejidad y deficiencias (sin embargo, esto no se aplica mucho a TLS 1.3). Pero no lo consideramos por la complejidad.
- IPsec con IKE : no tiene problemas criptográficos graves, aunque tampoco son simples. Si lee acerca de IKEv1 e IKEv2, su fuente son los protocolos STS , ISO / IEC IS 9798-3 y SIGMA (SIGn-and-MAc), lo suficientemente simple como para implementarlos en una noche.
¿Cómo es bueno SIGMA, como último enlace en el desarrollo de protocolos STS / ISO? Satisface todos nuestros requisitos (incluyendo "ocultar" los identificadores de los interlocutores), no tiene problemas criptográficos conocidos. Es minimalista: eliminar al menos un elemento del mensaje del protocolo conducirá a su inseguridad.
Pasemos del protocolo de cosecha propia más simple a SIGMA. La operación más básica que nos interesa es la
coincidencia de teclas : una función a la salida de la cual ambos participantes recibirán el mismo valor que se puede usar como una clave simétrica. Sin entrar en detalles: cada una de las partes genera un par de claves efímeras (utilizadas solo dentro de la misma sesión) (claves públicas y privadas), intercambia claves públicas, llama a la función de correspondencia, a cuya entrada transmiten su clave privada y la clave pública del interlocutor.
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│ IdA, PubA │ ╔════════════════════╗
│────────────── >> │ rPrvA, PubA = DHgen () ║
│ │ ╚═══════════════════╝
│ IdB, PubB │ ╔════════════════════╗
│ <───────────────│ ║PrvB, PubB = DHgen () ║
│ │ ╚═══════════════════╝
────┐ ╔═══════════════════╗
│ ║Key = DH (PrvA, PubB) ║
<───┘ ╚═══════╤═══════════╝
│ │
│ │
Cualquiera puede intervenir en el medio y reemplazar las claves públicas por las suyas propias: en este protocolo no hay autenticación de los interlocutores. Agregue una firma con claves de larga duración.
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│IdA, PubA, sign (SignPrvA, (PubA)) │ ╔═══════════════════════╗
│ ─ ─ ─ ign ign ign ign ign ─ ign ign ign ign ign ign ign ign ign SignPrvA, SignPubA = load () ║
│ │ ║PrvA, PubA = DHgen () ║
│ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
│IdB, PubB, sign (SignPrvB, (PubB)) │ ╔═══════════════════════╗
│ <─────────────────────────────────│ ║SignPrvB, SignPubB = load () ║
│ │ ║PrvB, PubB = DHgen () ║
│ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
────┐ ╔═════════════════════│ │
│ ║verificar (SignPubB, ...) ║ │
<───┘ ║Key = DH (PrvA, PubB) ║ │
│ ╚═════════════════════╝ │
│ │
Dicha firma no funcionará, ya que no está vinculada a una sesión específica. Dichos mensajes también son adecuados para sesiones con otros participantes. Todo el contexto debe estar suscrito. Esto también obliga a agregar otro mensaje de A.
Además, es fundamental agregar su propio identificador como firma, ya que, de lo contrario, podemos reemplazar IdXXX y volver a firmar el mensaje con la clave de otro interlocutor conocido. Para evitar
ataques de reflexión , es necesario que los elementos bajo la firma estén en lugares claramente definidos en su significado: si A firma (PubA, PubB), entonces B debe firmar (PubB, PubA). Esto también indica la importancia de elegir la estructura y el formato de los datos serializados. Por ejemplo, los conjuntos en la codificación ASN.1 DER están ordenados: SET OF (PubA, PubB) será idéntico a SET OF (PubB, PubA).
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│ IdA, PubA │ ╔══════════════════════════
Ign ─ ─ ─ ─ ─ ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign ign SignPubA = load () ║
│ │ ║PrvA, PubA = DHgen () ║
│ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
│IdB, PubB, sign (SignPrvB, (IdB, PubA, PubB)) │ ╔═════════════════════╗
│ SignPrvB, │ SignPubB = load () ║
│ │ ║PrvB, PubB = DHgen () ║
│ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
│ sign (SignPrvA, (IdA, PubB, PubA)) │ ╔═══════════════════╗
│ ─ ─ ─ ─ ─> ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify SignPubB, ...) ║
│ │ ║Key = DH (PrvA, PubB) ║
│ │ ╚══ ═ ═ ═ ╝ ╝ ╝ ═ ╝ ╝ ╝
│ │
Sin embargo, todavía no hemos "probado" que hemos desarrollado la misma clave común para esta sesión. En principio, puede prescindir de este paso: la primera conexión de transporte no será válida, pero queremos que cuando se complete el apretón de manos, nos aseguremos de que todo esté realmente de acuerdo. Por el momento, tenemos en nuestras manos el protocolo ISO / IEC IS 9798-3.
Podríamos firmar la clave en sí. Esto es peligroso, ya que es posible que pueda haber fugas en el algoritmo de firma utilizado (dejar bits por firma, pero aún fugas). Puede firmar un hash de la clave generada, pero incluso una fuga de hash de la clave generada puede ser valiosa en un ataque de fuerza bruta en la función de generación. SIGMA utiliza una función MAC que autentica la identificación del remitente.
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│ IdA, PubA │ ╔══════════════════════════
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ │ ─ ─ │ ─ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ > │ ║SignPrvA, SignPubA = load () ║
│ │ ║PrvA, PubA = DHgen () ║
│ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
│IdB, PubB, sign (SignPrvB, (PubA, PubB)), MAC (IdB) │ ╔═════════════════╗
│ <──── ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ ─ │ ─ ─│ ║SignPrvB, SignPubB = load () ║
│ │ ║PrvB, PubB = DHgen () ║
│ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
│ │ ╔══ ═ ═ ═ ╗ ╗ ╗ ═ ╗ ╗ ╗
│ signo (SignPrvA, (PubB, PubA)), MAC (IdA) │ ║Key = DH (PrvA, PubB) ║
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ > │ ║verificar (Clave, IdB) ║
│ │ ║verificar (SignPubB, ...) ║
│ │ ╚══ ═ ═ ═ ╝ ╝ ╝ ═ ╝ ╝ ╝
│ │
Como optimización, algunos pueden querer reutilizar sus claves efímeras (que, por supuesto, es deplorable para PFS). Por ejemplo, generamos un par de claves, intentamos conectarnos, pero el TCP no estaba disponible o se interrumpió en algún punto en el medio del protocolo. Es una pena gastar los recursos de entropía y procesador gastados en un nuevo par. Por lo tanto, presentamos la llamada cookie, un valor pseudoaleatorio que protegerá contra posibles ataques de repetición accidentales al reutilizar claves públicas efímeras. Debido al enlace entre la cookie y la clave pública efímera, la clave pública de la parte opuesta puede eliminarse de la firma como innecesaria.
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│ IdA, PubA, CookieA │ ╔═════════════════════════
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ │ ─ ─ │ ─ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ──────────────────── >> │ │ SignPrvA, SignPubA = load () ║
│ │ ║PrvA, PubA = DHgen () ║
│ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
│IdB, PubB, CookieB, signo (SignPrvB, (CookieA, CookieB, PubB)), MAC (IdB) │ ╔ ═ ═ ═ ═ ╔ ═══╗
│ <──── ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │ ─ │ ─ ─ ─ ─ ─ │ ──────────────────────│ ║SignPrvB, SignPubB = load () ║
│ │ ║PrvB, PubB = DHgen () ║
│ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
│ │ ╔══ ═ ═ ═ ╗ ╗ ╗ ═ ╗ ╗ ╗
│ signo (SignPrvA, (CookieB, CookieA, PubA)), MAC (IdA) │ ║Key = DH (PrvA, PubB) ║
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ify ify ify ify ify ify ify ify ify ify ify ify Verificar (Clave, IdB) ║
│ │ ║verificar (SignPubB, ...) ║
│ │ ╚══ ═ ═ ═ ╝ ╝ ╝ ═ ╝ ╝ ╝
│ │
Finalmente, queremos obtener la privacidad de nuestros identificadores de interlocutor de un observador pasivo. Para hacer esto, SIGMA sugiere primero intercambiar claves efímeras, elaborando una clave común para autenticar y autenticar mensajes. SIGMA describe dos opciones:
- SIGMA-I: protege al iniciador de los ataques activos, al respondedor de los pasivos: el iniciador autentica al respondedor y si algo no encaja, no da su identificación. El acusado da su identificación si comienza un protocolo activo con él. El observador pasivo no sabrá nada;
SIGMA-R: protege al respondedor de los ataques activos, el iniciador de los pasivos. Todo es exactamente lo contrario, pero en este protocolo ya se transmiten cuatro mensajes de protocolo de enlace.
Elegimos SIGMA-I como más similar a lo que esperamos de las cosas habituales de servidor-cliente: solo un servidor autenticado reconoce al cliente, y todos conocen el servidor de todos modos. Además, es más fácil de implementar debido a la menor cantidad de mensajes de saludo. Todo lo que agregamos al protocolo es el cifrado de la parte del mensaje y la transferencia del identificador A a la parte cifrada del último mensaje:
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│ PubA, CookieA │ ╔══════════════════════════╗
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ign ign ign ign ign ign ign ign ign ign ign ign ,,,,,,,, Sign Sign Sign Sign Sign Sign carga carga carga carga carga carga carga carga (((((((
│ │ ║PrvA, PubA = DHgen () ║
│ │ ╚═══════════════════════════╝
│PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB))) │ ╔═══════════════════════════╗
│<─────────────────────────────────────────────────────────────────────────────│ ║SignPrvB, SignPubB = load()║
│ │ ║PrvB, PubB = DHgen() ║
│ │ ╚═══════════════════════════╝
│ │ ╔═════════════════════╗
│ Enc((IdA, sign(SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA))) │ ║Key = DH(PrvA, PubB) ║
│─────────────────────────────────────────────────────────────────────────────>│ ║verify(Key, IdB) ║
│ │ ║verify(SignPubB, ...)║
│ │ ╚═════════════════════╝
│ │
- 34.10-2012 256- .
- 34.10-2012 VKO.
- MAC CMAC. , 34.13-2015. — (34.12-2015).
- . -256 (34.11-2012 256 ).
. . : , , (MAC) , . , , . , , ? . , KDF (key derivation function). , - : HKDF , . , Python , hkdf . HKDF HMAC , , , -. Python Wikipedia . 34.10-2012, - -256. , :
kdf = Hkdf(None, key_session, hash=GOST34112012256) kdf.expand(b"handshake1-mac-identity") kdf.expand(b"handshake1-enc") kdf.expand(b"handshake1-mac") kdf.expand(b"handshake2-mac-identity") kdf.expand(b"handshake2-enc") kdf.expand(b"handshake2-mac") kdf.expand(b"transport-initiator-enc") kdf.expand(b"transport-initiator-mac") kdf.expand(b"transport-responder-enc") kdf.expand(b"transport-responder-mac")
/
ASN.1 :
class Msg(Choice): schema = (( ("text", MsgText()), ("handshake0", MsgHandshake0(expl=tag_ctxc(0))), ("handshake1", MsgHandshake1(expl=tag_ctxc(1))), ("handshake2", MsgHandshake2(expl=tag_ctxc(2))), )) class MsgText(Sequence): schema = (( ("payload", MsgTextPayload()), ("payloadMac", MAC()), )) class MsgTextPayload(Sequence): schema = (( ("nonce", Integer(bounds=(0, float("+inf")))), ("ciphertext", OctetString(bounds=(1, MaxTextLen))), )) class MsgHandshake0(Sequence): schema = (( ("cookieInitiator", Cookie()), ("pubKeyInitiator", PubKey()), )) class MsgHandshake1(Sequence): schema = (( ("cookieResponder", Cookie()), ("pubKeyResponder", PubKey()), ("ukm", OctetString(bounds=(8, 8))), ("ciphertext", OctetString()), ("ciphertextMac", MAC()), )) class MsgHandshake2(Sequence): schema = (( ("ciphertext", OctetString()), ("ciphertextMac", MAC()), )) class HandshakeTBE(Sequence): schema = (( ("identity", OctetString(bounds=(32, 32))), ("signature", OctetString(bounds=(64, 64))), ("identityMac", MAC()), )) class HandshakeTBS(Sequence): schema = (( ("cookieTheir", Cookie()), ("cookieOur", Cookie()), ("pubKeyOur", PubKey()), )) class Cookie(OctetString): bounds = (16, 16) class PubKey(OctetString): bounds = (64, 64) class MAC(OctetString): bounds = (16, 16)
HandshakeTBS — , (to be signed). HandshakeTBE — , (to be encrypted). ukm MsgHandshake1. 34.10 VKO, , UKM (user keying material) — .
, ( , , ).
, - . JSON :
{ "our": { "prv": "21254cf66c15e0226ef2669ceee46c87b575f37f9000272f408d0c9283355f98", "pub": "938c87da5c55b27b7f332d91b202dbef2540979d6ceaa4c35f1b5bfca6df47df0bdae0d3d82beac83cec3e353939489d9981b7eb7a3c58b71df2212d556312a1" }, "their": { "alice": "d361a59c25d2ca5a05d21f31168609deeec100570ac98f540416778c93b2c7402fd92640731a707ec67b5410a0feae5b78aeec93c4a455a17570a84f2bc21fce", "bob": "aade1207dd85ecd283272e7b69c078d5fae75b6e141f7649ad21962042d643512c28a2dbdc12c7ba40eb704af920919511180c18f4d17e07d7f5acd49787224a" } }
our — , . their — . JSON :
from pygost import gost3410 from pygost.gost34112012256 import GOST34112012256 CURVE = gost3410.GOST3410Curve( *gost3410.CURVE_PARAMS["GostR3410_2001_CryptoPro_A_ParamSet"] ) parser = argparse.ArgumentParser(description="GOSTIM") parser.add_argument( "--keys-gen", action="store_true", help="Generate JSON with our new keypair", ) parser.add_argument( "--keys", default="keys.json", required=False, help="JSON with our and their keys", ) parser.add_argument( "--bind", default="::1", help="Address to listen on", ) parser.add_argument( "--port", type=int, default=6666, help="Port to listen on", ) args = parser.parse_args() if args.keys_gen: prv_raw = urandom(32) pub = gost3410.public_key(CURVE, gost3410.prv_unmarshal(prv_raw)) pub_raw = gost3410.pub_marshal(pub) print(json.dumps({ "our": {"prv": hexenc(prv_raw), "pub": hexenc(pub_raw)}, "their": {}, })) exit(0)
34.10 — . 256- 256- . PyGOST , , (urandom(32)) , gost3410.prv_unmarshal(). , gost3410.public_key(). 34.10 — , , gost3410.pub_marshal().
JSON , , , , gost3410.pub_unmarshal(). , . -256 gost34112012256.GOST34112012256(), hashlib -.
? , : cookie (128- ), 34.10, VKO .
395 async def initiator(host, port): 396 _id = repr((host, port)) 397 logging.info("%s: dialing", _id) 398 reader, writer = await asyncio.open_connection(host, port) 399
423 logging.info("%s: got %s message", _id, msg.choice) 424 if msg.choice != "handshake1": 425 logging.warning("%s: unexpected message, disconnecting", _id) 426 writer.close() 427 return 428
UKM 64- (urandom(8)), , gost3410_vko.ukm_unmarshal(). VKO 34.10-2012 256- gost3410_vko.kek_34102012256() (KEK — key encryption key).
256- . HKDF . GOST34112012256 hashlib , Hkdf . ( Hkdf) , - . kdf.expand() 256-, .
TBE TBS :
- MAC ;
- ;
- TBE ;
- ;
- MAC ;
- TBS , cookie . .
441 try: 442 peer_name = validate_tbe( 443 msg_handshake1, 444 key_handshake1_mac_identity, 445 key_handshake1_enc, 446 key_handshake1_mac, 447 cookie_our, 448 cookie_their, 449 pub_their_raw, 450 ) 451 except ValueError as err: 452 logging.warning("%s: %s, disconnecting", _id, err) 453 writer.close() 454 return 455
, 34.13-2015 34.12-2015. , MAC-. PyGOST gost3413.mac(). ( ), , , . hardcode- ? 34.12-2015 128- , 64- — 28147-89, .
gost.3412.GOST3412Kuznechik(key) .encrypt()/.decrypt() , 34.13 . MAC : gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). MAC- (==) , , , , BEAST TLS. Python hmac.compare_digest .
. , , . 34.13-2015 : ECB, CTR, OFB, CBC, CFB. . , ( CCM, OCB, GCM ) — MAC. (CTR): , , , ( CBC, ).
.mac(), .ctr() : ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). , . ( ), . handshake .
gost3410.verify() : ( GOSTIM ), ( , , ), 34.11-2012 .
, handshake2 , , : , .…
456
, ( , , ), MAC-:
499
msg_sender , TCP-. nonce, . .
async def msg_sender(peer_name: str, key_enc: bytes, key_mac: bytes, writer) -> None: nonce = 0 encrypter = GOST3412Kuznechik(key_enc).encrypt macer = GOST3412Kuznechik(key_mac).encrypt in_queue = IN_QUEUES[peer_name] while True: text = await in_queue.get() if text is None: break ciphertext = ctr( encrypter, KUZNECHIK_BLOCKSIZE, text.encode("utf-8"), long2bytes(nonce, 8), ) payload = MsgTextPayload(( ("nonce", Integer(nonce)), ("ciphertext", OctetString(ciphertext)), )) mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) writer.write(Msg(("text", MsgText(( ("payload", payload), ("payloadMac", MAC(mac_tag)), )))).encode()) nonce += 1
msg_receiver, :
async def msg_receiver( msg_text: MsgText, nonce_expected: int, macer, encrypter, peer_name: str, ) -> None: payload = msg_text["payload"] if int(payload["nonce"]) != nonce_expected: raise ValueError("unexpected nonce value") mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])): raise ValueError("invalid MAC") plaintext = ctr( encrypter, KUZNECHIK_BLOCKSIZE, bytes(payload["ciphertext"]), long2bytes(nonce_expected, 8), ) text = plaintext.decode("utf-8") await OUT_QUEUES[peer_name].put(text)
Conclusión
GOSTIM ( , )! (-256 : 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). , GoGOST , PyDERASN , NNCP , GoVPN , GOSTIM , GPLv3+ .
, , , Python/Go-, « „“ .