GOSTIM: P2P F2F E2EE IM en una noche con criptografía GOST

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.

Audiencia

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.

# Msg ::= CHOICE { # text MsgText, # handshake [0] EXPLICIT MsgHandshake } class Msg(Choice): schema = (( ("text", MsgText()), ("handshake", MsgHandshake(expl=tag_ctxc(0))), )) # MsgText ::= SEQUENCE { # text UTF8String (SIZE(1..MaxTextLen))} class MsgText(Sequence): schema = (( ("text", UTF8String(bounds=(1, MaxTextLen))), )) # MsgHandshake ::= SEQUENCE { # peerName UTF8String (SIZE(1..256)) } class MsgHandshake(Sequence): schema = (( ("peerName", UTF8String(bounds=(1, 256))), )) 

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 # Handshake message {{{ 135 writer.write(Msg(("handshake", MsgHandshake(( 136 ("peerName", OUR_NAME), 137 )))).encode()) 138 # }}} 139 await writer.drain() 

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 # Wait for Handshake message {{{ 142 data = await reader.read(256) 143 if data == b"": 144 logging.warning("%s: no answer, disconnecting", _id) 145 writer.close() 146 return 147 try: 148 msg, _ = Msg().decode(data) 149 except ASN1Error: 150 logging.warning("%s: undecodable answer, disconnecting", _id) 151 writer.close() 152 return 153 logging.info("%s: got %s message", _id, msg.choice) 154 if msg.choice != "handshake": 155 logging.warning("%s: unexpected message, disconnecting", _id) 156 writer.close() 157 return 158 # }}} 

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 # Run text message sender, initialize transport decoder {{{ 167 peer_alive = PEER_ALIVES.pop(peer_name, None) 168 if peer_alive is not None: 169 peer_alive.close() 170 await IN_QUEUES[peer_name].put(None) 171 PEER_ALIVES[peer_name] = writer 172 asyncio.ensure_future(msg_sender(peer_name, writer)) 173 # }}} 

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 # Wait for test messages {{{ 176 while True: 177 data = await reader.read(MaxMsgLen) 178 if data == b"": 179 break 180 buf += data 181 if len(buf) > MaxMsgLen: 182 logging.warning("%s: max buffer size exceeded", _id) 183 break 184 try: 185 msg, tail = Msg().decode(buf) 186 except ASN1Error: 187 continue 188 buf = tail 189 if msg.choice != "text": 190 logging.warning("%s: unexpected %s message", _id, msg.choice) 191 break 192 try: 193 await msg_receiver(msg.value, peer_name) 194 except ValueError as err: 195 logging.warning("%s: %s", err) 196 break 197 # }}} 198 logging.info("%s: disconnecting: %s", _id, peer_name) 199 IN_QUEUES[peer_name].put(None) 200 writer.close() 66 async def msg_receiver(msg_text: MsgText, peer_name: str) -> None: 67 text = str(msg_text["text"]) 68 logging.info("%s: received %d characters message", peer_name, len(text)) 69 await OUT_QUEUES[peer_name].put(text) 

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 # Read until we get Msg message {{{ 80 data = await reader.read(MaxMsgLen) 81 if data == b"": 82 logging.info("%s: closed connection", _id) 83 break 84 buf += data 85 if len(buf) > MaxMsgLen: 86 logging.warning("%s: max buffer size exceeded", _id) 87 break 88 try: 89 msg, tail = Msg().decode(buf) 90 except ASN1Error: 91 continue 92 buf = tail 93 # }}} 94 if msg.choice != msg_expected: 95 logging.warning("%s: unexpected %s message", _id, msg.choice) 96 break 97 if msg_expected == "text": 98 try: 99 await msg_receiver(msg.value, peer_name) 100 except ValueError as err: 101 logging.warning("%s: %s", err) 102 break 103 # Process Handshake message {{{ 104 elif msg_expected == "handshake": 105 logging.info("%s: got %s message", _id, msg_expected) 106 msg_handshake = msg.value 107 peer_name = str(msg_handshake["peerName"]) 108 if peer_name not in THEIR_NAMES: 109 logging.warning("unknown peer name: %s", peer_name) 110 break 111 writer.write(Msg(("handshake", MsgHandshake(( 112 ("peerName", OUR_NAME), 113 )))).encode()) 114 await writer.drain() 115 logging.info("%s: session established: %s", _id, peer_name) 116 peer_alive = PEER_ALIVES.pop(peer_name, None) 117 if peer_alive is not None: 118 peer_alive.close() 119 await IN_QUEUES[peer_name].put(None) 120 PEER_ALIVES[peer_name] = writer 121 asyncio.ensure_future(msg_sender(peer_name, writer)) 122 msg_expected = "text" 123 # }}} 124 logging.info("%s: disconnecting", _id) 125 if msg_expected == "text": 126 IN_QUEUES[peer_name].put(None) 127 writer.close() 

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) # Parse and unmarshal our and their keys {{{ with open(args.keys, "rb") as fd: _keys = json.loads(fd.read().decode("utf-8")) KEY_OUR_SIGN_PRV = gost3410.prv_unmarshal(hexdec(_keys["our"]["prv"])) _pub = hexdec(_keys["our"]["pub"]) KEY_OUR_SIGN_PUB = gost3410.pub_unmarshal(_pub) KEY_OUR_SIGN_PUB_HASH = OctetString(GOST34112012256(_pub).digest()) for peer_name, pub_raw in _keys["their"].items(): _pub = hexdec(pub_raw) KEYS[GOST34112012256(_pub).digest()] = { "name": peer_name, "pub": gost3410.pub_unmarshal(_pub), } # }}} 

    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 # Generate our ephemeral public key and cookie, send Handshake 0 message {{{ 400 cookie_our = Cookie(urandom(16)) 401 prv = gost3410.prv_unmarshal(urandom(32)) 402 pub_our = gost3410.public_key(CURVE, prv) 403 pub_our_raw = PubKey(gost3410.pub_marshal(pub_our)) 404 writer.write(Msg(("handshake0", MsgHandshake0(( 405 ("cookieInitiator", cookie_our), 406 ("pubKeyInitiator", pub_our_raw), 407 )))).encode()) 408 # }}} 409 await writer.drain() 

    • Msg ;
    • handshake1;
    • ;
    • TBE .

      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 # }}} 429 msg_handshake1 = msg.value 430 # Validate Handshake message {{{ 431 cookie_their = msg_handshake1["cookieResponder"] 432 pub_their_raw = msg_handshake1["pubKeyResponder"] 433 pub_their = gost3410.pub_unmarshal(bytes(pub_their_raw)) 434 ukm_raw = bytes(msg_handshake1["ukm"]) 435 ukm = ukm_unmarshal(ukm_raw) 436 key_session = kek_34102012256(CURVE, prv, pub_their, ukm, mode=2001) 437 kdf = Hkdf(None, key_session, hash=GOST34112012256) 438 key_handshake1_mac_identity = kdf.expand(b"handshake1-mac-identity") 439 key_handshake1_enc = kdf.expand(b"handshake1-enc") 440 key_handshake1_mac = kdf.expand(b"handshake1-mac") 

    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 # }}} 128 def validate_tbe( 129 msg_handshake: Union[MsgHandshake1, MsgHandshake2], 130 key_mac_identity: bytes, 131 key_enc: bytes, 132 key_mac: bytes, 133 cookie_their: Cookie, 134 cookie_our: Cookie, 135 pub_key_our: PubKey, 136 ) -> str: 137 ciphertext = bytes(msg_handshake["ciphertext"]) 138 mac_tag = mac(GOST3412Kuznechik(key_mac).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext) 139 if not compare_digest(mac_tag, bytes(msg_handshake["ciphertextMac"])): 140 raise ValueError("invalid MAC") 141 plaintext = ctr( 142 GOST3412Kuznechik(key_enc).encrypt, 143 KUZNECHIK_BLOCKSIZE, 144 ciphertext, 145 8 * b"\x00", 146 ) 147 try: 148 tbe, _ = HandshakeTBE().decode(plaintext) 149 except ASN1Error: 150 raise ValueError("can not decode TBE") 151 key_sign_pub_hash = bytes(tbe["identity"]) 152 peer = KEYS.get(key_sign_pub_hash) 153 if peer is None: 154 raise ValueError("unknown identity") 155 mac_tag = mac( 156 GOST3412Kuznechik(key_mac_identity).encrypt, 157 KUZNECHIK_BLOCKSIZE, 158 key_sign_pub_hash, 159 ) 160 if not compare_digest(mac_tag, bytes(tbe["identityMac"])): 161 raise ValueError("invalid identity MAC") 162 tbs = HandshakeTBS(( 163 ("cookieTheir", cookie_their), 164 ("cookieOur", cookie_our), 165 ("pubKeyOur", pub_key_our), 166 )) 167 if not gost3410.verify( 168 CURVE, 169 peer["pub"], 170 GOST34112012256(tbs.encode()).digest(), 171 bytes(tbe["signature"]), 172 ): 173 raise ValueError("invalid signature") 174 return peer["name"] 

    , 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 # Prepare and send Handshake 2 message {{{ 457 tbs = HandshakeTBS(( 458 ("cookieTheir", cookie_their), 459 ("cookieOur", cookie_our), 460 ("pubKeyOur", pub_our_raw), 461 )) 462 signature = gost3410.sign( 463 CURVE, 464 KEY_OUR_SIGN_PRV, 465 GOST34112012256(tbs.encode()).digest(), 466 ) 467 key_handshake2_mac_identity = kdf.expand(b"handshake2-mac-identity") 468 mac_tag = mac( 469 GOST3412Kuznechik(key_handshake2_mac_identity).encrypt, 470 KUZNECHIK_BLOCKSIZE, 471 bytes(KEY_OUR_SIGN_PUB_HASH), 472 ) 473 tbe = HandshakeTBE(( 474 ("identity", KEY_OUR_SIGN_PUB_HASH), 475 ("signature", OctetString(signature)), 476 ("identityMac", MAC(mac_tag)), 477 )) 478 tbe_raw = tbe.encode() 479 key_handshake2_enc = kdf.expand(b"handshake2-enc") 480 key_handshake2_mac = kdf.expand(b"handshake2-mac") 481 ciphertext = ctr( 482 GOST3412Kuznechik(key_handshake2_enc).encrypt, 483 KUZNECHIK_BLOCKSIZE, 484 tbe_raw, 485 8 * b"\x00", 486 ) 487 mac_tag = mac( 488 GOST3412Kuznechik(key_handshake2_mac).encrypt, 489 KUZNECHIK_BLOCKSIZE, 490 ciphertext, 491 ) 492 writer.write(Msg(("handshake2", MsgHandshake2(( 493 ("ciphertext", OctetString(ciphertext)), 494 ("ciphertextMac", MAC(mac_tag)), 495 )))).encode()) 496 # }}} 497 await writer.drain() 498 logging.info("%s: session established: %s", _id, peer_name) 

    , ( , , ), MAC-:

      499 # Run text message sender, initialize transport decoder {{{ 500 key_initiator_enc = kdf.expand(b"transport-initiator-enc") 501 key_initiator_mac = kdf.expand(b"transport-initiator-mac") 502 key_responder_enc = kdf.expand(b"transport-responder-enc") 503 key_responder_mac = kdf.expand(b"transport-responder-mac") ... 509 asyncio.ensure_future(msg_sender( 510 peer_name, 511 key_initiator_enc, 512 key_initiator_mac, 513 writer, 514 )) 515 encrypter = GOST3412Kuznechik(key_responder_enc).encrypt 516 macer = GOST3412Kuznechik(key_responder_mac).encrypt 517 # }}} 519 nonce_expected = 0 520 # Wait for test messages {{{ 521 while True: 522 data = await reader.read(MaxMsgLen) ... 530 msg, tail = Msg().decode(buf) ... 537 try: 538 await msg_receiver( 539 msg.value, 540 nonce_expected, 541 macer, 542 encrypter, 543 peer_name, 544 ) 545 except ValueError as err: 546 logging.warning("%s: %s", err) 547 break 548 nonce_expected += 1 549 # }}} 

    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-, « „“ .

Source: https://habr.com/ru/post/452200/


All Articles