GOSTIM: P2P F2F E2EE IM en une soirée avec la cryptographie GOST

En tant que développeur de la bibliothèque PyGOST (primitives cryptographiques GOST en Python pur), j'ai souvent des questions sur la façon d'implémenter la messagerie sécurisée la plus simple sur mon genou. Beaucoup considèrent la cryptographie appliquée comme une chose assez simple, et un appel .encrypt () à un chiffrement par bloc sera suffisant pour envoyer en toute sécurité sur un canal de communication. D'autres croient que la cryptographie appliquée est le destin de quelques-uns et il est acceptable que des sociétés riches telles que Telegram avec des olympiades mathématiques ne puissent pas mettre en œuvre un protocole sécurisé.

Tout cela m'a incité à écrire cet article pour montrer que la mise en œuvre de protocoles cryptographiques et de messagerie instantanée sécurisée n'est pas une tâche si difficile. Cependant, inventer vos propres protocoles d'authentification et d'accord de clé ne vaut pas la peine.

Entendre

L'article sera écrit poste à poste , ami à ami , messagerie instantanée chiffrée de bout en bout avec authentification SIGMA-I et protocole d'accord de clé (sur la base duquel IPsec IKE est implémenté) en utilisant exclusivement les algorithmes cryptographiques GOST, les bibliothèques PyGOST et Encodage ASN.1 des messages avec la bibliothèque PyDERASN (à propos de laquelle j'ai déjà écrit auparavant ). Prérequis: il doit être si simple qu'il puisse être écrit à partir de zéro en une seule soirée (ou journée de travail), sinon ce n'est plus un simple programme. Il a probablement des erreurs, des difficultés inutiles, des lacunes, et c'est mon premier programme utilisant la bibliothèque asyncio.

Design IM


Pour commencer, vous devez comprendre à quoi ressemblera notre messagerie instantanée. Par souci de simplicité, qu'il s'agisse d'un réseau peer-to-peer, sans aucune découverte de participants. Nous indiquerons personnellement à quelle adresse: le port à connecter pour communiquer avec l'interlocuteur.

Je comprends qu'à l'heure actuelle, l'hypothèse de la disponibilité d'une communication directe entre deux ordinateurs arbitraires est une limitation importante de l'applicabilité de la messagerie instantanée dans la pratique. Mais plus les développeurs implémenteront toutes sortes de béquilles de traversée NAT, plus nous resterons longtemps sur Internet IPv4, avec une probabilité déprimante de communication entre des ordinateurs arbitraires. Eh bien, combien pouvez-vous supporter le manque d'IPv6 à la maison et au travail?

Nous aurons un réseau ami à ami: tous les interlocuteurs possibles doivent être connus à l'avance. Tout d'abord, cela simplifie grandement tout: s'est présenté, a trouvé ou n'a pas trouvé le nom / la clé, s'est déconnecté ou continue de travailler, connaissant l'interlocuteur. Deuxièmement, dans le cas général, il est sûr et exclut de nombreuses attaques.

L'interface IM sera proche des solutions classiques des projets sans suceurs , que j'aime beaucoup pour leur minimalisme et leur philosophie Unix-way. Un programme de messagerie instantanée pour chaque interlocuteur crée un répertoire avec trois sockets de domaine Unix:

  • in - les messages envoyés à l'interlocuteur y sont enregistrés;
  • out - les messages reçus de l'interlocuteur y sont lus;
  • état - en lisant, nous verrons si l'interlocuteur est connecté maintenant, l'adresse / le port de connexion.

De plus, une socket conn est créée en écrivant sur quel port hôte, nous établissons une connexion avec un interlocuteur distant.

 | - alice
 |  | - dans
 |  | - sur
 |  `- indiquer
 | - bob
 |  | - dans
 |  | - sur
 |  `- indiquer
 `- conn

Cette approche vous permet de réaliser des implémentations indépendantes du transport IM et de l'interface utilisateur, car il n'y a pas d'ami pour le goût et la couleur, vous ne plairez pas à tout le monde. En utilisant tmux et / ou multitail , vous pouvez obtenir une interface multi-fenêtres avec mise en évidence de la syntaxe. Et avec rlwrap, vous pouvez obtenir une chaîne compatible GNU Readline pour entrer des messages.

En fait, les projets suckless utilisent des fichiers FIFO. Personnellement, je ne pouvais pas comprendre comment en asyncio travailler avec des fichiers de manière compétitive sans un substrat fait à la main à partir des threads sélectionnés (j'utilise le langage Go depuis de telles choses depuis longtemps). Par conséquent, j'ai décidé de me débrouiller avec les sockets de domaine Unix. Malheureusement, cela ne permet pas de faire l'écho 2001: 470: dead :: babe 6666> conn. J'ai résolu ce problème en utilisant socat : echo 2001: 470: dead :: babe 6666 | socat - UNIX-CONNECT: conn, socat READLINE UNIX-CONNECT: alice / in.

Protocole initial dangereux


TCP est utilisé comme moyen de transport: il garantit la livraison et sa commande. UDP ne garantit ni l'un ni l'autre (ce qui serait utile lorsque la cryptographie est appliquée), et la prise en charge SCTP en Python est prête à l'emploi.

Malheureusement, dans TCP, il n'y a pas de concept de message, mais seulement un flux d'octets. Par conséquent, il est nécessaire de trouver un format pour les messages afin qu'ils puissent être partagés entre eux dans ce flux. Nous pouvons accepter d'utiliser le caractère de saut de ligne. Pour commencer, il convient, cependant, lorsque nous commençons à crypter nos messages, ce symbole peut apparaître n'importe où dans le texte chiffré. Par conséquent, les protocoles sont populaires sur les réseaux, envoyant d'abord la longueur du message en octets. Par exemple, en Python, il existe xdrlib, qui vous permet de travailler avec un format XDR similaire.

Nous ne travaillerons pas correctement et efficacement avec la lecture TCP - nous simplifions le code. Nous lisons les données du socket dans une boucle sans fin jusqu'à ce que nous décodions le message complet. Vous pouvez également utiliser JSON avec XML comme format pour cette approche. Mais lorsque la cryptographie est ajoutée, les données devront être signées et authentifiées - et cela nécessitera une représentation identique octet par octet des objets, ce que JSON / XML ne fournit pas (les vidages peuvent varier).

XDR convient à une telle tâche, cependant, je choisis ASN.1 avec l'encodage DER et la bibliothèque PyDERASN , car nous aurons des objets de haut niveau à portée de main, qui sont souvent plus agréables et plus pratiques à utiliser. Contrairement au bencode sans schéma , MessagePack ou CBOR , ASN.1 validera automatiquement les données par rapport à un schéma codé en dur.

# 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))), )) 

Le message reçu sera Msg: soit un texte MsgText (avec un champ de texte jusqu'à présent), soit un message de prise de contact MsgHandshake (dans lequel le nom de l'interlocuteur est transmis). Maintenant, cela semble trop compliqué, mais c'est un défi pour l'avenir.

      ┌─────┐ ┌─────┐
      │PeerA│ │PeerB│
      └──┬──┘ └──┬──┘
         │MsgHandshake (IdA) │
         │───────────────── >> │
         │ │
         │MsgHandshake (IdB) │
         │ <─────────────────│
         │ │
         │ MsgText () │
         │───────────────── >> │
         │ │
         │ MsgText () │
         │ <─────────────────│
         │ │


IM sans cryptographie


Comme je l'ai dit, pour toutes les opérations avec sockets, la bibliothèque asyncio sera utilisée. Déclarez ce que nous attendons au lancement:

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

Définissez votre propre nom (--our-name alice). Une virgule répertorie tous les interlocuteurs attendus (- leurs noms bob, eve). Pour chacun des interlocuteurs, un répertoire avec les sockets Unix est créé, ainsi qu'une coroutine pour chaque état d'entrée, de sortie:

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

Les messages de la socket in de l'utilisateur sont envoyés à la file d'attente 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")) 

Les messages des interlocuteurs sont envoyés à la file d'attente OUT_QUEUES, à partir de laquelle les données sont écrites sur le socket de sortie:

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

Lors de la lecture de la socket d'état, le programme recherche dans le dictionnaire PEER_ALIVE l'adresse de l'interlocuteur. S'il n'y a pas encore de connexion avec l'interlocuteur, une ligne vide est écrite.

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

Lorsqu'une adresse est écrite dans le socket conn, la fonction «initiateur» de la connexion est lancée:

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

Considérez l'initiateur. Tout d'abord, il ouvre évidemment une connexion à l'hôte / port spécifié et envoie un message de prise de contact avec son nom:

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

Il attend ensuite une réponse du côté distant. Tente de décoder la réponse reçue selon le schéma Msg ASN.1. Nous supposons que le message entier sera envoyé par un segment TCP et nous le recevrons atomiquement lorsque .read () sera appelé. Nous vérifions que nous avons reçu exactement le message de prise de contact.

  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 # }}} 

Nous vérifions que le nom de la personne à qui nous parlons nous est connu. Sinon, coupez la connexion. Nous vérifions si nous avons déjà établi une connexion avec lui (l'interlocuteur a à nouveau donné l'ordre de nous connecter) et le fermons. Les chaînes Python avec le texte du message sont placées dans la file d'attente IN_QUEUES, mais il existe une valeur spéciale None, qui signale que msg_sender à la coroutine cesse de fonctionner afin qu'elle oublie son auteur, qui est connecté à une connexion TCP obsolète.

  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 accepte les messages sortants (mis en file d'attente à partir d'un socket in), les sérialise dans un message MsgText et les envoie via une connexion TCP. Il peut se rompre à tout moment - nous l'interceptons clairement.

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

À la fin, l'initiateur entre dans un cycle sans fin de lecture de messages depuis le socket. Vérifie s'il s'agit d'un message texte et place dans les OUT_QUEUES la file d'attente à partir de laquelle ils seront envoyés au socket de sortie de l'interlocuteur correspondant. Pourquoi ne pouvez-vous pas simplement faire .read () et décoder le message? Parce qu'il est possible que plusieurs messages de l'utilisateur soient agrégés dans la mémoire tampon du système d'exploitation et envoyés par un segment TCP. Nous pouvons décoder le premier, puis une partie du suivant peut rester dans le tampon. En cas d'urgence, nous fermons la connexion TCP et arrêtons la coroutine msg_sender (en envoyant None à la file d'attente 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) 

Revenons au code principal. Après avoir créé toutes les coroutines, au moment du démarrage du programme, nous démarrons le serveur TCP. Pour chaque connexion établie, il crée une coroutine répondeur.

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

Le répondeur est similaire à l'initiateur et reflète toutes les mêmes actions, mais une boucle sans fin de messages de lecture démarre immédiatement, pour plus de simplicité. Maintenant, le protocole de prise de contact envoie un message de chaque côté, mais à l'avenir, il y en aura deux de l'initiateur de la connexion, après quoi les messages texte pourront être immédiatement envoyés.

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

Protocole sécurisé


Le moment est venu de sécuriser notre communication. Qu'entendons-nous par sécurité et ce que nous voulons:

  • confidentialité des messages transmis;
  • authenticité et intégrité des messages transmis - leur modification doit être détectée;
  • protection contre les attaques par rejeu - le fait que les messages aient été perdus ou réessayés doit être détecté (et nous décidons de nous déconnecter);
  • identification et authentification des interlocuteurs par des clés publiques pré-pilotées - nous avons déjà décidé plus tôt de créer un réseau ami-à-ami. Ce n'est qu'après l'authentification que nous comprendrons avec qui nous communiquons;
  • la présence de parfaites propriétés de secret avancé (PFS) - le compromis de notre clé de signature à longue durée de vie ne doit pas conduire à la possibilité de lire toute la correspondance précédente. L'enregistrement du trafic intercepté devient inutile;
  • validité / validité des messages (transport et poignée de main) uniquement dans la même session TCP. L'insertion de messages correctement signés / authentifiés d'une autre session (même avec le même interlocuteur) ne devrait pas être possible;
  • l'observateur passif ne doit pas voir les identifiants des utilisateurs, les clés publiques à longue durée de vie transmises, ni leurs hachages. Une sorte d'anonymat d'un observateur passif.

Étonnamment, presque tout le monde veut avoir ce minimum dans n'importe quel protocole de prise de contact, et très peu de ce qui précède sont finalement effectués pour des protocoles locaux. Alors maintenant, nous n'inventerons pas de nouvelles choses. Je recommanderais certainement d'utiliser le framework Noise pour construire des protocoles, mais choisissons quelque chose de plus simple.

Les plus populaires sont deux protocoles:

  • TLS est un protocole complexe avec une longue histoire de bogues, d'écoles, de vulnérabilités, de mauvaise réflexion, de complexité et de lacunes (cependant, cela ne s'applique pas beaucoup à TLS 1.3). Mais nous ne le considérons pas en raison de la complexité.
  • IPsec avec IKE - n'ont pas de problèmes cryptographiques graves, bien qu'ils ne soient pas non plus simples. Si vous lisez les protocoles IKEv1 et IKEv2, leur source est les protocoles STS , ISO / IEC IS 9798-3 et SIGMA (SIGn-et-MAc) - assez simple à mettre en œuvre en une soirée.

En quoi SIGMA, dernier maillon du développement des protocoles STS / ISO, est-il bon? Il répond à toutes nos exigences (y compris "masquer" les identifiants des interlocuteurs), n'a pas de problèmes cryptographiques connus. C'est minimaliste - la suppression d'au moins un élément du message de protocole entraînera son insécurité.

Passons du protocole local le plus simple à SIGMA. L'opération la plus fondamentale qui nous intéresse est la correspondance des clés : une fonction à la sortie de laquelle les deux participants recevront la même valeur qui peut être utilisée comme clé symétrique. Sans entrer dans les détails: chacune des parties génère une paire de clés éphémères (utilisées uniquement dans la même session) (clés publiques et privées), échange des clés publiques, appelle la fonction de réconciliation, à l'entrée de laquelle elles transmettent leur clé privée et la clé publique de l'interlocuteur.

 ┌─────┐ ┌─────┐
 │PeerA│ │PeerB│
 └──┬──┘ └──┬──┘
    │ IdA, PubA │ ╔════════════════════╗
    │────────────── >> >> rPrvA, PubA = DHgen () ║
    │ │ ╚═══════════════════╝
    │ IdB, PubB │ ╔════════════════════╗
    │ <───────────────│ ║PrvB, PubB = DHgen () ║
    │ │ ╚═══════════════════╝
    ────┐ ╔═══════════════════╗
        │ ║ Clé = DH (PrvA, PubB) ║
    <───┘ ╚═══════╤═══════════╝
    │ │
    │ │


N'importe qui peut intervenir au milieu et remplacer les clés publiques par les leurs - dans ce protocole il n'y a pas d'authentification des interlocuteurs. Ajoutez une signature avec des clés longue durée.

 ┌─────┐ ┌─────┐
 │PeerA│ │PeerB│
 └──┬──┘ └──┬──┘
    │IdA, PubA, signe (SignPrvA, (PubA)) │ ╔═══════════════════════╗
    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, signe (SignPrvB, (PubB)) │ ╔═══════════════════════╗
    │ <─────────────────────────────────│ ║SignPrvB, SignPubB = load () ║
    │ │ ║PrvB, PubB = DHgen () ║
    │ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
    ────┐ ╔═════════════════════│ │
        │ ║verify (SignPubB, ...) ║ │
    <───┘ ║Key = DH (PrvA, PubB) ║ │
    │ ╚═════════════════════╝ │
    │ │


Une telle signature ne fonctionnera pas, car elle n'est pas liée à une session spécifique. Ces messages conviennent également aux sessions avec d'autres participants. Le contexte entier doit être souscrit. Cela force également l'ajout d'un autre message de A.

De plus, il est essentiel d'ajouter votre propre identifiant comme signature, car sinon, nous pouvons remplacer IdXXX et re-signer le message avec la clé d'un autre interlocuteur bien connu. Pour éviter les attaques par réflexion , il est nécessaire que les éléments sous la signature soient dans des endroits clairement définis dans leur sens: si A signe (PubA, PubB), alors B doit signer (PubB, PubA). Cela indique également l'importance de choisir la structure et le format des données sérialisées. Par exemple, les ensembles dans le codage ASN.1 DER sont triés: SET OF (PubA, PubB) sera identique à 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 SignPubA = charge () ║
    │ │ ║PrvA, PubA = DHgen () ║
    │ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
    │IdB, PubB, signe (SignPrvB, (IdB, PubA, PubB)) │ ╔════════════════════╗
    │ <───────────────────────────────────────────── ║SignPrvB, SignPubB = charge () ║
    │ │ ║PrvB, PubB = DHgen () ║
    │ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
    │ signe (SignPrvA, (IdA, PubB, PubA)) │ ╔═══════════════════╗
    │ ─ ─ ─ ─ ─> ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify ify SignPubB, ...) ║
    │ │ ║ Clé = DH (PrvA, PubB) ║
    │ │ ╚══ ═ ═ ═ ╝ ╝ ╝ ═ ╝ ╝ ╝
    │ │


Cependant, nous n'avons toujours pas «prouvé» que nous avons développé la même clé commune pour cette session. En principe, vous pouvez vous passer de cette étape - la première connexion de transport sera invalide, mais nous voulons que lorsque la prise de contact soit terminée, nous serions sûrs que tout est vraiment convenu. Pour le moment, nous avons entre nos mains le protocole ISO / IEC IS 9798-3.

Nous pourrions signer la clé elle-même. C'est dangereux, car il est possible qu'il y ait des fuites dans l'algorithme de signature utilisé (laissez bits par signature, mais fuit toujours). Vous pouvez signer un hachage à partir de la clé générée, mais même une fuite de hachage à partir de la clé générée peut être utile dans une attaque par force brute contre la fonction de génération. SIGMA utilise une fonction MAC qui authentifie l'ID de l'expéditeur.

 ┌─────┐ ┌─────┐
 │PeerA│ │PeerB│
 └──┬──┘ └──┬──┘
    │ IdA, PubA │ ╔══════════════════════════
    │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ │ ─ ─ │ ─ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ > │ ║SignPrvA, SignPubA = load () ║
    │ │ ║PrvA, PubA = DHgen () ║
    │ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
    │IdB, PubB, signe (SignPrvB, (PubA, PubB)), MAC (IdB) │ ╔═════════════════╗
    │ <─── ─ ─ ─ ─ ─ │ │ │ │ │ │ ─│ ║SignPrvB, SignPubB = load () ║
    │ │ ║PrvB, PubB = DHgen () ║
    │ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
    │ │ ╔══ ═ ═ ═ ╗ ╗ ╗ ═ ╗ ╗ ╗
    │ signe (SignPrvA, (PubB, PubA)), MAC (IdA) │ ║Key = DH (PrvA, PubB) ║
    │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ │ ─ ─ │ ─ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ > │ ║verify (Clé, IdB) ║
    │ │ ║verify (SignPubB, ...) ║
    │ │ ╚══ ═ ═ ═ ╝ ╝ ╝ ═ ╝ ╝ ╝
    │ │


À titre d'optimisation, certains voudront peut-être réutiliser leurs clés éphémères (ce qui, bien sûr, est déplorable pour PFS). Par exemple, nous avons généré une paire de clés, essayé de nous connecter, mais TCP n'était pas disponible ou s'est interrompu quelque part au milieu du protocole. Il est dommage de dépenser les ressources d'entropie et de processeur dépensées sur une nouvelle paire. Par conséquent, nous introduisons le soi-disant cookie - une valeur pseudo-aléatoire qui protégera contre d'éventuelles attaques de relecture accidentelles lors de la réutilisation de clés publiques éphémères. En raison de la liaison entre le cookie et la clé publique éphémère, la clé publique de la partie adverse peut être supprimée de la signature comme inutile.

 ┌─────┐ ┌─────┐
 │PeerA│ │PeerB│
 └──┬──┘ └──┬──┘
    │ IdA, PubA, CookieA │ ╔═════════════════════════
    │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ │ ─ ─ │ ─ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ──────────────────── >> >> │ Pr SignPrvA, SignPubA = load () ║
    │ │ ║PrvA, PubA = DHgen () ║
    │ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
    │IdB, PubB, CookieB, signe (SignPrvB, (CookieA, CookieB, PubB)), MAC (IdB) │ ╔═══════════════════════ ═══╗
    │ <─── ─ ─ ─ ─ ─ │ │ │ │ │ │ ──────────────────────│ ignSignPrvB, SignPubB = load () ║
    │ │ ║PrvB, PubB = DHgen () ║
    │ │ ╚══ ═ ═ ═ ═ ╝ ╝ ═ ╝ ╝ ╝ ╝ ╝
    │ │ ╔══ ═ ═ ═ ╗ ╗ ╗ ═ ╗ ╗ ╗
    │ signe (SignPrvA, (CookieB, CookieA, PubA)), MAC (IdA) │ ║Key = DH (PrvA, PubB) ║
    │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ │ ─ ─ │ ─ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ify ify ify ify ify ify ify ify ify ify ify ify ify Vérifier (Clé, IdB) ║
    │ │ ║verify (SignPubB, ...) ║
    │ │ ╚══ ═ ═ ═ ╝ ╝ ╝ ═ ╝ ╝ ╝
    │ │


Enfin, nous voulons obtenir la confidentialité de nos identifiants d'interlocuteur auprès d'un observateur passif. Pour ce faire, SIGMA suggère d'abord d'échanger des clés éphémères, en élaborant une clé commune sur laquelle authentifier les messages d'authentification. SIGMA décrit deux options:

  • SIGMA-I - protège l'initiateur des attaques actives, le répondeur des attaques passives: l'initiateur authentifie le répondeur et si quelque chose ne va pas, il ne donne pas son identification. Le défendeur donne son identité si vous commencez un protocole actif avec lui. L'observateur passif ne saura rien;
    SIGMA-R - protège le répondeur des attaques actives, l'initiateur du passif. Tout est exactement le contraire, mais dans ce protocole, quatre messages de prise de contact sont déjà transmis.


    Nous choisissons SIGMA-I comme plus similaire à ce que nous attendons des choses serveur-client habituelles: seul un serveur authentifié reconnaît le client, et tout le monde connaît le serveur de toute façon. De plus, il est plus facile à mettre en œuvre en raison de moins de messages de négociation. Tout ce que nous ajoutons au protocole est le chiffrement de la partie message et le transfert de l'identifiant A vers la partie chiffrée du dernier message:

     ┌─────┐ ┌─────┐
     │PeerA│ │PeerB│
     └──┬──┘ └──┬──┘
        │ PubA, CookieA │ ╔══════════════════════════╗
        │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ─ │ ─ ─ │ ─ │ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ign ign ign ign ign ign ign ign ign ign ign ign ║ ign ,,,,,, signe signe signe signe signe signe signe charge charge charge charge charge charge charge (((((((((((
        │ │ ║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) 

    Conclusion


    GOSTIM ( , )! (-256 : 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). , GoGOST , PyDERASN , NNCP , GoVPN , GOSTIM , GPLv3+ .

    , , , Python/Go-, « „“ .

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


All Articles