GOSTIM: P2P F2F E2EE IM på én aften med GOST-kryptering

At være udvikler PyGOST biblioteker (GOST kryptografiske primitiver i ren Python), modtager jeg ofte spørgsmål om, hvordan man implementerer den enkleste sikre beskeder på knæet. Mange mennesker anser anvendt kryptografi for at være ret simpelt, og at kalde .encrypt() på en blokchiffer vil være nok til at sende det sikkert over en kommunikationskanal. Andre mener, at anvendt kryptografi er nogle fås skæbne, og det er acceptabelt, at rige virksomheder kan lide Telegram med olympiade-matematikere ikke kan gennemføre sikker protokol.

Alt dette fik mig til at skrive denne artikel for at vise, at implementering af kryptografiske protokoller og sikker IM ikke er så vanskelig en opgave. Det er dog ikke værd at opfinde dine egne godkendelses- og nøgleaftaleprotokoller.

GOSTIM: P2P F2F E2EE IM på én aften med GOST-kryptering
Artiklen vil skrive peer-to-peer, ven-til-ven, ende-til-ende krypteret instant messenger med SIGMA-I autentificering og nøgleaftaleprotokol (på grundlag af hvilken den implementeres IPsec IKE), udelukkende ved hjælp af GOST kryptografiske algoritmer PyGOST-bibliotek og ASN.1-meddelelseskodningsbibliotek PyDERASN (som jeg allerede skrev før). En forudsætning: det skal være så enkelt, at det kan skrives fra bunden på én aften (eller hverdag), ellers er det ikke længere et simpelt program. Det har sandsynligvis fejl, unødvendige komplikationer, mangler, plus dette er mit første program, der bruger asyncio-biblioteket.

IM design

Først skal vi forstå, hvordan vores IM vil se ud. For nemheds skyld, lad det være et peer-to-peer-netværk uden nogen opdagelse af deltagere. Vi vil personligt angive hvilken adresse: port der skal oprettes forbindelse til for at kommunikere med samtalepartneren.

Jeg forstår, at på nuværende tidspunkt er antagelsen om, at direkte kommunikation er tilgængelig mellem to vilkårlige computere, en væsentlig begrænsning af anvendeligheden af ​​IM i praksis. Men jo flere udviklere implementerer alle mulige NAT-traversal krykker, jo længere vil vi forblive på IPv4 Internettet, med en deprimerende sandsynlighed for kommunikation mellem vilkårlige computere. Hvor længe kan du tolerere manglen på IPv6 derhjemme og på arbejdet?

Vi vil have et ven-til-ven-netværk: alle mulige samtalepartnere skal kendes på forhånd. For det første forenkler dette alt i høj grad: vi præsenterede os selv, fandt eller fandt ikke navnet/nøglen, afbrød forbindelsen eller fortsatte med at arbejde, idet vi kendte samtalepartneren. For det andet er det generelt sikkert og eliminerer mange angreb.

IM-grænsefladen vil være tæt på klassiske løsninger sugeløse projekter, som jeg virkelig godt kan lide for deres minimalisme og Unix-way filosofi. IM-programmet opretter en mappe med tre Unix-domæne-sockets til hver samtalepartner:

  • in—beskeder sendt til samtalepartneren optages i den;
  • ud - beskeder modtaget fra samtalepartneren læses fra den;
  • tilstand - ved at læse fra den finder vi ud af, om samtalepartneren i øjeblikket er tilsluttet, forbindelsesadressen/porten.

Derudover oprettes en conn socket, ved at skrive værtsporten, som vi initierer en forbindelse til den eksterne samtalepartner.

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

Denne tilgang giver dig mulighed for at lave uafhængige implementeringer af IM-transport og brugergrænseflade, fordi der ikke er nogen ven, du kan ikke behage alle. Ved brug af tmux og / eller multitail, kan du få en multi-vindues grænseflade med syntaksfremhævning. Og med hjælpen rlwrap du kan få en GNU Readline-kompatibel beskedinputlinje.

Faktisk bruger sugeløse projekter FIFO-filer. Personligt kunne jeg ikke forstå, hvordan man arbejder med filer konkurrencedygtigt i asyncio uden en håndskrevet baggrund fra dedikerede tråde (jeg har brugt sproget til sådanne ting i lang tid Go). Derfor besluttede jeg at nøjes med Unix-domæne-sockets. Desværre gør dette det umuligt at udføre echo 2001:470:dead::babe 6666 > conn. Jeg løste dette problem ved hjælp af socat: echo 2001:470:dead::babe 6666 | socat - UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.

Den originale usikre protokol

TCP bruges som transport: det garanterer levering og dets bestilling. UDP garanterer hverken (hvilket ville være nyttigt, når der bruges kryptografi), men support SCTP Python kommer ikke ud af boksen.

Desværre er der i TCP ikke noget begreb om en besked, kun en strøm af bytes. Derfor er det nødvendigt at komme med et format for beskeder, så de kan deles indbyrdes i denne tråd. Vi kan aftale at bruge line feed-tegnet. Det er fint til at begynde med, men når vi først begynder at kryptere vores beskeder, kan dette tegn forekomme hvor som helst i chifferteksten. I netværk er populære protokoller derfor dem, der først sender længden af ​​beskeden i bytes. For eksempel har Python ud af boksen xdrlib, som giver dig mulighed for at arbejde med et lignende format XDR.

Vi vil ikke arbejde korrekt og effektivt med TCP-læsning – vi vil forenkle koden. Vi læser data fra stikkontakten i en endeløs løkke, indtil vi afkoder hele beskeden. JSON med XML kan også bruges som format til denne tilgang. Men når kryptografi tilføjes, skal dataene signeres og autentificeres - og dette vil kræve en byte-for-byte identisk repræsentation af objekter, hvilket JSON/XML ikke leverer (dumps-resultater kan variere).

XDR er velegnet til denne opgave, dog vælger jeg ASN.1 med DER-kodning og PyDERASN bibliotek, da vi vil have genstande på højt niveau ved hånden, som det ofte er mere behageligt og bekvemt at arbejde med. I modsætning til skemaløse bencode, MessagePack eller CBOR, vil ASN.1 automatisk kontrollere dataene mod et hårdkodet skema.

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

Den modtagne besked vil være Msg: enten en tekst MsgText (med ét tekstfelt indtil videre) eller en MsgHandshake handshake besked (som indeholder navnet på samtalepartneren). Nu ser det overkompliceret ud, men dette er et fundament for fremtiden.

     jeg A) │ │───────── ────────>│ │ │ │MsgHandshake(IdB) │ │<──────────────── │ │ MsgText() │ │──── MsgText() │ │ │

IM uden kryptografi

Som jeg allerede har sagt, vil asyncio-biblioteket blive brugt til alle socket-operationer. Lad os annoncere, hvad vi forventer ved lanceringen:

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

Indstil dit eget navn (--vores-navn alice). Alle forventede samtalepartnere er opført adskilt af kommaer (-deres navne bob, eve). For hver af samtalepartnerne oprettes en mappe med Unix-sokler, samt en koroutine for hver ind-, ud-tilstand:

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

Beskeder, der kommer fra brugeren fra in-socket, sendes til IN_QUEUES-køen:

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

Beskeder, der kommer fra samtalepartnere, sendes til OUT_QUEUES køer, hvorfra data skrives til udgangsstikket:

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

Når du læser fra en tilstandsstik, søger programmet efter samtalepartnerens adresse i PEER_ALIVE-ordbogen. Hvis der endnu ikke er forbindelse til samtalepartneren, skrives en tom linje.

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

Når du skriver en adresse til en stikkontakt, startes funktionen "initiator" af forbindelsen:

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

Lad os overveje initiativtageren. Først åbner den åbenbart en forbindelse til den angivne vært/port og sender en håndtrykmeddelelse med dens navn:

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

Derefter venter den på et svar fra den eksterne part. Forsøger at afkode det indgående svar ved hjælp af Msg ASN.1-skemaet. Vi antager, at hele beskeden vil blive sendt i ét TCP-segment, og vi vil modtage den atomært, når vi kalder .read(). Vi tjekker, at vi har modtaget håndtryksmeddelelsen.

 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     # }}}

