Som utvecklare av biblioteket (GOST-kryptografiska primitiva funktioner i ren Python) får jag ofta frågor om hur man snabbt kan implementera den enklaste säkra meddelandeutbytet. Många anser att tillämpad kryptografi är ganska enkel och att ett .encrypt()-anrop från en blockchiffer är tillräckligt för en säker överföring över kommunikationskanalen. Andra anser att tillämpad kryptografi är för ett fåtal och det är acceptabelt att rika företag som Telegram med sina matematiska olympiader ett säkert protokoll.
Allt detta har motiverat mig att skriva denna artikel för att visa att implementeringen av kryptografiska protokoll och säker IM inte är en så svår uppgift. Dock är det inte värt att hitta på egna autentiserings- och nyckelöverenskommelseprotokoll.

I artikeln kommer det att skrivas , , instant messenger med autentiserings- och nyckelöverenskommelseprotokoll (på vilket ) är baserat, som endast använder GOST-kryptografiska algoritmer från PyGOST-biblioteket och ASN.1 kodning av meddelanden från biblioteket (om vilken jag redan har ). Ett krav är att det ska vara så enkelt att det går att skriva från grunden på en kväll (eller arbetsdag), annars är det ingen enkel programvara. Det kommer säkert finnas fel, onödiga komplikationer och brister, plus att detta är mitt första program som använder asyncio-biblioteket.
Design av IM
För att börja med, måste vi förstå hur vår IM kommer att se ut. För enkelhetens skull låt det vara ett peer-to-peer-nätverk, utan någon upptäcktsmekanism för deltagarna. Vi kommer manuellt att ange vilken adress:port vi ansluter till för att kommunicera med samtalspartnern.
Jag förstår att, i nuläget, antagandet av tillgången till direkt kommunikation mellan två godtyckliga datorer är en betydande begränsning av tillämpningen av IM i praktiken. Men ju fler utvecklare som implementerar olika NAT-traversal-lösningar, desto längre kommer vi att förbli i IPv4 Internet, med en dyster sannolikhet för kommunikation mellan godtyckliga datorer. Hur länge kan vi tolerera avsaknaden av IPv6 hemma och på jobbet?
Vi kommer att ha ett friend-to-friend nätverk: alla möjliga samtalspartners måste vara kända i förväg. För det första förenklar det allt avsevärt: introducerat, hittade eller hittade inte namn/nyckel, kopplade bort eller fortsätter arbete, med vetskap om samtalspartnern. För det andra är det generellt sett säkert och utesluter många attacker.
IM-gränssnittet kommer att likna klassiska lösningar , som jag verkligen gillar för sin minimalism och Unix-way filosofi. IM-programmet skapar en katalog för varje samtalspartner med tre Unix-domänsockets:
- in — som tar emot meddelanden som skickas till samtalspartnern;
- out — som tar emot meddelanden från samtalspartnern;
- state — genom att läsa från den, får vi veta om samtalspartnern är ansluten, adress/port för anslutningen.
Dessutom skapas en conn-socket, där vi skriver värdporten för att initiera en anslutning till den fjärranslutne samtalspartnern.
|-- alice
| |-- in
| |-- out
| `-- state
|-- bob
| |-- in
| |-- out
| `-- state
`- conn
Denna metod tillåter oberoende implementationer av IM-transport och användargränssnitt, eftersom smaken är olika, och det är omöjligt att tillfredsställa alla. Genom att använda och/eller , kan man få ett flervindigt gränssnitt med syntaktisk belysning. Och med hjälp av kan man få en GNU Readline-kompatibel rad för meddelandeinmatning.
I själva verket använder suckless-projekt FIFO-filer. Personligen har jag inte lyckats förstå hur man arbetar med filer konkurrensmässigt i asyncio utan en egen handgjord uppsättning av dedikerade trådar (för sådana saker har jag länge använt språket ). Därför bestämde jag mig för att nöja mig med Unix-domänsockets. Tyvärr utesluter det möjligheten att göra echo 2001:470:dead::babe 6666 > conn. Jag löste detta problem genom att använda : echo 2001:470:dead::babe 6666 | socat — UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.
Den ursprungliga osäkra protokollen
Använder TCP som transport: det garanterar leverans och dess ordning. UDP garanterar varken det ena eller det andra (vilket skulle vara användbart när kryptografi tillämpas), och stöd för finns inte i Python från början.
Tyvärr finns det inget begrepp för meddelanden i TCP, bara en ström av byte. Därför måste vi komma på ett format för meddelanden så att de kan separeras i denna ström. Vi kan komma överens om att använda radbrytning som separator. Det fungerar i början, men när vi börjar kryptera våra meddelanden kan detta tecken dyka upp var som helst i krypterad text. Därför är protokoll som först skickar längden på meddelandet i byte populära i nätverk. Till exempel, i Python finns det ett inbyggt bibliotek, xdrlib, som låter oss arbeta med sådana format. .
Vi kommer inte att arbeta korrekt och effektivt med TCP-läsning — vi förenklar koden. Vi läser data från socketen i en oändlig loop tills vi dekoderar hela meddelandet. JSON eller XML kan användas som format för detta tillvägagångssätt. Men när kryptering läggs till måste data signeras och autentiseras — och detta kräver en byte-för-byte identisk representation av objekten, vilket inte tillhandahålls av JSON/XML (resultat av dumpningar kan skilja sig).
XDR passar för denna uppgift, men jag väljer ASN.1 med DER-kodning och biblioteket, eftersom vi kommer att ha hög-nivå objekt som ofta är trevligare och enklare att arbeta med. Till skillnad från schemaless , eller , ASN.1 kommer automatiskt att kontrollera data mot en hårt definierad schema.
# 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))),
))
Det mottagna meddelandet kommer att vara Msg: antingen ett textmeddelande MsgText (tills vidare med ett enda textfält), eller handskakningsmeddelande MsgHandshake (där namnet på konversatören överförs). Det ser för tillfället överkomplicerat ut, men det är en grund för framtiden.
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│MsgHandshake(IdA) │
│─────────────────>│
│ │
│MsgHandshake(IdB) │
││
│ │
│ MsgText() │
│<─────────────────│
│ │
IM utan kryptering
Som jag redan nämnt, kommer asyncio-biblioteket att användas för alla operationer med sockets. Vi deklarerar vad vi förväntar oss vid starttillfället:
parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
"--our-name",
required=True,
help="Vårt kamratnamn",
)
parser.add_argument(
"--their-names",
required=True,
help="Deras kamratnamn, kommaseparerade",
)
parser.add_argument(
"--bind",
default="::1",
help="Adress att lyssna på",
)
parser.add_argument(
"--port",
type=int,
default=6666,
help="Port att lyssna på",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))
Ange eget namn (—our-name alice). Lista alla förväntade samtalspartner genom kommatecken (—their-names bob,eve). För varje samtalspartner skapas en katalog med Unix-sockets samt en korutin för varje 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"))
Meddelanden från användaren som kommer in via in-socket skickas till IN_QUEUES köer:
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"))
Meddelanden från samtalspartnerna skickas till OUT_QUEUES köer, därifrån skrivs data till out-socket:
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()
Vid läsning från state-socket letar programmet i PEER_ALIVE-ordboken efter samtalspartners adress. Om det ännu inte finns någon anslutning till samtalspartnern, skrivs en tom sträng.
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()
Vid skrivning av adressen till conn-socket startar funktionen "initiator" för anslutningen:
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))
Låt oss titta på initiatorn. Först öppnar han tydligt en anslutning till den angivna värden/porten och skickar ett handshake-meddelande med sitt namn:
130 async def initiator(host, port):
131 _id = repr((host, port))
132 logging.info("%s: ringar", _id)
133 reader, writer = await asyncio.open_connection(host, port)
134 # Handshake-meddelande {{{
135 writer.write(Msg(("handshake", MsgHandshake((
136 ("peerName", OUR_NAME),
137 )))).encode())
138 # }}}
139 await writer.drain()
Därefter väntar den på svar från den fjärran sidan. Försöker avkoda det mottagna svaret enligt Msg ASN.1 schemat. Anta att hela meddelandet kommer att skickas i en enda TCP-segment och vi får det atomärt vid anrop av .read(). Vi kontrollerar att vi faktiskt har fått ett handshake-meddelande.
141 # Vänta på Handshake-meddelande {{{
142 data = await reader.read(256)
143 if data == b"":
144 logging.warning("%s: inget svar, kopplar ifrån", _id)
145 writer.close()
146 return
147 try:
148 msg, _ = Msg().decode(data)
149 except ASN1Error:
150 logging.warning("%s: icke-dekodat svar, kopplar ifrån", _id)
151 writer.close()
152 return
153 logging.info("%s: fick %s meddelande", _id, msg.choice)
154 if msg.choice != "handshake":
155 logging.warning("%s: oväntat meddelande, kopplar ifrån", _id)
156 writer.close()
157 return
158 # }}}
Kontrollerar att det mottagna namnet på samtalspartnern är känt för oss. Om inte, så bryter vi kopplingen. Kontrollerar om vi redan har etablerat en anslutning med dem (samtalspartnern har återigen begärt anslutning) och stänger den. I IN_QUEUES-kön placeras Python-strängar med meddelandetexten, men det finns ett särskilt värde None som signalerar till msg_sender-korutinen att den ska sluta arbeta, så att den glömmer sitt writer kopplat till den föråldrade TCP-anslutningen.
159 msg_handshake = msg.value
160 peer_name = str(msg_handshake["peerName"])
161 if peer_name not in THEIR_NAMES:
162 logging.warning("okänt namn på partner: %s", peer_name)
163 writer.close()
164 return
165 logging.info("%s: session etablerad: %s", _id, peer_name)
166 # Kör textmeddelande-sändare, initiera transportdecoder {{{
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 tar emot utgående meddelanden (som läggs in i kön från in-socket), serialiserar dem till MsgText-meddelanden och skickar dem över TCP-anslutningen. Den kan brytas när som helst — detta fångar vi tydligt.
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: skickade %d tecken meddelande", peer_name, len(text))
I slutet går initiatorn in i en oändlig loop för att läsa meddelanden från socketen. Den kontrollerar om det är textmeddelanden och placerar dem i OUT_QUEUES kön, från vilken de kommer att skickas till ut socketen för motsvarande samtalspartner. Varför kan man inte bara använda .read() och avkoda meddelandet? För att det är möjligt att flera meddelanden från användaren ackumuleras i operativsystemets buffert och skickas som ett enda TCP-segment. Vi kan avkoda det första, men det kan fortfarande finnas en del av det följande i bufferten. Vid vilken oegentlighet som helst stänger vi TCP-anslutningen och stoppar msg_sender-korutin (genom att skicka None till OUT_QUEUES kön).
174 buf = b""
175 # Vänta på testmeddelanden {{{
176 while True:
177 data = await reader.read(MaxMsgLen)
178 if data == b"":
179 break
180 buf += data
181 if len(buf) > MaxMsgLen:
182 logging.warning("%s: max buffer size exceeded", _id)
183 break
184 try:
185 msg, tail = Msg().decode(buf)
186 except ASN1Error:
187 continue
188 buf = tail
189 if msg.choice != "text":
190 logging.warning("%s: unexpected %s message", _id, msg.choice)
191 break
192 try:
193 await msg_receiver(msg.value, peer_name)
194 except ValueError as err:
195 logging.warning("%s: %s", err)
196 break
197 # }}}
198 logging.info("%s: disconnecting: %s", _id, peer_name)
199 IN_QUEUES[peer_name].put(None)
200 writer.close()
66 async def msg_receiver(msg_text: MsgText, peer_name: str) -> None:
67 text = str(msg_text["text"])
68 logging.info("%s: received %d characters message", peer_name, len(text))
69 await OUT_QUEUES[peer_name].put(text)
Låt oss återgå till huvudkoden. Efter att alla korutiner har skapats vid programmets start, startar vi en TCP-server. För varje etablerad anslutning skapar den en responder-korutin.
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("Lyssnar på: %s", server.sockets[0].getsockname())
loop.run_forever()
responder är liknande initiatorn och utför exakt samma åtgärder, men den oändliga loopen för att läsa meddelanden startar omedelbart för enkelhetens skull. Just nu skickar handskakningsprotokollet ett meddelande från varje sida, men i framtiden kommer det att vara två från initiatorn av anslutningen, efter vilket textmeddelanden kan skickas omedelbart.
72 async def responder(reader, writer):
73 _id = writer.get_extra_info("peername")
74 logging.info("%s: connected", _id)
75 buf = b""
76 msg_expected = "handshake"
77 peer_name = None
78 while True:
79 # Read until we get Msg message {{{
80 data = await reader.read(MaxMsgLen)
81 if data == b"":
82 logging.info("%s: closed connection", _id)
83 break
84 buf += data
85 if len(buf) > MaxMsgLen:
86 logging.warning("%s: max buffer size exceeded", _id)
87 break
88 try:
89 msg, tail = Msg().decode(buf)
90 except ASN1Error:
91 continue
92 buf = tail
93 # }}}
94 if msg.choice != msg_expected:
95 logging.warning("%s: unexpected %s message", _id, msg.choice)
96 break
97 if msg_expected == "text":
98 try:
99 await msg_receiver(msg.value, peer_name)
100 except ValueError as err:
101 logging.warning("%s: %s", err)
102 break
103 # Process Handshake message {{{
104 elif msg_expected == "handshake":
105 logging.info("%s: got %s message", _id, msg_expected)
106 msg_handshake = msg.value
107 peer_name = str(msg_handshake["peerName"])
108 if peer_name not in THEIR_NAMES:
109 logging.warning("unknown peer name: %s", peer_name)
110 break
111 writer.write(Msg(("handshake", MsgHandshake((
112 ("peerName", OUR_NAME),
113 )))).encode())
114 await writer.drain()
115 logging.info("%s: session established: %s", _id, peer_name)
116 peer_alive = PEER_ALIVES.pop(peer_name, None)
117 if peer_alive is not None:
118 peer_alive.close()
119 await IN_QUEUES[peer_name].put(None)
120 PEER_ALIVES[peer_name] = writer
121 asyncio.ensure_future(msg_sender(peer_name, writer))
122 msg_expected = "text"
123 # }}}
124 logging.info("%s: disconnecting", _id)
125 if msg_expected == "text":
126 IN_QUEUES[peer_name].put(None)
127 writer.close()
Säkert protokoll
Det är dags att säkra vår kommunikation. Vad menar vi med säkerhet och vad vill vi:
- konfidentialitet för skickade meddelanden;
- autenticitet och integritet för skickade meddelanden — förändringar ska upptäckas;
- skydd mot återspelningattacker (replay attack) — förlust eller upprepning av meddelanden ska upptäckas (och vi bestämmer oss för att bryta anslutningen);
- identifiering och autentisering av samtalspartners via fördefinierade publika nycklar — vi har redan bestämt att skapa ett vän-till-vän-nätverk. Endast efter autentisering vet vi vem vi kommunicerar med;
- tillgång egenskaper (PFS) — kompromittering av vår långlivade signeringsnyckel ska inte leda till att hela tidigare kommunikation kan läsas. Inspelning av avlyssnad trafik blir meningslös;
- Giltighet av meddelanden (transport och handskakningar) endast inom en TCP-session. Införandet av korrekt signerade/autentiserade meddelanden från en annan session (även med samma part) bör inte vara möjligt;
- En passiv observatör ska varken se användaridentifierare, långlivade offentliga nycklar eller hashvärden från dem. Någon form av anonymitet gentemot den passiva observatören.
Uppseendeväckande nog är det här minimumet något som nästan alla vill ha i vilken handsshake-protokoll som helst, och mycket lite av det som listas uppfylls till slut för "hemgjorda" protokoll. Låt oss inte uppfinna något nytt just nu. Jag skulle definitivt rekommendera att använda för att bygga protokoll, men vi väljer något enklare.
De två mest populära protokollen är:
- — ett komplicerat protokoll med en lång historia av buggar, misstag, sårbarheter, bristande genomtänkthet, komplexitet och brister (även om det knappt rör TLS 1.3). Men vi överväger inte det på grund av dess överkomplexitet.
- med — har inga allvarliga kryptografiska problem, även om de inte är enkla. Om man läser om IKEv1 och IKEv2, så är deras ursprung , ISO/IEC IS 9798-3 och SIGMA (SIGn-and-MAc) protokoll — tillräckligt enkla att implementera på en kväll.
Vad är bra med SIGMA, som det sista steget i utvecklingen av STS/ISO-protokollen? Det uppfyller alla våra krav (inklusive "dölja" identiteterna hos parterna), har inga kända kryptografiska problem. Det är minimalistiskt — att ta bort ens en enda komponent från meddelandet i protokollet kommer att göra det osäkert.
Låt oss gå från den enklaste hemmagjorda protokollet till SIGMA. Den mest grundläggande operationen vi är intresserade av är : en funktion, där båda parter kommer att få samma värde, som kan användas som en symmetrisk nyckel. Utan att gå in på detaljer: varje part genererar ett ephemeral (används endast inom en session) nyckelpår (offentliga och privata nycklar), byter offentliga nycklar, anropar överenskommelsefunktionen, där man skickar in sin privata nyckel och den andra partens offentliga nyckel.
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│ IdA, PubA │ ╔════════════════════╗
│───────────────›│ ║PrvA, PubA = DHgen()║
│ │ ╚════════════════════╝
│ IdB, PubB │ ╔════════════════════╗
│‹───────────────│ ║PrvB, PubB = DHgen()║
│ │ ╚════════════════════╝
────┐ ╔═══════╧════════════╗
│ ║Key = DH(PrvA, PubB)║
‹───┘ ╚═══════╤════════════╝
│ │
│ │
Vem som helst kan blanda sig i och ersätta publika nycklar med sina egna — i detta protokoll finns det ingen autentisering av deltagarna. Vi lägger till en signatur med långlivade nycklar.
┌─────┐ ┌─────┐
│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) ║ │
│ ╚═════════════════════╝ │
│ │
En sådan signatur fungerar inte, eftersom den inte är kopplad till en specifik session. Sådana meddelanden "passar" även för sessioner med andra deltagare. Hela kontexten måste signeras. Detta tvingar oss också att lägga till ett meddelande från A.
Dessutom är det avgörande att lägga till en egen identifierare till signaturen, eftersom vi annars kan byta ut IdXXX och signera om meddelandet med nyckeln från en annan känd deltagare. För att förhindra , det är nödvändigt att elementen under underrubriken finns i klart definierade positioner enligt sin innebörd: om A signerar (PubA, PubB), så måste B signera (PubB, PubA). Detta talar också om vikten av att välja struktur och format för serialiserade data. Till exempel sorteras mängder i ASN.1 DER-kodning: SET OF(PubA, PubB) kommer att vara identiskt med 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) ║ │ │ ╚═════════════════════╝ │ │
Vi har dock fortfarande inte "bevisat" att vi har genererat en gemensam nyckel för denna session. I princip kan vi klara oss utan detta steg – det första transportmeddelandet kommer att vara ogiltigt, men vi vill försäkra oss om att när handskakningen är klar så är allt verkligen överenskommet. För närvarande har vi ISO/IEC IS 9798-3 protokollet i vår hand.
Vi skulle kunna signera själva den genererade nyckeln. Detta är farligt eftersom det inte kan uteslutas att det används en signaturalgoritm där läckor kan förekomma (även om det är bitar-för-signatur, men ändå läckor). Man kan signera hashen av den genererade nyckeln, men en läcka av hashen från den genererade nyckeln kan vara värdefull vid brute-force attacker på nyckelgeneratorn. SIGMA använder en MAC-funktion för att autentisera avsändarens identifierare.
┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └──┬──┘ │ IdA, PubA │ ╔═══════════════════════════╗ │─────────────────────────────────────────────────>│ ║SignPrvA, SignPubA = load()║ │ │ ║PrvA, PubA = DHgen() ║ │ │ ╚═══════════════════════════╝ │IdB, PubB, sign(SignPrvB, (PubA, PubB)), MAC(IdB) │ ╔═══════════════════════════╗ ││ ║verify(Key, IdB) ║ │ │ ║verify(SignPubB, ...)║ │ │ ╚═════════════════════╝ │ │
Som vissa kan önska återanvända sina ephemeral-nycklar (vilket, förstås, är dåligt för PFS), kan vi ge ett exempel: vi har genererat ett nyckelpar, försökt ansluta, men TCP var inte tillgängligt eller bröts någonstans mitt under protokollet. Det är synd att slösa bort den entropi och processorkraft på ett nytt par. Därför inför vi, så kallad, cookie — ett pseudo-slumptal som skyddar mot möjliga oavsiktliga replay-attacker vid återanvändning av efemära offentliga nycklar. På grund av bindningen mellan cookie och den efemära offentliga nyckeln, kan den offentliga nyckeln från motparten tas bort från signaturen eftersom den inte längre är nödvändig.
┌─────┐ ┌─────┐ │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, ...)║ │ │ ╚═════════════════════╝ │ │
Slutligen vill vi skydda våra samtalspartners identifieringsuppgifter från passiva observatörer. För detta syfte föreslår SIGMA att vi först byter ephemeral nycklar, och sedan genererar en gemensam nyckel som används för att kryptera autentiserings- och identifieringsmeddelanden. SIGMA beskriver två alternativ:
- SIGMA-I — skyddar initiativtagaren från aktiva attacker och svararen från passiva: initiativtagaren autentiserar svararen och om något inte stämmer, avslöjar han inte sin egen identifiering. Svararen å sin sida avslöjar sin identifiering om en aktiv protokollavtal inleds. En passiv observatör får ingen information;
SIGMA-R — skyddar svararen från aktiva attacker, initiativtagaren från passiva. Allt är omvändat, men i detta protokoll involveras redan fyra handskakningsmeddelanden.Vi väljer SIGMA-I eftersom den liknar det vi förväntar oss av traditionella klient-server-lösningar: klienten känner endast till den autentiserade servern, medan servern redan vet allt. Dessutom är den enklare att implementera på grund av ett mindre antal handskakningsmeddelanden. Allt vi lägger till i protokollet är kryptering av en del av meddelandet och överföring av identifieraren A till den krypterade delen av det sista meddelandet:
┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └──┬──┘ │ PubA, CookieA │ ╔═══════════════════════════╗ │─────────────────────────────────────────────────────────────────────────────>│ ║SignPrvA, SignPubA = load()║ │ │ ║PrvA, PubA = DHgen() ║ │ │ ╚═══════════════════════════╝ │PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB))) │ ╔═══════════════════════════╗ │<─────────────────────────────────────────────────────────────────────────────│ ║SignPrvB, SignPubB = load()║ │ │ ║PrvB, PubB = DHgen() ║ │ │ ╚═══════════════════════════╝ │ │ ╔═════════════════════╗ │ Enc((IdA, sign(SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA))) │ ║Key = DH(PrvA, PubB) ║ │─────────────────────────────────────────────────────────────────────────────>│ ║verify(Key, IdB) ║ │ │ ║verify(SignPubB, ...)║ │ │ ╚═════════════════════╝ │ │
- För signering används GOST R algoritm med 256-bitars nycklar.
- För att generera en gemensam nyckel används 34.10-2012 VKO.
- Som MAC används CMAC. Tekniskt sett är detta ett särskilt arbetsläge för en blockchiffer, beskrivet i GOST R 34.13-2015. Som krypteringsfunktion för detta läge — (34.12-2015).
- Som hash från den offentliga nyckeln används som identifierare för motparten. För hash används (34.11-2012 256 bit).
Efter handskakningen kommer vi att ha en gemensam nyckel. Den kan vi använda för autentiserad kryptering av transportmeddelanden. Denna del är helt enkel och det är svårt att göra fel: vi inkrementerar meddelandets räknare, krypterar meddelandet, autentiserar (MAC) räknaren och krypterad text, och skickar. Vid mottagning av meddelandet kontrollerar vi att räknaren har det förväntade värdet, autentiserar den krypterade texten med räknaren och dekrypterar. Vilken nyckel ska användas för att kryptera handske-, transportmeddelanden och vilken för autentisering? Att använda en nyckel för alla dessa uppgifter är farligt och oklokt. Det är nödvändigt att generera nycklar, med hjälp av specialiserade funktioner (key derivation function). Återigen, låt oss inte komplicera det och uppfinna något: är välkänt, välundersökt och har inga kända problem. Tyvärr finns det inte denna funktion i den inhemska Python-biblioteket, så vi använder paketet. HKDF använder inuti , som i sin tur använder hash-funktionen. Ett exempel på implementation i Python på Wikipedia tar några få rader kod. Som i fallet med 34.10-2012 kommer vi att använda Streebog-256 som hash-funktion. Utdata från vår nyckelavtalsfunktion kommer att kallas sessionsnyckel, från vilken de saknade symmetriska kommer att genereras:
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")Strukturer/scheman
Låt oss överväga vilka ASN.1-strukturer vi nu fått för att överföra alla dessa data:
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 - det som kommer att signeras (to be signed). HandshakeTBE - det som kommer att krypteras (to be encrypted). Observera fältet ukm i MsgHandshake1. 34.10 VKO, för att öka slumpmässigheten i de genererade nycklarna, inkluderar parametern UKM (user keying material) - helt enkelt extra entropi.
Tillägg av kryptografi i koden
Vi kommer endast att betrakta de förändringar som gjorts i den ursprungliga koden, eftersom ramverket förblir detsamma (i verkligheten skrevs först den slutliga implementationen och sedan togs all kryptografi bort).
Eftersom autentisering och identifiering av parterna kommer att göras via offentliga nycklar, måste de nu lagras på något långsiktigt sätt. För enkelhetens skull använder vi JSON av följande typ:
{ "our": { "prv": "21254cf66c15e0226ef2669ceee46c87b575f37f9000272f408d0c9283355f98", "pub": "938c87da5c55b27b7f332d91b202dbef2540979d6ceaa4c35f1b5bfca6df47df0bdae0d3d82beac83cec3e353939489d9981b7eb7a3c58b71df2212d556312a1" }, "their": { "alice": "d361a59c25d2ca5a05d21f31168609deeec100570ac98f540416778c93b2c7402fd92640731a707ec67b5410a0feae5b78aeec93c4a455a17570a84f2bc21fce", "bob": "aade1207dd85ecd283272e7b69c078d5fae75b6e141f7649ad21962042d643512c28a2dbdc12c7ba40eb704af920919511180c18f4d17e07d7f5acd49787224a" } }our - vårt nyckelpaar, hexadecimala privata och offentliga nycklar. their - namnen på parterna och deras offentliga nycklar. Låt oss ändra kommandoradsargumenten och lägga till efterbearbetning av JSON-data:
från pygost importera gost3410 från pygost.gost34112012256 importera 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="Generera JSON med vårt nya nyckelpar", ) parser.add_argument( "--keys", default="keys.json", required=False, help="JSON med våra och deras nycklar", ) parser.add_argument( "--bind", default="::1", help="Adress att lyssna på", ) parser.add_argument( "--port", type=int, default=6666, help="Port att lyssna på", ) 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({ "våra": {"prv": hexenc(prv_raw), "pub": hexenc(pub_raw)}, "deras": {}, })) exit(0) # Parsar och avmarshalar våra och deras nycklar {{{ with open(args.keys, "rb") as fd: _keys = json.loads(fd.read().decode("utf-8")) KEY_OUR_SIGN_PRV = gost3410.prv_unmarshal(hexdec(_keys["våra"]["prv"])) _pub = hexdec(_keys["våra"]["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["deras"].items(): _pub = hexdec(pub_raw) KEYS[GOST34112012256(_pub).digest()] = { "namn": peer_name, "pub": gost3410.pub_unmarshal(_pub), } # }}}Den privata nyckeln för 34.10-algoritmen är ett slumpmässigt nummer. Den är 256 bitar stor för 256-bits elliptiska kurvor. PyGOST arbetar inte med en byte-sträng utan med , så vår privata nyckel (urandom(32)) måste omvandlas till ett nummer med hjälp av gost3410.prv_unmarshal(). Den offentliga nyckeln beräknas deterministiskt från den privata med hjälp av gost3410.public_key(). Den offentliga nyckeln för 34.10 består av två stora nummer som också måste omvandlas till en byte-sträng för att underlätta lagring och överföring med hjälp av gost3410.pub_marshal().
Efter att ha läst JSON-filen måste de offentliga nycklarna omvandlas tillbaka med hjälp av gost3410.pub_unmarshal(). Eftersom vi kommer att få identifieringar för samarbetspartnerna i form av en hash från den offentliga nyckeln kan vi förbereda och placera dem i en ordlista för snabb sökning. Stribog-256 hash är gost34112012256.GOST34112012256(), som helt uppfyller hashlib-gränssnittet för hash-funktioner.
Hur har initiatorens korutin förändrats? Allt är som i schemat för handskakning: vi genererar en cookie (128 bitar är mer än tillräckligt), en tillfällig nyckelpar för 34.10 som kommer att användas för VKO-funktionen för nyckelöverenskommelse.
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 # Generera vår temporära offentliga nyckel och cookie, skicka Handshake 0-meddelande {{{ 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()- väntar på svar och avkodar det mottagna Msg-meddelandet;
- ser till att vi får handshake1;
- avkodar den temporära offentliga nyckeln från den andra parten och beräknar sessionsnyckeln;
- genererar symmetriska nycklar som behövs för att bearbeta TBE-delen av meddelandet.
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 # Validera Handshake-meddelande {{{ 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 är ett 64-bitarsnummer (urandom(8)) som också kräver deserialisering från byte-representation, med hjälp av gost3410_vko.ukm_unmarshal(). VKO-funktionen för 34.10-2012 256-bit är gost3410_vko.kek_34102012256() (KEK — key encryption key).
Den genererade sessionsnyckeln är redan en 256-bitars bytes pseudo-slumptalssekvens. Därför kan den omedelbart användas i HKDF-funktionen. Eftersom GOST34112012256 uppfyller hashlib-gränssnittet kan den omedelbart användas i Hkdf-klassen. Saltet (första argumentet i Hkdf) anges inte, eftersom den genererade nyckeln på grund av temporära delaktiga nyckelpar kommer att vara olika för varje session och den har redan tillräckligt med entropi. kdf.expand() ger per standard nycklar med en längd av 256 bitar, som krävs för Kuznetsen i framtiden.
Därefter kontrolleras TBE- och TBS-delen av det mottagna meddelandet:
- beräknar och kontrollerar MAC över den mottagna ciphertext;
- avkodar ciphertext;
- avkodar TBE-strukturen;
- därifrån hämtas identifieraren för motparten och det kontrolleras om den är känd för oss;
- beräknar och kontrollerar MAC över denna identifierare;
- Signaturen över TBS-strukturen kontrolleras, vilken inkluderar cookies från båda parter och den offentliga temporära nyckeln från den andra parten. Signaturen kontrolleras med hjälp av en långlivad signeringsnyckel från motparten.
441 try: 442 peer_name = validate_tbe( 443 msg_handshake1, 444 key_handshake1_mac_identity, 445 key_handshake1_enc, 446 key_handshake1_mac, 447 cookie_our, 448 cookie_their, 449 pub_their_raw, 450 ) 451 except ValueError as err: 452 logging.warning("%s: %s, disconnecting", _id, err) 453 writer.close() 454 return 455 # }}} 128 def validate_tbe( 129 msg_handshake: Union[MsgHandshake1, MsgHandshake2], 130 key_mac_identity: bytes, 131 key_enc: bytes, 132 key_mac: bytes, 133 cookie_their: Cookie, 134 cookie_our: Cookie, 135 pub_key_our: PubKey, 136 ) -> str: 137 ciphertext = bytes(msg_handshake["ciphertext"]) 138 mac_tag = mac(GOST3412Kuznechik(key_mac).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext) 139 if not compare_digest(mac_tag, bytes(msg_handshake["ciphertextMac"])): 140 raise ValueError("invalid MAC") 141 plaintext = ctr( 142 GOST3412Kuznechik(key_enc).encrypt, 143 KUZNECHIK_BLOCKSIZE, 144 ciphertext, 145 8 * b"x00", 146 ) 147 try: 148 tbe, _ = HandshakeTBE().decode(plaintext) 149 except ASN1Error: 150 raise ValueError("can not decode TBE") 151 key_sign_pub_hash = bytes(tbe["identity"]) 152 peer = KEYS.get(key_sign_pub_hash) 153 if peer is None: 154 raise ValueError("unknown identity") 155 mac_tag = mac( 156 GOST3412Kuznechik(key_mac_identity).encrypt, 157 KUZNECHIK_BLOCKSIZE, 158 key_sign_pub_hash, 159 ) 160 if not compare_digest(mac_tag, bytes(tbe["identityMac"])): 161 raise ValueError("invalid identity MAC") 162 tbs = HandshakeTBS(( 163 ("cookieTheir", cookie_their), 164 ("cookieOur", cookie_our), 165 ("pubKeyOur", pub_key_our), 166 )) 167 if not gost3410.verify( 168 CURVE, 169 peer["pub"], 170 GOST34112012256(tbs.encode()).digest(), 171 bytes(tbe["signature"]), 172 ): 173 raise ValueError("invalid signature") 174 return peer["name"]Som nämnts tidigare beskriver 34.13-2015 olika från 34.12-2015. Bland dessa finns lägen för generering av MAC och beräkning av MAC. I PyGOST är detta gost3413.mac(). Denna läge kräver att krypteringsfunktionen (som tar emot och returnerar ett block av data), blockstorleken och själva datan överförs. Varför kan man inte hardcoda blockstorleken? 34.12-2015 beskriver inte bara 128-bits Kuznechik-kryptering, utan även 64-bits — en något modifierad GOST 28147-89, skapad ännu under KGB och som fortfarande har en av de högsta säkerhetströsklarna.
Gräshoppa initieras gost.3412.GOST3412Kuznechik(key) genom anrop och returnerar ett objekt med .encrypt()/.decrypt() metoder, lämpliga för överföring i 34.13 funktioner. MAC beräknas som följande: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). För att jämföra den beräknade och mottagna MAC:en kan man inte använda vanlig jämförelse (==) av byte-strängar, eftersom denna operation ger tidsläckage vid jämförelse, vilket i allmänhet kan leda till fatala sårbarheter av typ attacker mot TLS. I Python finns det en speciell hmac.compare_digest funktion för detta.
Blockchifferfunktionen kan endast kryptera ett block data åt gången. För fler block, särskilt de vars längd inte är en multipel, måste man använda en krypteringsläge. I 34.13-2015 beskrivs följande: ECB, CTR, OFB, CBC, CFB. Varje läge har sina egna tillåtna användningsområden och egenskaper. Tyvärr har vi fortfarande inte standardiserade (som CCM, OCB, GCM och liknande) — vi är tvungna att åtminstone lägga till MAC själva. Jag väljer (CTR): det kräver ingen padding till blockstorleken, kan parallelliseras, använder endast krypteringsfunktionen och kan användas säkert för att kryptera ett stort antal meddelanden (till skillnad från CBC, där kollisioner uppstår relativt snabbt).
Precis som .mac(), .ctr() tar liknande data som ingång: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Det krävs att ange ett initialiseringsvektor, vars längd är exakt hälften av chifferblockstorleken. Om vår krypteringsnyckel används endast för att kryptera ett meddelande (även om det innehåller flera block), är det säkert att ange en nollställande initialiseringsvektor. För kryptering av handshake-meddelanden används varje gång en separat nyckel.
Signaturverifiering gost3410.verify() är trivial: vi skickar elliptiska kurvan vi arbetar inom (vilken vi bara fastställer i vårt GOSTIM-protokoll), offentliga nyckeln för undertecknaren (kom ihåg att detta måste vara en tuple av två stora tal, inte en byte-sträng), 34.11-2012 hash och själva mottagna signaturen.
Sedan förbereder vi och skickar handshake2 meddelandet av handskakning, genom att utföra samma åtgärder som vi gjorde vid verifieringen, men symmetriskt: signatur på våra nycklar istället för verifiering, osv…
456 # Förbered och skicka Handshake 2 meddelande {{{ 457 tbs = HandshakeTBS(( 458 ("cookieTheir", cookie_their), 459 ("cookieOur", cookie_our), 460 ("pubKeyOur", pub_our_raw), 461 )) 462 signature = gost3410.sign( 463 CURVE, 464 KEY_OUR_SIGN_PRV, 465 GOST34112012256(tbs.encode()).digest(), 466 ) 467 key_handshake2_mac_identity = kdf.expand(b"handshake2-mac-identity") 468 mac_tag = mac( 469 GOST3412Kuznechik(key_handshake2_mac_identity).encrypt, 470 KUZNECHIK_BLOCKSIZE, 471 bytes(KEY_OUR_SIGN_PUB_HASH), 472 ) 473 tbe = HandshakeTBE(( 474 ("identity", KEY_OUR_SIGN_PUB_HASH), 475 ("signature", OctetString(signature)), 476 ("identityMac", MAC(mac_tag)), 477 )) 478 tbe_raw = tbe.encode() 479 key_handshake2_enc = kdf.expand(b"handshake2-enc") 480 key_handshake2_mac = kdf.expand(b"handshake2-mac") 481 ciphertext = ctr( 482 GOST3412Kuznechik(key_handshake2_enc).encrypt, 483 KUZNECHIK_BLOCKSIZE, 484 tbe_raw, 485 8 * b"x00", 486 ) 487 mac_tag = mac( 488 GOST3412Kuznechik(key_handshake2_mac).encrypt, 489 KUZNECHIK_BLOCKSIZE, 490 ciphertext, 491 ) 492 writer.write(Msg(("handshake2", MsgHandshake2(( 493 ("ciphertext", OctetString(ciphertext)), 494 ("ciphertextMac", MAC(mac_tag)), 495 )))).encode()) 496 # }}} 497 await writer.drain() 498 logging.info("%s: session established: %s", _id, peer_name)När sessionen har etablerats skapas transportnycklar (en separat nyckel för kryptering, för autentisering, för varje part) och Kuznechik initieras för dekryptering och MAC-verifiering:
499 # Kör textmeddelandesändaren, initiera transportavkodaren {{{ 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 # Vänta på testmeddelanden {{{ 521 while True: 522 data = await reader.read(MaxMsgLen) ... 530 msg, tail = Msg().decode(buf) ... 537 try: 538 await msg_receiver( 539 msg.value, 540 nonce_expected, 541 macer, 542 encrypter, 543 peer_name, 544 ) 545 except ValueError as err: 546 logging.warning("%s: %s", err) 547 break 548 nonce_expected += 1 549 # }}}msg_sender korutinen krypterar nu meddelanden innan de skickas över TCP-anslutningen. Varje meddelande har ett monotoniskt växande nonce, som också fungerar som initialiseringsvektor vid kryptering i räknarläge. Varje meddelande och meddelandeblock kommer garanterat att ha olika värden för räknaren.
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 += 1Mottagna meddelanden behandlas av korutinen msg_receiver, som hanterar autentisering och avkryptering:
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("oväntat nonce-värde") mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])): raise ValueError("ogiltig 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)Slutsats
GOSTIM avses att användas uteslutande för utbildningsändamål (då den inte är testad, åtminstone)! Källkoden till programmet kan laddas ner (Stribog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Precis som alla mina projekt, typ , , , , GOSTIM är helt , som distribueras under villkoren .
, , medlem av , Python/Go-utvecklare, huvudspecialist .
Källa: habr.com
