Som udvikler biblioteket (GOST kryptografiske primitiva i ren Python) fÄr jeg ofte spÞrgsmÄl om, hvordan man kan implementere en simpel sikker beskedudveksling pÄ egen hÄnd. Mange mener, at anvendt kryptografi er noget simpelt, og at et .encrypt() kald fra en blokkrypter er tilstrÊkkeligt til sikker afsendelse over kommunikationskanalen. Andre mener, at anvendt kryptografi er for de fÄ, og det er acceptabelt, at rige virksomheder som Telegram med matematikere fra olympiaderne en sikker protokol.
Alt dette har fÄet mig til at skrive denne artikel for at vise, at implementeringen af kryptografiske protokoller og sikker IM ikke er en sÄ vanskelig opgave. Men det er ikke vÊrd at finde pÄ egne autentifikations- og nÞgleudvekslingsprotokoller.

Artiklen vil beskrive , , instant messenger med autentifikations- og nÞgleudvekslingsprotokol (hvorpÄ ), der udelukkende bruger GOST kryptografiske algoritmer fra PyGOST biblioteket og ASN.1 kodning af meddelelserne fra biblioteket (som jeg allerede ). En grundlÊggende betingelse er: det skal vÊre sÄ enkelt, at det kan skrives fra bunden pÄ en aften (eller arbejdsdag), ellers er det ikke et simpelt program. Der vil helt sikkert vÊre fejl, unÞdvendige kompleksiteter, mangler, plus det er mit fÞrste program ved brug af asyncio biblioteket.
IM-designet
For at starte, skal vi forstÄ, hvordan vores IM vil se ud. For enkelhedens skyld, lad os antage, at det er et peer-to-peer netvÊrk uden nogen form for deltagende opdagelse. Vi vil selv angive, hvilken adresse: port vi skal oprette forbindelse til for at kommunikere med vores samtalepartner.
Jeg forstÄr, at i Þjeblikket, antagelsen om tilgÊngeligheden af en direkte forbindelse mellem to vilkÄrlige computere er en vÊsentlig begrÊnsning i anvendeligheden af IM i praksis. Men jo flere udviklere der implementerer forskellige NAT-traversal lÞsninger, jo lÊngere tid vil vi blive i IPv4 internettet med en nedslÄende sandsynlighed for forbindelse mellem vilkÄrlige computere. Hvor lÊnge kan vi holde ud at mangle IPv6 derhjemme og pÄ arbejdet?
Vi vil have et friend-to-friend netvÊrk: alle mulige samtalepartnere skal vÊre kendt pÄ forhÄnd. For det fÞrste forenkler det det hele betydeligt: vi prÊsenterer os, finder eller finder ikke et navn/nÞgle, afbryder forbindelsen eller fortsÊtter arbejdet, velvidende hvem samtalepartneren er. For det andet, i almindelighed, er det sikkert og udelukker mange angreb.
IM-grÊnsefladen vil vÊre tÊt pÄ klassiske lÞsninger , som jeg virkelig kan lide for deres minimalisme og Unix-way filosofi. IM-programmet opretter et bibliotek for hver samtalepartner med tre Unix-domÊnesokker:
- in â her skrives de beskeder, der sendes til samtalepartneren;
- out â her lĂŠses de beskeder, der modtages fra samtalepartneren;
- state â ved at lĂŠse herfra kan vi se, om samtalepartneren i Ăžjeblikket er tilsluttet, adresse/port for forbindelsen.
Derudover oprettes en conn sokkel, hvor vi ved at skrive vĂŠrtsport initierer forbindelsen til den fjerne samtalepartner.
|-- alice
| |-- in
| |-- out
| `-- state
|-- bob
| |-- in
| |-- out
| `-- state
`- conn
Denne tilgang tillader uafhÊngige implementationer af IM-transport og brugergrÊnseflade, da smag og farve ikke kan tilfredsstille alle. Ved at bruge og/eller , kan du fÄ en multi-vinduegrÊnseflade med syntaksfremhÊvning. Og med hjÊlp af kan man fÄ en GNU Readline-kompatibel linje til indtastning af beskeder.
I virkeligheden bruger suckless-projekterne FIFO-filer. Personligt kunne jeg ikke finde ud af, hvordan man arbejder med filer samtidigt i asyncio uden at lave en egen understĂžttelse med dedikerede trĂ„de (til sĂ„danne ting har jeg lĂŠnge brugt sproget ). Derfor besluttede jeg at klare mig med Unix-domĂŠnesokler. DesvĂŠrre fratager det muligheden for at bruge echo 2001:470:dead::babe 6666 > conn. Jeg lĂžste dette problem ved at bruge : echo 2001:470:dead::babe 6666 | socat â UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.
Den oprindelige usikre protokol
Som transport bruges TCP: det garanterer levering og rÊkkefÞlge. UDP garanterer hverken det ene eller det andet (hvilket ville vÊre nyttigt, nÄr kryptografi anvendes), og understÞttelse af findes ikke i Python som standard.
DesvÊrre findes der ikke begrebet besked i TCP, kun en strÞm af bytes. Derfor er det nÞdvendigt at finde et format til beskeder, sÄ de kan adskilles i denne strÞm. Vi kan aftale at bruge linjeskift-tegnet. Det er fint til at starte med, men nÄr vi begynder at kryptere vores beskeder, kan dette tegn komme hvor som helst i krypteringen. Derfor er protokoller, der fÞrst sender lÊngden af beskeden i bytes, populÊre i netvÊrk. For eksempel har Python som standard xdrlib, der gÞr det muligt at arbejde med et sÄdant format. .
Vi vil ikke arbejde korrekt og effektivt med TCP-lĂŠsning â lad os forsimple koden. Vi lĂŠser data fra soklen i en uendelig lĂžkke, indtil vi dekoder hele beskeden. JSON kan bruges som format til denne tilgang, sĂ„vel som XML. Men nĂ„r kryptografi tilfĂžjes, skal dataene underskrives og autentificeres â og det krĂŠver byte-for-byte identisk reprĂŠsentation af objekter, noget som JSON/XML ikke garanterer (dumps kan variere).
XDR er passende til en sÄdan opgave, men jeg vÊlger ASN.1 med DER-kodning og biblioteket, da vi vil arbejde med hÞjniveauobjekter, som ofte er lettere og mere bekvemme at arbejde med. I modsÊtning til schemaless , eller , ASN.1 vil automatisk kontrollere dataene mod en strengt defineret skema.
# 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))),
))
Den modtagne besked vil vÊre Msg: enten tekstbesked MsgText (for nu med et enkelt tekstfelt), eller en hÄndtrykbesked MsgHandshake (hvor navnene pÄ samtalepartnerne overfÞres). Det ser lidt overkompliceret ud nu, men det er en investering i fremtiden.
âââââââ âââââââ
âPeerAâ âPeerBâ
ââââŹâââ ââââŹâââ
âMsgHandshake(IdA) â
ââââââââââââââââââ>â
â â
âMsgHandshake(IdB) â
ââ
â â
â MsgText() â
â<ââââââââââââââââââ
â â
IM uden kryptografi
Som jeg allerede har sagt, vil asyncio-biblioteket blive brugt til alle operationer med sokler. Lad os erklĂŠre, hvad vi forventer ved opstart:
parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
"--our-name",
required=True,
help="Vores peer navn",
)
parser.add_argument(
"--their-names",
required=True,
help="Deres peer navne, adskilt med komma",
)
parser.add_argument(
"--bind",
default="::1",
help="Adresse at lytte pÄ",
)
parser.add_argument(
"--port",
type=int,
default=6666,
help="Port at lytte pÄ",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))
Selvnavn angives (âour-name alice). Alle forventede samtalepartnere listes op med komma (âtheir-names bob,eve). For hver samtalepartner oprettes et bibliotek med Unix-sockets samt en koroutine for hver 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"))
Modtagne beskeder fra brugeren sendes fra in-socket til IN_QUEUES kĂžerne:
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"))
Modtagne beskeder fra samtalepartnere sendes til OUT_QUEUES kĂžerne, fra hvilke data skrives til 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()
NÄr der lÊses fra state-socket, sÞger programmet i PEER_ALIVE ordbogen efter adressen til samtalepartneren. Hvis der endnu ikke er forbindelse til samtalepartneren, skrives en tom streng.
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()
NÄr adressen skrives til conn-socket, startes "initiator"-funktionen til forbindelsen:
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))
Lad os se pÄ initiatoren. FÞrst skal han Äbenlyst Äbne en forbindelse til den angivne vÊrt/port og sende en handshake-besked med sit navn:
130 async def initiator(host, port):
131 _id = repr((host, port))
132 logging.info("%s: ringer op", _id)
133 reader, writer = await asyncio.open_connection(host, port)
134 # Handshake besked {{{
135 writer.write(Msg(("handshake", MsgHandshake((
136 ("peerName", OUR_NAME),
137 )))).encode())
138 # }}}
139 await writer.drain()
Derefter venter pÄ svar fra den fjerne part. ForsÞger at dekode den modtagne svarmelodi i henhold til Msg ASN.1 skemaet. Antager at hele beskeden vil blive sendt i et enkelt TCP-segment, og vi modtager det atomisk ved at kalde .read(). Kontrollerer, at vi/modtager netop handshake-beskeden.
141 # Vent pÄ Handshake besked {{{
142 data = await reader.read(256)
143 if data == b"":
144 logging.warning("%s: intet svar, afbryder", _id)
145 writer.close()
146 return
147 try:
148 msg, _ = Msg().decode(data)
149 except ASN1Error:
150 logging.warning("%s: udecoderbart svar, afbryder", _id)
151 writer.close()
152 return
153 logging.info("%s: fik %s besked", _id, msg.choice)
154 if msg.choice != "handshake":
155 logging.warning("%s: uventet besked, afbryder", _id)
156 writer.close()
157 return
158 # }}}
Kontrollerer, at det modtagne navn fra samtalepartneren er kendt for os. Hvis ikke, sÄ afbryder vi forbindelsen. Kontrollerer, om vi har haft en forbindelse til dem fÞr (samtalepartneren har gentagne gange givet kommando til at connecte til os) og lukker den. I IN_QUEUES kÞen placeres Python-strenge med beskedens tekst, men der er en sÊrlig vÊrdi None, der signalerer msg_sender koroutine om at stoppe arbejdet, sÄ den glemmer sin writer, der er knyttet til det forÊldede TCP-forbindelse.
159 msg_handshake = msg.value
160 peer_name = str(msg_handshake["peerName"])
161 if peer_name not in THEIR_NAMES:
162 logging.warning("ukendt peer navn: %s", peer_name)
163 writer.close()
164 return
165 logging.info("%s: session etableret: %s", _id, peer_name)
166 # KĂžr tekstbesked afsender, initialiser transport dekoder {{{
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 modtager udgĂ„ende beskeder (indsat i kĂžen fra in-socket), serialiserer dem i MsgText besked og sender dem over TCP-forbindelsen. Den kan brydes nĂ„r som helst â det er noget, vi eksplicit fanger.
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: sendte %d tegns besked", peer_name, len(text))
Til sidst gÄr initiatoren ind i en uendelig loop for at lÊse meddelelser fra socketen. Den tjekker, om det er tekstbeskeder, og placerer dem i OUT_QUEUES-kÞen, hvorfra de vil blive sendt til out-socketen for den tilsvarende samtalepartner. Hvorfor kan man ikke blot gÞre .read() og afkode meddelelsen? Fordi der ikke kan udelukkes en situation, hvor flere meddelelser fra brugeren bliver samlet i operativsystemets buffer og sendt som et enkelt TCP-segment. Vi kan afkode den fÞrste, men derefter kan der vÊre en del af den efterfÞlgende i bufferen. Ved enhver unormal situation lukker vi TCP-forbindelsen og stopper msg_sender koroutinen (ved at sende None til OUT_QUEUES-kÞen).
174 buf = b""
175 # Vent pÄ testmeddelelser {{{
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: maksimal bufferstĂžrrelse overskredet", _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: uventet %s meddelelse", _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: frakobler: %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: modtaget %d tegns meddelelse", peer_name, len(text))
69 await OUT_QUEUES[peer_name].put(text)
Lad os vende tilbage til hovedkoden. Efter oprettelsen af alle koroutiner, nÄr programmet startes, starter vi TCP-serveren. For hver etableret forbindelse opretter den en responder (responsor) koroutine.
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("Lytter pÄ: %s", server.sockets[0].getsockname())
loop.run_forever()
responder ligner initiatoren og udfĂžrer spejlvendt alle de samme handlinger, men den uendelige loop for at lĂŠse meddelelser starter straks for enkelhedens skyld. I Ăžjeblikket sender handshake-protokollen en meddelelse fra hver side, men senere vil der blive sendt to fra initiatoren af forbindelsen, efterfulgt af umiddelbar mulighed for at sende tekst.
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()
Sikker protokol
Det er tid til at sikre vores kommunikation. Hvad mener vi egentlig med sikkerhed, og hvad Ăžnsker vi:
- fortrolighed for de overfĂžrte meddelelser;
- autenticitet og integritet af de overfĂžrte meddelelser - enhver ĂŠndring skal opdages;
- beskyttelse mod genafspilningsangreb (replay attack) - enhver forsvinden eller gentagelse af meddelelser skal opdages (og vi beslutter at afbryde forbindelsen);
- identifikation og autentifikation af samtalepartnere baseret pÄ forudindlagte offentlige nÞgler - vi har tidligere besluttet at lave et friend-to-friend-netvÊrk. Kun efter autentifikation forstÄr vi, hvem vi kommunikerer med;
- tilstedevÊrelse egenskaber (PFS) - kompromittering af vores langtidsholdbare signeringsnÞgle mÄ ikke fÞre til muligheden for at lÊse hele tidligere kommunikation. Optagelser af aflyttet trafik bliver ubrugelige;
- Gyldighed af meddelelser (transport og hÄndtryk) gÊlder kun inden for en enkelt TCP-session. IndsÊttelse af korrekt signerede/autentificerede meddelelser fra en anden session (selv med den samme modpart) bÞr ikke vÊre muligt;
- En passiv observatĂžr bĂžr ikke se hverken brugeridentifikatorer, langlivet offentlige nĂžgler eller hashes af dem. En slags anonymitet over for den passive observatĂžr.
Det er overraskende, men dette minimum Þnsker nÊsten alle i enhver hÄndtryksprotokol, og meget lidt af det nedenstÄende bliver faktisk overholdt for "hjemmelavede" protokoller. Lad os ikke finde pÄ noget nyt. Jeg vil bestemt anbefale at bruge til at opbygge protokoller, men lad os vÊlge noget simplere.
De to mest populĂŠre protokoller er:
- â en kompleks protokol med en lang historie af fejl, problemer, sĂ„rbarheder, dĂ„rlig planlĂŠgning, kompleksitet og mangler (dog gĂŠlder dette i meget lidt for TLS 1.3). Men vi overvejer ikke den pĂ„ grund af overkompleksitet.
- med â har ikke alvorlige kryptografiske problemer, selvom de heller ikke er enkle. Hvis man lĂŠser om IKEv1 og IKEv2, er deres oprindelse , ISO/IEC IS 9798-3 og SIGMA (SIGn-and-MAc) protokoller â der er enkle nok til at implementere pĂ„ en aften.
Hvad gĂžr SIGMA, som det sidste led i udviklingen af STS/ISO protokoller, godt? Det opfylder alle vores krav (inklusive "skjule" modparters identifikatorer), har ikke kendte kryptografiske problemer. Det er minimalistisk â fjernelse af bare Ă©t element fra protokolens besked vil gĂžre den usikker.
Lad os bevÊge os fra den simpleste hjemmelavede protokol til SIGMA. Den mest grundlÊggende operation, der interesserer os, er : en funktion, hvormed begge parter fÄr den samme vÊrdi, der kan bruges som en symmetrisk nÞgle. Uden at gÄ i detaljer: hver side genererer et ephemeral (kun brugt inden for en enkelt session) nÞglepar (offentlig og privat nÞgle), udveksler offentlige nÞgler, kalder en udvekslingsfunktion, hvor de sender deres private nÞgle og den offentlige nÞgle fra modparten som input.
âââââââ âââââââ
âPeerAâ âPeerBâ
ââââŹâââ ââââŹâââ
â IdA, PubA â ââââââââââââââââââââââ
ââââââââââââââââïŒâ âPrvA, PubA = DHgen()â
â â ââââââââââââââââââââââ
â IdB, PubB â ââââââââââââââââââââââ
âïŒââââââââââââââââ âPrvB, PubB = DHgen()â
â â ââââââââââââââââââââââ
âââââ âââââââââ§âââââââââââââ
â âKey = DH(PrvA, PubB)â
ïŒââââ âââââââââ€âââââââââââââ
â â
â â
Enhver kan blande sig ind og erstatte de offentlige nĂžgler med sine egne â i denne protokol er der ingen autentificering af samtalepartnere. Lad os tilfĂžje en signatur med langtidsholdbare nĂžgler.
âââââââ âââââââ
â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 vil ikke vÊre tilstrÊkkelig, da den ikke er knyttet til en specifik session. SÄdanne meddelelser vil vÊre "tilstrÊkkelige" for sessioner med andre deltagere. Hele konteksten skal underskrives. Dette tvinger ogsÄ til at tilfÞje en ny meddelelse fra A.
Desuden er det kritisk at tilfÞje sin egen identifikator til signaturen, da vi ellers kan Êndre IdXXX og genskrive meddelelsen med en anden kendt samtaleparts nÞgle. For at forhindre , det er nÞdvendigt, at elementerne under underskriften er placeret prÊcist i henhold til deres betydning: hvis A underskriver (PubA, PubB), sÄ skal B underskrive (PubB, PubA). Dette understreger ogsÄ vigtigheden af at vÊlge strukturen og formatet af de serialiserede data. For eksempel bliver mÊngder i ASN.1 DER-kodning sorteret: SET OF(PubA, PubB) vil vÊre identisk 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) â â â âââââââââââââââââââââââ â â
Men vi har stadig ikke "bevist", at vi har udarbejdet den samme fĂŠlles nĂžgle til denne session. Man kan teknisk set undvĂŠre dette trin â den fĂžrste transportmeddelelse ville vĂŠre ugyldig, men vi Ăžnsker, at nĂ„r handshake er afsluttet, er vi sikre pĂ„, at alt faktisk er blevet aftalt. I Ăžjeblikket har vi ISO/IEC IS 9798-3 protokollen i vores hĂŠnder.
Vi kunne ogsÄ underskrive den genererede nÞgle. Dette er farligt, da det ikke er udelukket, at den anvendte underskriftsalgoritme kan have lÊkager (selvom det er bits-til-underskrift, men stadig lÊkager). Man kan underskrive hashen af den genererede nÞgle, men lÊkagen af selv hashen fra den genererede nÞgle kan have vÊrdi i en brute-force attack pÄ generate-funktionen. SIGMA bruger en MAC-funktion, der autentificerer afsenderens identitet.
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââ ââââŹâââ â IdA, PubA â âââââââââââââââââââââââââââââ âââââââââââââââââââââââââââââââââââââââââââââââââââșâ âSignPrvA, SignPubA = load()â â â âPrvA, PubA = DHgen() â â â âââââââââââââââââââââââââââââ âIdB, PubB, sign(SignPrvB, (PubA, PubB)), MAC(IdB) â âââââââââââââââââââââââââââââ ââčââââââââââââââââââââââââââââââââââââââââââââââââââ âSignPrvB, SignPubB = load()â â â âPrvB, PubB = DHgen() â â â âââââââââââââââââââââââââââââ â â âââââââââââââââââââââââ â sign(SignPrvA, (PubB, PubA)), MAC(IdA) â âKey = DH(PrvA, PubB) â âââââââââââââââââââââââââââââââââââââââââââââââââââșâ âverify(Key, IdB) â â â âverify(SignPubB, ...)â â â âââââââââââââââââââââââ â â
Som nogle kan have interesse i at genbruge deres ephemeral nĂžgler som en optimering (hvilket selvfĂžlgelig er skadelig for PFS). For eksempel har vi genereret et nĂžglepar, forsĂžgt at oprette forbindelse, men TCP var ikke tilgĂŠngelig eller afbrudt et sted midt i protokollen. Det er en skam at spilde den brugte entropi og CPU-ressourcer pĂ„ et nyt par. Derfor introducerer vi et sĂ„kaldt cookie â en pseudorandom vĂŠrdi, der beskytter mod mulige tilfĂŠldige angreb ved gentagen brug af ephemeral offentlige nĂžgler. PĂ„ grund af bindingen mellem cookie og den ephemeral offentlige nĂžgle, kan den offentlige nĂžgle fra den modsatte deltager fjernes fra signaturen uden behov.
âââââââ âââââââ â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, ...)â â â âââââââââââââââââââââââ â â
Endelig Þnsker vi at beskytte vores identiteters privatliv mod passive observatÞrer. For dette foreslÄr SIGMA fÞrst at udveksle ephemeral nÞgler, skabe en fÊlles nÞgle, som kan kryptere autentificerende og identificerende meddelelser. SIGMA beskriver to muligheder:
- SIGMA-I â beskytter initiatoren mod aktive angreb, og respondenten mod passive: initiatoren autentificerer respondenten, og hvis der er noget galt, videregiver han ikke sin identifikation. Respondenten videregiver sin identifikation, hvis der startes et aktivt protokol. Passive observatĂžrer vil ikke lĂŠre noget;
SIGMA-R â beskytter respondenten mod aktive angreb, initiatoren mod passive. Alt er omvendt, men i denne protokol overfĂžres der allerede fire beskeder under hĂ„ndtrykket.Vi vĂŠlger SIGMA-I, da det minder mere om, hvad vi forventer af klient-server-ordninger: klienten kender kun den autentificerede server, mens serveren kender alt. Derudover er det lettere at implementere pĂ„ grund af det lavere antal hĂ„ndtrykmeddelelser. Alt, hvad vi tilfĂžjer til protokollen, er kryptering af dele af meddelelsen og overfĂžrsel af identifikatoren A til den krypterede del af den sidste meddelelse:
âââââââ âââââââ â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, ...)â â â âââââââââââââââââââââââ â â
- Til signering anvendes GOST R algoritmen med 256-bit nĂžgler.
- Til generering af den fĂŠlles nĂžgle anvendes 34.10-2012 VKO.
- Som MAC anvendes CMAC. Tekniskt set er dette en sĂŠrlig driftstilstand af en blokchiffer, beskrevet i GOST R 34.13-2015. Som krypteringsfunktion for denne tilstand â (34.12-2015).
- Som identificering af modparten anvendes en hash af hans offentlige nĂžgle. Hashen anvendes som (34.11-2012 256 bit).
Efter hÄndtrykket vil vi have en aftalt fÊlles nÞgle. Denne kan vi bruge til autentificeret kryptering af transportsignaler. Denne del er ganske enkel og svÊr at fÄ galt: vi inkrementerer meddelelses tÊlleren, krypterer meddelelsen, autentificerer (MAC) tÊlleren og krypteringen, og sender. Ved modtagelse af meddelelsen tjekker vi, at tÊlleren har den forventede vÊrdi, autentificerer krypteringen med tÊlleren, og dekrypterer. Hvilken nÞgle skal vi bruge til at kryptere hÄndtryksmeddelelser, transportmeddelelser og hvilken til autentificering? At bruge én nÞgle til alle disse opgaver er farligt og irrationelt. Det er nÞdvendigt at generere nÞgler med specialiserede funktioner (key derivation function). Igen, lad os ikke gÞre det kompliceret eller finde pÄ noget: er lÊnge kendt, godt undersÞgt og har ingen kendte problemer. DesvÊrre findes denne funktion ikke i den indbyggede Python-bibliotek, sÄ vi bruger pakken. HKDF bruger internt , som igen bruger hash-funktionen. Et eksempel pÄ implementering i Python pÄ Wikipedia's side fylder kun fÄ linjer kode. Ligesom i tilfÊlde med 34.10-2012 vil vi bruge Stribog-256 som hash-funktion. Udbuddet af vores nÞgleaftalefunktion vil blive kaldt sessionsnÞglen, fra hvilken de manglende symmetriske nÞgler vil blive udledt:
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/skejer
Lad os se, hvilke ASN.1 strukturer vi nu har fÄet til transmission af alle disse 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, der skal underskrives (to be signed). HandshakeTBE â det, der skal krypteres (to be encrypted). Jeg vil gerne pĂ„pege feltet ukm i MsgHandshake1. 34.10 VKO, for at Ăžge randomiseringen af de genererede nĂžgler, omfatter parameteren UKM (user keying material) â bare ekstra entropi.
TilsĂŠtning af kryptografi til koden
Lad os kun overveje de ĂŠndringer, der er foretaget i den originale kode, da rammen forblev den samme (faktisk blev den endelige implementering skrevet fĂžrst, og derefter blev al kryptografi fjernet fra den).
Da autentificering og identifikation af kommunikationspartnere vil blive udfĂžrt via offentlige nĂžgler, skal de nu opbevares et langt stykke tid et sted. For enkelhedens skyld bruger vi JSON af fĂžlgende type:
{ "our": { "prv": "21254cf66c15e0226ef2669ceee46c87b575f37f9000272f408d0c9283355f98", "pub": "938c87da5c55b27b7f332d91b202dbef2540979d6ceaa4c35f1b5bfca6df47df0bdae0d3d82beac83cec3e353939489d9981b7eb7a3c58b71df2212d556312a1" }, "their": { "alice": "d361a59c25d2ca5a05d21f31168609deeec100570ac98f540416778c93b2c7402fd92640731a707ec67b5410a0feae5b78aeec93c4a455a17570a84f2bc21fce", "bob": "aade1207dd85ecd283272e7b69c078d5fae75b6e141f7649ad21962042d643512c28a2dbdc12c7ba40eb704af920919511180c18f4d17e07d7f5acd49787224a" } }our â vores nĂžglepar, hexadecimale private og offentlige nĂžgler. their â kommunikatorernes navne og deres offentlige nĂžgler. Lad os ĂŠndre kommandolinjeargumenterne og tilfĂžje postbehandling af JSON-data:
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="Generer JSON med vores nye nÞglepar", ) parser.add_argument( "--keys", default="keys.json", required=False, help="JSON med vores og deres nÞgler", ) parser.add_argument( "--bind", default="::1", help="Adresse at lytte pÄ", ) parser.add_argument( "--port", type=int, default=6666, help="Port at lytte 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({ "our": {"prv": hexenc(prv_raw), "pub": hexenc(pub_raw)}, "their": {}, })) exit(0) # Parse og afmonter vores og deres nÞgler {{{ 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), } # }}}Den private nÞgle for algoritmen 34.10 er et tilfÊldigt tal. Den har en stÞrrelse pÄ 256 bit for 256-bit elliptiske kurver. PyGOST arbejder ikke med et sÊt bytes, men med , derfor skal vores private nÞgle (urandom(32)) konverteres til et tal ved hjÊlp af gost3410.prv_unmarshal(). Den offentlige nÞgle beregnes deterministisk fra den private nÞgle ved hjÊlp af gost3410.public_key(). Den offentlige nÞgle 34.10 bestÄr af to store tal, som ogsÄ skal konverteres til en byte-sekvens for nem opbevaring og overfÞrsel ved hjÊlp af gost3410.pub_marshal().
Efter lÊsning af JSON-filen skal de offentlige nÞgler konverteres tilbage ved hjÊlp af gost3410.pub_unmarshal(). Da vi vil modtage identifikatorer for samtalepartnere i form af hash fra den offentlige nÞgle, kan de beregnes pÄ forhÄnd og placeres i et ordforrÄd for hurtig sÞgning. Streebog-256 hash er gost34112012256.GOST34112012256(), som fuldt ud opfylder hashlib interface for hash-funktioner.
Hvordan er initiatorkoroutinen Êndret? Alt, som i hÄndtryksordningen: generer cookie (128-bit er helt tilstrÊkkeligt), en efemÊr nÞglepar 34.10, som vil blive brugt til VKO-funktionen for nÞgleaftale.
395 async def initiator(host, port): 396 _id = repr((host, port)) 397 logging.info("%s: dialer", _id) 398 reader, writer = await asyncio.open_connection(host, port) 399 # Generer vores ephemerale offentlige nÞgle og cookie, send Handshake 0 besked {{{ 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()- venter pÄ svar og dekoder den ankomne Msg besked;
- sikrer os om, at vi har modtaget handshake1;
- dekoder den ephemerale offentlige nĂžgle fra modparten og beregner sessionnĂžglen;
- genererer de symmetriske nĂžgler, der er nĂždvendige for at behandle TBE delen af beskeden.
423 logging.info("%s: fik %s besked", _id, msg.choice) 424 if msg.choice != "handshake1": 425 logging.warning("%s: uventet besked, afbryder", _id) 426 writer.close() 427 return 428 # }}} 429 msg_handshake1 = msg.value 430 # Valider Handshake besked {{{ 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 er et 64-bit tal (urandom(8)), som ogsĂ„ krĂŠver deserialisering fra byte-reprĂŠsentationen ved hjĂŠlp af gost3410_vko.ukm_unmarshal(). VKO funktionen for 34.10-2012 256-bit er gost3410_vko.kek_34102012256() (KEK â nĂžglekrypteringsnĂžgle).
Den genererede sessionnÞgle er allerede en 256-bit byte pseudotilfÊldig sekvens. SÄ den kan straks bruges i HKDF funktionen. Da GOST34112012256 opfylder hashlib interface, kan den bruges direkte i Hkdf klassen. Salt (den fÞrste argument til Hkdf) angiver vi ikke, da den genererede nÞgle pÄ grund af de ephemerale nÞgler vil vÊre forskellig for hver session, og den har allerede tilstrÊkkelig entropi. kdf.expand() giver som standard allerede nÞgler af 256-bit lÊngde, der er nÞdvendige for Kuznetsov i det fÞlgende.
DernĂŠst kontrolleres TBE og TBS dele af den modtagne besked:
- beregner og kontrollerer MAC over den ankomne ciphertext;
- dekripterer ciphertext;
- dekoder TBE strukturen;
- der tages et identifikationsnummer fra samtalepartneren og kontrolleres, om det er kendt for os;
- beregner og kontrollerer MAC over dette identifikationsnummer;
- signaturen over TBS-strukturen kontrolleres, som omfatter cookies fra begge parter og den offentlige, midlertidige nĂžgle fra den modsatte part. Signaturen verificeres med modpartens langsigtede signaturnĂžgle.
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ĂŠvnt tidligere, beskriver 34.13-2015 forskellige fra 34.12-2015. Blandt dem er der tilstand for generering af MAC, beregning af MAC. I PyGOST er det gost3413.mac(). Denne tilstand krĂŠver overfĂžrsel af krypteringsfunktionen (der modtager og returnerer Ă©n blok data), blockstĂžrrelsen og selve dataene. Hvorfor kan man ikke hardcode blockstĂžrrelsen? 34.12-2015 beskriver ikke kun den 128-bit krypteringsalgoritme Kuznechik, men ogsĂ„ den 64-bit â en let ĂŠndret version af GOST 28147-89, skabt i KGB og stadig med en af de hĂžjeste sikkerhedsspecialiteter.
Grashopper initialiseres gost.3412.GOST3412Kuznechik(key) ved at kalde det og returnerer et objekt med .encrypt()/ .decrypt() metoder, der er egnet til at blive sendt til 34.13 funktioner. MAC beregnes som fÞlger: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). For at sammenligne den beregnede MAC med den modtagne kan man ikke bruge almindelig sammenligning (==) af byte-strenge, da denne operation giver tidslÊkningsproblemer, som generelt kan fÞre til fatale sÄrbarheder af typen angreb pÄ TLS. I Python findes der en sÊrlig hmac.compare_digest funktion til dette.
En blokkeringskrypteringsfunktion kan kun kryptere Ă©n blok data. For et stĂžrre antal og ikke-kvart lĂŠngde er det nĂždvendigt at anvende en krypteringsmetode. I 34.13-2015 er fĂžlgende beskrevet: ECB, CTR, OFB, CBC, CFB. Hver har sine egne anvendelsesomrĂ„der og egenskaber. Til vores store beklagelse har vi stadig ikke standardiserede (sĂ„som CCM, OCB, GCM og lignende) â vi er tvunget til selv at tilfĂžje en MAC. Jeg vĂŠlger (CTR): den krĂŠver ikke udfyldning til blokstĂžrrelse, kan paralleliseres, bruger kun krypteringsfunktionen, kan sikkert bruges til at kryptere et stort antal beskeder (i modsĂŠtning til CBC, hvor der relativt hurtigt opstĂ„r kollisioner).
Som .mac(), tager .ctr() lignende data som input: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Det krÊver at initialiseringsvektoren er prÊcist halv stÞrrelse af krypteringsblokken. Hvis vores krypteringsnÞgle kun bruges til at kryptere én besked (selvom den bestÄr af flere blokke), kan en nul initialiseringsvektor sikkert angives. For kryptering af handshake beskeder bruges der hver gang en separat nÞgle.
Verifikationen af signaturen gost3410.verify() er triviel: vi sender den elliptiske kurve inden for hvilken vi arbejder (den noterer vi blot i vores GOSTIM protokol), den offentlige nĂžgle til signaturen (husk at dette skal vĂŠre et tuppel af to store tal, ikke en byte-streng), 34.11-2012 hash og den modtagne signatur.
Herefter forbereder vi og sender et handshake2 meddelelse i initiatoren, idet vi udfĂžrer de samme handlinger som vi gjorde ved verifikationen, blot symmetrisk: signatur pĂ„ vores nĂžgler i stedet for verifikation, osvâŠ
456 # Forbered og send Handshake 2 besked {{{ 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 etableret: %s", _id, peer_name)NÄr sessionen er etableret, genereres transportnÞgler (en separat nÞgle til kryptering, en til autentifikation, for hver af parterne), og Kuznetsov initialiseres til dekryptering og kontrol af MAC:
499 # KÞr tekstbesked sender, initialiser transportdekoder {{{ 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 # Vent pÄ testbeskeder {{{ 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 krypterer nu beskederne, inden de sendes over TCP-forbindelsen. Hver besked har en monoton stigende nonce, som ogsÄ fungerer som initialiseringsvektor ved kryptering i tÊllermodus. Hver besked og blok af beskeder vil garanteret have forskellige tÊllevÊrdier.
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 += 1Indkommende beskeder behandles af koroutinen msg_receiver, som stÄr for autentificering og dekryptering:
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("uventet nonce vĂŠrdi") mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])): raise ValueError("ugyldig 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)Konklusion
GOSTIM er kun beregnet til uddannelsesmÊssige formÄl (da det ikke er dÊkket af test, i hvert fald)! Kildekoden til programmet kan downloades (Stribog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Ligesom alle mine projekter, sÄsom , , , , GOSTIM er helt , der distribueres under betingelserne for .
, , medlem af , Python/Go-udvikler, hovedspecialist .
Kilde: habr.com
