GOSTIM: P2P F2F E2EE IM ĂŒhe Ă”htu jooksul GOST-krĂŒptograafiaga

Olles arendaja PyGOST 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 turvalist protokolli. 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

GOSTIM: P2P F2F E2EE IM ĂŒhe Ă”htu jooksul GOST-krĂŒptograafiaga
peer-to-peer friend-to-friend, lĂ”puni ĆĄifreeritud, instant messengerit, millel on SIGMA-I autentimis- ja vĂ”tmevahetusprotokoll (mille alusel on rakendatud IPsec IKE ), kasutades eranditult GOST-i krĂŒptograafilisi algoritme PyGOST raamatukogust ja ASN.1 sĂ”numite kodeerimist raamatukogustPyDERASN (millest ma olen juba varem kirjutanud ). vajalik tingimus: see peab olema piisavalt lihtne, et saate selle nullist ĂŒhe Ă”htu (vĂ”i tööpĂ€eva) jooksul kirjutada, muidu pole see enam lihtne programm. Seda saadakse kindlasti vigu, tarbetud keerukusi, puudusi, pluss see on minu esimene programm, kus kasutatakse asyncio raamatukogu.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. suckless-projektidega, 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 tmux ja/or multitail, on vĂ”imalik saada mitme aknaga liides sĂŒntaksivĂ€rviga. Ja rlwrap 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 Go). 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 socat: 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 SCTP 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. XDR.

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 (millest ma olen juba raamatukogu, kuna meil on kĂ”rgetasemelised objektid, millega on sageli meeldivam ja mugavam töötada. Erinevalt skeemivabadest bencode, MessagePack vĂ”i CBOR, 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 tĂ€iustatud edasiuleku saladus 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 Noise framework , et protokolle ĂŒles ehitada, kuid valime midagi lihtsamat.

Kaks protokolli on kÔige populaarsemad:

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 vĂ”tmete kokkuleppimine: 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 reflection rĂŒnnakud, 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 34.10-2012 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 — Kuznecik (34.12-2015).
    • Kuna osaleja identifikaatorina kasutatakse tema avaliku vĂ”tme rĂ€situd versiooni. RĂ€siks kasutatakse Stribog-256 (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 KDF (vĂ”tme genereerimise funktsioon). JĂ€tkame lihtsusega ega hakka midagi vĂ€lja mĂ”tlema: HKDF on ammu tuntud, hĂ€sti uuritud ja ei sisalda teadaolevaid probleeme. Kahjuks ei ole selle funktsioon Python'i pĂ”hiteegis, seega kasutame hkdf 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: 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")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 suuremate arvudega, 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 plokkĆĄifreerimise tööreĆŸiime 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 Magmat — 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 BEAST 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 autentimise reĆŸiime (nĂ€iteks CCM, OCB, GCM ja sarnased) — me peame vĂ€hemalt ise MAC-i lisama. Ma valin loenduri reĆŸiimi (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 += 1
    

    Saabuvad 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 siit (Stribog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Nagu kÔik minu projektid, stiilis GoGOST, (millest ma olen juba, NNCP, GoVPN, GOSTIM on tÀielikult avatud tarkvara, levitatakse jÀrgmistel tingimustel GPLv3+.

    Sergei Matveev, krĂŒptopank, liige AVT Fond, Python/Go arendaja, peamine spetsialist Riigi Ühing "NKT Atlas".

Allikas: habr.com

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster