GOSTIM: P2P F2F E2EE IM ühe õhtuga GOST krüptograafiaga

Olles arendaja PyGOST raamatukogud (GOST-i krüptoprimitiivid puhtas Pythonis), saan sageli küsimusi selle kohta, kuidas kõige lihtsamat turvalist sõnumivahetust rakendada. Paljud inimesed peavad rakenduslikku krüptograafiat üsna lihtsaks ja .encrypt() kutsumisest plokkšifril piisab selle turvaliseks saatmiseks sidekanali kaudu. Teised usuvad, et rakenduslik krüptograafia on väheste saatus ja on vastuvõetav, et rikkad ettevõtted nagu Telegram koos olümpiaad-matemaatikutega ei saa rakendada turvaline protokoll.

Kõik see ajendas mind kirjutama seda artiklit, et näidata, et krüptoprotokollide ja turvalise kiirsuhtluse rakendamine pole nii keeruline ülesanne. Siiski ei tasu välja mõelda oma autentimis- ja võtmekokkuleppeprotokolle.

GOSTIM: P2P F2F E2EE IM ühe õhtuga GOST krüptograafiaga
Artikkel kirjutab peer-to-peer, sõbralt sõbrale, otsast lõpuni krüptitud kiirsuhtlus koos SIGMA-I autentimis- ja võtmelepingu protokoll (mille alusel seda rakendatakse IPsec IKE), kasutades ainult GOST-i krüptoalgoritme PyGOST teeki ja ASN.1 sõnumite kodeerimise teeki PyDERASN (mille kohta ma juba kirjutas enne). Eeltingimus: see peab olema nii lihtne, et selle saaks ühe õhtu (või tööpäeva) algusest peale kirjutada, muidu pole see enam lihtne programm. Tõenäoliselt on sellel vigu, tarbetuid tüsistusi, puudusi, lisaks on see minu esimene programm, mis kasutab asyncio raamatukogu.

IM disain

Esiteks peame mõistma, milline meie IM välja näeb. Lihtsuse huvides olgu selleks peer-to-peer võrk, ilma osalejaid avastamata. Anname isiklikult teada, millise aadressi: pordiga suhelda, et vestluskaaslasega suhelda.

Ma saan aru, et praegu on eeldus, et otsesuhtlus on saadaval kahe suvalise arvuti vahel, oluliseks piiranguks IM praktikas rakendatavusele. Kuid mida rohkem arendajad rakendavad kõikvõimalikke NAT-i läbimise karkusid, seda kauem me IPv4 Internetti jääme, kusjuures suvaliste arvutite vahelise suhtluse tõenäosus on masendav. Kui kaua talute IPv6 puudumist kodus ja tööl?

Meil tekib sõbra-sõbra võrgustik: kõik võimalikud vestluskaaslased peavad olema ette teada. Esiteks lihtsustab see kõike oluliselt: tutvustasime end, leidsime või ei leidnud nime/võtit, katkestasime ühenduse või jätkame tööd, teades vestluspartnerit. Teiseks on see üldiselt turvaline ja välistab paljud rünnakud.

IM-liides saab olema klassikaliste lahenduste lähedane nõmedad projektid, mis mulle väga meeldivad nende minimalismi ja Unix-viisi filosoofia poolest. IM-programm loob iga vestluspartneri jaoks kolme Unixi domeeni pesaga kataloogi:

  • sisse — vestluskaaslasele saadetud sõnumid salvestatakse sellesse;
  • välja - sellest loetakse vestluskaaslaselt saadud sõnumeid;
  • olek - sellest lugedes saame teada, kas vestluskaaslane on hetkel ühenduses, ühenduse aadress/port.

Lisaks luuakse ühenduspesa, kirjutades hosti pordi, kuhu algatame ühenduse kaugvestlejaga.

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

Selline lähenemine võimaldab teha IM transpordi ja kasutajaliidese iseseisvaid teostusi, sest sõpra pole, kõigile ei saa meeldida. Kasutades tmux ja / või mitmeosaline, saate süntaksi esiletõstmisega mitme akna liidese. Ja abiga rlwrap saate GNU Readline'iga ühilduva sõnumisisestusrea.

Tegelikult kasutavad nõmedad projektid FIFO-faile. Isiklikult ei saanud ma aru, kuidas töötada failidega konkurentsivõimeliselt asünkrooniliselt ilma spetsiaalsetest lõimedest käsitsi kirjutatud taustata (olen seda keelt kasutanud selliste asjade jaoks pikka aega Go). Seetõttu otsustasin leppida Unixi domeeni pesadega. Kahjuks on see võimatu teha echo 2001:470:dead::babe 6666 > conn. Lahendasin selle probleemi kasutades socat: kaja 2001:470:surnud::babe 6666 | socat - UNIX-CONNECT:ühendus, socat READLINE UNIX-CONNECT:alice/in.