Vi kontrollerer, at det modtagne navn på samtalepartneren er kendt af os. Hvis ikke, så bryder vi forbindelsen. Vi tjekker, om vi allerede har etableret en forbindelse med ham (samtaleren gav igen kommandoen om at oprette forbindelse til os) og lukker den. IN_QUEUES-køen indeholder Python-strenge med teksten i beskeden, men har en speciel værdi på None, der signalerer, at msg_sender-koroutinen stopper med at virke, så den glemmer sin writer, der er forbundet med den gamle TCP-forbindelse.

 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 accepterer udgående beskeder (i kø fra en in-socket), serialiserer dem til en MsgText-meddelelse og sender dem over en TCP-forbindelse. Den kan gå i stykker når som helst - vi opsnapper tydeligt dette.

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

I slutningen kommer initiativtageren ind i en uendelig sløjfe af læsemeddelelser fra stikkontakten. Kontrollerer, om disse beskeder er tekstbeskeder, og placerer dem i OUT_QUEUES-køen, hvorfra de vil blive sendt til udgangen på den tilsvarende samtalepartner. Hvorfor kan du ikke bare lave .read() og afkode beskeden? Fordi det er muligt, at flere beskeder fra brugeren vil blive samlet i operativsystemets buffer og sendt i ét TCP-segment. Vi kan afkode den første, og så kan en del af den efterfølgende forblive i bufferen. I tilfælde af en unormal situation lukker vi TCP-forbindelsen og stopper msg_sender-coroutinen (ved at sende None til OUT_QUEUES-køen).

 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)

Lad os vende tilbage til hovedkoden. Efter at have oprettet alle koroutinerne på det tidspunkt, hvor programmet starter, starter vi TCP-serveren. For hver etableret forbindelse opretter den en svarkoroutine.

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

responder ligner initiator og afspejler alle de samme handlinger, men den uendelige løkke af læsning af beskeder starter med det samme, for nemheds skyld. I øjeblikket sender håndtryksprotokollen én besked fra hver side, men i fremtiden vil der være to fra forbindelsesinitiatoren, hvorefter der kan sendes sms’er med det samme.

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

Sikker protokol

Det er tid til at sikre vores kommunikation. Hvad mener vi med sikkerhed, og hvad ønsker vi:

  • fortrolighed af transmitterede meddelelser;
  • ægthed og integritet af transmitterede meddelelser - deres ændringer skal detekteres;
  • beskyttelse mod replay-angreb - faktum om manglende eller gentagne beskeder skal opdages (og vi beslutter at afbryde forbindelsen);
  • identifikation og autentificering af samtalepartnere ved hjælp af forudindtastede offentlige nøgler - vi besluttede allerede tidligere, at vi lavede et ven-til-ven-netværk. Først efter autentificering vil vi forstå, hvem vi kommunikerer med;
  • tilgængelighed perfekt fremadrettet hemmeligholdelse egenskaber (PFS) - kompromittering af vores langlivede signeringsnøgle bør ikke føre til muligheden for at læse al tidligere korrespondance. Optagelse af opsnappet trafik bliver ubrugelig;
  • gyldighed/gyldighed af meddelelser (transport og håndtryk) kun inden for én TCP-session. Det burde ikke være muligt at indsætte korrekt signerede/autentificerede beskeder fra en anden session (selv med den samme samtalepartner).
  • en passiv observatør bør hverken se brugeridentifikatorer, transmitterede offentlige nøgler med lang levetid eller hashes fra dem. En vis anonymitet fra en passiv iagttager.

Overraskende nok ønsker næsten alle at have dette minimum i enhver håndtryksprotokol, og meget lidt af ovenstående opfyldes i sidste ende for "hjemmelavede" protokoller. Nu opfinder vi ikke noget nyt. Jeg vil klart anbefale at bruge Støjramme til at bygge protokoller, men lad os vælge noget enklere.

De to mest populære protokoller er:

  • TLS - en meget kompleks protokol med en lang historie med fejl, jambs, sårbarheder, dårlig tankegang, kompleksitet og mangler (dette har dog lidt at gøre med TLS 1.3). Men vi overvejer det ikke, fordi det er overkompliceret.
  • IPsec с IKE — ikke har alvorlige kryptografiske problemer, selvom de heller ikke er enkle. Hvis du læser om IKEv1 og IKEv2, så er deres kilde STS, ISO/IEC IS 9798-3 og SIGMA (SIGn-and-MAc) protokoller - enkle nok til at implementere på én aften.

