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

Olen arendaja PyGOST raamatukogust (GOST-krĂŒptograafilised primitiivid puhtal Pythonil), saan sageli kĂŒsimusi selle kohta, kuidas kiiresti rakendada kĂ”ige lihtsamat turvalist sĂ”numivahetust. Paljud arvavad, et rakenduskrĂŒptograafia on piisavalt lihtne asi ja .encrypt() vĂ€ljakutse plokkĆĄifril on piisav, et turvaliselt edastada sĂ”numeid suhtluskanali kaudu. Teised aga usuvad, et rakenduskrĂŒptograafia on vaid vĂ€heste privileeg ja on lubatav, et rikkad ettevĂ”tted nagu Telegram, kellel on matemaatikaolĂŒmpia vĂ”itjad, ei suuda rakendada turvalist protokolli.

See kĂ”ik on inspireerinud mind kirjutama seda artiklit, et nĂ€idata, et krĂŒptograafiliste protokollide ja turvalise IM-i rakendamine ei ole nii keeruline ĂŒlesanne. Siiski ei tasu leiutada oma autentimis- ja vĂ”tmevahetusprotokolle.

GOSTIM: P2P F2F E2EE IM ĂŒhe Ă”htu jooksul GOST-krĂŒptograafiaga
Artiklis kĂ€sitletakse peer-to-peer, friend-to-friend, lĂ”pp-lĂ”puni krĂŒptitud kiirsĂ”numiteenust koos SIGMA-I autentimise ja vĂ”tmevahetusprotokolliga (mille baasil on rakendatud IPsec IKE), kasutades ainult GOST-krĂŒptograafilisi algoritme PyGOST raamatukogust ja ASN.1 koodimist sĂ”numite jaoks raamatukogust PyDERASN (mille kohta ma juba olen varem kirjutanud). NĂ”utav tingimus: see peab olema piisavalt lihtne, et seda saaks kirjutada nullist ĂŒhe Ă”htu (vĂ”i tööpĂ€eva) jooksul, vastasel juhul ei ole see enam lihtne programm. TĂ”enĂ€oliselt sisaldab see vigu, liigset keerukust ja puudusi, lisaks on see minu esimene programm, kus kasutatakse asyncio teeki.

IM-i disain

KĂ€ivitamiseks peame mĂ”tlema, milline meie IM vĂ€lja nĂ€eb. Lihtsuse huvides olgu see peer-to-peer vĂ”rk, ilma mingisuguse osalejate avastamiseta. Üksikasjalikult nĂ€itame, millisele aadressile ja pordile me ĂŒhendust vĂ”tame, et suhelda vestluskaaslasega.

Ma mĂ”istan, et hetkel on eeldus kahe suvalise arvuti vahelise otsese ĂŒhenduse kĂ€ttesaadavuse osas praktiliselt rakendamisele suurt piirangut. Kuid mida rohkem arendajaid rakendavad igasuguseid NAT-traversali lahendusi, seda kauem me jÀÀme IPv4 Internetti, masendava tĂ”enĂ€osusega ĂŒhenduse puudumisega suvaliste arvutite vahel. Kui kaua me peame taluma IPv6 puudumist kodus ja tööl?

Meil on sĂ”bra-sĂ”brani vĂ”rk: kĂ”ik vĂ”imalikud vestluspartnerid peavad olema eelnevalt teada. Esiteks, see lihtsustab kĂ”ike: tutvustasime end, leidsime vĂ”i ei leidnud nime/klahvi, lahkusime vĂ”i jĂ€tkame tööd, teades vestluspartnerit. Teiseks, ĂŒldiselt on see ohutu ja vĂ€listab palju rĂŒnnakuid.

IM-liidese loomus jÀÀb klassikaliste lahenduste lÀhedale. suckless-projektide, mis mulle vÀga meeldivad oma minimalismi ja Unix-way filosoofia tÔttu. IM-programm loob iga vestluspartneri jaoks kausta, kus on kolm Unix domain socketit:

  • in — sinna kirjutatakse saadetavad sĂ”numid vestluspartnerile;
  • out — sealt loetakse vastu vĂ”etud sĂ”numeid vestluspartnerilt;
  • state — selle kaudu loeme, kas vestluspartner on hetkel ĂŒhendatud, aadress/port ĂŒhendusest.

Lisaks luuakse conn socket, kuhu kirjutades hosti ja porti, algatame ĂŒhenduse kaugel asuva vestluspartneriga.

