Als Entwickler einer Bibliothek (GOST-verschlĂŒsselnde primitive auf reinem Python) bekomme ich hĂ€ufig Fragen dazu, wie man auf die Schnelle einen einfachen sicheren Nachrichtenaustausch implementieren kann. Viele halten angewandte Kryptografie fĂŒr eine relativ einfache Angelegenheit, und der Aufruf .encrypt() bei einem Blockcipher ist ausreichend fĂŒr den sicheren Versand ĂŒber einen Kommunikationskanal. Andere glauben, dass angewandte Kryptografie nur wenigen vorbehalten ist und es akzeptabel ist, dass reiche Unternehmen wie Telegram mit Mathematikern aus der Olympiade sicheres Protokoll implementieren können.
All dies hat mich dazu gebracht, diesen Artikel zu schreiben, um zu zeigen, dass die Implementierung von kryptografischen Protokollen und sicherem IM keine allzu komplexe Aufgabe ist. Allerdings ist es nicht ratsam, eigene Authentifizierungs- und SchlĂŒsselvereinbarungsprotokolle zu erfinden.

In diesem Artikel wird , , Instant Messenger mit Authentifizierungs- und SchlĂŒsselvereinbarungsprotokoll (auf dessen Grundlage realisiert wurde ), und verwendet ausschlieĂlich GOST-kryptografische Algorithmen aus der PyGOST-Bibliothek und ASN.1 Kodierung von Nachrichten mit der Bibliothek (ĂŒber die ich bereits ). Eine Voraussetzung: Es muss so einfach sein, dass man es von Grund auf an einem Abend (oder Arbeitstag) schreiben kann, sonst ist es schon kein einfaches Programm mehr. Es sind mit Sicherheit Fehler, unnötige KomplexitĂ€ten und MĂ€ngel vorhanden, zudem ist es mein erstes Programm mit der asyncio-Bibliothek.
Design des IM
Um zu beginnen, mĂŒssen wir verstehen, wie unser IM aussehen wird. Zur Vereinfachung nehmen wir an, dass es sich um ein Peer-to-Peer-Netzwerk handelt, ohne irgendeine Teilnehmendenentdeckung. Wir geben manuell an, zu welcher Adresse und zu welchem Port wir fĂŒr die Kommunikation mit dem GesprĂ€chspartner verbinden wollen.
Ich verstehe, dass die Annahme von derzeitiger VerfĂŒgbarkeit einer direkten Verbindung zwischen zwei beliebigen Computern eine wesentliche EinschrĂ€nkung fĂŒr die praktische Anwendbarkeit von IM darstellt. Aber je mehr Entwickler verschiedene NAT-Traversal-Lösungen implementieren, desto lĂ€nger werden wir im IPv4-Internet feststecken, mit ernĂŒchternder Wahrscheinlichkeit einer Verbindung zwischen beliebigen Computern. Wie lange können wir das Fehlen von IPv6 zu Hause und bei der Arbeit noch ertragen?
Wir werden ein Freund-zu-Freund-Netzwerk haben: Alle möglichen GesprĂ€chspartner mĂŒssen im Voraus bekannt sein. Erstens vereinfacht das alles erheblich: Man stellt sich vor, findet oder findet nicht den Namen/SchlĂŒssel, trennt die Verbindung oder fĂ€hrt fort, im Wissen um den GesprĂ€chspartner. Zweitens ist es im Allgemeinen sicher und schlieĂt zahlreiche Angriffe aus.
Die IM-OberflĂ€che wird klassischen Lösungen Ă€hnlich sein , die mir wegen ihres Minimalismus und ihrer Unix-Way-Philosophie sehr gefallen. Das IM-Programm erstellt fĂŒr jeden GesprĂ€chspartner ein Verzeichnis mit drei Unix-Domain-Sockets:
- in â hier werden die an den GesprĂ€chspartner gesendeten Nachrichten geschrieben;
- out â hier werden die vom GesprĂ€chspartner empfangenen Nachrichten gelesen;
- state â indem wir daraus lesen, erfahren wir, ob der GesprĂ€chspartner gerade verbunden ist, Adresse/Port der Verbindung.
DarĂŒber hinaus wird ein conn-Socket erstellt; indem wir den Host-Port dort hineinschreiben, initiieren wir die Verbindung zu einem entfernten GesprĂ€chspartner.
|-- alice
| |-- in
| |-- out
| `-- state
|-- bob
| |-- in
| |-- out
| `-- state
`- conn
Dieser Ansatz ermöglicht unabhĂ€ngige Implementierungen des IM-Transports und der BenutzeroberflĂ€che, denn ĂŒber Geschmack lĂ€sst sich nicht streiten, es ist unmöglich, es jedem recht zu machen. Mit und/oder , kann man eine mehrfenstrige OberflĂ€che mit Syntaxhervorhebung erhalten. Und mithilfe von kann man eine GNU Readline-kompatible Eingabezeile fĂŒr Nachrichten erhalten.
TatsĂ€chlich verwenden suckless-Projekte FIFO-Dateien. Persönlich konnte ich nicht verstehen, wie man mit asyncio konkurrierend mit Dateien arbeitet, ohne selbsthandwerkliche Unterlagen in Form von dedizierten Threads (fĂŒr solche Dinge benutze ich seit langem die Sprache ). Daher entschied ich mich, mit Unix-Domain-Sockets auszukommen. Leider beraubt das die Möglichkeit, echo 2001:470:dead::babe 6666 > conn zu machen. Ich habe dieses Problem gelöst, indem ich : echo 2001:470:dead::babe 6666 | socat â UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.
Das ursprĂŒngliche unsichere Protokoll
TCP wird als Transport verwendet: es garantiert die Lieferung und deren Reihenfolge. UDP garantiert weder das eine noch das andere (was hilfreich wĂ€re, wenn Kryptographie zum Einsatz kĂ€me), und UnterstĂŒtzung fĂŒr gibt es in Python nicht direkt.
Leider gibt es im TCP kein Konzept fĂŒr Nachrichten, sondern nur fĂŒr einen Byte-Stream. Daher ist es notwendig, ein Format fĂŒr Nachrichten zu entwickeln, damit sie in diesem Stream voneinander getrennt werden können. Wir können uns darauf einigen, das Zeilenumbruchzeichen zu verwenden. Das ist fĂŒr den Anfang in Ordnung, jedoch, wenn wir anfangen, unsere Nachrichten zu verschlĂŒsseln, könnte dieses Zeichen irgendwo im Chiffretext auftauchen. In Netzwerken sind daher Protokolle beliebt, die zuerst die LĂ€nge der Nachricht in Bytes senden. Zum Beispiel gibt es in Python out-of-the-box die xdrlib, die es ermöglicht, mit einem solchen Format zu arbeiten. .
Wir werden nicht korrekt und effizient mit dem Lesen von TCP arbeiten â wir vereinfachen den Code. Wir lesen in einer Endlosschleife die Daten aus dem Socket, bis wir die vollstĂ€ndige Nachricht dekodiert haben. Als Format fĂŒr diesen Ansatz kann auch JSON mit XML verwendet werden. Aber wenn Kryptografie hinzukommt, mĂŒssen die Daten signiert und authentifiziert werden â und das erfordert eine byte-genaue identische Darstellung von Objekten, was JSON/XML nicht gewĂ€hrleistet (das Ergebnis der Dumps kann variieren).
XDR eignet sich fĂŒr diese Aufgabe, jedoch wĂ€hle ich ASN.1 mit DER-Codierung und eine Bibliothek, da wir es mit hochgradigen Objekten zu tun haben, mit denen oft angenehmer und bequemer zu arbeiten ist. Im Gegensatz zu schemalo , oder , wird ASN.1 die Daten automatisch gegen ein fest definiertes Schema ĂŒberprĂŒfen.
# 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))),
))
Die empfohlene Nachricht wird Msg sein: entweder eine Textnachricht MsgText (bis jetzt mit einem einzigen Textfeld) oder eine Handshake-Nachricht MsgHandshake (in der der Name des GesprĂ€chspartners ĂŒbermittelt wird). Im Moment sieht es ĂŒberkompliziert aus, aber das ist eine Grundlage fĂŒr die Zukunft.
âââââââ âââââââ
âPeerAâ âPeerBâ
ââââŹâââ ââââŹâââ
âMsgHandshake(IdA) â
ââââââââââââââââââ>â
â â
âMsgHandshake(IdB) â
ââ
â â
â MsgText() â
â<ââââââââââââââââââ
â â
IM ohne Kryptografie
Wie ich bereits erwĂ€hnt habe, wird fĂŒr alle Operationen mit Sockets die asyncio-Bibliothek verwendet. Lassen Sie uns festlegen, was wir zum Zeitpunkt des Starts erwarten:
parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
"--our-name",
required=True,
help="Unser Peer-Name",
)
parser.add_argument(
"--their-names",
required=True,
help="Ihre Peer-Namen, durch Kommas getrennt",
)
parser.add_argument(
"--bind",
default="::1",
help="Adresse zum Lauschen",
)
parser.add_argument(
"--port",
type=int,
default=6666,
help="Port, auf dem gehört werden soll",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))
Es wird ein eigener Name festgelegt (âour-name alice). Alle erwarteten GesprĂ€chspartner werden durch Kommas aufgelistet (âtheir-names bob,eve). FĂŒr jeden der GesprĂ€chspartner wird ein Verzeichnis mit Unix-Sockets sowie eine Coroutine fĂŒr jeden in, out, state erstellt:
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"))
Eingehende Nachrichten vom Benutzer werden in die IN_QUEUES-Warteschlangen vom in-Socket gesendet:
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"))
Eingehende Nachrichten von GesprÀchspartnern werden in die OUT_QUEUES-Warteschlangen gesendet, aus denen die Daten in den out-Socket geschrieben werden:
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()
Beim Lesen aus dem state-Socket sucht das Programm im PEER_ALIVE-Wörterbuch die Adresse des GesprÀchspartners. Wenn keine Verbindung zum GesprÀchspartner besteht, wird eine leere Zeile geschrieben.
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()
Beim Schreiben der Adresse in den conn-Socket wird die Funktion des "Initiators" der Verbindung gestartet:
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))
Betrachten wir den Initiator. ZunÀchst öffnet er offensichtlich eine Verbindung zum angegebenen Host/Port und sendet eine Handshake-Nachricht mit seinem Namen:
130 async def initiator(host, port):
131 _id = repr((host, port))
132 logging.info("%s: WĂ€hle", _id)
133 reader, writer = await asyncio.open_connection(host, port)
134 # Handshake-Nachricht {{{
135 writer.write(Msg(("handshake", MsgHandshake((
136 ("peerName", OUR_NAME),
137 )))).encode())
138 # }}}
139 await writer.drain()
Dann wartet es auf eine Antwort von der Remote-Seite. Es versucht, die eingegangene Antwort gemÀà dem Msg ASN.1-Schema zu dekodieren. Wir gehen davon aus, dass die gesamte Nachricht in einem TCP-Segment gesendet wird und wir sie atomar bei dem Aufruf .read() erhalten. Wir ĂŒberprĂŒfen, ob wir tatsĂ€chlich die Handshake-Nachricht erhalten haben.
141 # Warte auf Handshake-Nachricht {{{
142 data = await reader.read(256)
143 if data == b"":
144 logging.warning("%s: keine Antwort, trennen", _id)
145 writer.close()
146 return
147 try:
148 msg, _ = Msg().decode(data)
149 except ASN1Error:
150 logging.warning("%s: unverarbeitbare Antwort, trennen", _id)
151 writer.close()
152 return
153 logging.info("%s: erhaltene %s Nachricht", _id, msg.choice)
154 if msg.choice != "handshake":
155 logging.warning("%s: unerwartete Nachricht, trennen", _id)
156 writer.close()
157 return
158 # }}}
Wir ĂŒberprĂŒfen, ob der empfangene Name des GesprĂ€chspartners uns bekannt ist. Wenn nicht, trennen wir die Verbindung. Wir prĂŒfen, ob bereits eine Verbindung zu ihm hergestellt war (der GesprĂ€chspartner hat erneut den Befehl zur Verbindung zu uns gegeben) und schlieĂen diese. In IN_QUEUES werden Python-Strings mit dem Text der Nachricht abgelegt, jedoch gibt es einen speziellen Wert None, der dem msg_sender-Koroutine signalisiert, die Arbeit zu beenden, damit sie ihren writer, der mit der veralteten TCP-Verbindung verbunden ist, vergisst.
159 msg_handshake = msg.value
160 peer_name = str(msg_handshake["peerName"])
161 if peer_name not in THEIR_NAMES:
162 logging.warning("unbekannter Peer-Name: %s", peer_name)
163 writer.close()
164 return
165 logging.info("%s: Sitzung hergestellt: %s", _id, peer_name)
166 # Starte Textnachrichtensender, initiiere Transportdekoder {{{
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 empfĂ€ngt ausgehende Nachrichten (die aus der in-Queue gelegt werden), serialisiert sie in die MsgText-Nachricht und sendet sie ĂŒber die TCP-Verbindung. Diese kann jederzeit unterbrochen werden â das fangen wir explizit ab.
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: gesendete Nachricht mit %d Zeichen", peer_name, len(text))
Am Ende tritt der Initiator in einen endlosen Zyklus des Lesens von Nachrichten aus dem Socket ein. Er ĂŒberprĂŒft, ob es sich um Textnachrichten handelt und fĂŒgt sie in die OUT_QUEUES-Warteschlange ein, aus der sie an den entsprechenden GegenĂŒber-Socket gesendet werden. Warum kann man nicht einfach .read() verwenden und die Nachricht dekodieren? Weil nicht ausgeschlossen werden kann, dass mehrere Nachrichten vom Benutzer im Puffer des Betriebssystems aggregiert und in einem TCP-Segment gesendet werden. Wir können die erste dekodieren, aber im Puffer kann noch ein Teil der nachfolgenden Nachrichten bleiben. Bei jeder auĂergewöhnlichen Situation schlieĂen wir die TCP-Verbindung und stoppen die msg_sender-Koroutine (indem wir None in die OUT_QUEUES-Warteschlange senden).
174 buf = b""
175 # Auf Testnachrichten warten {{{
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: maximale PuffergröĂe ĂŒberschritten", _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: unerwartete %s Nachricht", _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: trennt sich: %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: empfangen %d Zeichen Nachricht", peer_name, len(text))
69 await OUT_QUEUES[peer_name].put(text)
Kehren wir zum Hauptcode zurĂŒck. Nach der Erstellung aller Koroutinen starten wir zu Beginn des Programms den TCP-Server. FĂŒr jede hergestellte Verbindung wird eine Responder-Koroutine erstellt.
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("Hört auf: %s", server.sockets[0].getsockname())
loop.run_forever()
Der Responder Ă€hnelt dem Initiator und fĂŒhrt alle gleichen Aktionen spiegelbildlich aus, aber die endlose Schleife zum Lesen von Nachrichten wird sofort gestartet, um es einfacher zu machen. Momentan sendet das Handshake-Protokoll jeweils eine Nachricht von jeder Seite, wird aber in Zukunft zwei vom Initiator der Verbindung senden, nach denen sofort das Senden von Textnachrichten möglich ist.
72 async def responder(reader, writer):
73 _id = writer.get_extra_info("peername")
74 logging.info("%s: verbunden", _id)
75 buf = b""
76 msg_expected = "handshake"
77 peer_name = None
78 while True:
79 # Lies, bis wir eine Msg-Nachricht erhalten {{{
80 data = await reader.read(MaxMsgLen)
81 if data == b"":
82 logging.info("%s: Verbindung geschlossen", _id)
83 break
84 buf += data
85 if len(buf) > MaxMsgLen:
86 logging.warning("%s: maximale PuffergröĂe ĂŒberschritten", _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: unerwartete %s Nachricht", _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 # Verarbeite die Handshake-Nachricht {{{
104 elif msg_expected == "handshake":
105 logging.info("%s: %s Nachricht erhalten", _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("Unbekannter 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: Sitzung etabliert: %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: trennt sich", _id)
125 if msg_expected == "text":
126 IN_QUEUES[peer_name].put(None)
127 writer.close()
Sicheres Protokoll
Es ist Zeit, unsere Kommunikation abzusichern. Was verstehen wir unter Sicherheit und was wollen wir:
- Vertraulichkeit der ĂŒbermittelten Nachrichten;
- AuthentizitĂ€t und IntegritĂ€t der ĂŒbermittelten Nachrichten â VerĂ€nderungen sollten festgestellt werden;
- Schutz vor Wiederholungsangriffen â das Fehlen oder die Wiederholung von Nachrichten sollte erkannt werden (und wir entscheiden uns, die Verbindung zu trennen);
- Identifikation und Authentifizierung der GesprĂ€chspartner anhand vordefinierter öffentlicher SchlĂŒssel â wir haben bereits entschieden, dass wir ein friend-to-friend Netzwerk aufbauen. Nur nach der Authentifizierung verstehen wir, mit wem wir kommunizieren;
- die Existenz Eigenschaften (PFS) â die Kompromittierung unseres langlebigen SignaturschlĂŒssels sollte nicht zur Möglichkeit fĂŒhren, die gesamte vorherige Korrespondenz zu lesen. Aufgezeichnete abgefangene Daten werden wertlos;
- Die GĂŒltigkeit der Nachrichten (Transport- und Handshake-Nachrichten) nur innerhalb einer TCP-Sitzung. Das EinfĂŒgen korrekt signierter/authentifizierter Nachrichten aus einer anderen Sitzung (selbst mit demselben GesprĂ€chspartner) sollte nicht möglich sein;
- Ein passiver Beobachter darf weder die Benutzeridentifikatoren noch die ĂŒbermittelten langlebigen öffentlichen SchlĂŒssel oder deren Hashes sehen. Eine gewisse AnonymitĂ€t gegenĂŒber einem passiven Beobachter.
Ăberraschenderweise möchten fast alle dieses Minimum in jedem Handshake-Protokoll haben, und nur sehr wenig davon wird letztendlich fĂŒr 'hausgemachte' Protokolle umgesetzt. Lassen Sie uns daher auch jetzt nichts Neues erfinden. Ich wĂŒrde eindeutig empfehlen, fĂŒr den Aufbau von Protokollen zu verwenden, aber etwas Einfacheres wĂ€hlen.
Die beiden beliebtesten Protokolle sind:
- â ein Ă€uĂerst komplexes Protokoll mit einer langen Geschichte von Bugs, Fehlern, Schwachstellen, mangelnder Ăberlegung, KomplexitĂ€t und MĂ€ngeln (wenngleich dies wenig mit TLS 1.3 zu tun hat). Wir betrachten es jedoch wegen seiner ĂberkomplexitĂ€t nicht.
- c â weisen keine ernsthaften kryptografischen Probleme auf, sind jedoch ebenfalls nicht einfach. Wenn man ĂŒber IKEv1 und IKEv2 liest, stammen sie von , ISO/IEC IS 9798-3 und SIGMA (SIGn-and-MAc)-Protokollen â die recht einfach in einer Nacht implementiert werden können.
Was macht SIGMA, als letzte Entwicklungsstufe der STS/ISO-Protokolle, gut? Es erfĂŒllt all unsere Anforderungen (einschlieĂlich des 'Versteckens' der Identifikatoren der GesprĂ€chspartner) und hat keine bekannten kryptografischen Probleme. Es ist minimalistischer â das Entfernen auch nur eines Elements aus der Nachrichtenprotokoll fĂŒhrt zu seiner Unsicherheit.
Lassen Sie uns vom einfachsten hausgemachten Protokoll bis hin zu SIGMA gehen. Die grundlegendste Operation, die uns interessiert, ist : eine Funktion, bei der beide Teilnehmer dasselbe Ergebnis erhalten, das als symmetrischer SchlĂŒssel verwendet werden kann. Ohne ins Detail zu gehen: Jede der Parteien generiert ein ephemeres (nur innerhalb einer Sitzung verwendetes) SchlĂŒsselpaar (öffentliche und private SchlĂŒssel), tauscht die öffentlichen SchlĂŒssel aus und ruft die Aushandlungsfunktion auf, der sie ihren privaten SchlĂŒssel und den öffentlichen SchlĂŒssel des GesprĂ€chspartners ĂŒbergeben.
âââââââ âââââââ
âPeerAâ âPeerBâ
ââââŹâââ ââââŹâââ
â IdA, PubA â ââââââââââââââââââââââ
ââââââââââââââââ>â âPrvA, PubA = DHgen()â
â â ââââââââââââââââââââââ
â IdB, PubB â ââââââââââââââââââââââ
â<ââââââââââââââââ âPrvB, PubB = DHgen()â
â â ââââââââââââââââââââââ
âââââ âââââââââ§âââââââââââââ
â âKey = DH(PrvA, PubB)â
<ââââ âââââââââ€âââââââââââââ
â â
â â
Jeder kann dazwischenfunken und die öffentlichen SchlĂŒssel durch eigene ersetzen â in diesem Protokoll gibt es keine Authentifizierung der GesprĂ€chspartner. Wir fĂŒgen eine Signatur mit langlebigen SchlĂŒsseln hinzu.
âââââââ âââââââ
â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) â â
â âââââââââââââââââââââââ â
â â
Eine solche Signatur eignet sich nicht, da sie nicht an eine bestimmte Sitzung gebunden ist. Solche Nachrichten âpassenâ auch fĂŒr Sitzungen mit anderen Teilnehmern. Der gesamte Kontext muss signiert werden. Das zwingt uns auch dazu, eine weitere Nachricht von A zu senden.
DarĂŒber hinaus ist es entscheidend, die eigene Identifikationsnummer unter die Signatur zu setzen, da wir sonst IdXXX ersetzen und die Nachricht mit dem SchlĂŒssel eines anderen bekannten GesprĂ€chspartners neu signieren können. Zur Verhinderung von , es ist notwendig, dass die Elemente unter der Beschriftung an klar definierten Orten entsprechend ihrem Sinn platziert sind: wenn A (PubA, PubB) signiert, dann muss B (PubB, PubA) signieren. Das weist auch auf die Wichtigkeit der Auswahl der Struktur und des Formats der serialisierten Daten hin. Zum Beispiel werden Mengen im ASN.1 DER-Codierung sortiert: SET OF(PubA, PubB) ist identisch mit SET OF(PubB, PubA).
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââ ââââŹâââ â IdA, PubA â âââââââââââââââââââââââââââââ âââââââââââââââââââââââââââââââââââââââââââââ>â âSignPrvA, SignPubA = load()â â â âPrvA, PubA = DHgen() â â â âââââââââââââââââââââââââââââ âIdB, PubB, sign(SignPrvB, (IdB, PubA, PubB)) â âââââââââââââââââââââââââââââ ââ âverify(SignPubB, ...)â â â âKey = DH(PrvA, PubB) â â â âââââââââââââââââââââââ â â
Wir haben jedoch immer noch nicht âbewiesenâ, dass wir fĂŒr diese Sitzung denselben gemeinsamen SchlĂŒssel entwickelt haben. Im Grunde genommen könnte man diesen Schritt auch auslassen â die erste Transportnachricht wĂ€re ungĂŒltig, aber wir möchten sicher sein, dass nach Abschluss des Handshakes alles tatsĂ€chlich abgestimmt ist. Derzeit haben wir das ISO/IEC IS 9798-3 Protokoll zur VerfĂŒgung.
Wir könnten auch den generierten SchlĂŒssel selbst signieren. Das ist riskant, da es nicht ausgeschlossen ist, dass es im verwendeten Signaturalgorithmus Lecks geben kann (auch wenn es sich um Bits zur Signatur handelt, sind es dennoch Lecks). Man könnte den Hash des generierten SchlĂŒssels signieren, aber selbst ein Leak des Hashes des generierten SchlĂŒssels könnte bei Brute-Force-Angriffen auf die SchlĂŒsselgenerierungsfunktion wertvoll sein. SIGMA verwendet eine MAC-Funktion, die den Absender-Identifikator authentifiziert.
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââ ââââŹâââ â IdA, PubA â âââââââââââââââââââââââââââââ ââââââââââââââââââââââââââââââââââââââââââââââââââ>â âSignPrvA, SignPubA = load()â â â âPrvA, PubA = DHgen() â â â âââââââââââââââââââââââââââââ âIdB, PubB, sign(SignPrvB, (PubA, PubB)), MAC(IdB) â âââââââââââââââââââââââââââââ ââ âverify(Key, IdB) â â â âverify(SignPubB, ...)â â â âââââââââââââââââââââââ â â
Als Optimierung möchten einige vielleicht ihre ephemeral SchlĂŒssel wiederverwenden (was natĂŒrlich fĂŒr PFS nachteilig ist). Zum Beispiel haben wir ein SchlĂŒsselpaar generiert, versucht uns zu verbinden, aber TCP war nicht verfĂŒgbar oder brach irgendwo mitten im Protokoll ab. Es wĂ€re schade, die aufgebrachte Entropie und die Prozessorressourcen fĂŒr ein neues Paar zu verschwenden. Daher fĂŒhren wir sozusagen ein Cookie ein â einen pseudorandomisierten Wert, der vor möglichen Replay-Angriffen beim Wiederverwenden ephemeral öffentlicher SchlĂŒssel schĂŒtzt. Aufgrund der Bindung zwischen dem Cookie und dem ephemeral öffentlichen SchlĂŒssel kann der öffentliche SchlĂŒssel des anderen Teilnehmers aus der Signatur entfernt werden, da er nicht mehr benötigt wird.
âââââââ âââââââ â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, ...)â â â âââââââââââââââââââââââ â â
Letztendlich wollen wir die PrivatsphĂ€re unserer Identifikatoren gegenĂŒber einem passiven Beobachter wahren. Zu diesem Zweck schlĂ€gt SIGMA vor, zunĂ€chst ephemeral SchlĂŒssel auszutauschen, um einen gemeinsamen SchlĂŒssel zu erarbeiten, mit dem authentifizierende und identifizierende Nachrichten verschlĂŒsselt werden. SIGMA beschreibt zwei Varianten:
- SIGMA-I â schĂŒtzt den Initiator vor aktiven Angriffen und den Antwortenden vor passiven: Der Initiator authentifiziert den Antwortenden und gibt seine Identifizierung nicht preis, wenn etwas nicht stimmt. Der Antwortende hingegen gibt seine Identifizierung preis, wenn er in ein aktives Protokoll eingebunden wird. Ein passiver Beobachter erfĂ€hrt nichts;
SIGMA-R â schĂŒtzt den Antwortenden vor aktiven Angriffen und den Initiator vor passiven. Alles ist genau umgekehrt, aber in diesem Protokoll werden bereits vier Nachrichten im Handshake ĂŒbertragen.Wir wĂ€hlen SIGMA-I, da es mehr mit dem ĂŒbereinstimmt, was wir von gewohnten Client-Server-Szenarien erwarten: Nur der authentifizierte Server erkennt den Client, wĂ€hrend der Server bereits alles weiĂ. AuĂerdem ist es aufgrund der geringeren Anzahl an Handshake-Nachrichten einfacher in der Implementierung. Alles, was wir in das Protokoll einfĂŒgen, ist die VerschlĂŒsselung eines Teils der Nachricht und der Transfer der ID A in den verschlĂŒsselten Teil der letzten Nachricht:
âââââââ âââââââ â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, ...)â â â âââââââââââââââââââââââ â â
- FĂŒr die Signatur wird GOST R verwendet. Der Algorithmus verwendet 256-Bit-SchlĂŒssel.
- FĂŒr die Ableitung des gemeinsamen SchlĂŒssels wird 34.10-2012 VKO verwendet.
- Als MAC wird CMAC verwendet. Technisch handelt es sich um einen speziellen Betriebsmodus eines Blockchiffriers, der in GOST R 34.13-2015 beschrieben ist. FĂŒr diesen Modus wird â (34.12-2015).
- Als Identifikator des GesprĂ€chspartners wird der Hash seines öffentlichen SchlĂŒssels verwendet. FĂŒr den Hash wird (34.11-2012 256 Bit) verwendet.
Nach dem Handshake wird unser gemeinsamer SchlĂŒssel vereinbart. Diesen können wir fĂŒr die authentifizierte VerschlĂŒsselung von Transportnachrichten verwenden. Dieser Teil ist ganz einfach und dabei kaum fehleranfĂ€llig: Wir inkrementieren den NachrichtenzĂ€hler, verschlĂŒsseln die Nachricht, authentifizieren (MAC) den ZĂ€hler und den Chiffretext und senden dann. Beim Empfang der Nachricht ĂŒberprĂŒfen wir, ob der ZĂ€hler den erwarteten Wert hat, authentifizieren den Chiffretext mit dem ZĂ€hler und entschlĂŒsseln. Mit welchem SchlĂŒssel sollen wir die Handshake-, Transportnachrichten verschlĂŒsseln und authentifizieren? Einen SchlĂŒssel fĂŒr all diese Aufgaben zu verwenden, ist riskant und unklug. Es ist notwendig, SchlĂŒssel zu erzeugen, indem spezialisierte Funktionen verwendet werden (SchlĂŒsselerzeugungsfunktion). Noch einmal, wir werden es uns nicht schwer machen und etwas erfinden: ist seit langem bekannt, gut erforscht und hat keine bekannten Probleme. Leider gibt es in der Standardbibliothek von Python keine solche Funktion, daher verwenden wir Paket. HKDF verwendet dabei , das seinerseits eine Hash-Funktion verwendet. Ein Beispiel fĂŒr eine Implementierung in Python auf der Wikipedia-Seite umfasst nur wenige Zeilen Code. Wie bei 34.10-2012 werden wir als Hash-Funktion Streebog-256 verwenden. Der Ausgang unserer SchlĂŒsselvereinbarungsfunktion wird als SitzungsschlĂŒssel bezeichnet, aus dem die fehlenden symmetrischen SchlĂŒssel erzeugt werden:
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")Strukturen/Schemata
Betrachten wir, welche ASN.1-Strukturen wir jetzt fĂŒr die Ăbertragung all dieser Daten erhalten haben:
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 â das, was unterschrieben werden soll (to be signed). HandshakeTBE â das, was verschlĂŒsselt werden soll (to be encrypted). Ich weise auf das Feld ukm in MsgHandshake1 hin. 34.10 VKO, zur weiteren Randomisierung der generierten SchlĂŒssel, enthĂ€lt den Parameter UKM (user keying material) â einfach zusĂ€tzliche Entropie.
HinzufĂŒgen von Kryptographie zum Code
Betrachten wir nur die Ănderungen, die am ursprĂŒnglichen Code vorgenommen wurden, da das GerĂŒst unverĂ€ndert geblieben ist (tatsĂ€chlich wurde zuerst die endgĂŒltige Implementierung geschrieben und dann die gesamte Kryptographie daraus entfernt).
Da die Authentifizierung und Identifizierung der GesprĂ€chspartner ĂŒber öffentliche SchlĂŒssel durchgefĂŒhrt wird, mĂŒssen diese nun langfristig irgendwo gespeichert werden. Zur Vereinfachung verwenden wir JSON in folgender Form:
{ "our": { "prv": "21254cf66c15e0226ef2669ceee46c87b575f37f9000272f408d0c9283355f98", "pub": "938c87da5c55b27b7f332d91b202dbef2540979d6ceaa4c35f1b5bfca6df47df0bdae0d3d82beac83cec3e353939489d9981b7eb7a3c58b71df2212d556312a1" }, "their": { "alice": "d361a59c25d2ca5a05d21f31168609deeec100570ac98f540416778c93b2c7402fd92640731a707ec67b5410a0feae5b78aeec93c4a455a17570a84f2bc21fce", "bob": "aade1207dd85ecd283272e7b69c078d5fae75b6e141f7649ad21962042d643512c28a2dbdc12c7ba40eb704af920919511180c18f4d17e07d7f5acd49787224a" } }our â unser SchlĂŒsselpaar, hexadezimale private und öffentliche SchlĂŒssel. their â die Namen der GesprĂ€chspartner und deren öffentliche SchlĂŒssel. Ăndern wir die Argumente der Befehlszeile und fĂŒgen eine Nachbearbeitung der JSON-Daten hinzu:
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="JSON mit unserem neuen SchlĂŒsselpaar generieren", ) parser.add_argument( "--keys", default="keys.json", required=False, help="JSON mit unseren und ihren SchlĂŒsseln", ) parser.add_argument( "--bind", default="::1", help="Adresse, um zuzuhören", ) parser.add_argument( "--port", type=int, default=6666, help="Port, um zuzuhören", ) 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) # SchlĂŒsseldaten parsen und unmarshallen {{{ 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), } # }}}Der private SchlĂŒssel des Algorithmus 34.10 ist eine Zufallszahl. Er hat eine GröĂe von 256 Bit fĂŒr 256-Bit elliptische Kurven. PyGOST arbeitet nicht mit einem Byte-Array, sondern mit , daher muss unser privater SchlĂŒssel (urandom(32)) in eine Zahl umgewandelt werden, indem wir gost3410.prv_unmarshal() verwenden. Der öffentliche SchlĂŒssel wird deterministisch aus dem privaten SchlĂŒssel berechnet, indem wir gost3410.public_key() verwenden. Der öffentliche SchlĂŒssel 34.10 besteht aus zwei groĂen Zahlen, die ebenfalls zur Vereinfachung der Speicherung und Ăbertragung in eine Bytefolge umgewandelt werden mĂŒssen, unter Verwendung von gost3410.pub_marshal().
Nach dem Lesen der JSON-Datei mĂŒssen die öffentlichen SchlĂŒssel entsprechend mit gost3410.pub_unmarshal() zurĂŒckgewandelt werden. Da wir die Identifikatoren der GesprĂ€chspartner in Form eines Hashs des öffentlichen SchlĂŒssels erhalten, können diese sofort vorausberechnet und in ein Dictionary fĂŒr eine schnelle Suche eingefĂŒgt werden. Der Streibog-256-Hash ist gost34112012256.GOST34112012256(), der den Anforderungen des hashlib-Schnittstelle fĂŒr Hashfunktionen vollstĂ€ndig entspricht.
Wie hat sich die Coroutine des Initiators geĂ€ndert? Alles lĂ€uft wie im Handshake-Schema: Wir generieren ein Cookie (128 Bit sind vollkommen ausreichend), ein ephemeral key pair 34.10, das fĂŒr die VKO-Funktion zur SchlĂŒsselvereinbarung verwendet wird.
395 async def initiator(host, port): 396 _id = repr((host, port)) 397 logging.info("%s: wĂ€hlt", _id) 398 reader, writer = await asyncio.open_connection(host, port) 399 # Generiere unseren temporĂ€ren öffentlichen SchlĂŒssel und Cookie, sende Handshake 0 Nachricht {{{ 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()- Wir warten auf die Antwort und dekodieren die empfangene Msg-Nachricht;
- Wir stellen sicher, dass wir handshake1 erhalten haben;
- Wir dekodieren den temporĂ€ren öffentlichen SchlĂŒssel der Gegenpartei und berechnen den SitzungsschlĂŒssel;
- Wir erzeugen die symmetrischen SchlĂŒssel, die zur Verarbeitung des TBE-Teils der Nachricht erforderlich sind.
423 logging.info("%s: erhielt %s Nachricht", _id, msg.choice) 424 if msg.choice != "handshake1": 425 logging.warning("%s: unerwartete Nachricht, trennen", _id) 426 writer.close() 427 return 428 # }}} 429 msg_handshake1 = msg.value 430 # Validieren der Handshake-Nachricht {{{ 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 ist eine 64-Bit-Zahl (urandom(8)), die ebenfalls aus ihrer byte-reprĂ€sentation deserialisiert werden muss, unter Verwendung von gost3410_vko.ukm_unmarshal(). Die VKO-Funktion fĂŒr 34.10-2012 256-Bit ist gost3410_vko.kek_34102012256() (KEK â SchlĂŒsselverschlĂŒsselungsschlĂŒssel).
Der erzeugte SitzungsschlĂŒssel ist bereits eine 256-Bit-byte pseudorandom sequenz. Daher kann er sofort in der HKDF-Funktion verwendet werden. Da GOST34112012256 das hashlib-Interface erfĂŒllt, kann er sofort in der Hkdf-Klasse verwendet werden. Das Salt (erster Argument von Hkdf) geben wir nicht an, da der erzeugte SchlĂŒssel aufgrund der TemporĂ€rheit der beteiligten SchlĂŒsselpaare fĂŒr jede Sitzung unterschiedlich sein wird und bereits genĂŒgend Entropie enthĂ€lt. kdf.expand() gibt standardmĂ€Ăig bereits die benötigten 256-Bit-SchlĂŒssel fĂŒr den Kuznechik aus.
AnschlieĂend werden die TBE- und TBS-Teile der empfangenen Nachricht ĂŒberprĂŒft:
- Es wird ein MAC ĂŒber den empfangenen Chiffretext berechnet und ĂŒberprĂŒft;
- Der Chiffretext wird entschlĂŒsselt;
- Die TBE-Struktur wird dekodiert;
- Daraus wird die Identifikation des GesprĂ€chspartners entnommen und ĂŒberprĂŒft, ob wir ihn ĂŒberhaupt kennen;
- Es wird ein MAC ĂŒber diese Identifikation berechnet und ĂŒberprĂŒft;
- Die Signatur ĂŒber der TBS-Struktur wird ĂŒberprĂŒft, in die die Cookies beider Parteien und der öffentliche temporĂ€re SchlĂŒssel der Gegenseite eingehen. Die Signatur wird mit dem langlebigen SchlĂŒssel des GesprĂ€chspartners ĂŒberprĂŒft.
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("ungĂŒltiges 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("kann TBE nicht decodieren") 151 key_sign_pub_hash = bytes(tbe["identity"]) 152 peer = KEYS.get(key_sign_pub_hash) 153 if peer is None: 154 raise ValueError("unbekannte IdentitĂ€t") 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("ungĂŒltiges IdentitĂ€ts-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("ungĂŒltige Signatur") 174 return peer["name"]Wie bereits oben erwĂ€hnt, beschreibt 34.13-2015 verschiedene aus 34.12-2015. Darunter gibt es den Modus zur Erzeugung von MACs. In PyGOST wird dies durch gost3413.mac() realisiert. Dieser Modus erfordert die Ăbergabe einer VerschlĂŒsselungsfunktion (die einen Block Daten entgegennimmt und zurĂŒckgibt), die GröĂe des Geheimblocks und schlieĂlich der Daten selbst. Warum kann die GröĂe des Geheimblocks nicht hardcodiert werden? 34.12-2015 beschreibt nicht nur die 128-Bit-Chiffre Kuznechik, sondern auch die 64-Bit- â ein leicht modifiziertes GOST 28147-89, das noch zu Zeiten des KGB geschaffen wurde und immer noch einen der höchsten Sicherheitsstandards hat.
Der Katalog wird mit gost.3412.GOST3412Kuznechik(key) initialisiert und gibt ein Objekt mit den Methoden .encrypt() und .decrypt() zurĂŒck, die fĂŒr die Ăbergabe an die 34.13-Funktion geeignet sind. Der MAC wird wie folgt berechnet: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). Zum Vergleich des berechneten und empfangenen MACs dĂŒrfen ĂŒbliche Vergleiche (==) von Byte-Strings nicht verwendet werden, da diese Operation Zeitvergleiche leckt, was im Allgemeinen zu schwerwiegenden SicherheitsanfĂ€lligkeiten fĂŒhren kann. Angriffe auf TLS. In Python gibt es eine spezielle Funktion hmac.compare_digest dafĂŒr.
Die BlockverschlĂŒsselungsfunktion kann nur einen Datenblock verschlĂŒsseln. FĂŒr gröĂere Mengen, insbesondere solche, die keine Vielfachen der BlockgröĂe sind, muss der VerschlĂŒsselungsmodus verwendet werden. In 34.13-2015 sind die folgenden beschrieben: ECB, CTR, OFB, CBC, CFB. Jeder hat seine eigenen zulĂ€ssigen Anwendungsbereiche und Eigenschaften. Leider verfĂŒgen wir nach wie vor nicht ĂŒber standardisierte (wie CCM, OCB, GCM und Ă€hnliche) â wir sind gezwungen, zumindest MAC selbst hinzuzufĂŒgen. Ich wĂ€hle (CTR): Er erfordert keine AuffĂŒllung auf die BlockgröĂe, kann parallelisiert werden, verwendet nur die VerschlĂŒsselungsfunktion und kann sicher zum VerschlĂŒsseln groĂer Nachrichtenmengen verwendet werden (im Gegensatz zu CBC, bei dem relativ schnell Kollisionen auftreten).
Wie .mac() nimmt .ctr() Ă€hnliche Daten als Eingabe: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Ein Initialisierungsvektor muss angegeben werden, der genau die HĂ€lfte der BlockgröĂe umfasst. Wenn unser VerschlĂŒsselungsschlĂŒssel nur fĂŒr die VerschlĂŒsselung einer Nachricht verwendet wird (auch wenn diese aus mehreren Blöcken besteht), kann ein Nullvektor fĂŒr die Initialisierung sicher angegeben werden. FĂŒr die VerschlĂŒsselung von Handshake-Nachrichten verwenden wir jedes Mal einen separaten SchlĂŒssel.
Die SignaturĂŒberprĂŒfung gost3410.verify() ist trivial: Wir ĂŒbergeben die elliptische Kurve, innerhalb derer wir arbeiten (diese fixieren wir einfach in unserem GOSTIM-Protokoll), den öffentlichen SchlĂŒssel des Unterzeichners (wir dĂŒrfen nicht vergessen, dass dies ein Tupel aus zwei groĂen Zahlen sein muss, nicht ein Byte-String), den 34.11-2012 Hash und die tatsĂ€chlich empfangene Signatur.
AnschlieĂend bereiten wir im Initiator die handshake2-Nachricht vor und senden sie ab, wobei wir die gleichen Schritte wie bei der ĂberprĂŒfung durchfĂŒhren, jedoch symmetrisch: eine Signatur auf unseren SchlĂŒsseln anstelle der ĂberprĂŒfung usw.
456 # Bereite die Handshake-2-Nachricht vor und sende sie {{{ 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: Sitzung hergestellt: %s", _id, peer_name)Wenn die Sitzung hergestellt ist, werden die Transportkey (ein separater SchlĂŒssel fĂŒr die VerschlĂŒsselung, fĂŒr die Authentifizierung, fĂŒr jede der Parteien) generiert und Kuznechik fĂŒr die EntschlĂŒsselung und MAC-PrĂŒfung initialisiert:
499 # Starte den Textnachrichtensender, initialisiere den Transportdecoder {{{ 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 # Warte auf Testnachrichten {{{ 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 # }}}Die msg_sender-Koroutine verschlĂŒsselt nun Nachrichten, bevor sie ĂŒber die TCP-Verbindung gesendet werden. Jede Nachricht hat einen monoton wachsenden nonce, der auch als Initialisierungsvektor beim VerschlĂŒsseln im ZĂ€hlermodus dient. FĂŒr jede Nachricht und Nachrichtenblock sind unterschiedliche ZĂ€hlerwerte garantiert.
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 += 1Eingehende Nachrichten werden von der Coroutine msg_receiver verarbeitet, die sich mit der Authentifizierung und Dechiffrierung beschÀftigt:
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("unerwarteter Nonce-Wert") mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])): raise ValueError("ungĂŒltiger 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)Fazit
GOSTIM ist ausschlieĂlich zu Bildungszwecken gedacht (da es zumindest nicht getestet ist)! Der Quellcode des Programms kann heruntergeladen werden (Stribog-256 Hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Wie alle meine Projekte, ist , , , , GOSTIM ist vollstĂ€ndig , die unter den Bedingungen von .
, , Mitglied , Python/Go-Entwickler, Hauptspezialist .
Quelle: habr.com