Hvad er godt ved SIGMA, som det seneste led i udviklingen af ​​STS/ISO-protokoller? Det opfylder alle vores krav (inklusive at "skjule" samtalepartner-id'er) og har ingen kendte kryptografiske problemer. Det er minimalistisk - fjernelse af mindst ét ​​element fra protokolmeddelelsen vil føre til dets usikkerhed.

Lad os gå fra den enkleste hjemmedyrkede protokol til SIGMA. Den mest basale operation, vi er interesseret i, er nøgleaftale: En funktion, der udsender begge deltagere den samme værdi, som kan bruges som en symmetrisk nøgle. Uden at gå i detaljer: hver af parterne genererer et flygtigt (bruges kun inden for én session) nøglepar (offentlige og private nøgler), udveksler offentlige nøgler, kalder aftalefunktionen, til hvis input de sender deres private nøgle og den offentlige nøgle. samtalepartnerens nøgle.

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

Enhver kan hoppe i midten og erstatte offentlige nøgler med deres egne - der er ingen godkendelse af samtalepartnere i denne protokol. Lad os tilføje en signatur med nøgler med lang levetid.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘┘─── ignPrvA, (PubA)) │ ╔═ │──────────── ────────── ─────────┑n,S PubA = load()║ │ │ ║PrvA, PubA = DHgen() ║ │ │ ╚═══════ ═══════ ═════════════ ═══════ ═════════════❕I Publikation (SignPrvB, (PubB)) │ ╔══════════════ ═══════ ═ ═══ ─────────── ───────────── ──│ ║SignPrvB, SignPubB = load( )║ │ │ ║PrvB, ║┕┕┕┕┕ ═════════ ══════════════ ══╝ ────┐ ╔ ═══════════ ═════╗ │ │ ║bekræft( SignPubB, ...)║ │ <───┘ ║Key = DH(Pr vA, PubB) ║ │ │ ╚════════════════════ ═╝ │ │ │

En sådan signatur vil ikke fungere, da den ikke er bundet til en bestemt session. Sådanne beskeder er også "egnede" til sessioner med andre deltagere. Hele konteksten skal abonnere. Dette tvinger os til også at tilføje en anden besked fra A.

Derudover er det vigtigt at tilføje din egen identifikator under signaturen, da vi ellers kan erstatte IdXXX og gensignere beskeden med nøglen fra en anden kendt samtalepartner. At forhindre refleksionsangreb, er det nødvendigt, at elementerne under signaturen er på klart definerede steder i henhold til deres betydning: hvis A tegner (PubA, PubB), så skal B underskrive (PubB, PubA). Dette taler også om vigtigheden af ​​at vælge struktur og format for serialiserede data. For eksempel sorteres sæt i ASN.1 DER-kodning: SET OF(PubA, PubB) vil være identisk med SET OF(PubB, PubA).

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

Vi har dog stadig ikke "bevist", at vi har genereret den samme delte nøgle til denne session. I princippet kan vi undvære dette trin - den allerførste transportforbindelse vil være ugyldig, men vi ønsker, at når håndtrykket er gennemført, vil vi være sikre på, at alt virkelig er aftalt. I øjeblikket har vi ISO/IEC IS 9798-3-protokollen ved hånden.

Vi kunne underskrive selve den genererede nøgle. Dette er farligt, da det er muligt, at der kan være lækager i den anvendte signaturalgoritme (omend bits-per-signatur, men stadig lækager). Det er muligt at signere en hash af afledningsnøglen, men at lække selv hashen af ​​den afledte nøgle kan være værdifuldt i et brute-force angreb på afledningsfunktionen. SIGMA bruger en MAC-funktion, der godkender afsender-id'et.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └── ╔══════════ ═════════════════╗ │──────────────── ────────── ──────────────────>│ ║SignPrvA, SignPubA = load()║ │ │ │ ┕(PubA) ╚ ═══════ ════════════════════╝ │IdB, PubB, tegn (SignPrvB, (PubA, PubB)) ══╝ ═══ │<───────────────── ──────────────────── ────────── ─│ ║SignPrvB, SignPubB = load()║ │ │ ║PrvB, PubB = DHgen() ║ │ │ ╚════ ═══════════ ═════════ XNUMX │ ║Nøgle = DH( PrvA, PubB) ║ │───────────────────── ── ─────────── ───────── ─────>│ ║verify(Key, IdB) ║ │ │ ║verify(SignPubB, ...)║ │ │ ╚═════════════ ═════ ═╝ │ │

