En tant que dĂ©veloppeur de la bibliothĂšque (primitives cryptographiques conformes aux normes GOST en Python pur), je reçois souvent des questions sur la façon de mettre en Ćuvre un Ă©change de messages sĂ©curisĂ© de maniĂšre rudimentaire. Beaucoup pensent que la cryptographie appliquĂ©e est une chose assez simple, et qu'un appel Ă .encrypt() d'un chiffre par bloc suffit pour un envoi sĂ©curisĂ© par un canal de communication. D'autres estiment que la cryptographie appliquĂ©e n'est la prĂ©rogative que de quelques-uns, et qu'il est acceptable que des entreprises riches comme Telegram, avec des mathĂ©maticiens olympiques, un protocole sĂ©curisĂ©.
Tout cela m'a poussĂ© Ă Ă©crire cet article pour montrer que la mise en Ćuvre de protocoles cryptographiques et d'un IM sĂ©curisĂ© n'est pas une tĂąche si complexe. Cependant, il n'est pas recommandĂ© d'inventer ses propres protocoles d'authentification et d'Ă©change de clĂ©s.

L'article décrira , , le protocole SIGMA-I IPsec IKE PyDERASN mentionné précédemment Design de l'IM
Pour commencer, il faut comprendre à quoi ressemblera notre IM. Pour simplifier, supposons qu'il s'agisse d'un réseau peer-to-peer, sans aucune détection des participants. Nous indiquerons manuellement à quel adresse : port nous connecter pour discuter avec notre correspondant.
Je comprends qu'Ă l'heure actuelle, l'hypothĂšse de la disponibilitĂ© d'une connexion directe entre deux ordinateurs arbitraires est une limitation majeure de l'applicabilitĂ© de l'IM en pratique. Mais plus de dĂ©veloppeurs mettront en Ćuvre divers dispositifs de contournement de NAT, plus nous resterons longtemps sur Internet en IPv4, avec une probabilitĂ© dĂ©primante de connexion entre des ordinateurs arbitraires. Combien de temps encore pourrons-nous supporter l'absence d'IPv6 Ă la maison et au travail ?
Je comprends qu'Ă l'heure actuelle, supposer qu'une connexion directe soit disponible entre deux ordinateurs arbitraires constitue une limitation significative de l'applicabilitĂ© de l'IM dans la pratique. Cependant, plus il y aura de dĂ©veloppeurs mettant en Ćuvre divers bricolages pour le NAT traversal, plus nous resterons longtemps dans un Internet IPv4, avec une probabilitĂ© dĂ©courageante de connexion entre des ordinateurs quelconques. Jusqu'Ă quand devrons-nous supporter l'absence d'IPv6 chez nous et au travail ?
Nous aurons un rĂ©seau friend-to-friend : tous les interlocuteurs possibles doivent ĂȘtre connus Ă l'avance. Tout d'abord, cela simplifie Ă©normĂ©ment les choses : vous vous ĂȘtes prĂ©sentĂ©s, vous avez trouvĂ© ou non le nom/clĂ©s, vous vous ĂȘtes dĂ©connectĂ©s ou nous continuons Ă travailler, sachant qui est notre interlocuteur. DeuxiĂšmement, en gĂ©nĂ©ral, c'est sĂ©curisĂ© et cela exclut de nombreuses attaques.
L'interface IM sera proche des solutions classiques. , qui me plaisent beaucoup pour leur minimalisme et leur philosophie Unix-way. Le programme IM crée un répertoire pour chaque interlocuteur avec trois sockets de domaine Unix :
- in â dans lequel sont enregistrĂ©s les messages envoyĂ©s Ă l'interlocuteur ;
- out â d'oĂč sont lus les messages reçus de l'interlocuteur ;
- state â en le lisant, nous savons si l'interlocuteur est actuellement connectĂ©, adresse/port de connexion.
De plus, un socket conn est créé, en y enregistrant l'hÎte et le port, nous initions la connexion à l'interlocuteur distant.
|-- alice
| |-- in
| |-- out
| `-- state
|-- bob
| |-- in
| |-- out
| `-- state
`- conn
Cette approche permet de rĂ©aliser des implĂ©mentations indĂ©pendantes du transport IM et de l'interface utilisateur, car les goĂ»ts et les couleurs ne se discutent pas, il est impossible de plaire Ă tout le monde. En utilisant et/ou , vous pouvez obtenir une interface multi-fenĂȘtres avec coloration syntaxique. Et grĂące Ă , vous pouvez obtenir une ligne de saisie de messages compatible avec GNU Readline.
En rĂ©alitĂ©, les projets suckless utilisent des fichiers FIFO. Personnellement, je n'ai pas pu comprendre comment travailler avec des fichiers de maniĂšre concurrente avec asyncio sans fournir ma propre couche de threads dĂ©diĂ©s (pour ce genre de choses, j'utilise depuis longtemps le langage ). C'est pourquoi j'ai dĂ©cidĂ© de m'en tenir aux sockets de domaine Unix. Malheureusement, cela empĂȘche de faire echo 2001:470:dead::babe 6666 > conn. J'ai rĂ©solu ce problĂšme en utilisant : echo 2001:470:dead::babe 6666 | socat â UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.
Le protocole initial non sécurisé
Utilise TCP comme transport : il garantit la livraison et son ordre. UDP ne garantit ni l'un ni l'autre (ce qui serait utile lors de l'application de la cryptographie), et le support n'est pas disponible en Python par défaut.
Malheureusement, il n'y a pas de notion de message dans TCP, seulement un flux de bytes. Il est donc nĂ©cessaire de concevoir un format pour les messages afin de pouvoir les sĂ©parer dans ce flux. Nous pouvons convenir d'utiliser le caractĂšre de retour Ă la ligne. Cela conviendra pour commencer, cependant, lorsque nous commencerons Ă chiffrer nos messages, ce caractĂšre pourrait apparaĂźtre n'importe oĂč dans le texte chiffrĂ©. C'est pourquoi, dans les rĂ©seaux, les protocoles qui envoient d'abord la longueur du message en bytes sont populaires. Par exemple, en Python, il existe la bibliothĂšque xdrlib qui permet de travailler avec un tel format. .
Nous ne travaillerons pas correctement et efficacement en lisant TCP â simplifions le code. Nous lisons en boucle sans fin les donnĂ©es du socket jusqu'Ă ce que nous ayons dĂ©codĂ© le message complet. Comme format pour cette approche, nous pouvons utiliser JSON ou XML. Cependant, lorsque la cryptographie sera ajoutĂ©e, les donnĂ©es devront ĂȘtre signĂ©es et authentifiĂ©es â et cela nĂ©cessitera une reprĂ©sentation identique byte Ă byte des objets, ce que JSON/XML ne garantit pas (le rĂ©sultat des dumps peut diffĂ©rer).
XDR convient pour cette tùche, mais je choisis ASN.1 avec codage DER et la bibliothÚque, car nous aurons à disposition des objets de haut niveau avec lesquels il est souvent plus agréable et plus pratique de travailler. Contrairement à schemaless , ou , ASN.1 vérifiera automatiquement les données par rapport à un schéma défini de maniÚre rigide.
# 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 accepté sera Msg : soit un MsgText textuel (pour l'instant avec un seul champ de texte), soit un message de poignée de main MsgHandshake (dans lequel le nom de l'interlocuteur est transmis). Cela semble pour l'instant trop complexe, mais c'est une base pour l'avenir.
âââââââ âââââââ
âPeerAâ âPeerBâ
ââââŹâââ ââââŹâââ
âMsgHandshake(IdA) â
ââââââââââââââââââ>>â
â â
âMsgHandshake(IdB) â
â<ââââââââââââââââââ
â â
â MsgText() â
ââââââââââââââââââ>>â
â â
â MsgText() â
â<ââââââââââââââââââ
â â
IM sans cryptographie
Comme je l'ai déjà mentionné, pour toutes les opérations avec les sockets, nous utiliserons la bibliothÚque asyncio. Déclarons ce que nous attendons au moment du lancement :
parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
"--our-name",
required=True,
help="Notre nom de pair",
)
parser.add_argument(
"--their-names",
required=True,
help="Leurs noms de pairs, séparés par des virgules",
)
parser.add_argument(
"--bind",
default="::1",
help="Adresse à écouter",
)
parser.add_argument(
"--port",
type=int,
default=6666,
help="Port à écouter",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))
Un nom propre est dĂ©fini (âour-name alice). Tous les pairs attendus sont Ă©numĂ©rĂ©s sĂ©parĂ©s par des virgules (âtheir-names bob,eve). Pour chaque pair, un rĂ©pertoire avec des sockets Unix est créé, ainsi qu'une coroutine pour chaque in, out, state :
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 entrants des utilisateurs depuis le socket in sont envoyés dans les queues 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 entrants des pairs sont envoyĂ©s dans les queues OUT_QUEUES, d'oĂč les donnĂ©es sont Ă©crites dans le socket out :
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()
En lisant depuis le socket state, le programme cherche dans le dictionnaire PEER_ALIVE l'adresse du pair. Si aucune connexion avec le pair n'existe, une chaßne vide est enregistrée.
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()
Lors de l'écriture de l'adresse 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érons l'initiateur. D'abord, il établit clairement une connexion avec l'hÎte/port spécifié et envoie un message de handshake avec son nom :
130 async def initiator(hĂŽte, port):
131 _id = repr((hĂŽte, port))
132 logging.info("%s : numérotation", _id)
133 lecteur, écrivain = await asyncio.open_connection(hÎte, port)
134 # Message de poignée de main {{{
135 écrivain.write(Msg(("poignée de main", MsgHandshake((
136 ("nomPeer", NOTRE_NOM),
137 )))).encode())
138 # }}}
139 await écrivain.drain()
Ensuite, il attend la réponse de la partie distante. Il tente de décoder la réponse reçue selon le schéma Msg ASN.1. Supposons que l'ensemble du message sera envoyé dans un seul segment TCP et que nous le recevrons de maniÚre atomique lors de l'appel à .read(). Vérifions que nous avons bien reçu un message de poignée de main.
141 # Attendre le message de poignée de main {{{
142 données = await lecteur.read(256)
143 if données == b"":
144 logging.warning("%s : pas de réponse, déconnexion", _id)
145 écrivain.close()
146 return
147 try:
148 msg, _ = Msg().decode(données)
149 except ASN1Error:
150 logging.warning("%s : réponse indécodable, déconnexion", _id)
151 écrivain.close()
152 return
153 logging.info("%s : reçu %s message", _id, msg.choice)
154 if msg.choice != "poignée de main":
155 logging.warning("%s : message inattendu, déconnexion", _id)
156 écrivain.close()
157 return
158 # }}}
VĂ©rifions que le nom de l'interlocuteur reçu nous est connu. Si ce n'est pas le cas, nous interrompons la connexion. VĂ©rifions s'il y avait dĂ©jĂ une connexion Ă©tablie avec lui (l'interlocuteur a de nouveau donnĂ© l'ordre de se connecter Ă nous) et fermons-la. Dans IN_QUEUES, des chaĂźnes Python contenant le texte du message sont placĂ©es, mais une valeur spĂ©ciale None signale Ă la coroutine msg_sender d'arrĂȘter de fonctionner, afin qu'elle oublie son Ă©crivain, liĂ© Ă l'ancienne connexion TCP.
159 msg_handshake = msg.value
160 nom_peer = str(msg_handshake["nomPeer"])
161 if nom_peer not in LEURS_NOMS:
162 logging.warning("nom peer inconnu : %s", nom_peer)
163 écrivain.close()
164 return
165 logging.info("%s : session établie : %s", _id, nom_peer)
166 # Exécuter l'expéditeur de messages texte, initialiser le décodeur de transport {{{
167 peer_alive = PEER_ALIVES.pop(nom_peer, None)
168 if peer_alive is not None:
169 peer_alive.close()
170 await IN_QUEUES[nom_peer].put(None)
171 PEER_ALIVES[nom_peer] = écrivain
172 asyncio.ensure_future(msg_sender(nom_peer, écrivain))
173 # }}}
msg_sender gĂšre les messages sortants (insĂ©rĂ©s dans la file d'attente depuis le socket in), les sĂ©rialise en message MsgText et les envoie via la connexion TCP. Celle-ci peut se rompre Ă tout moment â c'est ce que nous interceptons explicitement.
async def msg_sender(nom_peer: str, écrivain) -> None:
in_queue = IN_QUEUES[nom_peer]
while True:
texte = await in_queue.get()
if texte is None:
break
écrivain.write(Msg(("texte", MsgText((
("texte", UTF8String(texte)),
)))).encode())
try:
await écrivain.drain()
except ConnectionResetError:
del PEER_ALIVES[nom_peer]
return
logging.info("%s : message envoyé de %d caractÚres", nom_peer, len(texte))
Ă la fin, l'initiateur entre dans une boucle infinie pour lire les messages du socket. Il vĂ©rifie s'il s'agit de messages texte et les place dans la file OUT_QUEUES, d'oĂč ils seront envoyĂ©s au socket de l'interlocuteur correspondant. Pourquoi ne 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 le tampon du systĂšme d'exploitation et envoyĂ©s en un seul segment TCP. Nous pourrons dĂ©coder le premier, mais il se peut qu'il reste une partie du suivant dans le tampon. En cas de toute situation anormale, nous fermons la connexion TCP et arrĂȘtons la coroutine msg_sender (en envoyant None dans la file OUT_QUEUES).
174 buf = b""
175 # Attendre les messages de test {{{
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: taille maximale du tampon dépassée", _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: message inattendu %s", _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: déconnexion : %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: message de %d caractÚres reçu", peer_name, len(text))
69 await OUT_QUEUES[peer_name].put(text)
Revenons au code principal. AprÚs la création de toutes les coroutines au moment du lancement du programme, nous démarrons le serveur TCP. Pour chaque connexion établie, il crée une coroutine de réponse (responder).
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("En écoute sur : %s", server.sockets[0].getsockname())
loop.run_forever()
Le responder est similaire Ă l'initiateur et effectue toutes les mĂȘmes actions de façon miroir, mais la boucle infinie de lecture des messages dĂ©marre immĂ©diatement, pour la simplicitĂ©. Actuellement, le protocole de poignĂ©e de main envoie un message de chaque cĂŽtĂ©, mais Ă l'avenir, il y en aura deux de la part de l'initiateur de la connexion, aprĂšs quoi l'envoi de texte sera immĂ©diatement possible.
72 async def responder(reader, writer):
73 _id = writer.get_extra_info("peername")
74 logging.info("%s: connecté", _id)
75 buf = b""
76 msg_expected = "handshake"
77 peer_name = None
78 while True:
79 # Lire jusqu'Ă obtenir un message Msg {{{
80 data = await reader.read(MaxMsgLen)
81 if data == b"":
82 logging.info("%s: connexion fermée", _id)
83 break
84 buf += data
85 if len(buf) > MaxMsgLen:
86 logging.warning("%s: taille maximale du tampon dépassée", _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: message %s inattendu", _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 # Traiter le message de Handshake {{{
104 elif msg_expected == "handshake":
105 logging.info("%s: reçu le message %s", _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("nom de pair inconnu : %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 établie : %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: déconnexion", _id)
125 if msg_expected == "text":
126 IN_QUEUES[peer_name].put(None)
127 writer.close()
Protocole sécurisé
Il est temps de sécuriser notre communication. Que entendons-nous par sécurité et que voulons-nous :
- la confidentialité des messages transmis ;
- l'authenticitĂ© et l'intĂ©gritĂ© des messages transmis â toute modification doit ĂȘtre dĂ©tectĂ©e ;
- protection contre les attaques de rĂ©pĂ©tition (replay attack) â la perte ou la rĂ©pĂ©tition des messages doit ĂȘtre dĂ©tectĂ©e (et nous dĂ©cidons de couper la connexion) ;
- identification et authentification des interlocuteurs par des clĂ©s publiques prĂ©enregistrĂ©es â nous avons dĂ©jĂ dĂ©cidĂ© de faire un rĂ©seau ami-Ă -ami. Ce n'est qu'aprĂšs authentification que nous saurons avec qui nous communiquons ;
- la prĂ©sence propriĂ©tĂ©s (PFS) â la compromission de notre clĂ© de signature Ă longue durĂ©e de vie ne doit pas permettre de lire toute la correspondance prĂ©cĂ©dente. L'enregistrement du trafic interceptĂ© devient inutile ;
- La validitĂ© des messages (de transport et de poignĂ©e de main) nâest valable que dans le cadre d'une seule 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;
- un observateur passif ne doit voir ni les identifiants des utilisateurs, ni les clés publiques à long terme transmises, ni les hachages correspondants. Une certaine anonymité vis-à -vis de l'observateur passif.
Ătonnamment, presque tous souhaitent avoir ce minimum dans tout protocole de poignĂ©e de main, et trĂšs peu de ce qui est mentionnĂ© est finalement respectĂ© pour les protocoles « maison ». Donc, cette fois, ne cherchons pas Ă inventer quelque chose de nouveau. Je recommanderais sans hĂ©sitation d'utiliser pour construire des protocoles, mais choisissons quelque chose de plus simple.
Deux protocoles sont les plus populaires :
- â un protocole trĂšs complexe avec une longue histoire de bogues, de dĂ©fauts, de vulnĂ©rabilitĂ©s, de mauvaises conceptions, de complexitĂ© et de ratĂ©s (cela dit, cela s'applique peu Ă TLS 1.3). Mais nous ne le considĂ©rons pas en raison dâune trop grande complexitĂ©.
- avec â n'ont pas de problĂšmes cryptographiques sĂ©rieux, bien qu'ils ne soient pas non plus simples. Si on lit sur IKEv1 et IKEv2, leur origine est , ISO/IEC IS 9798-3 et les protocoles SIGMA (SIGn-and-MAc) â suffisamment simples Ă mettre en Ćuvre en une soirĂ©e.
Qu'est-ce qui rend SIGMA, en tant que derniĂšre Ă©tape de lâĂ©volution des protocoles STS/ISO, si bon ? Il satisfait Ă toutes nos exigences (y compris la « dissimulation » des identifiants des interlocuteurs), nâa pas de problĂšmes cryptographiques connus. Il est minimaliste : la suppression d'au moins un Ă©lĂ©ment du message du protocole entraĂźnerait son insĂ©curitĂ©.
Passons d'un protocole maison le plus simple Ă SIGMA. L'opĂ©ration de base qui nous intĂ©resse est : une fonction Ă l'issue de laquelle les deux participants obtiendront la mĂȘme valeur, qui pourra ĂȘtre utilisĂ©e comme clĂ© symĂ©trique. Sans entrer dans les dĂ©tails : chaque partie gĂ©nĂšre une paire de clĂ©s Ă©phĂ©mĂšres (utilisĂ©e uniquement dans le cadre d'une session) (clĂ©s publique et privĂ©e), Ă©change des clĂ©s publiques, appelle la fonction d'accord Ă laquelle elle transmet sa clĂ© privĂ©e et la clĂ© publique de l'interlocuteur.
âââââââ âââââââ
âPeerAâ âPeerBâ
ââââŹâââ ââââŹâââ
â IdA, PubA â ââââââââââââââââââââââ
ââââââââââââââââ >â âPrvA, PubA = DHgen()â
â â ââââââââââââââââââââââ
â IdB, PubB â ââââââââââââââââââââââ
â <ââââââââââââââââ âPrvB, PubB = DHgen()â
â â ââââââââââââââââââââââ
âââââ âââââââââ§âââââââââââââ
â âKey = DH(PrvA, PubB)â
<ââââ âââââââââ€âââââââââââââ
â â
â â
Tout le monde peut sâimmiscer et remplacer les clĂ©s publiques par les siennes â ce protocole nâa pas d'authentification des interlocuteurs. Ajoutons une signature avec des clĂ©s de longue durĂ©e.
âââââââ âââââââ
âPeerAâ âPeerBâ
ââââŹâââ ââââŹâââ
âIdA, PubA, sign(SignPrvA, (PubA)) â âââââââââââââââââââââââââââââ
ââââââââââââââââââââââââââââââââââ >â âSignPrvA, SignPubA = load()â
â â âPrvA, PubA = DHgen() â
â â âââââââââââââââââââââââââââââ
âIdB, PubB, sign(SignPrvB, (PubB)) â âââââââââââââââââââââââââââââ
â <ââââââââââââââââââââââââââââââââââ âSignPrvB, SignPubB = load()â
â â âPrvB, PubB = DHgen() â
â â âââââââââââââââââââââââââââââ
âââââ âââââââââââââââââââââââ â
â âverify(SignPubB, ...)â â
<ââââ âKey = DH(PrvA, PubB) â â
â âââââââââââââââââââââââ â
â â
Une telle signature ne conviendra pas, car elle n'est pas liĂ©e Ă une session spĂ©cifique. De tels messages « conviendront » Ă©galement pour des sessions avec d'autres participants. Tout le contexte doit ĂȘtre signĂ©. Cela nĂ©cessite Ă©galement d'ajouter l'envoi d'un autre message de A.
De plus, il est crucial d'ajouter sous la signature notre propre identifiant, sinon nous pouvons modifier IdXXX et resigner le message avec la clé d'un autre interlocuteur connu. Pour éviter , il est nécessaire que les éléments sous la signature soient clairement positionnés selon leur signification : si A signe (PubA, PubB), alors B doit signer (PubB, PubA). Cela souligne également l'importance du choix de la structure et du format des données sérialisées. Par exemple, les ensembles en codage ASN.1 DER sont triés : SET OF(PubA, PubB) sera identique à SET OF(PubB, PubA).
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââ ââââŹâââ â IdA, PubA â âââââââââââââââââââââââââââââ âââââââââââââââââââââââââââââââââââââââââââââ>â âSignPrvA, SignPubA = load()â â â âPrvA, PubA = DHgen() â â â âââââââââââââââââââââââââââââ âIdB, PubB, sign(SignPrvB, (IdB, PubA, PubB)) â âââââââââââââââââââââââââââââ â<âââââââââââââââââââââââââââââââââââââââââââââ âSignPrvB, SignPubB = load()â â â âPrvB, PubB = DHgen() â â â âââââââââââââââââââââââââââââ â sign(SignPrvA, (IdA, PubB, PubA)) â âââââââââââââââââââââââ âââââââââââââââââââââââââââââââââââââââââââââ>â âverify(SignPubB, ...)â â â âKey = DH(PrvA, PubB) â â â âââââââââââââââââââââââ â â
Cependant, nous n'avons toujours pas « prouvĂ© » que nous avons gĂ©nĂ©rĂ© une clĂ© secrĂšte commune pour cette session. En principe, nous pourrions nous passer de cette Ă©tape â le premier message de transport serait invalide, mais nous voulons nous assurer qu'une fois l handshake terminĂ©, tout est rĂ©ellement validĂ©. Pour l'instant, nous avons entre les mains le protocole ISO/IEC IS 9798-3.
Nous pourrions signer la clĂ© gĂ©nĂ©rĂ©e elle-mĂȘme. C'est dangereux, car il se peut que l'algorithme de signature utilisĂ© prĂ©sente des fuites (mĂȘme si ce ne sont que des bits de signature, ce sont tout de mĂȘme des fuites). On peut signer le hachage de la clĂ© gĂ©nĂ©rĂ©e, mais une fuite mĂȘme du hachage de la clĂ© gĂ©nĂ©rĂ©e peut avoir de la valeur lors d'attaques par force brute sur la fonction de gĂ©nĂ©ration. SIGMA utilise une fonction MAC, authentifiant l'identifiant de l'expĂ©diteur.
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââ ââââŹâââ â IdA, PubA â âââââââââââââââââââââââââââââ ââââââââââââââââââââââââââââââââââââââââââââââââââ>>â âSignPrvA, SignPubA = load()â â â âPrvA, PubA = DHgen() â â â âââââââââââââââââââââââââââââ âIdB, PubB, sign(SignPrvB, (PubA, PubB)), MAC(IdB) â âââââââââââââââââââââââââââââ â<ââââââââââââââââââââââââââââââââââââââââââââââââââ âSignPrvB, SignPubB = load()â â â âPrvB, PubB = DHgen() â â â âââââââââââââââââââââââââââââ â â âââââââââââââââââââââââ â sign(SignPrvA, (PubB, PubA)), MAC(IdA) â âKey = DH(PrvA, PubB) â ââââââââââââââââââââââââââââââââââââââââââââââââââ>>â âverify(Key, IdB) â â â âverify(SignPubB, ...)â â â âââââââââââââââââââââââ â â
Dans un souci d'optimisation, certains pourraient souhaiter rĂ©utiliser leurs clĂ©s Ă©phĂ©mĂšres (ce qui, bien sĂ»r, est dĂ©savantageux pour PFS). Par exemple, nous avons gĂ©nĂ©rĂ© une paire de clĂ©s, tentĂ© de nous connecter, mais TCP n'Ă©tait pas disponible ou s'est interrompu en cours de protocole. Il est dommage de gaspiller l'entropie et les ressources processeur sur une nouvelle paire. Nous allons donc introduire ce que l'on appelle un cookie â une valeur pseudo-alĂ©atoire qui protĂ©gera contre de potentielles attaques de rĂ©pĂ©tition lors de la rĂ©utilisation des 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 l'autre participant peut ĂȘtre omise de la signature pour des raisons de non-utilitĂ©.
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââ ââââŹâââ â IdA, PubA, CookieA â âââââââââââââââââââââââââââââ âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ>â âSignPrvA, SignPubA = load()â â â âPrvA, PubA = DHgen() â â â âââââââââââââââââââââââââââââ âIdB, PubB, CookieB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) â âââââââââââââââââââââââââââââ ââ âverify(Key, IdB) â â â âverify(SignPubB, ...)â â â âââââââââââââââââââââââ â â
Enfin, nous souhaitons protéger la confidentialité de nos identifiants d'interlocuteurs vis-à -vis des observateurs passifs. Pour cela, SIGMA propose d'abord d'échanger des clés éphémÚres, de générer une clé commune sur laquelle seront chiffrés les messages d'authentification et d'identification. SIGMA décrit deux options :
- SIGMA-I â protĂšge l'initiateur contre les attaques actives, le rĂ©pondant contre les attaques passives : l'initiateur authentifie le rĂ©pondant et s'il y a un problĂšme, il ne rĂ©vĂšle pas son identification. Le rĂ©pondant, quant Ă lui, rĂ©vĂšle son identification s'il commence un protocole actif. Un observateur passif n'apprendra rien ;
SIGMA-R â protĂšge le rĂ©pondant contre les attaques actives, l'initiateur contre les attaques passives. Tout se fait exactement Ă l'inverse, mais dans ce protocole, il y a dĂ©jĂ quatre messages de poignĂ©e de main qui sont transmis.Nous choisissons SIGMA-I, qui ressemble davantage Ă ce que nous attendons des choses familiĂšres client-serveur : le client ne reconnaĂźt que le serveur authentifiĂ©, tandis que le serveur connaĂźt dĂ©jĂ tout. De plus, il est plus simple Ă mettre en Ćuvre en raison du nombre rĂ©duit de messages d'initialisation. Tout ce que nous ajoutons au protocole, c'est le chiffrement d'une partie du message et le transfert de l'identifiant A dans la partie chiffrĂ©e du dernier message :
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââ ââââŹâââ â PubA, CookieA â âââââââââââââââââââââââââââââ ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ>â âSignPrvA, SignPubA = load()â â â âPrvA, PubA = DHgen() â â â âââââââââââââââââââââââââââââ âPubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB))) â âââââââââââââââââââââââââââââ ââ âverify(Key, IdB) â â â âverify(SignPubB, ...)â â â âââââââââââââââââââââââ â â
- Pour la signature, nous utilisons GOST R un algorithme avec des clés de 256 bits.
- Pour la génération de la clé commune, le 34.10-2012 VKO est utilisé.
- Comme MAC, CMAC est utilisĂ©. Techniquement, c'est un mode de fonctionnement particulier d'un chiffre de bloc, dĂ©crit dans GOST R 34.13-2015. Comme fonction de chiffrement pour ce mode â (34.12-2015).
- L'identifiant de l'interlocuteur est un hachage de sa clé publique. Pour le hachage, nous utilisons (34.11-2012 256 bits).
AprÚs l'établissement de la connexion, nous aurons une clé commune convenue. Nous pouvons l'utiliser pour le chiffrement authentifié des messages de transport. Cette partie est assez simple et il est difficile de se tromper : nous incrémentons le compteur de messages, chiffrons le message, authentifions (MAC) le compteur et le texte chiffré, puis envoyons. Lors de la réception du message, nous vérifions que le compteur a la valeur attendue, authentifions le texte chiffré avec le compteur, puis déchiffrons. Quelle clé utiliser pour chiffrer les messages d'établissement, ceux de transport, et quelle clé pour l'authentification ? Utiliser une seule clé pour toutes ces tùches est dangereux et imprudent. Il est nécessaire de générer des clés en utilisant des fonctions spécialisées (key derivation function). Encore une fois, ne compliquons pas les choses en inventant quelque chose : est bien connu, bien étudié et ne présente pas de problÚmes connus. Malheureusement, la bibliothÚque native de Python ne contient pas cette fonction, nous allons donc utiliser package. HKDF utilise à l'intérieur , qui utilise à son tour la fonction de hachage. Un exemple d'implémentation en Python sur la page Wikipedia ne nécessite que quelques lignes de code. Comme dans le cas de 34.10-2012, nous allons utiliser Stribog-256 comme fonction de hachage. La sortie de notre fonction de dérivation de clés sera appelée la clé de session, à partir de laquelle seront dérivées les clés symétriques manquantes :
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")Structures / schémas
Examinons quelles structures ASN.1 nous avons maintenant obtenues pour la transmission de toutes ces données :
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 â ce qui sera signĂ© (to be signed). HandshakeTBE â ce qui sera chiffrĂ© (to be encrypted). Je souligne le champ ukm dans MsgHandshake1. 34.10 VKO, pour une randomisation encore plus grande des clĂ©s gĂ©nĂ©rĂ©es, inclut le paramĂštre UKM (user keying material) â simplement de l'entropie supplĂ©mentaire.
Ajout de la cryptographie dans le code
Nous allons examiner uniquement les modifications apportĂ©es au code original, car la structure est restĂ©e la mĂȘme (en rĂ©alitĂ©, l'implĂ©mentation finale a d'abord Ă©tĂ© Ă©crite, puis toute la cryptographie a Ă©tĂ© retirĂ©e).
Ătant donnĂ© que l'authentification et l'identification des interlocuteurs se feront par des clĂ©s publiques, il faut dĂ©sormais les stocker de maniĂšre durable. Pour simplifier, utilisons un JSON de ce type :
{ "our": { "prv": "21254cf66c15e0226ef2669ceee46c87b575f37f9000272f408d0c9283355f98", "pub": "938c87da5c55b27b7f332d91b202dbef2540979d6ceaa4c35f1b5bfca6df47df0bdae0d3d82beac83cec3e353939489d9981b7eb7a3c58b71df2212d556312a1" }, "their": { "alice": "d361a59c25d2ca5a05d21f31168609deeec100570ac98f540416778c93b2c7402fd92640731a707ec67b5410a0feae5b78aeec93c4a455a17570a84f2bc21fce", "bob": "aade1207dd85ecd283272e7b69c078d5fae75b6e141f7649ad21962042d643512c28a2dbdc12c7ba40eb704af920919511180c18f4d17e07d7f5acd49787224a" } }our â notre paire de clĂ©s, clĂ©s privĂ©es et publiques hexadĂ©cimales. their â noms des interlocuteurs et leurs clĂ©s publiques. Modifions les arguments de la ligne de commande et ajoutons un post-traitement des donnĂ©es 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="GĂ©nĂ©rez un JSON avec notre nouvelle paire de clĂ©s", ) parser.add_argument( "--keys", default="keys.json", required=False, help="JSON avec nos clĂ©s et leurs clĂ©s", ) parser.add_argument( "--bind", default="::1", help="Adresse Ă Ă©couter", ) parser.add_argument( "--port", type=int, default=6666, help="Port Ă Ă©couter", ) 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) # Analysez et dĂ©sĂ©rialisez nos clĂ©s et leurs clĂ©s {{{ 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), } # }}}La clĂ© privĂ©e de l'algorithme 34.10 est un nombre alĂ©atoire. De taille 256 bits pour les courbes elliptiques 256 bits. PyGOST ne fonctionne pas avec un ensemble d'octets, mais avec , c'est pourquoi notre clĂ© privĂ©e (urandom(32)) doit ĂȘtre convertie en un nombre Ă l'aide de gost3410.prv_unmarshal(). La clĂ© publique est dĂ©terminĂ©e Ă partir de la clĂ© privĂ©e, Ă l'aide de gost3410.public_key(). La clĂ© publique 34.10 est un couple de grands nombres qui doivent Ă©galement ĂȘtre convertis en une sĂ©quence d'octets pour faciliter le stockage et le transfert, Ă l'aide de gost3410.pub_marshal().
AprĂšs avoir lu le fichier JSON, les clĂ©s publiques doivent ĂȘtre converties Ă nouveau, Ă l'aide de gost3410.pub_unmarshal(). Comme nous allons recevoir les identifiants des interlocuteurs sous forme de hachage de la clĂ© publique, nous pouvons les prĂ©-calculer et les placer dans un dictionnaire pour une recherche rapide. Le hachage Streebog-256 est dĂ©fini par gost34112012256.GOST34112012256(), qui satisfait complĂštement l'interface de hachage hashlib.
Comment la coroutine de l'initiateur a-t-elle changé ? Tout comme dans le schéma de la poignée de main : nous générons un cookie (128 bits est amplement suffisant), une paire de clés éphémÚres 34.10, qui sera utilisée pour la fonction d'accord de clés 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 # Générer notre clé publique éphémÚre et cookie, envoyer le message Handshake 0 {{{ 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()- Nous attendons la réponse et décodons le message Msg reçu;
- Nous assurons que nous avons reçu handshake1;
- Nous décodons la clé publique éphémÚre de l'autre partie et calculons la clé de session;
- Nous générons les clés symétriques nécessaires pour le traitement de la partie TBE du message.
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 # Valider le message Handshake {{{ 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 est un nombre de 64 bits (urandom(8)), qui nĂ©cessite Ă©galement une dĂ©sĂ©rialisation Ă partir de la reprĂ©sentation par bytes, en utilisant gost3410_vko.ukm_unmarshal(). La fonction VKO pour 34.10-2012 256 bits est gost3410_vko.kek_34102012256() (KEK â clĂ© de chiffrement).
La clĂ© de session gĂ©nĂ©rĂ©e est dĂ©jĂ une sĂ©quence alĂ©atoire de bytes de 256 bits. Elle peut donc ĂȘtre immĂ©diatement utilisĂ©e dans la fonction HKDF. Ătant donnĂ© que GOST34112012256 satisfait Ă l'interface hashlib, elle peut ĂȘtre directement utilisĂ©e dans la classe Hkdf. Le sel (premier argument Hkdf) n'est pas spĂ©cifiĂ©, car la clĂ© gĂ©nĂ©rĂ©e en raison de l'Ă©phĂ©mĂ©ritĂ© des paires de clĂ©s impliquĂ©es sera diffĂ©rente pour chaque session et contient dĂ©jĂ suffisamment d'entropie. kdf.expand() gĂ©nĂšre par dĂ©faut des clĂ©s d'une longueur de 256 bits, requises pour le Kuznechik par la suite.
Ensuite, les parties TBE et TBS du message reçu sont vérifiées :
- MAC est calculé et vérifié sur le texte chiffré reçu ;
- Le texte chiffré est déchiffré ;
- La structure TBE est décodée ;
- De celle-ci, l'identifiant de l'interlocuteur est pris et il est vérifié s'il nous est connu ;
- MAC est calculé et vérifié sur cet identifiant ;
- la signature est vérifiée au-dessus de la structure TBS, qui inclut les cookies des deux parties et la clé éphémÚre publique de l'autre partie. La signature est vérifiée avec la clé de signature à long terme de l'interlocuteur.
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, dĂ©connexion", _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("MAC invalide") 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("impossible de dĂ©coder 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("identitĂ© inconnue") 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("MAC d'identitĂ© invalide") 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("signature invalide") 174 return peer["name"]Comme mentionnĂ© prĂ©cĂ©demment, la norme 34.13-2015 dĂ©crit divers de la norme 34.12-2015. Parmi eux se trouve le mode de gĂ©nĂ©ration de l'IMyD et de calcul du MAC. Dans PyGOST, il s'agit de gost3413.mac(). Ce mode nĂ©cessite le passage de la fonction de chiffrement (qui prend et renvoie un bloc de donnĂ©es), de la taille du bloc de chiffrement, et des donnĂ©es elles-mĂȘmes. Pourquoi ne pas hardcoder la taille du bloc de chiffrement ? La norme 34.12-2015 ne dĂ©crit pas seulement le chiffre Ă 128 bits Kuznechik, mais aussi le 64 bits â une version lĂ©gĂšrement modifiĂ©e du GOST 28147-89, créée encore au KGB et ayant toujours l'un des niveaux de sĂ©curitĂ© les plus Ă©levĂ©s.
Le GOST.3412.KUZNECHIK s'initialise en appelant GOST3412Kuznechik(key) et retourne un objet avec les méthodes .encrypt() et .decrypt(), pratiques pour la transmission aux fonctions 34.13. Le MAC est calculé comme suit : gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). Pour comparer le MAC calculé et celui reçu, il ne faut pas utiliser la comparaison standard (==) des chaßnes d'octets, car cela entraßne des fuites de temps de comparaison, ce qui peut, en général, conduire à des vulnérabilités fatales de type attaques sur TLS. En Python, il existe une fonction hmac.compare_digest spéciale pour cela.
La fonction du chiffreur par blocs ne peut chiffrer qu'un seul bloc de donnĂ©es. Pour un nombre plus Ă©levĂ©, et aussi de longueur non multiple, il est nĂ©cessaire d'utiliser un mode de chiffrement. Les modes suivants sont dĂ©crits dans 34.13-2015 : ECB, CTR, OFB, CBC, CFB. Chacun a ses propres domaines d'application et caractĂ©ristiques. Malheureusement, il n'existe toujours pas de modes de chiffrement authentifiĂ©s, le mode compteur Comme pour .mac(), .ctr() accepte des donnĂ©es similaires en entrĂ©e : ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Il est nĂ©cessaire de dĂ©finir un vecteur d'initialisation, d'une longueur exactement Ă©gale Ă la moitiĂ© de la taille du bloc de chiffrement. Si notre clĂ© de chiffrement est utilisĂ©e uniquement pour chiffrer un seul message (mĂȘme s'il comporte plusieurs blocs), il est sĂ»r de dĂ©finir un vecteur d'initialisation nul. Pour le chiffrement des messages de handshake, nous utilisons Ă chaque fois une clĂ© distincte.
La vĂ©rification de la signature avec gost3410.verify() est triviale : nous passons la courbe elliptique dans laquelle nous opĂ©rons (cela est simplement fixĂ© dans notre protocole GOSTIM), la clĂ© publique du signataire (n'oublions pas que cela doit ĂȘtre un tuple de deux grands nombres, et non une chaĂźne d'octets), le hachage 34.11-2012 et la signature reçue elle-mĂȘme.
Ensuite, dans l'initiateur, nous prĂ©parons et envoyons le message handshake2 de nĂ©gociation, en effectuant les mĂȘmes actions que nous avons faites lors de la vĂ©rification, seulement de maniĂšre symĂ©trique : crĂ©ation de signatures sur nos propres clĂ©s au lieu de vĂ©rification, etc.
...
456 # Préparer et envoyer le message Handshake 2 {{{ 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 établie : %s", _id, peer_name)Une fois la session établie, des clés de transport sont générées (une clé distincte pour le chiffrement, pour l'authentification, pour chaque partie), et le Kuznechik est initialisé pour le déchiffrement et la vérification du MAC :
499 # Exécuter l'envoi de messages texte, initialiser le décodeur de transport {{{ 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 # Attendre les messages de test {{{ 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 # }}}La coroutine msg_sender chiffre désormais les messages avant de les envoyer dans la connexion TCP. Chaque message a un nonce croissant de maniÚre monotone, qui sert également de vecteur d'initialisation lors du chiffrement en mode compteur. Chaque message et bloc de message auront des valeurs de compteur qui diffÚrent assurément.
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 += 1Les messages entrants sont traités par la coroutine msg_receiver, qui s'occupe de l'authentification et du déchiffrement :
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("valeur de nonce inattendue") mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])): raise ValueError("MAC invalide") 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 est destinĂ© Ă ĂȘtre utilisĂ© uniquement Ă des fins Ă©ducatives (car il n'est pas couvert par des tests, au minimum) ! Le code source du programme peut ĂȘtre tĂ©lĂ©chargĂ© (Hachage Stribog-256 : 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Comme tous mes projets, type , , , , GOSTIM est entiĂšrement , distribuĂ© sous les conditions de .
, , membre de , développeur Python/Go, expert principal .
Source : habr.com