Algne ebaturvaline protokoll

Transpordina kasutatakse TCP-d: see tagab kohaletoimetamise ja selle tellimuse. UDP ei garanteeri kumbagi (mis oleks krüptograafia kasutamisel kasulik), vaid tuge SCTP Python ei tule kastist välja.

Kahjuks pole TCP-s sõnumi mõistet, on vaid baitide voog. Seetõttu on vaja välja mõelda sõnumite vorming, et neid saaks selles lõimes omavahel jagada. Võime nõustuda reavahetuse märgi kasutamisega. Alustuseks sobib see hästi, kuid kui hakkame oma sõnumeid krüpteerima, võib see märk ilmuda šifritekstis kõikjal. Seetõttu on võrkudes populaarsed protokollid, mis saadavad kõigepealt sõnumi pikkuse baitides. Näiteks Pythonil on juba kasutusel xdrlib, mis võimaldab töötada sarnase vorminguga XDR.

Me ei tööta TCP lugemisega õigesti ja tõhusalt – lihtsustame koodi. Loeme pesast andmeid lõputu tsüklina, kuni dekodeerime kogu sõnumi. Selle lähenemisviisi vorminguna saab kasutada ka JSON-i koos XML-iga. Kui aga lisatakse krüptograafia, tuleb andmed allkirjastada ja autentida – selleks on vaja bait-baidi kaupa identset objektide esitust, mida JSON/XML ei paku (väljavõtte tulemused võivad erineda).

Selle ülesande jaoks sobib XDR, kuid valin ASN.1 DER-kodeeringuga ja PyDERASN raamatukogu, sest meil on käepärast kõrgetasemelised objektid, millega on sageli meeldivam ja mugavam töötada. Erinevalt skeemita bencode, MessagePack või CBOR, kontrollib ASN.1 andmeid automaatselt kodeeritud skeemi alusel.

# 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õetud sõnum on Msg: kas tekstisõnum MsgText (praegu ühe tekstiväljaga) või MsgHandshake käepigistuse sõnum (mis sisaldab vestluskaaslase nime). Nüüd tundub see liiga keeruline, kuid see on tuleviku aluseks.

     ┌─────┐ ┌─────┐ │PeerA│ │PeerB│ ja haha ​​( IdA) │ │───────── ────────>│ │ │ │MsgHandshake (IdB) │ │<───────────────────────────── │ │ MsgText() │ │──── MsgText() │ │ │

IM ilma krüptograafiata

Nagu ma juba ütlesin, kasutatakse asyncio teeki kõigi soklitoimingute jaoks. Anname teada, mida me käivitamisel ootame:

parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
    "--our-name",
    required=True,
    help="Our peer name",
)
parser.add_argument(
    "--their-names",
    required=True,
    help="Their peer names, comma-separated",
)
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()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))

Määra oma nimi (--meie-nimi alice). Kõik oodatud vestluskaaslased on loetletud komadega eraldatuna (-nende nimed bob,eve). Iga vestluspartneri jaoks luuakse Unixi pesadega kataloog, samuti iga sisse-, välja- ja oleku jaoks korutiin:

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 sisendpesast tulevad sõnumid saadetakse IN_QUEUES järjekorda:

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ärjekorda, millest kirjutatakse andmed väljundpesasse:

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()

Olekupesast lugedes otsib programm vestluspartneri aadressi PEER_ALIVE sõnastikust. Kui vestluskaaslasega veel ühendust pole, siis kirjutatakse 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 ühenduspesasse 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))

Mõelgem algatajale. Kõigepealt avab see ilmselgelt ühenduse määratud hosti/pordiga ja saadab käepigistuse teate oma nimega:

 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 see eemalolevalt osapoolelt vastust. Püüab dekodeerida sissetulevat vastust skeemi Msg ASN.1 abil. Eeldame, et kogu sõnum saadetakse ühes TCP segmendis ja saame selle aatomiliselt vastu .read() kutsumisel. Kontrollime, kas saime käepigistuse teate kätte.

 141     # Wait for Handshake message {{{
 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 saadud vestluskaaslase nimi on meile teada. Kui ei, siis katkestame ühenduse. Kontrollime, kas oleme temaga juba ühenduse loonud (vestleja andis taas käsu meiega ühenduse loomiseks) ja sulgeme selle. Järjekord IN_QUEUES sisaldab Pythoni stringe koos sõnumi tekstiga, kuid sellel on spetsiaalne väärtus None, mis annab msg_sender korutiinile märku töö lõpetamisest, nii et see unustab pärand-TCP-ühendusega seotud kirjutaja.

 159     msg_handshake = msg.value
 160     peer_name = str(msg_handshake["peerName"])
 161     if peer_name not in THEIR_NAMES:
 162         logging.warning("unknown peer name: %s", peer_name)
 163         writer.close()
 164         return
 165     logging.info("%s: session established: %s", _id, peer_name)
 166     # Run text message sender, initialize transport decoder {{{
 167     peer_alive = PEER_ALIVES.pop(peer_name, None)
 168     if peer_alive is not None:
 169         peer_alive.close()
 170         await IN_QUEUES[peer_name].put(None)
 171     PEER_ALIVES[peer_name] = writer
 172     asyncio.ensure_future(msg_sender(peer_name, writer))
 173     # }}}

msg_sender aktsepteerib väljaminevaid sõnumeid (järjekorras sisendpesast), serialiseerib need MsgText sõnumiks ja saadab need TCP-ühenduse kaudu. See võib igal hetkel puruneda – me võtame selle selgelt vahele.

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õpuks siseneb algataja pistikupesast sõnumite lugemise lõputusse ahelasse. Kontrollib, kas need sõnumid on tekstisõnumid, ja asetab need järjekorda OUT_QUEUES, kust need saadetakse vastava vestluspartneri väljundpesasse. Miks sa ei saa lihtsalt teha .read() ja sõnumit dekodeerida? Sest on võimalik, et mitu kasutaja sõnumit koondatakse operatsioonisüsteemi puhvrisse ja saadetakse ühes TCP segmendis. Saame esimese dekodeerida ja siis võib osa järgmisest jääda puhvrisse. Iga ebatavalise olukorra korral sulgeme TCP-ühenduse ja peatame korutiini msg_sender (saadetes järjekorda OUT_QUEUES parameetri None).

 174     buf = b""
 175     # Wait for test messages {{{
 176     while True:
 177         data = await reader.read(MaxMsgLen)
 178         if data == b"":
 179             break
 180         buf += data
 181         if len(buf) > MaxMsgLen:
 182             logging.warning("%s: max buffer size exceeded", _id)
 183             break
 184         try:
 185             msg, tail = Msg().decode(buf)
 186         except ASN1Error:
 187             continue
 188         buf = tail
 189         if msg.choice != "text":
 190             logging.warning("%s: unexpected %s message", _id, msg.choice)
 191             break
 192         try:
 193             await msg_receiver(msg.value, peer_name)
 194         except ValueError as err:
 195             logging.warning("%s: %s", err)
 196             break
 197     # }}}
 198     logging.info("%s: disconnecting: %s", _id, peer_name)
 199     IN_QUEUES[peer_name].put(None)
 200     writer.close()

  66 async def msg_receiver(msg_text: MsgText, peer_name: str) -> None:
  67     text = str(msg_text["text"])
  68     logging.info("%s: received %d characters message", peer_name, len(text))
  69     await OUT_QUEUES[peer_name].put(text)

Pöördume tagasi põhikoodi juurde. Pärast kõigi korutiinide loomist programmi käivitamise ajal käivitame TCP-serveri. Iga loodud ühenduse jaoks loob see 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("Listening on: %s", server.sockets[0].getsockname())
loop.run_forever()

vastaja sarnaneb algatajaga ja peegeldab kõiki samu toiminguid, kuid sõnumite lugemise lõputu silmus algab lihtsuse huvides kohe. Praegu saadab kätlemisprotokoll kummaltki poolt ühe sõnumi, edaspidi on aga ühenduse algatajalt kaks, misjärel saab kohe tekstisõnumeid saata.

  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 oma side kindlustada. Mida me mõtleme turvalisuse all ja mida me tahame?

  • edastatud sõnumite konfidentsiaalsus;
  • edastatud sõnumite autentsus ja terviklikkus – nende muutused tuleb tuvastada;
  • kaitse kordusrünnete eest – tuleb tuvastada teadete puudumise või korduvate teadete fakt (ja otsustame ühenduse katkestada);
  • vestluspartnerite tuvastamine ja autentimine eelsisestatud avalike võtmete abil – otsustasime juba varem, et teeme sõbralt-sõbrale võrgustiku. Alles pärast autentimist saame aru, kellega suhtleme;
  • kättesaadavus täiuslik salajasus omadused (PFS) – meie pikaealise allkirjastamisvõtme kompromiteerimine ei tohiks kaasa tuua kogu varasema kirjavahetuse lugemise võimalust. Peatatud liikluse salvestamine muutub kasutuks;
  • sõnumite kehtivus/kehtivus (transport ja käepigistus) ainult ühe TCP seansi jooksul. Korrektselt allkirjastatud/autentitud sõnumite sisestamine teisest seansist (isegi sama vestluskaaslasega) ei tohiks olla võimalik;
  • passiivne vaatleja ei peaks nägema kasutajatunnuseid, edastatud pikaealisi avalikke võtmeid ega nendest pärinevaid räsi. Teatud anonüümsus passiivselt vaatlejalt.

Üllataval kombel soovivad peaaegu kõik, et see miinimum oleks igas käepigistuse protokollis, ja „kodumaiste” protokollide puhul on ülaltoodust väga vähe täidetud. Nüüd me ei leiuta midagi uut. Soovitan kindlasti kasutada Müra raamistik protokollide koostamiseks, kuid valime midagi lihtsamat.

Kaks kõige populaarsemat protokolli on:

  • TLS - väga keeruline protokoll, millel on pikk ajalugu vigadest, jambidest, haavatavusest, kehva mõtlemise, keerukuse ja puudustega (sellel on aga TLS 1.3-ga vähe pistmist). Kuid me ei võta seda arvesse, sest see on liiga keeruline.
  • IPsec с IKE — neil pole tõsiseid krüptoprobleeme, kuigi need pole ka lihtsad. Kui lugeda IKEv1 ja IKEv2 kohta, siis nende allikas on STS, ISO/IEC IS 9798-3 ja SIGMA (SIGn-and-MAc) protokollid – piisavalt lihtsad, et neid ühe õhtuga rakendada.

Mis on SIGMA-s kui STS/ISO-protokollide arendamise uusimas lülis head? See vastab kõigile meie nõuetele (sealhulgas vestluspartneri identifikaatorite "peitmine") ja sellel pole teadaolevaid krüptograafilisi probleeme. See on minimalistlik – protokolli sõnumist vähemalt ühe elemendi eemaldamine toob kaasa selle ebaturvalisuse.

Liigume lihtsaima kodus kasvatatud protokolli juurest SIGMA juurde. Kõige elementaarsem toiming, millest oleme huvitatud, on võtmekokkulepe: funktsioon, mis väljastab mõlemale osalejale sama väärtuse, mida saab kasutada sümmeetrilise võtmena. Detailidesse laskumata: kumbki osapool genereerib lühiajalise (kasutatakse ainult ühe seansi jooksul) võtmepaari (avalikud ja privaatvõtmed), vahetab avalikke võtmeid, kutsub välja lepingufunktsiooni, mille sisendisse edastavad oma privaatvõtme ja avaliku vestluskaaslase võti.

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

Igaüks võib hüpata keskele ja asendada avalikud võtmed enda omadega – selles protokollis pole vestluspartnerite autentimist. Lisame pikaealiste võtmetega allkirja.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬───┬──┘ ┬──┘ ───┘ ───┘ ────, märk(SignPrvA, (PubA)) │ ╔═ Allkirjad PubA = load()║ │ │ ║PrvA, PubA = DHgen() ║ │ │ ╚═══════ ═══════ ════════════════════════════════════════❐❐═══B , märk (SignPrvB, (PubB)) - ─────────── ───────────── ──│ ║SignPrvB, SignPubB = koormus( )║ │ │ ║PrvB, ││║║│║║│║║│║ ═════════ - ═════╗ │ │ ║verify( SignPubB, ...)║ │ <───┘ ║Võti = DH(Pr vA, PubB) ║ │ │ ╚═════════════════════════════════════════════ ═╝ │ │ │

Selline allkiri ei tööta, kuna see pole konkreetse seansiga seotud. Sellised sõnumid sobivad ka teiste osalejatega seanssideks. Kogu kontekst peab märkima. See sunnib meid lisama veel ühe sõnumi A-lt.

Lisaks on ülioluline lisada allkirja alla oma identifikaator, kuna vastasel juhul saame asendada IdXXX ja allkirjastada sõnumi uuesti mõne muu tuntud vestluspartneri võtmega. Ennetama peegeldusrünnakud, on vajalik, et allkirja all olevad elemendid oleksid oma tähenduse järgi selgelt määratletud kohtades: kui A märgib (PubA, PubB), siis B peab allkirjastama (PubB, PubA). See räägib ka jadaandmete struktuuri ja vormingu valimise tähtsusest. Näiteks sorteeritakse ASN.1 DER-kodeeringus olevad komplektid: SET OF(PubA, PubB) on identne SET OF(PubB, PubA).

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬───┬──┘ ┬──┘ ───┘ ─────── │ ╔══════════ ═════════════════╗ │────────────────────────── ────────── ─────────────>│ ║SignPrvA, SignPubA = load()║ │ │ ║PrvA, PubA = DHgen()─│─││ ═ ═══════ ═══════════════╝ │IdB, PubB, märk(SignPrvB, (IdB, PubA, PubB)) │════════════ ═ ═════ ════════════╗ │<───────────────────────────── ────────── ─────────│ ║SignPrvB, SignPubB = load()║ │ │ ║PrvB, PubB = DHgen() ║ │ ───││────═ ═ ════════ märk ════╗ │─ ─────────────────────────────- ───>│ ║verify(SignPubB, ...) ║ │ │ ║klahv = dh (prva, PUBB) ║ │ │ │

Kuid me pole ikka veel "tõestanud", et oleme selle seansi jaoks loonud sama jagatud võtme. Põhimõtteliselt saame ka ilma selle sammuta hakkama - kõige esimene transpordiühendus läheb kehtetuks, kuid soovime, et käepigistuse lõppedes oleksime kindlad, et kõiges on tõesti kokku lepitud. Hetkel on meil käepärast ISO/IEC IS 9798-3 protokoll.

Võiksime genereeritud võtme ise allkirjastada. See on ohtlik, kuna on võimalik, et kasutatavas allkirjaalgoritmis võib esineda lekkeid (ehkki bitti allkirja kohta, kuid siiski lekkeid). Tuletusvõtme räsi on võimalik allkirjastada, kuid isegi tuletatud võtme räsi lekkimine võib olla väärtuslik tuletusfunktsiooni jõhkra jõuga rünnak. SIGMA kasutab MAC-funktsiooni, mis autentib saatja ID.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬───┬──┘ ┬──┘ ───┘ ─────── │ ╔══════════ ═════════════════╗ │────────────────────────── ────────── ──────────────────>│ ║SignPrvA, SignPubA = koormus()║ │ ││││││││││││││││││││││││││─ ╚ ═══════ ════════════════════╝ │IdB, PubB, märk(SignPrvB, (PubA, PubB)╔┐(IdB)═══ ═ ═══ │<───────────────── ────────── ─│ ║SignPrvB, SignPubB = load()║ │ │ ║PrvB, PubB = DHgen() ║ │ │ ╚════ ═════ ═════ ══════════ ═════════ ══╝ │ │ ╔════════════ ═════════════ Publ, ()P)vb,(S,AC) │märk │ ║klahv = DH( PrvA, PubB) ║ │──────────────────── │──────────────── ────────── ─────>│ ║kinnita(võti, IdB) ║ │ │ ║kinnita(SignPubB, ...)║ │ │ ╚═══│ ╚═══│ ╚═══════════════││ ═════ ═╝ │ │

Optimeerimisena võivad mõned soovida oma lühiajalisi võtmeid uuesti kasutada (mis on PFS-i puhul muidugi kahetsusväärne). Näiteks genereerisime võtmepaari, proovisime ühendust luua, kuid TCP polnud saadaval või katkes kuskil protokolli keskel. Kahju on raisatud entroopiat ja protsessoriressursse uuele paarile raisata. Seetõttu võtame kasutusele nn küpsise – pseudojuhusliku väärtuse, mis kaitseb lühiajaliste avalike võtmete taaskasutamisel võimalike juhuslike kordusrünnakute eest. Küpsise ja lühiajalise avaliku võtme vahelise seose tõttu saab vastaspoole avaliku võtme allkirjast eemaldada kui mittevajalikku.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ ┬──┘ ┬──┘ ───┘ ───, I CookieA │ ╔════════ ═══════════════════╗ │────────────────────────── ────────── ─────────────────────────────- ─>│ ║SignPrvA, SignPubA = load( )║ │ │ ║PrvA, PubA = DHgen() ║ │ │ ╚═══════════════════════════════════════════════════ ══╝ │IdB, PubB, CookieB , märk(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) │ ╔═════════════════════════════════════════ ═ ╗ │< ─────────────────────────────- ────────── ────────────────────│ ║SignPrvB, SignPubB = koormus()║ │││vB, (Pub) │ ╚══════ ═════════════════════╝ │ │ │ ═══════╗ │ märk( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA) │ ║klahv = DH(PrvA, PubB) ║ │──────────────────────────── ─ ── ─────────────────────────────- ───────>│ ║ verify(Key, IdB) ║ │ │ ║kinnita(SignPubB, ...)║ │ │ ╚═══════════════════════════════════════════ │ │