Som en optimering vil nogle måske genbruge deres flygtige nøgler (hvilket selvfølgelig er uheldigt for PFS). For eksempel genererede vi et nøglepar, forsøgte at oprette forbindelse, men TCP var ikke tilgængelig eller blev afbrudt et sted midt i protokollen. Det er en skam at spilde spildte entropi- og processorressourcer på et nyt par. Derfor vil vi introducere den såkaldte cookie - en pseudo-tilfældig værdi, der vil beskytte mod mulige tilfældige genafspilningsangreb ved genbrug af flygtige offentlige nøgler. På grund af bindingen mellem cookien og den flygtige offentlige nøgle, kan den modsatte parts offentlige nøgle fjernes fra signaturen som unødvendig.

Cookies │ ╔════════ ═══════════════════╗ │──────────── ────────── ───────────────────────────────────── ─>│ ║SignPrvA, SignPubA = load( )║ │ │ ║PrvA, PubA = DHgen() ║ │ │ ╚══════════════════════════ ══╝ │IdB, PubB, CookieB , sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) │ ╔════════════════════════════ ╗ │< ───────────────────────────────────── ────────── ────────────────────│ ║SignPrvB, SignPubB = load()║ ┕┑ ┑ DHgen │ ╚══════ ═════════════════════╝ │ │ ╔═══════ ═══════╗ │ tegn( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA) │ ║Key = DH(PrvA, PubB) ║ │───────────────────── ── ───────────────────────────────────── ───────>│ ║ verify(Nøgle, IdB) ║ │ │ ║verify(SignPubB, ...)║ │ │ ╚═════════════════❕══❕ │ │

