Olles arendaja raamatukogu (GOST-i krüptograafilised primitiivid puhta Pythoniga), saan ma sageli küsimusi selle kohta, kuidas kiiresti rakendada kõige lihtsamat turvalist sõnumivahetust. Paljud arvavad, et rakenduslik krüptograafia on piisavalt lihtne asi ja .encrypt() kutsest blokksümbolile piisab turvalise edastamise jaoks suhtluskanali kaudu. Teised aga arvavad, et rakenduslik krüptograafia on mõne eriala, ja on aktsepteeritav, et rikkad ettevõtted nagu Telegram, kellel on matemaatikud, ei suuda rakendada Kõik see tõukas mind kirjutama seda artiklit, et näidata, et krüptograafiliste protokollide ja turvalise IM-i rakendamine ei ole nii keeruline ülesanne. Siiski ei tasu välja mõelda oma autentimismeetodeid ja võtmevahetusprotokolle.
Artiklis kirjeldatakse

peer-to-peer , , SIGMA-I IPsec IKE PyDERASN varem kirjutanud IM-i disain
Esiteks, tuleb aru saada, milline meie IM välja näeb. Lihtsuse huvides olgu see peer-to-peer võrk, ilma osalejate tuvastamiseta. Meie poolt määratakse täpselt, millisele aadressile: porti ühendada suhtlemiseks kaaslasega.
Ma mõistan, et hetkel on eeldus, et kahe suvalise arvuti vahel on otsekontakt, tõsine piirang IM-i rakendamisel praktikas. Kuid mida rohkem arendajaid laseb ellu igasuguseid NAT-läbimurdmisvahendeid, seda kauem jääme me IPv4 internetti, masendavate suhete tõenäosusega suvaliste arvutite vahel. Kui kaua võib ikka taluda IPv6 puudumist kodus ja tööl?
Ma mõistan, et praegu on eeldus kahte suvalist arvutit ühendava otsese sideme olemasolu suhteliselt suur piirang IM-i praktilisele rakendusele. Kuid mida rohkem arendajaid rakendavad erinevaid NAT-traversal nippe, seda kauem jääme me IPv4 interneti piirangute alla, kus suvaliste arvutite ühendamise tõenäosus on masendavalt madal. Kui kaua me peame veel taluma IPv6 puudumist kodus ja tööl?
Meil on sõbralt-sõbrale võrgustik: kõik võimalikud vestluskaaslased peavad olema eelnevalt teada. Esiteks lihtsustab see kõike oluliselt: tutvustatakse end, leidis või ei leidnud nime/võtit, katkestas või jätkab tööd, teades vestluskaaslast. Teiseks on see üldiselt turvaline ja välistab mitmeid rünnakute võimalusi.
IM-i liides on sarnane klassikaliste lahendustega. , mis mulle väga meeldivad oma minimalismi ja Unix-way filosoofia poolest. IM programm iga vestluskaaslase jaoks loob kausta, kus on kolm Unix domain socketit:
- in — sinna kirjutatakse saadetud sõnumid vestluskaaslasele;
- out — sealt loetakse vastuvõetud sõnumid vestluskaaslaselt;
- state — lugedes sealt, saame teada, kas vestluskaaslane on hetkel ühenduses, aadress/port on ühenduses.
Lisaks luuakse conn soket, kuhu kirjutades hosti port, käivitame ühenduse kaugel asuva vestluskaaslasega.
|-- alice
| |-- in
| |-- out
| `-- state
|-- bob
| |-- in
| |-- out
| `-- state
`- conn
Selline lähenemine võimaldab teha sõltumatuid IM-transpordi ja kasutajaliidese rakendusi, kuna igaühe maitse ja värv on erinev. Kasutades ja/or , on võimalik saada mitme aknaga liides süntaksivärviga. Ja abil võib saada GNU Readline'i ühilduva rea sõnumite sisestamiseks.
Tegelikult kasutavad suckless projektid FIFO-faile. Isiklikult ei suutnud ma mõista, kuidas asyncio-s failidega koos töötada konkurentsitult ilma oma käsitsi valmistatud toeta eraldatud lõngades (selliste asjade jaoks kasutan juba pikka aega keelt ). Seetõttu otsustasin jääda Unix domain soketide juurde. Kahjuks kustutab see võimaluse teha echo 2001:470:dead::babe 6666 > conn. Selle probleemi lahendasin, kasutades : echo 2001:470:dead::babe 6666 | socat — UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.
Algne ebaturvaline protokoll
Transpordina kasutatakse TCP-d: see garanteerib edastamise ja selle järjekorra. UDP ei garanteeri kumbagi (mis oleks kasulik, kui rakendatakse krüptograafiat), ja toetust Pythonis ei ole paketti vahetuses.
Kahjuks ei ole TCP-s mõistet sõnumite, vaid ainult byte-voogude kohta. Seetõttu peame välja mõtlema sõnumite formaadi, et neid saaks selles voos üksteisest eristada. Võime kokku leppida, et kasutame rea vahetuse sümbolit. Alustamiseks sobib see, kuid kui hakkame oma sõnumeid krüpteerima, võib see sümbol krüptotekstis igal pool ilmneda. Seetõttu on võrkudes populaarseks saanud protokollid, mis saadavad esmalt sõnumi pikkuse baidina. Näiteks on Pythonis saadaval xdrlib, mis võimaldab sellega sarnaste formatidega töötada. .
Me ei töötle TCP lukemisega õigesti ja tõhusalt — lihtsustame koodi. Lugema andmeid socketist lõpmatus tsüklis, kuni dekodeerime täieliku sõnumi. Sellise lähenemise jaoks saab kasutada ka JSON-i koos XML-iga. Kuid kui lisandub krüu, tuleb andmeid allkirjastada ja autentida — ja see nõuab bite-bite identset objekti esitlemist, mida JSON/XML ei taga (dumpsi tulemus võib erineda).
XDR sobib selliseks ülesandeks, kuid valin ASN.1 koos DER-kodeerimisega ja raamatukogu, kuna meil on kõrgetasemelised objektid, millega on sageli meeldivam ja mugavam töötada. Erinevalt skeemivabadest , või , ASN.1 kontrollib automaatselt andmeid range määratud skeemi vastu.
# 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))),
))
Vastuvõetav sõnum on Msg: kas tekstiline MsgText (praegu ühe tekstivälja vorminguga), või käepigistuses sõnum MsgHandshake (mille kaudu edastatakse vestluspartneri nimi). Praegu tundub see liialdatud, kuid see on tulevikuks põhialus.
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│MsgHandshake(IdA) │
│─────────────────>│
│ │
│MsgHandshake(IdB) │
│<─────────────────│
│ │
│ MsgText() │
│─────────────────>│
│ │
│ MsgText() │
│<─────────────────│
│ │
IM ilma krüptograafiana
Nagu juba ütlesin, kasutatakse kõigi soketitega seotud toimingute puhul asyncio raamatukogu. Deklareerime, mida me oodates jooksu ajal:
parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
"--our-name",
required=True,
help="Meie peer'i nimi",
)
parser.add_argument(
"--their-names",
required=True,
help="Nende peer'ide nimed, eraldatud komaga",
)
parser.add_argument(
"--bind",
default="::1",
help="Aadress, millele kuulata",
)
parser.add_argument(
"--port",
type=int,
default=6666,
help="Port, millele kuulata",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))
Seatakse oma nimi (—our-name alice). Komaga eraldatakse kõik oodatavad vestluspartnerid (—their-names bob,eve). Iga vestluspartneri jaoks luuakse kaust Unix soketite jaoks, samuti korutine iga in, out, state jaoks:
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"))
Kasutajalt tulevad sõnumid in soketist saadetakse IN_QUEUES järjekordadesse:
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"))
Vestluspartneritelt saabuvad sõnumid saadetakse OUT_QUEUES järjekordadesse, millest andmed kirjutatakse out soketisse:
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()
State soketist lugemisel otsib programm PEER_ALIVE sõnastikus vestluspartneri aadressi. Kui vestluspartneriga pole veel ühendust, kirjutatakse tühi reegel.
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()
Aadressi kirjutamisel conn soketisse käivitatakse 'algataja' ühenduse funktsioon:
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))
Vaatame algatajat. Esiteks avab ta ilmselgelt ühenduse märgitud hosti/porti ja saadab oma nimega handshake sõnumi:
130 async def initiator(host, port):
131 _id = repr((host, port))
132 logging.info("%s: dialing", _id)
133 reader, writer = await asyncio.open_connection(host, port)
134 # Handshake message {{{
135 writer.write(Msg(("handshake", MsgHandshake((
136 ("peerName", OUR_NAME),
137 )))).encode())
138 # }}}
139 await writer.drain()
Seejärel ootab ta vastust kaugemas küljest. Proovib dekodeerida saabunud vastust Msg ASN.1 skeemi järgi. Eeldame, et kogu sõnum saadetakse ühe TCP-segmendina ja saame selle aatomiliselt kätte .read() kutsumise ajal. Kontrollime, et oleme saanud täpselt handshake sõnumi.
141 # Oota Handshake sõnumit {{{
142 data = await reader.read(256)
143 if data == b"":
144 logging.warning("%s: no answer, disconnecting", _id)
145 writer.close()
146 return
147 try:
148 msg, _ = Msg().decode(data)
149 except ASN1Error:
150 logging.warning("%s: undecodable answer, disconnecting", _id)
151 writer.close()
152 return
153 logging.info("%s: got %s message", _id, msg.choice)
154 if msg.choice != "handshake":
155 logging.warning("%s: unexpected message, disconnecting", _id)
156 writer.close()
157 return
158 # }}}
Kontrollime, kas saabunud kaaslase nimi on meile tuttav. Kui ei, siis katkestame ühenduse. Kontrollime, kas meil on juba tema nimega ühendus loodud (kaaslase käsk on taas meiega ühenduda) ja sulgeme selle. IN_QUEUES järjekorda asetatakse Python-i stringid sõnumi tekstiga, kuid on olemas eriline väärtus None, mis annab teada, et msg_sender korutiin peab lõpetama oma töö, et unustada oma writer, mis on seotud aegunud TCP-ühendusega.
159 msg_handshake = msg.value
160 peer_name = str(msg_handshake["peerName"])
161 if peer_name not in THEIR_NAMES:
162 logging.warning("unknown peer name: %s", peer_name)
163 writer.close()
164 return
165 logging.info("%s: session established: %s", _id, peer_name)
166 # Käivita tekstisõnumi saatja, algata transport dekooder {{{
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 võtab vastu väljaminevad sõnumid (mis asetatakse järjekorda sissetulevast soketist), serialiseerib need MsgText sõnumiks ja saadab TCP-ühenduse kaudu. See võib igal hetkel katki minna — seda me loomulikult tabame.
async def msg_sender(peer_name: str, writer) -> None:
in_queue = IN_QUEUES[peer_name]
while True:
text = await in_queue.get()
if text is None:
break
writer.write(Msg(("text", MsgText((
("text", UTF8String(text)),
)))).encode())
try:
await writer.drain()
except ConnectionResetError:
del PEER_ALIVES[peer_name]
return
logging.info("%s: sent %d characters message", peer_name, len(text))
Lõpus siseneb algataja lõputusse tsüklisse, lugedes sõnumeid socket'ist. Kontrollib, kas need on tekstisõnumid, ja paneb need OUT_QUEUES järjekorda, kust need saadetakse välja vastava vestluskaaslase socket'isse. Miks ei saa lihtsalt teha .read() ja dekodeerida sõnumit? Sest ei saa välistada olukorda, kus mitu sõnumit kasutajalt kogunevad operatsioonisüsteemi puhverisse ja saadetakse ühe TCP-segmendina. Me suudame dekodeerida esimest, kuid puhvris võib alles jääda osa järgnevast. Iga rikke korral sulgeme TCP-ühenduse ja peatame msg_sender korutiini (saates None OUT_QUEUES järjekorda).
174 buf = b""
175 # Oodake testi sõnumeid {{{
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: maksimum puhver suurus ületatud", _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: ootamatu %s sõnum", _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: katkestamine: %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: saadud %d tähemärki sõnum", peer_name, len(text))
69 await OUT_QUEUES[peer_name].put(text)
Naaseme põhikoodi juurde. Pärast kõigi korutiinide loomist programmi käivitamise hetkel käivitame TCP-serveri. Iga seadistatud ühenduse jaoks loob see responder (vastaja) korutiini.
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("Kuulamine aadressil: %s", server.sockets[0].getsockname())
loop.run_forever()
responder on sarnane algatajale ja täidab kõiki samu toiminguid peegelpildis, kuid lõpmatu sõnumite lugemise tsükkel algab kohe, lihtsuse huvides. Praegu saadab käepigistuse protokoll iga poole kohta ühe sõnumi, kuid tulevikus saadab algataja ühenduse puhul kaks sõnumit, pärast mida on võimalik kohe saata tekstisõnumeid.
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()
Turvaline protokoll
On aeg kindlustada meie suhtlemine. Mida me turvalisuse all mõtleme ja mida tahame:
- edastatavate sõnumite konfidentsiaalsus;
- edastatavate sõnumite autentsus ja terviklikkus — nende muutmist tuleb tuvastada;
- taastekkimise rünnakute kaitse (replay attack) — sõnumite kadumist või kordumist peab tuvastama (ja me otsustame katkestada ühenduse);
- vestluspartnerite tuvastamine ja autentimine eelnevalt sisestatud avalike võtmete põhjal — oleme juba otsustanud, et loome friend-to-friend võrgu. Alles pärast autentimist mõistame, kellega suhtleme;
- erinevate omadused (PFS) — meie pikaajalise allkirjavõtme kompromiteerimine ei tohiks lubada kogu varasema vestluse lugemist. Peidetud liikluse salvestamine muutub kasutu;
- Sõnumite (transport ja kätlemine) kehtivus/validiteet on vaid ühe TCP-seansi piires. Teise sessiooni (isegi sama vestluspartneriga) korralikult allkirjastatud/autentitud sõnumite sisestamine ei tohiks olla võimalik;
- passiivne vaatleja ei tohi näha ega kasutajaidentifikaatoreid, ega edastatud pikaajalisi avalikke võtmeid, ega nende hashe. Mingi anonüümsus passiivse vaatleja eest.
Üllatav, kuid see miinimum, mida kõik soovivad omada igas kätlemisprotokollis, täidetakse väga harva kodumaiste protokollide puhul. Niisiis, ka praegu ei hakka me midagi uut välja mõtlema. Soovitan kindlasti kasutada , et protokolle üles ehitada, kuid valime midagi lihtsamat.
Kaks protokolli on kõige populaarsemad:
- — keeruline protokoll pika ajaloo vigadest, keerukusest, haavatavustest, halvast planeerimisest ja puudustest (kuid see ei kehti just TLS 1.3 kohta). Küll aga ei arutle me selle üle, kuna see on üleliia keeruline.
- jot — ei ole tõsiseid krüptograafilisi probleeme, kuigi nad ei ole lihtsad. Kui lugeda IKEv1 ja IKEv2 kohta, siis nende allikaks on , ISO/IEC IS 9798-3 ja SIGMA (SIGn-and-MAc) protokollid - piisavalt lihtsalt teostatavad ühe õhtuga.
Kuidas on SIGMA, kui viimane lüli STS/ISO protokollide arengus, hea? See vastab kõikidele meie nõuetele (sealhulgas vestluspartnerite identifikaatorite "varjamise" osas), neil ei ole teadaolevaid krüptograafilisi probleeme. See on minimalistik - ühegi protokolli sõnumi elemendi eemaldamine toob kaasa selle ebaturvalisuse.
Liigume lihtsaimast kodumaisest protokollist SIGMA juurde. Meid huvitav kõige põhilisem operatsioon on : funktsioon, mille tulemusena saavad osalejad sama väärtuse, mida saab kasutada sümmeetrilise võtmena. Ilma detailidesse laskumata: kumbki osapool genereerib efemeerse (ainult ühe seansi piires kasutatava) võtmepaari (avalikud ja privaatsed võtmed), vahetavad avalikke võtmeid, kutsuvad välja kokkuleppimise funktsiooni, mille sisendiks on nende privaatne võti ja vestluspartneri avalik võti.
┌─────┐ ┌─────┐
│PeerA│ │PeerB│
└──┬──┘ └──┬──┘
│ IdA, PubA │ ╔════════════════════╗
│───────────────►│ ║PrvA, PubA = DHgen()║
│ │ ╚════════════════════╝
│ IdB, PubB │ ╔════════════════════╗
│◄───────────────│ ║PrvB, PubB = DHgen()║
│ │ ╚════════════════════╝
────┐ ╔═══════╧════════════╗
│ ║Key = DH(PrvA, PubB)║
◄────┘ ╚═══════╤════════════╝
│ │
│ │
Igaüks võib keskkonda siseneda ning asendada avalikud võtmed oma omadega — selles protokolli ei ole suunatud osaliste autentimist. Lisame allkirja pikaealiste võtmetega.
┌─────┐ ┌─────┐
│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) ║ │
│ ╚═════════════════════╝ │
│ │
Selline allkiri ei sobi, kuna see ei ole seotud konkreetse sessiooniga. Sellised teadet võivad sobida ka teiste osaliste sessioonidele. Allkirjastama peaks kogu konteksti. See sunnib ka lisama veel ühe sõnumi A-lt.
Lisaks on kriitiline lisada allkirja juurde oma identifikaator, kuna vastasel juhul võime IdXXX asendada ja allkirjastada sõnumi teise tuntud osalise võtmega. Selle vältimiseks , on vajalik, et elemendid alloleva pealkirja all asuksid kindlates kohtades vastavalt oma tähendusele: kui A allkirjastab (PubA, PubB), siis B peab allkirjastama (PubB, PubA). See räägib ka struktureeritud ja serialiseeritud andmete valiku olulisusest. Näiteks ASN.1 DER kodeerimisel sorteeritakse kogusid: SET OF(PubA, PubB) on identne 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) ║ │ │ ╚═════════════════════╝ │ │
Kuid me pole veel "tõestanud", et oleme selle sessiooni jaoks sama ühise võtme loonud. Põhimõtteliselt saab ilma selle sammuta hakkama — esimene transportmessager on kehtetu, kuid me tahame, et käepigistuse lõpetamisel oleksime kindlad, et kõik on tõeliselt kokku lepitud. Praegu on käes ISO/IEC IS 9798-3 protokoll.
Me võiksime allkirjastada ka ise loodud võtme. See on ohtlik, kuna allkirjastamisalgoritm võib lekkida (kuigi bitid-allkirja, kuid ikkagi lekked). Saame allkirjastada loodud võtme hash'i, kuid ka loodud võtme hash'i leke võib olla väärtuslik brute-force rünnaku puhul genereerimisfunktsiooni vastu. SIGMA kasutab MAC funktsiooni, mis autentib saatja identifikaatori.
┌─────┐ ┌─────┐ │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, ...)║ │ │ ╚═════════════════════╝ │ │
Optimeerimise nimel võivad mõned soovida taaskasutada oma ajutisi võtmeid (mis, muidugi, on PFS jaoks kahjulik). Näiteks genereerisime võtme paari, üritasime ühendust luua, kuid TCP ei olnud saadaval või katkestas protokolli keskel. Kahju on raisata kulutatud entropiat ja CPU ressursse uue paari jaoks. Seega tutvustame nii-öelda küpsist — pseudojuhuslikku väärtust, mis kaitseb võimalike juhuslike tagasi mängimise rünnakute eest ajutiste avalike võtmete kordusel kasutamisel. Küpsise ja ajutise avaliku võtme vahelise sidumise tõttu võime vastase avaliku võtme allkirjast välja jätta.
┌─────┐ ┌─────┐ │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, ...)║ │ │ ╚═════════════════════╝ │ │
Lõpuks tahame kaitsta oma vestluspartnerite identifikaatoreid passiivse vaatleja eest. Selleks pakub SIGMA esmalt vahetada efemeerseid võtmeid, et luua ühine võti, millega krüpteerida autentimis- ja identifitseerimisteateid. SIGMA kirjeldab kahte varianti:
- SIGMA-I — kaitseb algatajat aktiivsete rünnakute eest ja vastajat passiivsete eest: algataja autentib vastajat ja kui midagi ei klapi, siis ta ei avalda oma identiteeti. Vastaja aga avaldab oma identiteedi, kui aktiveerib aktiivse protokolli. Passiivne vaatleja ei saa midagi teada;
SIGMA-R — kaitseb vastajat aktiivsete rünnakute eest ja algatajat passiivsete eest. Kõik on täpselt vastupidi, kuid selles protokollis edastatakse juba neli käepigistuse teadet.Valime SIGMA-I, kuna see on rohkem sarnane sellele, mida ootame kliendi-serveri tavapärastelt funktsioonidelt: klient tunneb ainult autentitud serverit, samas kui server teatab kõik. Lisaks on selle rakendamine lihtsam, kuna sõnumite käepigistusi on vähem. Kõik, mida me protokolli lisame, on sõnumi osa krüptimine ja A identifikaatori viimine viimase sõnumi krüptitud osasse:
┌─────┐ ┌─────┐ │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, ...)║ │ │ ╚═════════════════════╝ │ │
- Allkirjastamiseks kasutatakse GOST R algoritmi 256-bitiste võtmete põhjal.
- Ühise võtme genereerimiseks kasutatakse 34.10-2012 VKO.
- MAC-ina kasutatakse CMAC-i. Tehniliselt on see plokkšifre töörežiim, mis on kirjeldatud GOST R 34.13-2015. Selle režiimi krüptimisfunktsiooni jaoks — (34.12-2015).
- Kuna osaleja identifikaatorina kasutatakse tema avaliku võtme räsitud versiooni. Räsiks kasutatakse (34.11-2012 256 bitti).
Pärast käepigistust lepime kokku ühise võtme. Seda saame kasutada autentitud krüpteerimiseks transportimiseks mõeldud sõnumite puhul. See osa on üsna lihtne ja selles on raske eksida: inkrementeerime sõnumite loenduri, krüpteerime sõnumi, autentime (MAC) loenduri ja krüpteeritud teksti, saadame. Sõnumi vastuvõtmisel kontrollime, et loendur vastab oodatavale väärtusele, autentime krüpteeritud teksti koos loenduri, dekrüpteerime. Millise võtmega krüpteerida käepigistuse, transpordi sõnumeid, milliseid autentida? Ühte võtit nende kõigi ülesannete täitmiseks kasutada on ohtlik ja mõistlik. Vaja on genereerida võtmeid, kasutades spetsialiseeritud funktsioone (võtme genereerimise funktsioon). Jätkame lihtsusega ega hakka midagi välja mõtlema: on ammu tuntud, hästi uuritud ja ei sisalda teadaolevaid probleeme. Kahjuks ei ole selle funktsioon Python'i põhiteegis, seega kasutame paketti. HKDF kasutab sees HMAC-i, mis omakorda kasutab räsi funktsiooni. Näide rakendusest Pythonis Wikipedia leheküljelt koosneb vähestest koodiridade arvudest. Nagu 34.10-2012 puhul, kasutame räsi funktsioonina Stribog-256. Meie võtme kokkuleppe funktsiooni väljundit nimetatakse seansi võtme, millest genereeritakse puuduolevad sümmeetrilised: Struktuurid/skeemid
Vaatame, millised ASN.1 struktuurid on nüüd tekkinud, et edastada kõik need andmed:Struktuurid/skeemid
Räägime, millised ASN.1 struktuurid meil nüüd olemas on, et edastada kõiki neid andmeid:
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 — see what will be signed. HandshakeTBE — see what will be encrypted. Note the ukm field in MsgHandshake1. 34.10 VKO adds a UKM (user keying material) parameter for further randomization of the generated keys — just extra entropy.
Adding cryptography to the code
Let's only consider the changes made to the original code, as the structure remains the same (in fact, the final implementation was written first, and then all cryptography was removed from it).
Since authentication and identification of interlocutors will be conducted using public keys, they now need to be stored somewhere long-term. For simplicity, we will use JSON of the following form:
{ "our": { "prv": "21254cf66c15e0226ef2669ceee46c87b575f37f9000272f408d0c9283355f98", "pub": "938c87da5c55b27b7f332d91b202dbef2540979d6ceaa4c35f1b5bfca6df47df0bdae0d3d82beac83cec3e353939489d9981b7eb7a3c58b71df2212d556312a1" }, "their": { "alice": "d361a59c25d2ca5a05d21f31168609deeec100570ac98f540416778c93b2c7402fd92640731a707ec67b5410a0feae5b78aeec93c4a455a17570a84f2bc21fce", "bob": "aade1207dd85ecd283272e7b69c078d5fae75b6e141f7649ad21962042d643512c28a2dbdc12c7ba40eb704af920919511180c18f4d17e07d7f5acd49787224a" } }our — our key pair, hexadecimal private and public keys. their — the names of interlocutors and their public keys. Let's change the command line arguments and add post-processing to the 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="Generate JSON with our new keypair", ) parser.add_argument( "--keys", default="keys.json", required=False, help="JSON with our and their keys", ) parser.add_argument( "--bind", default="::1", help="Address to listen on", ) parser.add_argument( "--port", type=int, default=6666, help="Port to listen on", ) args = parser.parse_args() if args.keys_gen: prv_raw = urandom(32) pub = gost3410.public_key(CURVE, gost3410.prv_unmarshal(prv_raw)) pub_raw = gost3410.pub_marshal(pub) print(json.dumps({ "our": {"prv": hexenc(prv_raw), "pub": hexenc(pub_raw)}, "their": {}, })) exit(0) # Parse and unmarshal our and their keys {{{ 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), } # }}}34.10 algoritmi privaatvõti on juhuslik number. Suurus 256 bitti 256-bittiste elliptiliste kõverate jaoks. PyGOST ei toimi baytide kogumiga, vaid , seega tuleb meie privaatvõti (urandom(32)) muuta numbriks, kasutades gost3410.prv_unmarshal(). Avalik võti arvutatakse määramatult privaatvõtme põhjal, kasutades gost3410.public_key(). Avalik võti 34.10 on kaks suurt arvu, mis tuleb samuti muuta baytijadaks mugavaks hoidmiseks ja edastamiseks, kasutades gost3410.pub_marshal().
Pärast JSON-faili lugemist tuleb avalikud võtmised vastavalt tagasi muuta, kasutades gost3410.pub_unmarshal(). Kuna me saame osaliste identifikaatoreid avaliku võtme hashina, saame need kohe ette arvutada ja asetada sõnastikku kiireks otsinguks. Stribog-256 hash on gost34112012256.GOST34112012256(), mis vastab täielikult hashlib liidese hash-funktsioonidele.
Kuidas on muutunud algataja kooreline? Kõik on nagu käepigistuse skeemis: genereerime küpsise (128 bitti on täiesti piisav), ajutise 34.10 võtme paari, mida kasutatakse VKO võtme kokkuleppe funktsiooni jaoks.
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 # Generoime meie ajutise avaliku võtme ja küpsise, saadame Kätlemise 0 sõnumi {{{ 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()- ootame vastust ja dekodeerime saadud Msg sõnumi;
- veendume, et oleme saanud handshake1;
- dekodeerime vastaspoole ajutise avaliku võtme ja arvutame seansivõtme;
- kuvame sümmeetrilised võtmed, mis on vajalikud TBE sõnumi osa töötlemiseks.
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 # Kätlemise sõnumi valideerimine {{{ 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 on 64-bit arv (urandom(8)), mis nõuab samuti serialiseerimist baitide esitusest, kasutades gost3410_vko.ukm_unmarshal(). VKO funktsioon 34.10-2012 jaoks 256-bit on gost3410_vko.kek_34102012256() (KEK — võtme krüpteerimise võti).
Töötatud seansivõti on juba 256-bitine baitide pseudojuhuslik järjestus. Seetõttu saab seda kohe kasutada HKDF funktsioonis. Kuna GOST34112012256 vastab hashlib liidesele, saab seda kohe kasutada Hkdf klassis. Soola (esimene argument Hkdf) me ei märgi, kuna genereeritud võti ajutiste võtme paaride tõttu on iga seansi jaoks erinev ja selles on juba piisavalt entropiat. kdf.expand() väljastab vaikimisi juba 256-bitiseid võtmeid, mis on nõutud Kuku jaoks edaspidi.
Järgmiseks kontrollitakse TBE ja TBS osade saabunud sõnumist:
- arvutatakse ja kontrollitakse MAC saabunud krüpteeritud teksti üle;
- krüpteeritud tekst dekodeeritakse;
- TBE struktuur dekodeeritakse;
- sealt võetakse vestluspartneri identifikaator ja kontrollitakse, kas me teame seda üldse;
- arvutatakse ja kontrollitakse MAC selle identifikaatori üle;
- kontrollitakse TBS-struktuuri allkirja, kuhu kuuluvad mõlema poole küpsised ja vastaspoole avalik ajutine võti. Allkiri kontrollitakse kaassõnumi pikaajalise allkirja võtmega.
441 proovige: 442 partneri_nimi = valideeri_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 välja arvatud ValueError kui err: 452 logging.warning("%s: %s, katkestamine", _id, err) 453 writer.close() 454 return 455 # }}} 128 def valideeri_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 kui mitte compare_digest(mac_tag, bytes(msg_handshake["ciphertextMac"])): 140 tõsta ValueError("vale MAC") 141 plaintext = ctr( 142 GOST3412Kuznechik(key_enc).encrypt, 143 KUZNECHIK_BLOCKSIZE, 144 ciphertext, 145 8 * b"x00", 146 ) 147 proovige: 148 tbe, _ = HandshakeTBE().decode(plaintext) 149 välja arvatud ASN1Error: 150 tõsta ValueError("ei saa dekodeerida TBE") 151 key_sign_pub_hash = bytes(tbe["identity"]) 152 partner = KEYS.get(key_sign_pub_hash) 153 kui partner on None: 154 tõsta ValueError("tundmatu identiteet") 155 mac_tag = mac( 156 GOST3412Kuznechik(key_mac_identity).encrypt, 157 KUZNECHIK_BLOCKSIZE, 158 key_sign_pub_hash, 159 ) 160 kui mitte compare_digest(mac_tag, bytes(tbe["identityMac"])): 161 tõsta ValueError("vale identiteedi MAC") 162 tbs = HandshakeTBS(( 163 ("cookieTheir", cookie_their), 164 ("cookieOur", cookie_our), 165 ("pubKeyOur", pub_key_our), 166 )) 167 kui mitte gost3410.verify( 168 CURVE, 169 partner["pub"], 170 GOST34112012256(tbs.encode()).digest(), 171 bytes(tbe["signature"]), 172 ): 173 tõsta ValueError("vale allkiri") 174 return partner["name"]Nagu juba eespool mainitud, käsitleb 34.13-2015 erinevaid 34.12-2015. Nende seas on ka mode, mis genereerib haneosakesi ja arvutab MAC-i. PyGOST-is on see gost3413.mac(). See režiim nõuab krüptimisfunktsiooni edastamist (mis võtab ja tagastab üksiku andmeploki), plokkide suurust ja andmete endi. Miks ei tohi plokki suurust kõvakoodida? 34.12-2015 käsitleb mitte ainult 128-bitist Kurgi šifrit, vaid ka 64-bitist — veidi muudetud GOST 28147-89, loodud veel KGB-s ja omades endiselt üht kõrgeimat turvapiiri.
Kärbes initsialiseerib gost.3412.GOST3412Kuznechik(key) kutsumisega ja tagastab objekti, millel on meetodid .encrypt() / .decrypt(), mis sobivad edastamiseks 34.13 funktsioonidesse. MAC arvutatakse järgmiselt: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). Arvutatud ja saabunud MAC-i võrdlemiseks ei tohi kasutada tavalist võrreldes (==) baitide stringide vahel, kuna see operatsioon toob kaasa võrreldes ajalekke, mis võib üldiselt viia tõsiste haavatavusteni tüüpi ründed TLS-i vastu. Pythonis on selle jaoks spetsiaalne hmac.compare_digest funktsioon.
Blokiahela krüptimisfunktsioon suudab krüptida ainult ühe andmeploki. Suurema hulga, eriti mitte-mitmekordse pikkusega, jaoks on vajalik kasutada krüptimisrežiimi. 34.13-2015 kirjeldab järgmiseid: ECB, CTR, OFB, CBC, CFB. Igal neist on oma lubatud kasutusvaldkonnad ja omadused. Suureks kahetsuseks pole meil siiamaani standardiseeritud (näiteks CCM, OCB, GCM ja sarnased) — me peame vähemalt ise MAC-i lisama. Ma valin (CTR): see ei nõua täiendamist ploki suurusele, saab paralleelsetena töötada, kasutab ainult krüptimisfunktsiooni, võib olla ohutult kasutatav paljude sõnumite krüptimiseks (erinevalt CBC-st, kus konfliktid algavad suhteliselt kiiresti).
Nii nagu .mac(), võtab .ctr() sarnased andmed sisendiks: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). On vajalik määrata initsialiseerimisvektor, mille pikkus on täpselt pool krüptobloki pikkusest. Kui meie krüptimisvõti kasutatakse ainult ühe sõnumi (isegi mitmest plokist) krüptimiseks, siis on ohutu määrata nullse initsialiseerimisvektor. Käepigistuse sõnumite krüptimiseks kasutame iga kord eraldi võtit.
Allkirja kontrollimine gost3410.verify() on triviaalne: edastame elliptilise kõvera, mille piirides me töötame (mille me lihtsalt fikseerime meie GOSTIM protokollis), allkirjastaja avaliku võtme (ärge unustage, et see peab olema kahe suure arvu tupel, mitte baitide string), 34.11-2012 häsht ja ise saadud allkiri.
Edasi, algataja valmistab ette ja saadab handshake2 vilkumise sõnumi, tehes samu toiminguid, mida tegi kontrollimisel, vaid sümmeetriliselt: allkiri oma võtmete peal kontrollimise asemel ja nii edasi...
456 # Valmistage ja saatke Handshake 2 sõnum {{{ 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: sessioon loodud: %s", _id, peer_name)Kui sessioon on loodud, genereeritakse transportvõtmed (eraldi võtmed krüptimise, autentimise jaoks, iga poole jaoks), ja initsialiseeritakse Kuznechik dekrüpteerimiseks ja MAC kontrollimiseks:
499 # Käivitage tekstisõnumi saatja, initsialiseerige transport dekooder {{{ 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 # Oodake test sõnumeid {{{ 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 korutii nüüd krüpteerib sõnumid enne nende saatmist TCP-ühendusse. Iga sõnumil on monotoolselt kasvav nonce, mis on ka initsialiseerimise vektor krüptimise režiimis. Iga sõnumi ja sõnumiploki korral on loenduri väärtused garantii erinevad.
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 += 1Saabuvad sõnumid töödeldakse korutiiniga msg_receiver, mis tegeleb autentimise ja dešifreerimisega:
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("ootamatu nonce väärtus") mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])): raise ValueError("kehtetu 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)Kokkuvõte
GOSTIM on mõeldud kasutamiseks ainult õppeotstarbel (kuna ei ole katsetatud, vähemalt mitte)! Programmi allika koodi saab alla laadida (Stribog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Nagu kõik minu projektid, stiilis , , , , GOSTIM on täielikult , levitatakse järgmistel tingimustel .
, , liige , Python/Go arendaja, peamine spetsialist .
Allikas: habr.com
