GOSTIM: P2P F2F E2EE IM sa isang gabi na may GOST cryptography
Ang pagiging isang developer PyGOST mga aklatan (GOST cryptographic primitives sa purong Python), madalas akong makatanggap ng mga tanong tungkol sa kung paano ipatupad ang pinakasimpleng secure na pagmemensahe sa tuhod. Itinuturing ng maraming tao na ang inilapat na cryptography ay medyo simple, at ang pagtawag sa .encrypt() sa isang block cipher ay sapat na upang maipadala ito nang ligtas sa isang channel ng komunikasyon. Ang iba ay naniniwala na ang inilapat na cryptography ay ang kapalaran ng iilan, at ito ay katanggap-tanggap na ang mayayamang kumpanya tulad ng Telegram na may mga olympiad-mathematician hindi maipatupad secure na protocol.
Ang lahat ng ito ay nag-udyok sa akin na isulat ang artikulong ito upang ipakita na ang pagpapatupad ng mga cryptographic na protocol at secure na IM ay hindi napakahirap na gawain. Gayunpaman, hindi sulit ang pag-imbento ng iyong sariling pagpapatotoo at mga pangunahing protocol ng kasunduan.
Magsusulat ang artikulo aninagin-to-peer, kaibigan-sa-kaibigan, end-to-end na naka-encrypt instant messenger na may SIGMA-I pagpapatunay at pangunahing protocol ng kasunduan (batay sa kung saan ito ipinatupad IPsec IKE), gamit ang eksklusibong GOST cryptographic algorithm PyGOST library at ASN.1 message encoding library PyDERASN (tungkol sa kung saan ako na nagsulat noon). Isang paunang kinakailangan: ito ay dapat na napakasimple na maaari itong isulat mula sa simula sa isang gabi (o araw ng trabaho), kung hindi, ito ay hindi na isang simpleng programa. Marahil ito ay may mga error, hindi kinakailangang mga komplikasyon, mga pagkukulang, at ito ang aking unang programa gamit ang asyncio library.
disenyo ng IM
Una, kailangan nating maunawaan kung ano ang magiging hitsura ng ating IM. Para sa pagiging simple, hayaan itong maging isang peer-to-peer network, nang walang anumang pagtuklas ng mga kalahok. Personal naming ipahiwatig kung aling address: port upang kumonekta upang makipag-usap sa kausap.
Naiintindihan ko na, sa oras na ito, ang pagpapalagay na ang direktang komunikasyon ay magagamit sa pagitan ng dalawang di-makatwirang mga computer ay isang makabuluhang limitasyon sa applicability ng IM sa pagsasanay. Ngunit kung mas maraming mga developer ang nagpapatupad ng lahat ng uri ng NAT-traversal crutches, mas mananatili tayo sa IPv4 Internet, na may nakababahalang posibilidad ng komunikasyon sa pagitan ng mga arbitrary na computer. Gaano katagal mo matitiis ang kakulangan ng IPv6 sa bahay at sa trabaho?
Magkakaroon tayo ng network ng kaibigan-sa-kaibigan: lahat ng posibleng kausap ay dapat na malaman nang maaga. Una, lubos nitong pinapasimple ang lahat: ipinakilala namin ang aming sarili, natagpuan o hindi nakita ang pangalan/susi, nadiskonekta o patuloy na nagtatrabaho, alam ang kausap. Pangalawa, sa pangkalahatan, ito ay ligtas at inaalis ang maraming pag-atake.
Ang interface ng IM ay magiging malapit sa mga klasikong solusyon walang kabuluhang mga proyekto, na talagang gusto ko para sa kanilang minimalism at Unix-way na pilosopiya. Ang IM program ay lumilikha ng isang direktoryo na may tatlong Unix domain socket para sa bawat interlocutor:
saβmga mensaheng ipinadala sa kausap ay nakatala dito;
out - ang mga mensahe na natanggap mula sa interlocutor ay binabasa mula dito;
estado - sa pamamagitan ng pagbabasa mula dito, malalaman natin kung kasalukuyang konektado ang interlocutor, ang address/port ng koneksyon.
Bilang karagdagan, ang isang conn socket ay nilikha, sa pamamagitan ng pagsusulat ng host port kung saan kami magsisimula ng isang koneksyon sa remote interlocutor.
|-- alice
| |-- in
| |-- out
| `-- state
|-- bob
| |-- in
| |-- out
| `-- state
`- conn
Binibigyang-daan ka ng diskarteng ito na gumawa ng mga independiyenteng pagpapatupad ng IM transport at user interface, dahil walang kaibigan, hindi mo mapasaya ang lahat. Gamit tmux at / o multitail, maaari kang makakuha ng multi-window interface na may pag-highlight ng syntax. At sa tulong rlwrap maaari kang makakuha ng linya ng input ng mensahe na katugma sa GNU Readline.
Sa katunayan, ang mga walang sipsip na proyekto ay gumagamit ng FIFO file. Sa personal, hindi ko maintindihan kung paano magtrabaho sa mga file nang mapagkumpitensya sa asyncio nang walang sulat-kamay na background mula sa mga nakalaang thread (ginagamit ko ang wika para sa mga ganoong bagay sa mahabang panahon Go). Samakatuwid, nagpasya akong gawin ang mga socket ng domain ng Unix. Sa kasamaang palad, ginagawa nitong imposibleng gawin ang echo 2001:470:dead::babe 6666 > conn. Nalutas ko ang problemang ito gamit ang socat: echo 2001:470:dead::babe 6666 | socat - UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.
Ang orihinal na hindi secure na protocol
Ginagamit ang TCP bilang transportasyon: ginagarantiyahan nito ang paghahatid at pagkakasunud-sunod nito. Hindi ginagarantiyahan ng UDP ang alinman (na magiging kapaki-pakinabang kapag ginamit ang cryptography), ngunit suporta SCTP Ang Python ay hindi lumalabas sa kahon.
Sa kasamaang palad, sa TCP ay walang konsepto ng isang mensahe, isang stream lamang ng mga byte. Samakatuwid, kinakailangang makabuo ng isang format para sa mga mensahe upang maibahagi ang mga ito sa kanilang sarili sa thread na ito. Maaari kaming sumang-ayon na gamitin ang line feed character. Mabuti para sa mga nagsisimula, ngunit kapag sinimulan na naming i-encrypt ang aming mga mensahe, maaaring lumitaw ang character na ito kahit saan sa ciphertext. Sa mga network, samakatuwid, ang mga sikat na protocol ay ang mga unang nagpapadala ng haba ng mensahe sa mga byte. Halimbawa, out of the box ang Python ay may xdrlib, na nagbibigay-daan sa iyo upang gumana sa isang katulad na format XDR.
Hindi kami gagana nang tama at mahusay sa pagbabasa ng TCP - pasimplehin namin ang code. Binabasa namin ang data mula sa socket sa isang walang katapusang loop hanggang sa ma-decode namin ang kumpletong mensahe. Ang JSON na may XML ay maaari ding gamitin bilang isang format para sa diskarteng ito. Ngunit kapag idinagdag ang cryptography, ang data ay kailangang malagdaan at ma-authenticate - at mangangailangan ito ng isang byte-for-byte na magkaparehong representasyon ng mga bagay, na hindi ibinibigay ng JSON/XML (maaaring mag-iba ang mga resulta ng dump).
Ang XDR ay angkop para sa gawaing ito, gayunpaman pinili ko ang ASN.1 na may DER encoding at PyDERASN library, dahil magkakaroon tayo ng mga bagay na may mataas na antas na kadalasang mas kaaya-aya at maginhawang magtrabaho. Hindi tulad ng schemaless bencode, MessagePack o CBOR, awtomatikong susuriin ng ASN.1 ang data laban sa isang hard-coded na schema.
Ang natanggap na mensahe ay magiging Msg: alinman sa isang text MsgText (na may isang text field sa ngayon) o isang MsgHandshake handshake message (na naglalaman ng pangalan ng kausap). Ngayon mukhang sobrang kumplikado, ngunit ito ay isang pundasyon para sa hinaharap.
Gaya ng nasabi ko na, ang asyncio library ay gagamitin para sa lahat ng operasyon ng socket. Ipahayag natin kung ano ang inaasahan natin sa paglulunsad:
Itakda ang iyong sariling pangalan (--our-name alice). Ang lahat ng inaasahang kausap ay nakalista na pinaghihiwalay ng mga kuwit (βtheir-name bob,eve). Para sa bawat isa sa mga interlocutor, isang direktoryo na may mga Unix socket ay nilikha, pati na rin isang coroutine para sa bawat in, out, state:
Kapag nagbabasa mula sa isang state socket, hinahanap ng programa ang address ng kausap sa PEER_ALIVE na diksyunaryo. Kung wala pang koneksyon sa interlocutor, pagkatapos ay isang walang laman na linya ang nakasulat.
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()
Kapag nagsusulat ng isang address sa isang conn socket, ang function na "initiator" ng koneksyon ay inilunsad:
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))
Isaalang-alang natin ang nagpasimula. Una ay malinaw na nagbubukas ito ng koneksyon sa tinukoy na host/port at nagpapadala ng mensahe ng handshake na may pangalan nito:
Pagkatapos, naghihintay ito ng tugon mula sa malayong partido. Sinusubukang i-decode ang papasok na tugon gamit ang Msg ASN.1 scheme. Ipinapalagay namin na ang buong mensahe ay ipapadala sa isang TCP segment at matatanggap namin ito nang atomically kapag tumatawag sa .read(). Tinitingnan namin kung natanggap namin ang mensahe ng pagkakamay.
Sinusuri namin na ang natanggap na pangalan ng kausap ay kilala sa amin. Kung hindi, pagkatapos ay masira namin ang koneksyon. Sinusuri namin kung nakagawa na kami ng isang koneksyon sa kanya (muling nagbigay ng utos ang kausap na kumonekta sa amin) at isara ito. Ang IN_QUEUES queue ay nagtataglay ng mga string ng Python na may text ng mensahe, ngunit may espesyal na halaga na Wala na nagsenyas sa msg_sender coroutine na huminto sa paggana upang makalimutan nito ang tungkol sa manunulat nito na nauugnay sa legacy na koneksyon sa TCP.
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 # Run text message sender, initialize transport decoder {{{
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 # }}}
tumatanggap ang msg_sender ng mga papalabas na mensahe (nakapila mula sa isang in socket), ini-serialize ang mga ito sa isang mensaheng MsgText at ipinapadala ang mga ito sa isang koneksyon sa TCP. Maaari itong masira anumang sandali - malinaw nating naharang ito.
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))
Sa dulo, ang initiator ay pumapasok sa isang walang katapusang loop ng pagbabasa ng mga mensahe mula sa socket. Sinusuri kung ang mga mensaheng ito ay mga text message at inilalagay ang mga ito sa OUT_QUEUES queue, kung saan ipapadala ang mga ito sa out socket ng kaukulang kausap. Bakit hindi mo na lang gawin ang .read() at i-decode ang mensahe? Dahil posibleng maraming mensahe mula sa user ang pagsasama-samahin sa buffer ng operating system at ipapadala sa isang TCP segment. Maaari naming i-decode ang una, at pagkatapos ay ang bahagi ng kasunod na isa ay maaaring manatili sa buffer. Sa kaso ng anumang abnormal na sitwasyon, isinasara namin ang koneksyon ng TCP at ihihinto ang msg_sender coroutine (sa pamamagitan ng pagpapadala ng Wala sa OUT_QUEUES queue).
174 buf = b""
175 # Wait for test messages {{{
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)
Bumalik tayo sa pangunahing code. Matapos gawin ang lahat ng coroutine sa oras na magsimula ang programa, sisimulan namin ang TCP server. Para sa bawat naitatag na koneksyon, lumilikha ito ng coroutine ng responder.
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s %(asctime)s: %(funcName)s: %(message)s",
)
loop = asyncio.get_event_loop()
server = loop.run_until_complete(asyncio.start_server(responder, args.bind, args.port))
logging.info("Listening on: %s", server.sockets[0].getsockname())
loop.run_forever()
Ang responder ay katulad ng initiator at sinasalamin ang lahat ng parehong mga aksyon, ngunit ang walang katapusang loop ng pagbabasa ng mga mensahe ay nagsisimula kaagad, para sa pagiging simple. Sa kasalukuyan, ang protocol ng handshake ay nagpapadala ng isang mensahe mula sa bawat panig, ngunit sa hinaharap ay magkakaroon ng dalawa mula sa initiator ng koneksyon, pagkatapos ay maipapadala kaagad ang mga text message.
Oras na para i-secure ang ating mga komunikasyon. Ano ang ibig nating sabihin sa seguridad at ano ang gusto natin:
pagiging kompidensiyal ng mga ipinadalang mensahe;
pagiging tunay at integridad ng mga ipinadalang mensahe - dapat makita ang kanilang mga pagbabago;
proteksyon laban sa mga pag-atake ng replay - ang katotohanan ng nawawala o paulit-ulit na mga mensahe ay dapat makita (at nagpasya kaming wakasan ang koneksyon);
pagkakakilanlan at pagpapatunay ng mga kausap gamit ang paunang inilagay na mga pampublikong key - napagpasyahan na namin kanina na gagawa kami ng network ng kaibigan-sa-kaibigan. Pagkatapos lamang ng pagpapatunay ay mauunawaan natin kung kanino tayo nakikipag-usap;
availability perpektong pasulong na lihim properties (PFS) - ang pagkompromiso sa aming matagal nang signing key ay hindi dapat humantong sa kakayahang basahin ang lahat ng nakaraang sulat. Ang pagtatala ng naharang na trapiko ay nagiging walang silbi;
validity/validity ng mga mensahe (transport at handshake) sa loob lang ng isang TCP session. Ang pagpasok ng wastong nilagdaan/na-authenticate na mga mensahe mula sa isa pang session (kahit na may parehong kausap) ay hindi dapat posible;
hindi dapat makita ng passive observer ang alinman sa mga identifier ng user, ipinadala ang pangmatagalang public key, o mga hash mula sa kanila. Isang tiyak na anonymity mula sa isang passive observer.
Nakapagtataka, halos lahat ay gustong magkaroon ng pinakamababang ito sa anumang protocol ng handshake, at napakakaunti sa nabanggit sa huli ay natutugunan para sa "homegrown" na mga protocol. Ngayon hindi kami mag-iimbento ng bago. Talagang inirerekumenda ko ang paggamit balangkas ng ingay para sa pagbuo ng mga protocol, ngunit pumili tayo ng mas simple.
Ang dalawang pinakasikat na protocol ay:
TLS - isang napakakomplikadong protocol na may mahabang kasaysayan ng mga bug, jamb, mga kahinaan, hindi magandang pag-iisip, pagiging kumplikado at mga pagkukulang (gayunpaman, ito ay walang gaanong kinalaman sa TLS 1.3). Ngunit hindi namin ito isinasaalang-alang dahil ito ay sobrang kumplikado.
IPsec Ρ IKE β walang malubhang problema sa cryptographic, bagama't hindi rin sila simple. Kung nabasa mo ang tungkol sa IKEv1 at IKEv2, kung gayon ang kanilang pinagmulan ay STS, ISO/IEC IS 9798-3 at SIGMA (SIGn-and-MAc) na mga protocol - sapat na simple upang ipatupad sa isang gabi.
Ano ang mabuti sa SIGMA, bilang pinakabagong link sa pagbuo ng mga protocol ng STS/ISO? Natutugunan nito ang lahat ng aming kinakailangan (kabilang ang "pagtatago" ng mga identifier ng interlocutor) at walang alam na mga problema sa cryptographic. Ito ay minimalistic - ang pag-alis ng hindi bababa sa isang elemento mula sa mensahe ng protocol ay hahantong sa kawalan ng seguridad nito.
Pumunta tayo mula sa pinakasimpleng home-grown protocol hanggang sa SIGMA. Ang pinakapangunahing operasyon na interesado kami ay pangunahing kasunduan: Isang function na naglalabas ng parehong halaga sa parehong kalahok, na maaaring magamit bilang isang simetriko na key. Nang walang mga detalye: bawat isa sa mga partido ay bumubuo ng isang ephemeral (ginamit lamang sa loob ng isang session) key pair (pampubliko at pribadong key), pagpapalitan ng mga pampublikong susi, tawagan ang function ng kasunduan, sa input kung saan ipinapasa nila ang kanilang pribadong key at ang publiko susi ng kausap.
Kahit sino ay maaaring tumalon sa gitna at palitan ang mga pampublikong susi ng kanilang sarili - walang pagpapatunay ng mga kausap sa protocol na ito. Magdagdag tayo ng pirma na may matagal nang mga susi.
Ang gayong pirma ay hindi gagana, dahil hindi ito nakatali sa isang partikular na sesyon. Ang ganitong mga mensahe ay "angkop" din para sa mga sesyon kasama ang ibang mga kalahok. Ang buong konteksto ay dapat mag-subscribe. Pinipilit kami nitong magdagdag ng isa pang mensahe mula kay A.
Bilang karagdagan, mahalagang idagdag ang iyong sariling identifier sa ilalim ng lagda, dahil kung hindi, maaari naming palitan ang IdXXX at muling lagdaan ang mensahe gamit ang susi ng isa pang kilalang kausap. Iwasan pag-atake ng pagmuni-muni, kinakailangan na ang mga elemento sa ilalim ng lagda ay nasa malinaw na tinukoy na mga lugar ayon sa kanilang kahulugan: kung A sign (PubA, PubB), dapat lagdaan ng B (PubB, PubA). Ito rin ay nagsasalita sa kahalagahan ng pagpili ng istraktura at format ng serialized na data. Halimbawa, ang mga set sa ASN.1 DER encoding ay pinagbukod-bukod: SET OF(PubA, PubB) ay magiging magkapareho sa SET OF(PubB, PubA).
Gayunpaman, hindi pa rin namin "napatunayan" na nakabuo kami ng parehong nakabahaging key para sa session na ito. Sa prinsipyo, magagawa natin nang wala ang hakbang na ito - ang pinakaunang koneksyon sa transportasyon ay magiging di-wasto, ngunit gusto namin na kapag natapos na ang pakikipagkamay, sigurado kami na ang lahat ay talagang napagkasunduan. Sa ngayon mayroon kaming ISO/IEC IS 9798-3 na protocol sa kamay.
Maaari naming lagdaan ang nabuong susi mismo. Delikado ito, dahil posibleng may mga leaks sa signature algorithm na ginamit (kahit bits-per-signature, ngunit tumutulo pa rin). Posibleng pumirma ng hash ng derivation key, ngunit ang pagtagas kahit na ang hash ng derivated key ay maaaring maging mahalaga sa isang brute-force na pag-atake sa derivation function. Gumagamit ang SIGMA ng MAC function na nagpapatunay sa sender ID.
Bilang isang pag-optimize, maaaring gusto ng ilan na gamitin muli ang kanilang mga ephemeral key (na, siyempre, kapus-palad para sa PFS). Halimbawa, nakabuo kami ng key pair, sinubukang kumonekta, ngunit hindi available ang TCP o naantala sa isang lugar sa gitna ng protocol. Nakakahiya na sayangin ang nasayang na entropy at mga mapagkukunan ng processor sa isang bagong pares. Samakatuwid, ipapakilala namin ang tinatawag na cookie - isang pseudo-random na halaga na magpoprotekta laban sa mga posibleng random na pag-atake ng replay kapag muling gumagamit ng mga ephemeral na pampublikong key. Dahil sa pagbubuklod sa pagitan ng cookie at ng panandaliang pampublikong susi, ang pampublikong susi ng kabaligtaran na partido ay maaaring alisin sa lagda bilang hindi kinakailangan.
Sa wakas, gusto naming makuha ang privacy ng aming mga kasosyo sa pag-uusap mula sa isang passive observer. Upang gawin ito, iminumungkahi ng SIGMA na makipagpalitan muna ng mga ephemeral key at bumuo ng isang karaniwang key kung saan ie-encrypt ang pagpapatotoo at pagtukoy ng mga mensahe. Inilalarawan ng SIGMA ang dalawang opsyon:
SIGMA-I - pinoprotektahan ang nagpasimula mula sa mga aktibong pag-atake, ang tumutugon mula sa mga pasibo: pinapatotohanan ng initiator ang tumutugon at kung may hindi tumutugma, hindi nito ibinibigay ang pagkakakilanlan nito. Ang nasasakdal ay nagbibigay ng kanyang pagkakakilanlan kung ang isang aktibong protocol ay nagsimula sa kanya. Ang passive observer ay walang natutunan;
SIGMA-R - pinoprotektahan ang tumutugon mula sa mga aktibong pag-atake, ang pasimuno mula sa mga pasibo. Ang lahat ay eksaktong kabaligtaran, ngunit sa protocol na ito apat na mensahe ng pagkakamay ang naipadala na.
Pinipili namin ang SIGMA-I dahil mas katulad ito sa inaasahan namin mula sa mga pamilyar na bagay ng client-server: ang client ay kinikilala lamang ng authenticated server, at alam na ng lahat ang server. Dagdag pa, mas madaling ipatupad dahil sa mas kaunting mga mensahe ng handshake. Ang idinagdag lang namin sa protocol ay i-encrypt ang bahagi ng mensahe at ilipat ang identifier A sa naka-encrypt na bahagi ng huling mensahe:
GOST R ay ginagamit para sa lagda 34.10-2012 algorithm na may 256-bit key.
Upang buuin ang pampublikong key, 34.10/2012/XNUMX VKO ang ginagamit.
Ginagamit ang CMAC bilang MAC. Sa teknikal, ito ay isang espesyal na mode ng pagpapatakbo ng isang block cipher, na inilarawan sa GOST R 34.13-2015. Bilang isang function ng pag-encrypt para sa mode na ito β Grasshopper (34.12-2015).
Ang hash ng kanyang pampublikong key ay ginagamit bilang identifier ng kausap. Ginamit bilang hash Stribog-256 (34.11/2012/256 XNUMX bits).
Pagkatapos ng handshake, magkakasundo kami sa isang shared key. Magagamit namin ito para sa napatotohanang pag-encrypt ng mga mensahe sa transportasyon. Ang bahaging ito ay napakasimple at mahirap magkamali: dinadagdagan namin ang counter ng mensahe, ine-encrypt ang mensahe, pinapatotohanan (MAC) ang counter at ciphertext, ipinapadala. Kapag tumatanggap ng mensahe, tinitingnan namin na ang counter ay may inaasahang halaga, pinapatotohanan ang ciphertext gamit ang counter, at i-decrypt ito. Anong key ang dapat kong gamitin upang i-encrypt ang mga mensahe ng handshake, maghatid ng mga mensahe, at kung paano patotohanan ang mga ito? Ang paggamit ng isang susi para sa lahat ng mga gawaing ito ay mapanganib at hindi matalino. Ito ay kinakailangan upang makabuo ng mga susi gamit ang mga dalubhasang pag-andar Ang KDF (key derivation function). Muli, huwag nating hatiin ang buhok at mag-imbento ng isang bagay: HKDF matagal nang kilala, mahusay na sinaliksik at walang alam na mga problema. Sa kasamaang palad, ang katutubong Python library ay walang ganitong function, kaya ginagamit namin hkdf plastik na bag. Panloob na ginagamit ng HKDF HMAC, na gumagamit naman ng hash function. Ang isang halimbawang pagpapatupad sa Python sa pahina ng Wikipedia ay tumatagal lamang ng ilang linya ng code. Tulad ng sa kaso ng 34.10/2012/256, gagamitin namin ang Stribog-XNUMX bilang hash function. Ang output ng aming key agreement function ay tatawaging session key, kung saan bubuo ang mga nawawalang simetriko:
HandshakeTBS ang pipirmahan. HandshakeTBE - kung ano ang ie-encrypt. Iginuhit ko ang iyong pansin sa larangan ng ukm sa MsgHandshake1. 34.10 VKO, para sa mas malaking randomization ng mga nabuong key, kasama ang UKM (user keying material) parameter - karagdagang entropy lang.
Pagdaragdag ng Cryptography sa Code
Isaalang-alang lamang natin ang mga pagbabagong ginawa sa orihinal na code, dahil ang balangkas ay nanatiling pareho (sa katunayan, ang pangwakas na pagpapatupad ay isinulat muna, at pagkatapos ay ang lahat ng cryptography ay pinutol dito).
Dahil ang pagpapatotoo at pagkilala sa mga kausap ay isasagawa gamit ang mga pampublikong susi, kailangan na silang maiimbak sa isang lugar nang mahabang panahon. Para sa pagiging simple, ginagamit namin ang JSON tulad nito:
aming - aming key pair, hexadecimal pribado at pampublikong key. kanilang β mga pangalan ng mga kausap at kanilang mga pampublikong susi. Baguhin natin ang mga argumento ng command line at magdagdag ng post-processing ng data ng JSON:
from pygost import gost3410
from pygost.gost34112012256 import GOST34112012256
CURVE = gost3410.GOST3410Curve(
*gost3410.CURVE_PARAMS["GostR3410_2001_CryptoPro_A_ParamSet"]
)
parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
"--keys-gen",
action="store_true",
help="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),
}
# }}}
Ang pribadong key ng 34.10 algorithm ay isang random na numero. 256-bit na laki para sa 256-bit elliptic curves. Ang PyGOST ay hindi gumagana sa isang hanay ng mga byte, ngunit sa malalaking numero, kaya ang aming pribadong key (urandom(32)) ay kailangang ma-convert sa isang numero gamit ang gost3410.prv_unmarshal(). Ang pampublikong susi ay deterministikong tinutukoy mula sa pribadong susi gamit ang gost3410.public_key(). Ang pampublikong key 34.10 ay dalawang malalaking numero na kailangan ding i-convert sa isang byte sequence para sa kadalian ng pag-imbak at paghahatid gamit ang gost3410.pub_marshal().
Pagkatapos basahin ang JSON file, ang mga pampublikong key ay kailangang i-convert pabalik gamit ang gost3410.pub_unmarshal(). Dahil matatanggap namin ang mga identifier ng mga interlocutor sa anyo ng hash mula sa pampublikong key, maaari silang agad na kalkulahin nang maaga at ilagay sa isang diksyunaryo para sa mabilis na paghahanap. Ang hash ng Stribog-256 ay gost34112012256.GOST34112012256(), na ganap na nakakatugon sa interface ng hashlib ng mga function ng hash.
Paano nagbago ang initiator coroutine? Ang lahat ay ayon sa scheme ng handshake: bumubuo kami ng cookie (128-bit ay marami), isang ephemeral key pair na 34.10, na gagamitin para sa VKO key agreement function.
Ang UKM ay isang 64-bit na numero (urandom(8)), na nangangailangan din ng deserialization mula sa byte na representasyon nito gamit ang gost3410_vko.ukm_unmarshal(). Ang VKO function para sa 34.10/2012/256 3410-bit ay gost34102012256_vko.kek_XNUMX() (KEK - encryption key).
Ang nabuong session key ay isa nang 256-bit pseudo-random byte sequence. Samakatuwid, maaari itong agad na magamit sa mga function ng HKDF. Dahil natutugunan ng GOST34112012256 ang interface ng hashlib, maaari itong agad na magamit sa klase ng Hkdf. Hindi namin tinukoy ang asin (ang unang argumento ng Hkdf), dahil ang nabuong key, dahil sa ephemerality ng mga kalahok na pares ng key, ay magkakaiba para sa bawat session at naglalaman na ng sapat na entropy. Ang kdf.expand() bilang default ay gumagawa na ng mga 256-bit na key na kinakailangan para sa Grasshopper sa susunod.
Susunod, ang TBE at TBS na bahagi ng papasok na mensahe ay sinusuri:
ang MAC sa papasok na ciphertext ay kinakalkula at sinusuri;
ang ciphertext ay decrypted;
Na-decode ang istraktura ng TBE;
ang identifier ng kausap ay kinuha mula dito at ito ay nasuri kung siya ay kilala sa amin sa lahat;
Ang MAC sa ibabaw ng identifier na ito ay kinakalkula at sinusuri;
na-verify ang lagda sa istruktura ng TBS, na kinabibilangan ng cookie ng parehong partido at ang pampublikong pansamantalang key ng kabaligtaran na partido. Ang pirma ay napatunayan ng matagal nang signature key ng kausap.
Tulad ng isinulat ko sa itaas, 34.13/2015/XNUMX ay naglalarawan ng iba't ibang harangan ang mga mode ng pagpapatakbo ng cipher mula 34.12/2015/3413. Kabilang sa mga ito ay mayroong isang mode para sa pagbuo ng mga pagsingit ng imitasyon at mga kalkulasyon ng MAC. Sa PyGOST ito ay gost34.12.mac(). Ang mode na ito ay nangangailangan ng pagpasa sa encryption function (pagtanggap at pagbabalik ng isang bloke ng data), ang laki ng encryption block at, sa katunayan, ang data mismo. Bakit hindi mo ma-hardcode ang laki ng encryption block? Inilalarawan ng 2015/128/64 hindi lamang ang XNUMX-bit Grasshopper cipher, kundi pati na rin ang XNUMX-bit Magma - isang bahagyang binagong GOST 28147-89, na nilikha pabalik sa KGB at mayroon pa ring isa sa mga pinakamataas na threshold ng kaligtasan.
Sinisimulan ang Kuznechik sa pamamagitan ng pagtawag sa gost.3412.GOST3412Kuznechik(key) at ibinabalik ang isang bagay na may mga pamamaraang .encrypt()/.decrypt() na angkop para sa pagpasa sa 34.13 function. Ang MAC ay kinakalkula tulad ng sumusunod: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). Upang ihambing ang kinakalkula at natanggap na MAC, hindi mo maaaring gamitin ang karaniwang paghahambing (==) ng mga byte string, dahil ang operasyong ito ay naglalabas ng oras ng paghahambing, na, sa pangkalahatang kaso, ay maaaring humantong sa nakamamatay na mga kahinaan tulad ng BEAST pag-atake sa TLS. May espesyal na function ang Python, hmac.compare_digest, para dito.
Ang block cipher function ay maaari lamang mag-encrypt ng isang bloke ng data. Para sa mas malaking numero, at kahit na hindi isang multiple ng haba, kinakailangan na gamitin ang encryption mode. Inilalarawan ng 34.13-2015 ang sumusunod: ECB, CTR, OFB, CBC, CFB. Ang bawat isa ay may sariling katanggap-tanggap na mga lugar ng aplikasyon at mga katangian. Sa kasamaang palad, wala pa rin tayong standardized na-authenticate na mga mode ng pag-encrypt (tulad ng CCM, OCB, GCM at mga katulad nito) - napipilitan tayong idagdag man lang ang MAC mismo. pinili ko counter mode (CTR): hindi ito nangangailangan ng padding sa laki ng block, maaaring iparallelize, ginagamit lamang ang encryption function, maaaring ligtas na magamit upang i-encrypt ang malaking bilang ng mga mensahe (hindi tulad ng CBC, na may mga banggaan na medyo mabilis).
Tulad ng .mac(), .ctr() ay kumukuha ng katulad na input: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Kinakailangang tumukoy ng initialization vector na eksaktong kalahati ng haba ng encryption block. Kung ang aming encryption key ay ginagamit lamang upang i-encrypt ang isang mensahe (kahit na mula sa ilang mga bloke), pagkatapos ay ligtas na magtakda ng zero initialization vector. Upang i-encrypt ang mga mensahe ng handshake, gumagamit kami ng hiwalay na key sa bawat oras.
Ang pag-verify sa signature na gost3410.verify() ay trivial: ipinapasa namin ang elliptic curve kung saan kami nagtatrabaho (itinatala lang namin ito sa aming GOSTIM protocol), ang pampublikong susi ng pumirma (huwag kalimutan na dapat itong isang tuple ng dalawa malalaking numero, at hindi isang byte string), 34.11/2012/XNUMX hash at ang mismong lagda.
Susunod, sa initiator naghahanda kami at nagpapadala ng mensahe ng handshake sa handshake2, nagsasagawa ng parehong mga aksyon tulad ng ginawa namin sa panahon ng pag-verify, simetriko lang: pag-sign sa aming mga susi sa halip na suriin, atbp...
Kapag naitatag ang session, ang mga transport key ay nabuo (isang hiwalay na key para sa pag-encrypt, para sa pagpapatunay, para sa bawat isa sa mga partido), at ang Grasshopper ay sinisimulan upang i-decrypt at suriin ang MAC:
Ine-encrypt na ngayon ng msg_sender coroutine ang mga mensahe bago ipadala ang mga ito sa isang koneksyon sa TCP. Ang bawat mensahe ay may monotonically increase na nonce, na siya ring initialization vector kapag naka-encrypt sa counter mode. Ang bawat mensahe at bloke ng mensahe ay ginagarantiyahan na may iba't ibang halaga ng counter.
Ang GOSTIM ay nilayon na gamitin nang eksklusibo para sa mga layuning pang-edukasyon (dahil hindi ito saklaw ng mga pagsusulit, hindi bababa sa)! Maaaring ma-download ang source code ng programa dito (Π‘ΡΡΠΈΠ±ΠΎΠ³-256 Ρ ΡΡ: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). ΠΠ°ΠΊ ΠΈ Π²ΡΠ΅ ΠΌΠΎΠΈ ΠΏΡΠΎΠ΅ΠΊΡΡ, ΡΠΈΠΏΠ° GoGOST, PyDERASN, NCCP, GoVPN, GOSTIM ay ganap libreng software, ibinahagi sa ilalim ng mga tuntunin GPLv3 +.