Endelig ønsker vi at opnå privatlivets fred for vores samtalepartnere fra en passiv observatør. For at gøre dette foreslår SIGMA først at udveksle flygtige nøgler og udvikle en fælles nøgle til at kryptere autentificering og identificere meddelelser. SIGMA beskriver to muligheder:

  • SIGMA-I - beskytter initiatoren mod aktive angreb, responderen mod passive: initiatoren autentificerer responderen, og hvis noget ikke stemmer overens, giver den ikke sin identifikation. Den tiltalte udleverer sin identifikation, hvis der startes en aktiv protokol med ham. Den passive iagttager lærer intet;
    SIGMA-R - beskytter responderen mod aktive angreb, initiativtageren mod passive. Alt er præcis det modsatte, men i denne protokol er der allerede sendt fire håndtrykmeddelelser.

    Vi vælger SIGMA-I, da det minder mere om, hvad vi forventer af klient-server-kendte ting: Klienten genkendes kun af den autentificerede server, og alle kender allerede serveren. Derudover er det nemmere at implementere på grund af færre håndtryksmeddelelser. Alt vi tilføjer til protokollen er at kryptere en del af beskeden og overføre identifikatoren A til den krypterede del af den sidste besked:

    PubA, CookieA │ ╔══════════ ═════════════════════ ────────── ───── ────────── ──────────────────────── ─────────── ───── ──────>│ ║SignPrvA , SignPubA = load()║ │ │ ║PrvA, PubA = DHgen() ═┕┐┕ ═════════ ═════════ ════╝ │ PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB)═┕═════ ═══════════════ ═══════╗ │<────────── ────────── │ ════════ ════════════════╝ │ │ ╔════════ ═══ ══╗ │ Enc((IdA, tegn( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA))) │ ║Key = DH(PrvA, PubB) ║ │──────────────────── ────────────────── ────────────────────── ─────────── ──────>│ ║verify(Key, IdB) ║ │ │ ║verify( SignPubB, ...)║ │ │ ╚══════════ ═════ ══╝ │ │
    
    • GOST R bruges til signatur 34.10-2012 algoritme med 256-bit nøgler.
    • For at generere den offentlige nøgle bruges 34.10/2012/XNUMX VKO.
    • CMAC bruges som MAC. Teknisk set er dette en speciel driftsform for en blokchiffer, beskrevet i GOST R 34.13-2015. Som en krypteringsfunktion for denne tilstand − græshoppe (34.12-2015).
    • Hashen af ​​hans offentlige nøgle bruges som samtalepartnerens identifikator. Brugt som hash Stribog-256 (34.11/2012/256 XNUMX bit).

    Efter håndtrykket aftaler vi en fælles nøgle. Vi kan bruge det til autentificeret kryptering af transportmeddelelser. Denne del er meget enkel og svær at lave en fejl: vi øger meddelelsestælleren, krypterer meddelelsen, godkender (MAC) tælleren og chiffertekst, sender. Når vi modtager en besked, tjekker vi, at tælleren har den forventede værdi, autentificerer chifferteksten med tælleren og dekrypterer den. Hvilken nøgle skal jeg bruge til at kryptere håndtryksmeddelelser, transportere meddelelser, og hvordan man godkender dem? Det er farligt og uklogt at bruge én nøgle til alle disse opgaver. Det er nødvendigt at generere nøgler ved hjælp af specialiserede funktioner KDF (nøgleafledningsfunktion). Igen, lad os ikke flække hår og opfinde noget: HKDF har længe været kendt, godt undersøgt og har ingen kendte problemer. Desværre har det oprindelige Python-bibliotek ikke denne funktion, så vi bruger hkdf plastikpose. HKDF internt bruger HMAC, som igen bruger en hash-funktion. Et eksempel på implementering i Python på Wikipedia-siden tager kun et par linjer kode. Som i tilfældet med 34.10/2012/256 vil vi bruge Stribog-XNUMX som hash-funktion. Outputtet af vores nøgleaftalefunktion vil blive kaldt sessionsnøglen, hvorfra de manglende symmetriske vil blive genereret:

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

    Strukturer/skemaer

    Lad os se på, hvilke ASN.1-strukturer vi har nu til at overføre alle disse data:

    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 er, hvad der vil blive underskrevet. HandshakeTBE - hvad vil blive krypteret. Jeg henleder din opmærksomhed på ukm-feltet i MsgHandshake1. 34.10 VKO, for endnu større randomisering af de genererede nøgler, inkluderer parameteren UKM (brugernøglemateriale) - blot yderligere entropi.

    Tilføjelse af kryptografi til kode

    Lad os kun overveje ændringerne i den originale kode, da rammen forblev den samme (faktisk blev den endelige implementering skrevet først, og derefter blev al kryptografi skåret ud af den).

    Da autentificering og identifikation af samtalepartnere vil blive udført ved hjælp af offentlige nøgler, skal de nu opbevares et sted i lang tid. For nemheds skyld bruger vi JSON som dette:

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

    vores - vores nøglepar, hexadecimale private og offentlige nøgler. deres — navne på samtalepartnere og deres offentlige nøgler. Lad os ændre kommandolinjeargumenterne og tilføje efterbehandling af JSON-data:

    from pygost import gost3410
    from pygost.gost34112012256 import GOST34112012256
    
    CURVE = gost3410.GOST3410Curve(
        *gost3410.CURVE_PARAMS["GostR3410_2001_CryptoPro_A_ParamSet"]
    )
    
    parser = argparse.ArgumentParser(description="GOSTIM")
    parser.add_argument(
        "--keys-gen",
        action="store_true",
        help="Generate JSON with our new keypair",
    )
    parser.add_argument(
        "--keys",
        default="keys.json",
        required=False,
        help="JSON with our and their keys",
    )
    parser.add_argument(
        "--bind",
        default="::1",
        help="Address to listen on",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=6666,
        help="Port to listen on",
    )
    args = parser.parse_args()
    
    if args.keys_gen:
        prv_raw = urandom(32)
        pub = gost3410.public_key(CURVE, gost3410.prv_unmarshal(prv_raw))
        pub_raw = gost3410.pub_marshal(pub)
        print(json.dumps({
            "our": {"prv": hexenc(prv_raw), "pub": hexenc(pub_raw)},
            "their": {},
        }))
        exit(0)
    
    # Parse and unmarshal our and their keys {{{
    with open(args.keys, "rb") as fd:
        _keys = json.loads(fd.read().decode("utf-8"))
    KEY_OUR_SIGN_PRV = gost3410.prv_unmarshal(hexdec(_keys["our"]["prv"]))
    _pub = hexdec(_keys["our"]["pub"])
    KEY_OUR_SIGN_PUB = gost3410.pub_unmarshal(_pub)
    KEY_OUR_SIGN_PUB_HASH = OctetString(GOST34112012256(_pub).digest())
    for peer_name, pub_raw in _keys["their"].items():
        _pub = hexdec(pub_raw)
        KEYS[GOST34112012256(_pub).digest()] = {
            "name": peer_name,
            "pub": gost3410.pub_unmarshal(_pub),
        }
    # }}}
    

    Den private nøgle til 34.10-algoritmen er et tilfældigt tal. 256-bit størrelse til 256-bit elliptiske kurver. PyGOST virker ikke med et sæt bytes, men med store tal, så vores private nøgle (urandom(32)) skal konverteres til et tal ved hjælp af gost3410.prv_unmarshal(). Den offentlige nøgle bestemmes deterministisk ud fra den private nøgle ved hjælp af gost3410.public_key(). Den offentlige nøgle 34.10 er to store tal, der også skal konverteres til en byte-sekvens for at lette lagring og transmission ved hjælp af gost3410.pub_marshal().

    Efter at have læst JSON-filen, skal de offentlige nøgler følgelig konverteres tilbage ved hjælp af gost3410.pub_unmarshal(). Da vi vil modtage identifikatorerne for samtalepartnerne i form af en hash fra den offentlige nøgle, kan de umiddelbart beregnes på forhånd og placeres i en ordbog til hurtig søgning. Stribog-256 hash er gost34112012256.GOST34112012256(), som fuldt ud opfylder hashlib-grænsefladen for hash-funktioner.

    Hvordan har initiativtagerens koroutine ændret sig? Alt er som i håndtryksskemaet: vi genererer en cookie (128-bit er rigeligt), et flygtigt nøglepar 34.10, som vil blive brugt til VKO-nøgleaftalefunktionen.

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

    • vi venter på et svar og afkoder den indkommende besked;
    • sørg for at få håndtryk1;
    • afkode den modsatte parts flygtige offentlige nøgle og beregne sessionsnøglen;
    • Vi genererer symmetriske nøgler, der er nødvendige for at behandle TBE-delen af ​​meddelelsen.

     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 er et 64-bit nummer (urandom(8)), som også kræver deserialisering fra sin byte-repræsentation ved hjælp af gost3410_vko.ukm_unmarshal(). VKO funktion for 34.10/2012/256 3410-bit er gost34102012256_vko.kek_XNUMX() (KEK - krypteringsnøgle).

    Den genererede sessionsnøgle er allerede en 256-bit pseudo-tilfældig byte-sekvens. Derfor kan den straks bruges i HKDF-funktioner. Da GOST34112012256 opfylder hashlib-grænsefladen, kan den straks bruges i Hkdf-klassen. Vi specificerer ikke saltet (det første argument for Hkdf), da den genererede nøgle, på grund af flygtigheden af ​​de deltagende nøglepar, vil være forskellig for hver session og allerede indeholder nok entropi. kdf.expand() producerer som standard allerede de 256-bit nøgler, der kræves til Grasshopper senere.

    Dernæst kontrolleres TBE- og TBS-delene af den indgående meddelelse:

    • MAC'en over den indkommende chiffertekst beregnes og kontrolleres;
    • chifferteksten er dekrypteret;
    • TBE struktur er afkodet;
    • samtalepartnerens identifikator tages fra den, og det kontrolleres, om han overhovedet er kendt af os;
    • MAC over denne identifikator beregnes og kontrolleres;
    • signaturen over TBS-strukturen er verificeret, hvilket inkluderer begge parters cookie og den modsatte parts offentlige flygtige nøgle. Signaturen verificeres af samtalepartnerens langlivede signaturnøgle.

     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"]
    

    Som jeg skrev ovenfor, beskriver 34.13/2015/XNUMX forskellige blokchiffer-driftstilstande fra 34.12. Blandt dem er der en tilstand til generering af imiterede indsatser og MAC-beregninger. I PyGOST er dette gost2015.mac(). Denne tilstand kræver beståelse af krypteringsfunktionen (modtagelse og returnering af en blok data), størrelsen af ​​krypteringsblokken og faktisk selve dataene. Hvorfor kan du ikke hardkode størrelsen på krypteringsblokken? 3413/34.12/2015 beskriver ikke kun 128-bit Grasshopper-chifferet, men også 64-bit Magma - en let modificeret GOST 28147-89, skabt tilbage i KGB og stadig har en af ​​de højeste sikkerhedstærskler.

    Kuznechik initialiseres ved at kalde gost.3412.GOST3412Kuznechik(nøgle) og returnerer et objekt med .encrypt()/.decrypt()-metoder, der er egnede til at overføre til 34.13-funktioner. MAC beregnes som følger: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). For at sammenligne den beregnede og modtagne MAC kan du ikke bruge den sædvanlige sammenligning (==) af byte strenge, da denne operation lækker sammenligningstid, hvilket i det generelle tilfælde kan føre til fatale sårbarheder som f.eks. BEAST angreb på TLS. Python har en speciel funktion, hmac.compare_digest, til dette.

    Blokkrypteringsfunktionen kan kun kryptere én datablok. For et større antal, og endda ikke et multiplum af længden, er det nødvendigt at bruge krypteringstilstanden. 34.13-2015 beskriver følgende: ECB, CTR, OFB, CBC, CFB. Hver har sine egne acceptable anvendelsesområder og egenskaber. Desværre har vi stadig ikke standardiseret autentificerede krypteringstilstande (såsom CCM, OCB, GCM og lignende) - vi er tvunget til i det mindste selv at tilføje MAC. jeg vælger tællertilstand (CTR): det kræver ikke polstring til blokstørrelsen, kan paralleliseres, bruger kun krypteringsfunktionen, kan sikkert bruges til at kryptere et stort antal meddelelser (i modsætning til CBC, som har kollisioner relativt hurtigt).

    Ligesom .mac(), tager .ctr() lignende input: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, almindelig tekst, iv). Det er påkrævet at specificere en initialiseringsvektor, der er nøjagtigt halvdelen af ​​længden af ​​krypteringsblokken. Hvis vores krypteringsnøgle kun bruges til at kryptere én besked (omend fra flere blokke), så er det sikkert at indstille en nul initialiseringsvektor. For at kryptere håndtryksmeddelelser bruger vi en separat nøgle hver gang.

    At verificere signaturen gost3410.verify() er trivielt: vi passerer den elliptiske kurve, som vi arbejder inden for (vi registrerer den blot i vores GOSTIM-protokol), underskriverens offentlige nøgle (glem ikke, at dette skal være en tuple af to store tal og ikke en bytestreng), 34.11/2012/XNUMX hash og selve signaturen.

    Dernæst forbereder og sender vi i initiativtageren en håndtryk-besked til handshake2, og udfører de samme handlinger, som vi gjorde under verifikationen, kun symmetrisk: signering på vores nøgler i stedet for at tjekke, osv...

     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)
     

    Når sessionen er etableret, genereres transportnøgler (en separat nøgle til kryptering, til autentificering, for hver af parterne), og Grasshopperen initialiseres til at dekryptere og kontrollere MAC'en:

     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     # }}}
    

    msg_sender coroutine krypterer nu meddelelser, før de sendes på en TCP-forbindelse. Hver besked har en monotont stigende nonce, som også er initialiseringsvektoren, når den krypteres i tællertilstand. Hver besked og beskedblok er garanteret at have en anden tællerværdi.

    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
    

    Indgående meddelelser behandles af msg_receiver coroutine, som håndterer godkendelse og dekryptering:

    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)
    

    Konklusion

    GOSTIM er beregnet til udelukkende at blive brugt til uddannelsesformål (da det i det mindste ikke er omfattet af tests)! Kildekoden til programmet kan downloades her (Стрибог-256 хэш: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Как и все мои проекты, типа GoGOST, PyDERASN, NCCP, GoVPN, GOSTIM er fuldstændig gratis softwarefordelt på vilkårene GPLv3 +.

    Sergey Matveev, cypherpunk, medlem SPO Fonden, Python/Go-udvikler, chefspecialist Federal State Unitary Enterprise "STC "Atlas".

Kilde: www.habr.com

Tilføj en kommentar