|-- alice
|   |-- in
|   |-- out
|   `-- state
|-- bob
|   |-- in
|   |-- out
|   `-- state
`- conn

Selline lĂ€henemine vĂ”imaldab luua sĂ”ltumatuid IM-transportide ja kasutajaliidese realiseerimisi, sest igale maitsele ja vĂ€rvusele ei saa meeldida, igale ei meeldi. Kasutades tmux ja/vĂ”i multitail, saab luua mitmeakna vaate koos sĂŒntaktilise esiletĂ”stmisega. Ja abiga rlwrap saab luua GNU Readline'i ĂŒhilduva sisestusrea sĂ”numite jaoks.

Tegelikult kasutavad suckless projektid FIFO-faile. Isiklikult ei suutnud ma mĂ”ista, kuidas asyncio's faile korraga kasutada ilma eraldi niitide kĂ€sitsi seadistamiseta (selliseks otstarbeks olen pikka aega kasutanud keelt Go). SeetĂ”ttu otsustasin kasutada Unix domain sokette. Kahjuks takistab see teha echo 2001:470:dead::babe 6666 > conn. Otsustasin selle probleemi lahendada, kasutades socat: echo 2001:470:dead::babe 6666 | socat — UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.

Algne ebaohutu protokoll

Transportina kasutatakse TCP-d: see tagab kohaletoimetamise ja selle jĂ€rjekorra. UDP ei garanteeri kumbagi (mis oleks kasulik, kui rakendatakse krĂŒptograafiat), ja toetust SCTP Pythonis ei ole vaikimisi.

Kahjuks ei ole TCP-s mĂ”istet, mis oleks sĂ”num, vaid ainult baitide voog. SeetĂ”ttu tuleb vĂ€lja mĂ”elda formaat sĂ”numite jaoks, et neid voos eristada. Saame kokku leppida, et kasutame reavahetust. Alustuseks sobib see hĂ€sti, kuid kui hakkame meie sĂ”numeid krĂŒpteerima, vĂ”ib see sĂŒmbol ilmuda kuskil krĂŒpteeritud tekstis. SeetĂ”ttu on vĂ”rkudes populaarsed protokollid, mis saadavad esmalt sĂ”numi pikkuse baitides. NĂ€iteks Pythonis on olemas xdrlib, mis vĂ”imaldab sellise formaadiga töötada. XDR.

Me ei tööta TCP lugemisega korralikult ja tĂ”husalt — lihtsustame koodi. Loeme lĂ”putu tsĂŒkli jooksul andmeid soketist, kuni dekodeerime tĂ€ispika sĂ”numi. Sellise lĂ€henemisviisi jaoks saab kasutada ka JSON-i koos XML-iga. Kuid kui lisandub krĂŒptograafia, tuleb andmed allkirjastada ja autentida — see nĂ”uab bait-baidiga identset objekti esitlemist, mida JSON/XML ei taga (dumpsi tulemus vĂ”ib erineda).

XDR sobib selle ĂŒlesande jaoks, kuid ma valin ASN.1 DER-kodeeringuga. PyDERASN raamatukogu, sest meil on kĂ€sitlemiseks kĂ”rgematasemelised objektid, millega on sageli meeldivam ja mugavam töötada. Erinevalt schemaless-st bencode, MessagePack vĂ”i CBOR, ASN.1 kontrollib automaatselt andmeid vastavalt rangelt mÀÀratud skeemile.

# 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))),
    ))

Saadetav sĂ”num on Msg: kas tekstiline MsgText (praegu ĂŒhes tekstivĂ€ljas), vĂ”i sĂ”numikĂ€tlemine MsgHandshake (kus edastatakse vestluspartneri nimi). Praegu tundub see ĂŒlemÀÀra keeruline, kuid see on tulevikku suunatud lahendus.

     ┌─────┐            ┌─────┐
     │PeerA│            │PeerB│
     └──┬──┘            └──┬──┘
        │MsgHandshake(IdA) │
        │─────────────────>│
        │                  │
        │MsgHandshake(IdB) │
        ││
        │                  │
        │    MsgText()     │
        │<─────────────────│
        │                  │

IM ilma krĂŒptograafiata

Kuidas ma juba ĂŒtlesin, kĂ”igi soketite operatsioonide jaoks kasutatakse asyncio raamatukogu. Teeme eeldusi, mida me oodata saame kĂ€ivitamisel:

parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
    "--our-name",
    required=True,
    help="Meie kaaslase nimi",
)
parser.add_argument(
    "--their-names",
    required=True,
    help="Nende kaaslaste nimed, eraldatud komaga",
)
parser.add_argument(
    "--bind",
    default="::1",
    help="Aadress, millel kuulata",
)
parser.add_argument(
    "--port",
    type=int,
    default=6666,
    help="Port, millel kuulata",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))

MÀÀrake oma nimi (—our-name alice). KĂ”iki oodatavaid kaaslasi loetletakse komaga (—their-names bob,eve). Iga kaaslase jaoks luuakse Unix-soketide kaust, 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 saadud 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"))

Vestlused, mille saadame OUT_QUEUES jÀrjekordadesse, saadetakse vÀlja 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Ă”nastikust ĂŒhenduse partneri aadressi. Kui ĂŒhendust partneriga ei ole, kirjutatakse vĂ€lja tĂŒhi rida.

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 soketti kĂ€ivitatakse ĂŒhenduse "algataja" 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, nagu tavaliselt, ĂŒhenduse mÀÀratud hosti/portiga ja saadab kĂ€ivitamisĂ”iguse sĂ”numi koos oma nimega:

 130 async def initiator(host, port):
 131     _id = repr((host, port))
 132     logging.info("%s: dialimine", _id)
 133     reader, writer = await asyncio.open_connection(host, port)
 134     # KÀepideme sÔnum {{{
 135     writer.write(Msg(("kÀepide", MsgHandshake((
 136         ("partneriNimi", OUR_NAME),
 137     )))).encode())
 138     # }}}
 139     await writer.drain()

SeejĂ€rel ootab see vastust kaugserverilt. PĂŒĂŒab dekodeerida saadud vastust Msg ASN.1 skeemi jĂ€rgi. Eeldame, et kogu sĂ”num saadetakse ĂŒhes TCP-segmente ja me saame selle aatomiliselt kĂ€tte, kui kutsume vĂ€lja .read(). Kontrollime, et oleme saanud tĂ”epoolest kĂ€epideme sĂ”numi.

 141     # Oota kÀepideme sÔnumit {{{
 142     data = await reader.read(256)
 143     if data == b"":
 144         logging.warning("%s: ei ole vastust, katke ĂŒhendus", _id)
 145         writer.close()
 146         return
 147     try:
 148         msg, _ = Msg().decode(data)
 149     except ASN1Error:
 150         logging.warning("%s: dekodeerimata vastus, katke ĂŒhendus", _id)
 151         writer.close()
 152         return
 153     logging.info("%s: saadi %s sÔnum", _id, msg.choice)
 154     if msg.choice != "kÀepide":
 155         logging.warning("%s: ootamatu sĂ”num, katke ĂŒhendus", _id)
 156         writer.close()
 157         return
 158     # }}}

Kontrollime, kas me teame kĂ”nekaaslase saadetud nime. Kui ei, katkestame ĂŒhenduse. Kontrollime, kas meil on juba temaga ĂŒhendus loodud (kĂ”nekaaslane on taas andnud kĂ€su meiega ĂŒhendada) ja sulgeme selle. IN_QUEUES jĂ€rjekorda pannakse Python-read sĂ”numiga, kuid eriliseks vÀÀrtuseks on None, mis annab mĂ€rku msg_sender koorutine lĂ”petada töö, et ta unustaks 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("tundmatu kÔnekaaslase nimi: %s", peer_name)
 163         writer.close()
 164         return
 165     logging.info("%s: sessioon loodud: %s", _id, peer_name)
 166     # KÀivita tekstisÔnumite saatja, initsialiseeri 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Ă€ljuvaid sĂ”numeid (mis pannakse jĂ€rjekorda in soketist), serialiseerib need MsgText sĂ”numiks ja saadab TCP-ĂŒhenduse kaudu. See vĂ”ib igal hetkel katkeda — seda me selgelt pĂŒĂŒame kinni.

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 soketist. Kontrollib, kas need on tekstisĂ”numid, ja paneb need OUT_QUEUES jĂ€rjekorda, kust need saadetakse vĂ€lja vastava vestluspartneri soketti. Miks ei saa lihtsalt teha .read() ja sĂ”numit dekodeerida? Sest ei saa vĂ€listada olukorda, kus mitu kasutaja sĂ”numit liidetakse operatsioonisĂŒsteemi puhvris ja saadetakse ĂŒhes TCP-segmendis. Dekodeerida saame me esimese, kuid puhvris vĂ”ib jÀÀda osa jĂ€rgnevast. Igasuguste anomaaliate korral suletakse TCP-ĂŒhendus ja peatatakse msg_sender korutii, saates OUT_QUEUES jĂ€rjekorda None.

 174     buf = b""
 175     # Oota testisÔ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: maksimaalne 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)

LĂ€hme tagasi pĂ”hikoodi juurde. PĂ€rast kĂ”igi korutinate loomist programmi kĂ€ivitamise hetkel loome TCP-serveri. Iga kehtestatud ĂŒhenduse korral loob see responder (vastaja) korutina.

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("KĂŒlastatakse: %s", server.sockets[0].getsockname())
loop.run_forever()

responder sarnaneb initiatorile ja peegeldab kĂ”iki samu toiminguid, kuid lĂ”pmatu sĂ”numite lugemise tsĂŒkkel kĂ€ivitatakse kohe, lihtsuse huvides. Praegu saadab kĂ€epigistusprotokoll igast kĂŒljest ĂŒhe sĂ”numi, kuid tulevikus saadetakse algataja poolt kaks sĂ”numit, millele jĂ€rgneb kohene tekstisĂ”numite saatmise vĂ”imalus.

  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 tagada meie suhtluse turvalisus. Mida me julgeolekuga silmas peame ja mida soovime:

  • edastatud sĂ”numite konfidentsiaalsus;
  • edastatud sĂ”numite autentsus ja terviklikkus — nende muutmine peaks olema tuvastatav;
  • kaitse taaskĂ€ivitamisrĂŒnnakute (replay attack) eest — sĂ”numite kadumise vĂ”i kordumise fakt peab olema avastatav (ja me otsustame katkestada ĂŒhenduse);
  • kaasvestlejate tuvastamine ja autentimine eelnevalt sisestatud avalike vĂ”tmete pĂ”hjal — oleme juba otsustanud luua sĂ”bralt-sĂ”brale vĂ”rgu. Ainult pĂ€rast autentimist mĂ”istame, kellega suhtleme;
  • olemasolu tĂ€iuslik edasiulatuv salajasus omadused (PFS) — meie pikaajalise allkirja vĂ”tme kompromiteerimine ei tohi vĂ”imaldada kogu varasema kirjavahetuse lugemist. Salvestatud sissetungitud liiklus muutub kasutuks;
  • sĂ”numite (transpordi ja kĂ€tlemise) kehtivus/valiidus ainult ĂŒhe TCP-seansi piires. Teise seansi (isegi sama kaasvestlejaga) Ă”igesti allkirjastatud/autentitud sĂ”numite sisestamine ei tohi olla vĂ”imalik;
  • passiivne vaatleja ei tohiks nĂ€ha ei kasutajaid, ei toimetatavaid pikaealisi avalikke vĂ”tmeid, ei ka nende rĂ€simĂ€rke. Teatud anonymiteet passiivse vaatleja eest.

Üllatav, et seda minimaalset taset soovivad kĂ”ik igasuguste kĂ€epigistusprotokollide puhul, kuid vĂ€he sellest tegelikult rakendatakse "koduseid" protokolle. Seega, jĂ€tame uue vĂ€lja mĂ”tlemise. Soovitaksin kindlalt kasutada Noise framework protokollide loomiseks, kuid valime midagi lihtsamat.

Kaks protokolli on eriti populaarsed:

  • TLS — keeruline protokoll, millel on pikk ajaloo hulk vigu, puudujÀÀke, haavatavusi, halba planeerimist, keerukust ja ebatĂ€iuslikkust (kuigi TLS 1.3-ga see pole kuigi seotud). Kuid me ei kĂ€sitle seda ĂŒlekompleksuse tĂ”ttu.
  • IPsec koos IKE — ei oma tĂ”siseid krĂŒptograafilisi probleeme, kuigi nadki ei ole lihtsad. Kui lugeda IKEv1 ja IKEv2 kohta, siis nende allikaks on STS, ISO/IEC IS 9798-3 ja SIGMA (SIGn-and-MAc) protokollid — piisavalt lihtsad rakendamiseks ĂŒhe Ă”htu jooksul.

Miks on SIGMA, kui viimane etapp STS/ISO protokollide arengus, hea? See vastab meie kĂ”ikidele nĂ”udmistele (sealhulgas vestluspartnerite identifikaatorite "peitmisele"), tal ei ole tuntud krĂŒptograafilisi probleeme. See on minimalistlik — protokolli sĂ”numist vĂ€hemalt ĂŒhe elementi eemaldamine muudab selle ebaturvaliseks.

Alustame kĂ”ige lihtsamatest kodutooted protokollidest ja liigume SIGMA poole. Meid huvitav kĂ”ige baasem operatsioon on vĂ”tme kokkusobivus: funktsioon, mille vĂ€ljundiks on sama vÀÀrtus, mille saavad mĂ”lemad osalised, mida saab kasutada sĂŒmmeetrilise vĂ”tmena. Ilma detailidesse laskumata: iga osaline genereerib efemeerse (ainult ĂŒhe seansi jooksul kasutatava) vĂ”tme paari (avalik ja privaatne vĂ”ti), vahetavad avalikke vĂ”tmeid, kutsuvad esile kokkusobivuse funktsiooni, kuhu nad edastavad oma privaatvĂ”tme ja vestluspartneri avaliku vĂ”tme.

┌─────┐          ┌─────┐
│PeerA│          │PeerB│
└──┬──┘          └──┬──┘
   │   IdA, PubA    │ ╔════════════════════╗
   │───────────────>│ ║PrvA, PubA = DHgen()║
   │                │ ╚════════════════════╝
   │   IdB, PubB    │ ╔════════════════════╗
   │<───────────────│ ║PrvB, PubB = DHgen()║
   │                │ ╚════════════════════╝
   ────┐    ╔═══════╧════════════╗
       │    ║Key = DH(PrvA, PubB)║
   <───┘    ╚═══════╀════════════╝
   │                │
   │                │

Keenest saatus vĂ”ib vahele segada ja asendada avalikke vĂ”tmeid oma omadega — selles protokollis ei ole partnerite 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 sĂ”numid vĂ”ivad sobida ka teiste osalejatega sessioonide jaoks. Allkirjastamine peab toimuma kogu konteksti ulatuses. See sunnib ka A-l veel ĂŒhe sĂ”numi saatmist.

Lisaks on kriitiline, et allkirja alla lisataks koostisosade ja unikaalse identifikaator, vastasel juhul vĂ”ime IdXXX asendada ja allkirjastada sĂ”numi kellegi teise teadliku vĂ”tmega. Selle vĂ€ltimiseks reflection rĂŒnnakute, peab allkirja all olevate elementide paiknemine olema selgelt mÀÀratletud: kui A allkirjastab (PubA, PubB), siis peab B allkirjastama (PubB, PubA). See rĂ”hutab ka andmesarjade struktuuri ja formaadi valimise tĂ€htsust. NĂ€iteks ASN.1 DER kodeerimise kogumid sorteeritakse: 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 ei ole veel "tĂ”estanud", et oleme selle seansi jaoks sama ĂŒldise vĂ”tme vĂ€lja töötanud. PĂ”himĂ”tteliselt saaksime sellest sammust ka loobuda — esimene edastatav sĂ”num on kehtetu, kuid tahame, et pĂ€rast kĂ€epigistuse lĂ”ppemist oleksime kindlad, et kĂ”ik on tĂ”eliselt kooskĂ”lastatud. Praegu tugineme ISO/IEC IS 9798-3 protokollile.

Me vĂ”iksime allkirjastada ka vĂ€lja töötatud vĂ”tme. See on ohtlik, kuna on vĂ”imalik, et kasutatud allkirjastamisalgoritmis vĂ”ivad olla lekked (kuigi allkirja bitti, kuid ikkagi lekked). Saame allkirjastada vĂ€lja töötatud vĂ”tme rĂ€sima, kuid isegi rĂ€simise leke vĂ€lja töötatud vĂ”tme puhul vĂ”ib olla vÀÀrtus, kui rĂŒnnata genereerimise funktsiooni brute-force meetodil. 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 osas vĂ”ivad mĂ”ned soovida oma efemeerseid vĂ”tmeid uuesti kasutada (mida on loomulikult kahju teha PFS-i jaoks). NĂ€iteks, kui genereerisime vĂ”tme paari, ĂŒritasime ĂŒhenduda, kuid TCP ei olnud saadaval vĂ”i katkestas kuskil protokolli keskel. Kahju on raisata kulutatud entropiat ja protsessorite ressursse uue paari jaoks. SeetĂ”ttu tutvustame nn kĂŒpsist — pseudojuhuslikku vÀÀrtust, mis kaitseb vĂ”imalike juhuslike taaskasutamise rĂŒnnakute eest, kui efemeerseid avalikke vĂ”tmeid uuesti kasutatakse. TĂ€nu kĂŒpsise ja efemeerse avaliku vĂ”tme vahelisele sidumisele saab vastase avaliku vĂ”tme allkirjastamise mĂ”ttetuks 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Ă” finally m we want to protect the privacy of our interlocutor identifiers from passive observers. For this, SIGMA proposes first to exchange ephemeral keys, develop a shared key to encrypt authenticating and identifying messages. SIGMA describes two options:

  • SIGMA-I — protects the initiator from active attacks and the respondent from passive ones: the initiator authenticates the respondent, and if something does not match, he does not reveal his identification. The respondent, however, reveals his identification only if the active protocol is initiated. A passive observer learns nothing;
    SIGMA-R — protects the respondent from active attacks and the initiator from passive ones. Everything is the opposite, but in this protocol, four handshake messages are transmitted.

    Valime SIGMA-I, kuna see on rohkem sarnane sellele, mida me ootame kliendi-serveri tavapĂ€rastest lahendustest: klient tunneb Ă€ra ainult autentitud serveri, samas kui server teab kĂ”ike. Lisaks on see rakendamisel lihtsam, kuna vajalike kĂ€epigistuste sĂ”numite arv on vĂ€iksem. KĂ”ik, mida me protokolli lisame, on sĂ”numi osa krĂŒpteerimine ja A identifikaatori edastamine viimase sĂ”numi krĂŒpteeritud osas:

    ┌─────┐                                                                        ┌─────┐
    │PeerA│                                                                        │PeerB│
    └──┬──┘                                                                        └──┬──┘
       │                                PubA, CookieA                                 │ ╔═══════════════════════════╗
       │─────────────────────────────────────────────────────────────────────────────>| ║SignPrvA, SignPubA = load()║
       │                                                                              │ ║PrvA, PubA = DHgen()       ║
       │                                                                              │ ╚═══════════════════════════╝
       │PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB))) │ ╔═══════════════════════════╗
       │| ║verify(Key, IdB)     ║
       │                                                                              │ ║verify(SignPubB, ...)║
       │                                                                              │ ╚═════════════════════╝
       │                                                                              │
    
    • Allkirjastamiseks kasutatakse GOST R 34.10-2012 algoritmi 256-bitiste vĂ”tmetega.
    • Ühise vĂ”tme genereerimiseks kasutatakse 34.10-2012 VKO.
    • MAC-ina kasutatakse CMAC-i. Tehniliselt on see plokkĆĄifri tööreĆŸiim, mida kirjeldatakse GOST R 34.13-2015. Selle reĆŸiimi krĂŒpteerimisfunktsioonina — SÀÀse (34.12-2015).
    • Vestluspartneri identifikaatorina kasutatakse tema avaliku vĂ”tme hash'i. Hashina kasutatakse Stribog-256 (34.11-2012 256 bitti).

    PĂ€rast kĂ€epigistust on meil kokkulepitud ĂŒhine vĂ”ti. Seda saame kasutada autentitud transportside krĂŒpteerimiseks. See osa on tĂ€iesti lihtne ja selles on keeruline eksida: suurendame sĂ”numite loendurit, krĂŒpteerime sĂ”numi, autentime (MAC) loenduri ja krĂŒpteeritud teksti, saadame. SĂ”numi vastuvĂ”tmisel kontrollime, et loendur on oodatud vÀÀrtusega, autentime krĂŒpteeritud teksti loenduri abil, dekrĂŒpteerime. Millise vĂ”tmega krĂŒpteerida kĂ€epigistuse, transpordi sĂ”numid, mida autentida? Ühte vĂ”tmega kĂ”ikide nende ĂŒlesannete tĂ€itmiseks kasutamine on ohtlik ja arutu. Tuleb genereerida vĂ”tmeid, kasutades spetsialiseeritud funktsioone KDF (key derivation function). JĂ€tke see lihtsaks, ei hakka midagi vĂ€lja mĂ”tlema: HKDF on ammu tuntud, hĂ€sti uuritud ja sellel ei ole teadaolevaid probleeme. Kahjuks ei ole selle funktsiooni jaoks Pytonis sisseehitatud teeki, seega kasutame hkdf paketti. HKDF kasutab seespoolt HMAC, mis omakorda kasutab rĂ€sifunktsiooni. NĂ€ide Python’i rakendamisest on Wikipedia lehe peal vaid mĂ”ned koodiread. Nagu 34.10-2012 puhul, kasutame rĂ€sifunktsioonina Stribog-256. Meie vĂ”tme kooskĂ”lastamise funktsiooni vĂ€ljundit nimetatakse seansivĂ”tmiseks, 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/schema

    Vaadakem, millised ASN.1 struktuurid meil nĂŒĂŒd on, et edastada kĂ”ik need andmed:

    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 (to be signed). HandshakeTBE — see, what will be encrypted (to be encrypted). I want to draw attention to the ukm field in MsgHandshake1. 34.10 VKO, for even greater randomization of generated keys, includes the parameter UKM (user keying material) — just extra entropy.

    KrĂŒptograafia lisamine koodile

    KĂ€sitleme ainult originaalkoodis tehtud muudatusi, kuna raamistik on jÀÀnud samaks (tegelikult loodi kĂ”igepealt lĂ”plik teostus ja siis eemaldati sellest kogu krĂŒptograafia).

    Kuna autentimine ja suhtluse identifitseerimine toimub avalike vĂ”tmete alusel, tuleb neid nĂŒĂŒd kusagil pikaajaliselt hoida. Lihtsuse huvides kasutame sellist tĂŒĂŒpi JSON-i:

    {
        "our": {
            "prv": "21254cf66c15e0226ef2669ceee46c87b575f37f9000272f408d0c9283355f98",
            "pub": "938c87da5c55b27b7f332d91b202dbef2540979d6ceaa4c35f1b5bfca6df47df0bdae0d3d82beac83cec3e353939489d9981b7eb7a3c58b71df2212d556312a1"
        },
        "their": {
            "alice": "d361a59c25d2ca5a05d21f31168609deeec100570ac98f540416778c93b2c7402fd92640731a707ec67b5410a0feae5b78aeec93c4a455a17570a84f2bc21fce",
            "bob": "aade1207dd85ecd283272e7b69c078d5fae75b6e141f7649ad21962042d643512c28a2dbdc12c7ba40eb704af920919511180c18f4d17e07d7f5acd49787224a"
        }
    }
    

    our — meie vĂ”tmepaar, heksakraadilised privaatne ja avalik vĂ”ti. their — vestluspartnerite nimed ja nende avalikud vĂ”tmed. Muudame kĂ€surea argumente ja lisame JSON andmete postitöötluse:

    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="Loo JSON uue vÔtmepaariga",
    )
    parser.add_argument(
        "--keys",
        default="keys.json",
        required=False,
        help="JSON meie ja nende vÔtmetega",
    )
    parser.add_argument(
        "--bind",
        default="::1",
        help="Aadress, millel kuulata",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=6666,
        help="Port, millel kuulata",
    )
    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),
        }
    # }}}
    

    GOST algoritmi privaatvÔti on juhuslik number. Suurus on 256-bitine 256-bitiste elliptiliste kÔverate jaoks. PyGOST töötab mitte baitide kogumiga, vaid suurte arvudega, seega peab meie privaatvÔti (urandom(32)) olema muudetud numbriks, kasutades gost3410.prv_unmarshal(). Avalik vÔti arvutatakse deterministlikult privaatvÔtme pÔhjal, kasutades gost3410.public_key(). Avalik vÔti 34.10 on kaks suurt arvu, mis tuleb samuti muudetud baitide jÀrjestuseks, et neid oleks mugav hoida ja edastada, kasutades gost3410.pub_marshal().

    PĂ€rast JSON-faili lugemist tuleb avalikud jaotised vastavalt tagasi muuta, kasutades gost3410.pub_unmarshal(). Kuna me saame vestluspartnerite identifikaatoreid avaliku vĂ”ti hĂ€shtina, saab neid kohe ette arvutada ja paigutada sĂ”nastikku kiireks otsimiseks. StŃ€ĐžĐ±ĐŸĐł-256 hash on gost34112012256.GOST34112012256(), mis vastab tĂ€ielikult hashlib hash-funktsioonide liidesele.

    Kuidas algataja korutiin muutus? KĂ”ik on nagu kĂ€epigistuse skeem: genereerime kĂŒpsise (128-bitti on tĂ€iesti piisav), ajutise vĂ”tme paari 34.10, mida kasutatakse VKO vĂ”tme kokkuleppe funktsioonis.

     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     # Generate our ephemeral public key and cookie, send Handshake 0 message {{{
     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 saabunud Msg sĂ”numi;
    • veendume, et saime handshake1;
    • dekodeerime vastaspoolse isepĂ€rase avaliku vĂ”tme ja arvutame seansi vĂ”tme;
    • loome sĂŒmpetrilised vĂ”tmed, mis on vajalikud TBE osa töötlemiseks.

     423     logging.info("%s: sai %s sÔnum", _id, msg.choice)
     424     if msg.choice != "handshake1":
     425         logging.warning("%s: ootamatu sÔnum, katkestamine", _id)
     426         writer.close()
     427         return
     428     # }}}
     429     msg_handshake1 = msg.value
     430     # Kinnita Handshake sÔnum {{{
     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 number (urandom(8)), mis nĂ”uab ka deserialiseerimist baitide esituses, kasutades gost3410_vko.ukm_unmarshal(). VKO funktsioon 34.10-2012 256-bit jaoks on gost3410_vko.kek_34102012256() (KEK — vĂ”tme krĂŒpteerimise vĂ”ti).

    Genereeritud sessioonivĂ”ti on juba 256-bitine juhuslik bitijada. SeetĂ”ttu saab seda kohe kasutada HKDF funktsioonis. Kuna GOST34112012256 vastab hashlib liidesele, saab seda kohe rakendada Hkdf klassis. Soola (esimene argument Hkdf) me ei mÀÀra, kuna genereeritud vĂ”ti, arvestades osalevate vĂ”tmepaaride ajutisust, on iga sessiooni jaoks erinev ja see sisaldab piisavalt entropiat. kdf.expand() tagastab vaikimisi juba 256-bitiseid vĂ”tmeid, mis on vajalikud edasiseks KŃƒĐ·ĐœĐ”Ń‡ĐžĐșule.

    Edasi kontrollitakse saabunud sÔnumi TBE ja TBS osi:

    • arvutatakse ja kontrollitakse MAC saabunud ligipÀÀsetud tekstile;
    • deĆĄifreeritakse ligipÀÀsetud tekst;
    • dekoodeeritakse TBE struktuur;
    • sealt vĂ”etakse vestluspartneri identifikaator ja kontrollitakse, kas see on meile teada;
    • arvutatakse ja kontrollitakse MAC selle identifikaatori ĂŒle;
    • kontrollitakse allkirja TBS struktuuri ĂŒle, kuhu kuuluvad mĂ”lema poole kĂŒpsised ja avalik ajutine vĂ”ti vastaspoolelt. Allkiri kontrollitakse vestluspartneri pikaajalise allkirja vĂ”tmega.

     441     try:
     442         peer_name = validate_tbe(
     443             msg_handshake1,
     444             key_handshake1_mac_identity,
     445             key_handshake1_enc,
     446             key_handshake1_mac,
     447             cookie_our,
     448             cookie_their,
     449             pub_their_raw,
     450         )
     451     except ValueError as err:
     452         logging.warning("%s: %s, disconnecting", _id, err)
     453         writer.close()
     454         return
     455     # }}}
    
     128 def validate_tbe(
     129         msg_handshake: Union[MsgHandshake1, MsgHandshake2],
     130         key_mac_identity: bytes,
     131         key_enc: bytes,
     132         key_mac: bytes,
     133         cookie_their: Cookie,
     134         cookie_our: Cookie,
     135         pub_key_our: PubKey,
     136 ) -> str:
     137     ciphertext = bytes(msg_handshake["ciphertext"])
     138     mac_tag = mac(GOST3412Kuznechik(key_mac).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext)
     139     if not compare_digest(mac_tag, bytes(msg_handshake["ciphertextMac"])):
     140         raise ValueError("invalid MAC")
     141     plaintext = ctr(
     142         GOST3412Kuznechik(key_enc).encrypt,
     143         KUZNECHIK_BLOCKSIZE,
     144         ciphertext,
     145         8 * b"x00",
     146     )
     147     try:
     148         tbe, _ = HandshakeTBE().decode(plaintext)
     149     except ASN1Error:
     150         raise ValueError("can not decode TBE")
     151     key_sign_pub_hash = bytes(tbe["identity"])
     152     peer = KEYS.get(key_sign_pub_hash)
     153     if peer is None:
     154         raise ValueError("unknown identity")
     155     mac_tag = mac(
     156         GOST3412Kuznechik(key_mac_identity).encrypt,
     157         KUZNECHIK_BLOCKSIZE,
     158         key_sign_pub_hash,
     159     )
     160     if not compare_digest(mac_tag, bytes(tbe["identityMac"])):
     161         raise ValueError("invalid identity MAC")
     162     tbs = HandshakeTBS((
     163         ("cookieTheir", cookie_their),
     164         ("cookieOur", cookie_our),
     165         ("pubKeyOur", pub_key_our),
     166     ))
     167     if not gost3410.verify(
     168         CURVE,
     169         peer["pub"],
     170         GOST34112012256(tbs.encode()).digest(),
     171         bytes(tbe["signature"]),
     172     ):
     173         raise ValueError("invalid signature")
     174     return peer["name"]
    

    Nagu juba varem mainitud, kirjeldab 34.13-2015 erinevaid plokkĆĄifrite tööreĆŸiime 34.12-2015. Nende seas on imitsĂŒkli genereerimise reĆŸiim, MAC-i arvutamine. PyGOST-is on see gost3413.mac(). See reĆŸiim nĂ”uab ĆĄifreerimisfunktsiooni (mis vĂ”tab vastu ja tagastab ĂŒhe andmeploki), plokkĆĄifri suurust ja tegelikke andmeid. Miks ei saa plokkĆĄifri suurust kĂ”vade koodidega mÀÀrata? 34.12-2015 kirjeldab mitte ainult 128-bitist Kurgi ĆĄifrit, vaid ka 64-bitist Magma – veidi muudetud ГОСб 28147-89, mis loodi veel KGB poolt ja millel on endiselt ĂŒks kĂ”rgeimaid turvalisuse piire.

    Kurgi initsialiseeritakse gost.3412.GOST3412Kuznechik(key) kutse kaudu ja tagastab objekti, millel on .encrypt()/ .decrypt() meetodid, mida saab edastada 34.13 funktsioonile. MAC arvutatakse jĂ€rgmiselt: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). Arvutatud ja saadud MAC-i vĂ”rdlemiseks ei saa kasutada tavalist vĂ”rdlemist (==) baitide stringide vahel, kuna see toiming pĂ”hjustab ajavĂ”rdsuse lekkeid, mis vĂ”ib ĂŒldjuhul viia surmaga lĂ”ppevate haavatavusteni, nagu BEAST rĂŒnnakud TLS-i vastu. Pythonis on selle jaoks eriline hmac.compare_digest funktsioon.

    PlokkĆĄifreerimise funktsioon suudab krĂŒpteerida ainult ĂŒhe andmeploki. Suurema hulga, eriti mitte-korrutiste pikkusega andmete jaoks tuleb kasutada krĂŒpteerimisreĆŸiimi. 34.13-2015 kirjeldab jĂ€rgmisi: ECB, CTR, OFB, CBC, CFB. Igal neist on oma lubatud kasutusvaldkonnad ja omadused. Kahjuks pole meil siiani standardiseeritud autentimisreĆŸiime (nagu CCM, OCB, GCM ja sarnased) — oleme sunnitud vĂ€hemalt ise MAC lisama. Ma valin loenduri reĆŸiimi (CTR): see ei nĂ”ua ploki suurusele tĂ€iendamist, vĂ”ib olla paralleelselt töötav, kasutab ainult krĂŒpteerimisfunktsiooni ja seda saab ohutult kasutada suure hulga sĂ”numite krĂŒpteerimiseks (erinevalt CBC-st, kus suhteliselt kiiresti hakkavad ilmuma kokkulangemised).

    Nagu .mac(), .ctr() vĂ”tab sarnaselt sisendina: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). NĂ”utav on initsialiseerimissuuna mÀÀramine, mille pikkus on tĂ€pselt pool ĆĄifrisuurest. Kui meie krĂŒpteerimisvĂ”ti kasutatakse ainult ĂŒhe sĂ”numi (isegi kui see sisaldab mitmeid blokke) krĂŒpteerimiseks, siis on ohutu mÀÀrata null initsialiseerimissuund. KĂ€tlemise sĂ”numite krĂŒpteerimiseks kasutame iga kord eraldi vĂ”tit.

    Gost3410.verify() allkirja kontrollimine on triviaalne: edastame eliptilise kÔveraga, mille piires töötame (selle fikseerime lihtsalt meie GOSTIM protokollis), allkirjastaja avaliku vÔtme (Àrge unustage, et see peab olema kahe suure numbri paar, mitte baitide jada), 34.11-2012 rikke ja ise allkirja.

    SeejĂ€rel valmistame algatajas ette ja saadame handshake2 kĂ€epigistamise sĂ”numi, tehes samme sama, mida me tegime kontrollimisel, ainult sĂŒmmeetriliselt: allkiri oma vĂ”tmetega allkirjastamise asemel, jne...

     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 on loodud: %s", _id, peer_name)
     

    Kui sessioon on loodud, genereeritakse transportvĂ”tmed (eraldi vĂ”ti krĂŒpteerimiseks, tĂ”endamiseks, kummagi osapoole jaoks), ja initsialiseeritakse Kuznechik dekrĂŒpteerimiseks ja MAC-i kontrollimiseks:

     499     # KÀivita tekstisÔnumi saatja, initsialiseeri 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     # Oota 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 korutina krĂŒptib sĂ”numid enne nende saatmist TCP-ĂŒhendusse. Igal sĂ”numil on jĂ€rjestikku kasvav nonce, mis on samuti initsialiseerimisvektor, kui krĂŒpteerimine toimub loenduri reĆŸiimis. Igal sĂ”numil ja sĂ”numiplokil on garanteeritult erinevad loenduri vÀÀrtused.

    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öötleb korutine msg_receiver, mis tegeleb autentimise ja dekrĂŒpteerimisega:

    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("unexpected nonce value")
        mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode())
        if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])):
            raise ValueError("invalid 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 hariduslikel eesmÀrkidel (kuna pole katsetustega kaetud, vÀhemalt)! Programmi lÀhtekoodi saab alla laadida siin (StriBog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Nagu kÔik mu projektid, on GoGOST, PyDERASN, NNCP, GoVPN, GOSTIM tÀiesti avamood, mida levitatakse GPLv3+.

    Sergei Matvejev, krĂŒptopank, liige SPO Fondist, Python/Go arendaja, peaspetsialist FGUP „NTC Atlaas“.

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster