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.
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.
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
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
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
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
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
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)
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)
Conclusion
GOSTIM ( , )! (-256 : 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). , GoGOST , PyDERASN , NNCP , GoVPN , GOSTIM , GPLv3+ .
, , , Python/Go-, « „“ .