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 veebimajutus DDoS-kaitsega veebisaitidele, VPS VDS serverid 🔥 Osta usaldusväärne veebimajutus DDoS-kaitsega veebisaitidele, VPS VDS serverid - ProHoster