Lõpuks tahame saada oma vestluspartnerite privaatsust passiivselt vaatlejalt. Selleks teeb SIGMA ettepaneku vahetada esmalt lühiajalisi võtmeid ja töötada välja ühine võti, mille abil autentida ja tuvastada sõnumeid. SIGMA kirjeldab kahte võimalust:

  • SIGMA-I - kaitseb algatajat aktiivsete rünnakute eest, vastajat passiivsete eest: algataja autentib vastaja ja kui midagi ei klapi, siis ta ei anna oma tunnust välja. Kostja annab välja oma isikut tõendava dokumendi, kui temaga alustatakse aktiivset protokolli. Passiivne vaatleja ei õpi midagi;
    SIGMA-R - kaitseb reageerijat aktiivsete rünnakute eest, initsiaatorit passiivsete rünnakute eest. Kõik on täpselt vastupidine, kuid selles protokollis edastatakse juba neli käepigistuse teadet.

    Valime SIGMA-I, kuna see sarnaneb rohkem sellele, mida ootame klient-server tuttavatelt asjadelt: kliendi tunneb ära ainult autentitud server ja serverit teavad juba kõik. Lisaks on seda lihtsam rakendada, kuna käepigistuse sõnumeid on vähem. Protokolli lisame vaid osa sõnumi krüptimise ja identifikaatori A ülekandmise viimase sõnumi krüpteeritud osale:

    PubA, CookieA │ ╔══════════ ═════════════════════╗══════════════ ─────────── ───── ────────── ───────────────- ─────────── ───── ──────>│ ║SignPrvA , SignPubA = load()║ │ │ ║PrvA, PubA = DHgen() ═══│ ═════════ ═════════ ════╝ │ PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB))), MAC(IdB) ═══════════════ ═══════╗ │<───────────— ────────── ───── ────────── ║SignP rvB, SignPubB = load()║ │ │ ║ PrvB, PubB = DHgen() ║ ════════ - ══╗ │ Enc((IdA, märk( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA))) │ ║võti = DH(PrvA, PubB) ║ │───────────────────────────────── ─ ────────────────────────────- ─────────── ──────>│ ║kinnita(võti, IdB) ║ │ │ ║kinnita( SignPubB, ...)║ │ │ ╚═══│ ╚═══│ ╚════════════ ═════ ══╝ │ │
    
    • Allkirjaks kasutatakse GOST R-i 34.10-2012 256-bitiste võtmetega algoritm.
    • Avaliku võtme genereerimiseks kasutatakse 34.10 VKO-d.
    • CMAC-i kasutatakse MAC-ina. Tehniliselt on see plokkšifri spetsiaalne töörežiim, mida on kirjeldatud standardis GOST R 34.13-2015. Selle režiimi krüpteerimisfunktsioonina − Rohutirts (34.12-2015).
    • Tema avaliku võtme räsi kasutatakse vestluspartneri identifikaatorina. Kasutatakse räsina Stribog-256 (34.11 2012 bitti).

    Pärast kätlemist lepime kokku jagatud võtmes. Saame seda kasutada transpordisõnumite autentitud krüptimiseks. See osa on väga lihtne ja raskesti eksitav: suurendame sõnumiloendurit, krüpteerime sõnumi, autentisime (MAC) loenduri ja šifriteksti, saadame. Sõnumi saamisel kontrollime, et loenduril oleks oodatud väärtus, autendime loenduriga šifriteksti ja dekrüpteerime selle. Millist võtit peaksin kasutama käepigistuse sõnumite, sõnumite edastamiseks ja nende autentimiseks? Kõigi nende ülesannete jaoks ühe võtme kasutamine on ohtlik ja ebamõistlik. Võtmed tuleb genereerida spetsiaalsete funktsioonide abil KDF (võtme tuletusfunktsioon). Jällegi, ärgem lõhestagem juukseid ja leiutagem midagi: HKDF on ammu teada, hästi uuritud ja sellel pole teadaolevaid probleeme. Kahjuks pole Pythoni natiivsel teegil seda funktsiooni, seega kasutame seda hkdf kilekott. HKDF kasutab sisemiselt HMAC, mis omakorda kasutab räsifunktsiooni. Näidisrakendus Pythonis Wikipedia lehel võtab vaid mõne koodirea. Nagu 34.10 puhul, kasutame räsifunktsioonina Stribog-2012. Meie võtmekokkuleppe funktsiooni väljundit nimetatakse seansivõtmiks, millest genereeritakse puuduvad 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 meil praegu kõigi nende andmete edastamiseks:

    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 on see, mis allkirjastatakse. HandshakeTBE – mis krüpteeritakse. Juhin teie tähelepanu ukm väljale rakenduses MsgHandshake1. 34.10 VKO sisaldab genereeritud võtmete veelgi suuremaks randomiseerimiseks UKM-i (kasutaja võtmematerjali) parameetrit - lihtsalt täiendav entroopia.

    Krüptograafia lisamine koodile

    Vaatleme ainult algses koodis tehtud muudatusi, kuna raamistik jäi samaks (tegelikult kirjutati kõigepealt lõplik teostus ja seejärel lõigati sellest kogu krüptograafia välja).

    Kuna vestluspartnerite autentimine ja tuvastamine toimub avalike võtmete abil, tuleb neid nüüd pikka aega kuskil säilitada. Lihtsuse huvides kasutame JSON-i järgmiselt:

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

    meie – meie võtmepaar, kuueteistkümnendsüsteemi privaat- ja avalikud võtmed. nende — vestluspartnerite nimed ja nende avalikud võtmed. Muudame käsurea argumente ja lisame JSON-andmete järeltöö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="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),
        }
    # }}}
    

    Algoritmi 34.10 privaatvõti on juhuslik arv. 256-bitine suurus 256-bitiste elliptiliste kõverate jaoks. PyGOST ei tööta mitte baitide komplektiga, vaid koos suured numbrid, seega tuleb meie privaatvõti (urandom(32)) teisendada numbriks, kasutades gost3410.prv_unmarshal(). Avalik võti määratakse deterministlikult privaatvõtmest, kasutades gost3410.public_key(). Avalik võti 34.10 on kaks suurt numbrit, mis tuleb samuti salvestamise ja edastamise hõlbustamiseks teisendada baidijadaks, kasutades gost3410.pub_marshal().

    Pärast JSON-faili lugemist tuleb avalikud võtmed vastavalt gost3410.pub_unmarshal() abil tagasi teisendada. Kuna vestluspartnerite identifikaatorid saame avalikust võtmest räsi kujul, saab need kohe ette arvutada ja kiireks otsimiseks sõnastikku panna. Stribog-256 räsi on gost34112012256.GOST34112012256(), mis rahuldab täielikult räsifunktsioonide hashlib liidest.

    Kuidas on algataja korutiin muutunud? Kõik on käepigistuse skeemi järgi: genereerime küpsise (128-bitist piisab), lühiajalise võtmepaari 34.10, mida kasutatakse VKO võtmekokkuleppe 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     # 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 sissetuleva sõnumi;
    • veenduge, et saate käepigistuse1;
    • dekodeerida vastaspoole lühiajaline avalik võti ja arvutada seansivõti;
    • Loome sõnumi TBE osa töötlemiseks vajalikud sümmeetrilised võtmed.

     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     # Validate Handshake message {{{
     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-bitine arv (urandom(8)), mis nõuab ka deserialiseerimist selle baidi esitusest, kasutades gost3410_vko.ukm_unmarshal(). 34.10 2012-bitise VKO funktsioon on gost256_vko.kek_3410() (KEK - krüpteerimisvõti).

    Loodud seansivõti on juba 256-bitine pseudojuhuslik baidijada. Seetõttu saab seda kohe kasutada HKDF-i funktsioonides. Kuna GOST34112012256 rahuldab hashlib liidest, saab seda kohe kasutada Hkdf klassis. Me ei täpsusta soola (Hkdf esimene argument), kuna genereeritud võti on osalevate võtmepaaride lühiajalisuse tõttu iga seansi jaoks erinev ja sisaldab juba piisavalt entroopiat. kdf.expand() toodab vaikimisi juba Grasshopperi jaoks hiljem vajalikke 256-bitisi võtmeid.

    Järgmisena kontrollitakse sissetuleva sõnumi TBE ja TBS osi:

    • arvutatakse ja kontrollitakse sissetuleva šifriteksti MAC-i;
    • šifreeritud tekst dekrüpteeritakse;
    • TBE struktuur dekodeeritakse;
    • sealt võetakse vestluspartneri tunnus ja vaadatakse, kas ta on meile üldse teada;
    • Selle identifikaatori MAC arvutatakse ja kontrollitakse;
    • kontrollitakse allkirja üle TBS-i struktuuri, mis sisaldab mõlema poole küpsist ja vastaspoole avalikku lühiajalist võtit. Allkirja kontrollib vestluspartneri pikaealine allkirjavõti.

     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 eespool kirjutasin, kirjeldab 34.13 erinevaid plokkšifri töörežiimid alates 34.12. Nende hulgas on režiim imitatsioonide ja MAC-arvutuste genereerimiseks. PyGOSTis on see gost2015.mac(). See režiim nõuab krüpteerimisfunktsiooni (ühe andmeploki vastuvõtmist ja tagastamist), krüpteerimisploki suuruse ja tegelikult ka andmete enda läbimist. Miks te ei saa krüpteerimisploki suurust kõvasti kodeerida? 3413 ei kirjelda mitte ainult 34.12-bitist Grasshopperi šifrit, vaid ka 2015-bitist Magma - veidi muudetud GOST 28147-89, mis loodi KGB-s ja millel on endiselt üks kõrgeimaid ohutusläve.

    Kuznechik lähtestatakse kutsudes gost.3412.GOST3412Kuznechik(key) ja tagastab objekti .encrypt()/.decrypt() meetoditega, mis sobivad 34.13 funktsioonidele üleminekuks. MAC arvutatakse järgmiselt: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, šifreeritud tekst). Arvutatud ja vastuvõetud MAC-i võrdlemiseks ei saa kasutada tavalist baidistringide võrdlust (==), kuna see toiming lekib võrdlusaega, mis võib üldjuhul põhjustada surmavaid turvaauke, nagu beast rünnakud TLS-i vastu. Pythonil on selleks spetsiaalne funktsioon hmac.compare_digest.

    Plokišifri funktsioon saab krüpteerida ainult ühe andmeploki. Suurema arvu ja isegi mitte mitmekordse pikkuse jaoks on vaja kasutada krüpteerimisrežiimi. 34.13-2015 kirjeldab järgmist: EKP, CTR, OFB, CBC, CFB. Igal neist on oma vastuvõetavad rakendusvaldkonnad ja omadused. Kahjuks pole meil ikka veel standarditud autentitud krüpteerimisrežiimid (nagu CCM, OCB, GCM jms) – oleme sunnitud vähemalt ise MAC-i lisama. ma valin loenduri režiim (CTR): see ei vaja ploki suurusele polsterdamist, on paralleelne, kasutab ainult krüpteerimisfunktsiooni, seda saab turvaliselt kasutada suure hulga sõnumite krüpteerimiseks (erinevalt CBC-st, millel on kokkupõrked suhteliselt kiiresti).

    Nagu .mac(), kasutab .ctr() sarnast sisendit: šifreeritud tekst = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, tavatekst, iv). On vaja määrata initsialiseerimisvektor, mis on täpselt pool krüpteerimisploki pikkusest. Kui meie krüpteerimisvõtit kasutatakse ainult ühe sõnumi krüptimiseks (ehkki mitmest plokist), siis on turvaline seada nullinitsialiseerimisvektor. Käepigistussõnumite krüptimiseks kasutame iga kord eraldi võtit.

    Allkirja gost3410.verify() kontrollimine on triviaalne: läbime elliptilise kõvera, mille sees töötame (salvestame selle lihtsalt oma GOSTIM-protokolli), allkirjastaja avaliku võtme (ärge unustage, et see peaks olema kahe korteis suured numbrid, mitte baidistring), 34.11 räsi ja allkiri ise.

    Järgmisena valmistame algatajas ette ja saadame käepigistuse sõnumi handshake2-le, sooritades samad toimingud, mis kinnitamisel, ainult sümmeetriliselt: kontrollimise asemel oma võtmetele allkirjastamine jne...

     456     # Prepare and send Handshake 2 message {{{
     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: session established: %s", _id, peer_name)
     

    Kui seanss on loodud, genereeritakse transpordivõtmed (krüpteerimiseks ja autentimiseks eraldi võti kummagi osapoole jaoks) ning Grasshopper initsialiseeritakse MAC-i dekrüpteerimiseks ja kontrollimiseks:

     499     # Run text message sender, initialize transport decoder {{{
     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     # Wait for test messages {{{
     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     # }}}
    

    Korutiin msg_sender krüpteerib nüüd sõnumid enne nende saatmist TCP-ühenduse kaudu. Igal sõnumil on monotoonselt kasvav nonce, mis on loendurirežiimis krüptimisel ka lähtestamisvektor. Igal sõnumil ja sõnumiplokil on tagatud erinev loenduri väärtus.

    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
    

    Sissetulevaid sõnumeid töötleb msg_receiver korutiini, 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)
    

    Järeldus

    GOSTIM on ette nähtud kasutamiseks eranditult hariduslikel eesmärkidel (kuna seda vähemalt testid ei hõlma)! Programmi lähtekoodi saab alla laadida siin (Стрибог-256 хэш: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Как и все мои проекты, типа GoGOST, PyDERASN, NCCP, GoVPN, GOSTIM on täiesti tasuta tarkvaratingimuste alusel levitatakse GPLv3 +.

    Sergei Matvejev, küferpunk, liige SPO sihtasutus, Python/Go-arendaja, peaspetsialist Föderaalne ühtne ettevõte "STC "Atlas".

Allikas: www.habr.com

Lisa kommentaar