GOSTIM: P2P F2F E2EE IM pÄ en aften med GOST-kryptografi

Som udvikler PyGOST biblioteket (GOST kryptografiske primitiva i ren Python) fÄr jeg ofte spÞrgsmÄl om, hvordan man kan implementere en simpel sikker beskedudveksling pÄ egen hÄnd. Mange mener, at anvendt kryptografi er noget simpelt, og at et .encrypt() kald fra en blokkrypter er tilstrÊkkeligt til sikker afsendelse over kommunikationskanalen. Andre mener, at anvendt kryptografi er for de fÄ, og det er acceptabelt, at rige virksomheder som Telegram med matematikere fra olympiaderne ikke kan implementere en sikker protokol.

Alt dette har fÄet mig til at skrive denne artikel for at vise, at implementeringen af kryptografiske protokoller og sikker IM ikke er en sÄ vanskelig opgave. Men det er ikke vÊrd at finde pÄ egne autentifikations- og nÞgleudvekslingsprotokoller.

GOSTIM: P2P F2F E2EE IM pÄ en aften med GOST-kryptografi
Artiklen vil beskrive peer-to-peer, friend-to-friend, end-to-end krypteret instant messenger med SIGMA-I autentifikations- og nÞgleudvekslingsprotokol (hvorpÄ IPsec IKE), der udelukkende bruger GOST kryptografiske algoritmer fra PyGOST biblioteket og ASN.1 kodning af meddelelserne fra biblioteket PyDERASN (som jeg allerede har skrevet fÞr). En grundlÊggende betingelse er: det skal vÊre sÄ enkelt, at det kan skrives fra bunden pÄ en aften (eller arbejdsdag), ellers er det ikke et simpelt program. Der vil helt sikkert vÊre fejl, unÞdvendige kompleksiteter, mangler, plus det er mit fÞrste program ved brug af asyncio biblioteket.

IM-designet

For at starte, skal vi forstÄ, hvordan vores IM vil se ud. For enkelhedens skyld, lad os antage, at det er et peer-to-peer netvÊrk uden nogen form for deltagende opdagelse. Vi vil selv angive, hvilken adresse: port vi skal oprette forbindelse til for at kommunikere med vores samtalepartner.

Jeg forstÄr, at i Þjeblikket, antagelsen om tilgÊngeligheden af en direkte forbindelse mellem to vilkÄrlige computere er en vÊsentlig begrÊnsning i anvendeligheden af IM i praksis. Men jo flere udviklere der implementerer forskellige NAT-traversal lÞsninger, jo lÊngere tid vil vi blive i IPv4 internettet med en nedslÄende sandsynlighed for forbindelse mellem vilkÄrlige computere. Hvor lÊnge kan vi holde ud at mangle IPv6 derhjemme og pÄ arbejdet?

Vi vil have et friend-to-friend netvÊrk: alle mulige samtalepartnere skal vÊre kendt pÄ forhÄnd. For det fÞrste forenkler det det hele betydeligt: vi prÊsenterer os, finder eller finder ikke et navn/nÞgle, afbryder forbindelsen eller fortsÊtter arbejdet, velvidende hvem samtalepartneren er. For det andet, i almindelighed, er det sikkert og udelukker mange angreb.

IM-grÊnsefladen vil vÊre tÊt pÄ klassiske lÞsninger suckless-projekter, som jeg virkelig kan lide for deres minimalisme og Unix-way filosofi. IM-programmet opretter et bibliotek for hver samtalepartner med tre Unix-domÊnesokker:

  • in — her skrives de beskeder, der sendes til samtalepartneren;
  • out — her lĂŠses de beskeder, der modtages fra samtalepartneren;
  • state — ved at lĂŠse herfra kan vi se, om samtalepartneren i Ăžjeblikket er tilsluttet, adresse/port for forbindelsen.

Derudover oprettes en conn sokkel, hvor vi ved at skrive vĂŠrtsport initierer forbindelsen til den fjerne samtalepartner.

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

Denne tilgang tillader uafhÊngige implementationer af IM-transport og brugergrÊnseflade, da smag og farve ikke kan tilfredsstille alle. Ved at bruge tmux og/eller multitail, kan du fÄ en multi-vinduegrÊnseflade med syntaksfremhÊvning. Og med hjÊlp af rlwrap kan man fÄ en GNU Readline-kompatibel linje til indtastning af beskeder.

I virkeligheden bruger suckless-projekterne FIFO-filer. Personligt kunne jeg ikke finde ud af, hvordan man arbejder med filer samtidigt i asyncio uden at lave en egen understĂžttelse med dedikerede trĂ„de (til sĂ„danne ting har jeg lĂŠnge brugt sproget Go). Derfor besluttede jeg at klare mig med Unix-domĂŠnesokler. DesvĂŠrre fratager det muligheden for at bruge echo 2001:470:dead::babe 6666 > conn. Jeg lĂžste dette problem ved at bruge socat: echo 2001:470:dead::babe 6666 | socat — UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.

Den oprindelige usikre protokol

Som transport bruges TCP: det garanterer levering og rÊkkefÞlge. UDP garanterer hverken det ene eller det andet (hvilket ville vÊre nyttigt, nÄr kryptografi anvendes), og understÞttelse af SCTP findes ikke i Python som standard.

DesvÊrre findes der ikke begrebet besked i TCP, kun en strÞm af bytes. Derfor er det nÞdvendigt at finde et format til beskeder, sÄ de kan adskilles i denne strÞm. Vi kan aftale at bruge linjeskift-tegnet. Det er fint til at starte med, men nÄr vi begynder at kryptere vores beskeder, kan dette tegn komme hvor som helst i krypteringen. Derfor er protokoller, der fÞrst sender lÊngden af beskeden i bytes, populÊre i netvÊrk. For eksempel har Python som standard xdrlib, der gÞr det muligt at arbejde med et sÄdant format. XDR.

Vi vil ikke arbejde korrekt og effektivt med TCP-lĂŠsning — lad os forsimple koden. Vi lĂŠser data fra soklen i en uendelig lĂžkke, indtil vi dekoder hele beskeden. JSON kan bruges som format til denne tilgang, sĂ„vel som XML. Men nĂ„r kryptografi tilfĂžjes, skal dataene underskrives og autentificeres — og det krĂŠver byte-for-byte identisk reprĂŠsentation af objekter, noget som JSON/XML ikke garanterer (dumps kan variere).

XDR er passende til en sÄdan opgave, men jeg vÊlger ASN.1 med DER-kodning og PyDERASN biblioteket, da vi vil arbejde med hÞjniveauobjekter, som ofte er lettere og mere bekvemme at arbejde med. I modsÊtning til schemaless bencode, MessagePack eller CBOR, ASN.1 vil automatisk kontrollere dataene mod en strengt defineret 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 tekstbesked MsgText (for nu med et enkelt tekstfelt), eller en hÄndtrykbesked MsgHandshake (hvor navnene pÄ samtalepartnerne overfÞres). Det ser lidt overkompliceret ud nu, men det er en investering i fremtiden.

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

IM uden kryptografi

Som jeg allerede har sagt, vil asyncio-biblioteket blive brugt til alle operationer med sokler. Lad os erklĂŠre, hvad vi forventer ved opstart:

parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
    "--our-name",
    required=True,
    help="Vores peer navn",
)
parser.add_argument(
    "--their-names",
    required=True,
    help="Deres peer navne, adskilt med komma",
)
parser.add_argument(
    "--bind",
    default="::1",
    help="Adresse at lytte pÄ",
)
parser.add_argument(
    "--port",
    type=int,
    default=6666,
    help="Port at lytte pÄ",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))

Selvnavn angives (—our-name alice). Alle forventede samtalepartnere listes op med komma (—their-names bob,eve). For hver samtalepartner oprettes et bibliotek med Unix-sockets samt en koroutine for hver in, out, state:

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

Modtagne beskeder fra brugeren sendes fra in-socket til IN_QUEUES kĂžerne:

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

Modtagne beskeder fra samtalepartnere sendes til OUT_QUEUES kĂžerne, fra hvilke data skrives til out-socket:

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 der lÊses fra state-socket, sÞger programmet i PEER_ALIVE ordbogen efter adressen til samtalepartneren. Hvis der endnu ikke er forbindelse til samtalepartneren, skrives en tom streng.

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 adressen skrives til conn-socket, startes "initiator"-funktionen til 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 se pÄ initiatoren. FÞrst skal han Äbenlyst Äbne en forbindelse til den angivne vÊrt/port og sende en handshake-besked med sit navn:

 130 async def initiator(host, port):
 131     _id = repr((host, port))
 132     logging.info("%s: ringer op", _id)
 133     reader, writer = await asyncio.open_connection(host, port)
 134     # Handshake besked {{{
 135     writer.write(Msg(("handshake", MsgHandshake((
 136         ("peerName", OUR_NAME),
 137     )))).encode())
 138     # }}}
 139     await writer.drain()

Derefter venter pÄ svar fra den fjerne part. ForsÞger at dekode den modtagne svarmelodi i henhold til Msg ASN.1 skemaet. Antager at hele beskeden vil blive sendt i et enkelt TCP-segment, og vi modtager det atomisk ved at kalde .read(). Kontrollerer, at vi/modtager netop handshake-beskeden.

 141     # Vent pÄ Handshake besked {{{
 142     data = await reader.read(256)
 143     if data == b"":
 144         logging.warning("%s: intet svar, afbryder", _id)
 145         writer.close()
 146         return
 147     try:
 148         msg, _ = Msg().decode(data)
 149     except ASN1Error:
 150         logging.warning("%s: udecoderbart svar, afbryder", _id)
 151         writer.close()
 152         return
 153     logging.info("%s: fik %s besked", _id, msg.choice)
 154     if msg.choice != "handshake":
 155         logging.warning("%s: uventet besked, afbryder", _id)
 156         writer.close()
 157         return
 158     # }}}

Kontrollerer, at det modtagne navn fra samtalepartneren er kendt for os. Hvis ikke, sÄ afbryder vi forbindelsen. Kontrollerer, om vi har haft en forbindelse til dem fÞr (samtalepartneren har gentagne gange givet kommando til at connecte til os) og lukker den. I IN_QUEUES kÞen placeres Python-strenge med beskedens tekst, men der er en sÊrlig vÊrdi None, der signalerer msg_sender koroutine om at stoppe arbejdet, sÄ den glemmer sin writer, der er knyttet til det forÊldede 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("ukendt peer navn: %s", peer_name)
 163         writer.close()
 164         return
 165     logging.info("%s: session etableret: %s", _id, peer_name)
 166     # KĂžr tekstbesked afsender, initialiser transport dekoder {{{
 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 modtager udgĂ„ende beskeder (indsat i kĂžen fra in-socket), serialiserer dem i MsgText besked og sender dem over TCP-forbindelsen. Den kan brydes nĂ„r som helst — det er noget, vi eksplicit fanger.

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: sendte %d tegns besked", peer_name, len(text))

Til sidst gÄr initiatoren ind i en uendelig loop for at lÊse meddelelser fra socketen. Den tjekker, om det er tekstbeskeder, og placerer dem i OUT_QUEUES-kÞen, hvorfra de vil blive sendt til out-socketen for den tilsvarende samtalepartner. Hvorfor kan man ikke blot gÞre .read() og afkode meddelelsen? Fordi der ikke kan udelukkes en situation, hvor flere meddelelser fra brugeren bliver samlet i operativsystemets buffer og sendt som et enkelt TCP-segment. Vi kan afkode den fÞrste, men derefter kan der vÊre en del af den efterfÞlgende i bufferen. Ved enhver unormal situation lukker vi TCP-forbindelsen og stopper msg_sender koroutinen (ved at sende None til OUT_QUEUES-kÞen).

 174     buf = b""
 175     # Vent pÄ testmeddelelser {{{
 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: maksimal bufferstĂžrrelse overskredet", _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: uventet %s meddelelse", _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: frakobler: %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: modtaget %d tegns meddelelse", peer_name, len(text))
  69     await OUT_QUEUES[peer_name].put(text)

Lad os vende tilbage til hovedkoden. Efter oprettelsen af alle koroutiner, nÄr programmet startes, starter vi TCP-serveren. For hver etableret forbindelse opretter den en responder (responsor) koroutine.

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("Lytter pÄ: %s", server.sockets[0].getsockname())
loop.run_forever()

responder ligner initiatoren og udfĂžrer spejlvendt alle de samme handlinger, men den uendelige loop for at lĂŠse meddelelser starter straks for enkelhedens skyld. I Ăžjeblikket sender handshake-protokollen en meddelelse fra hver side, men senere vil der blive sendt to fra initiatoren af forbindelsen, efterfulgt af umiddelbar mulighed for at sende tekst.

  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 egentlig med sikkerhed, og hvad Ăžnsker vi:

  • fortrolighed for de overfĂžrte meddelelser;
  • autenticitet og integritet af de overfĂžrte meddelelser - enhver ĂŠndring skal opdages;
  • beskyttelse mod genafspilningsangreb (replay attack) - enhver forsvinden eller gentagelse af meddelelser skal opdages (og vi beslutter at afbryde forbindelsen);
  • identifikation og autentifikation af samtalepartnere baseret pĂ„ forudindlagte offentlige nĂžgler - vi har tidligere besluttet at lave et friend-to-friend-netvĂŠrk. Kun efter autentifikation forstĂ„r vi, hvem vi kommunikerer med;
  • tilstedevĂŠrelse perfect forward secrecy egenskaber (PFS) - kompromittering af vores langtidsholdbare signeringsnĂžgle mĂ„ ikke fĂžre til muligheden for at lĂŠse hele tidligere kommunikation. Optagelser af aflyttet trafik bliver ubrugelige;
  • Gyldighed af meddelelser (transport og hĂ„ndtryk) gĂŠlder kun inden for en enkelt TCP-session. IndsĂŠttelse af korrekt signerede/autentificerede meddelelser fra en anden session (selv med den samme modpart) bĂžr ikke vĂŠre muligt;
  • En passiv observatĂžr bĂžr ikke se hverken brugeridentifikatorer, langlivet offentlige nĂžgler eller hashes af dem. En slags anonymitet over for den passive observatĂžr.

Det er overraskende, men dette minimum Þnsker nÊsten alle i enhver hÄndtryksprotokol, og meget lidt af det nedenstÄende bliver faktisk overholdt for "hjemmelavede" protokoller. Lad os ikke finde pÄ noget nyt. Jeg vil bestemt anbefale at bruge Noise framework til at opbygge protokoller, men lad os vÊlge noget simplere.

De to mest populĂŠre protokoller er:

  • TLS — en kompleks protokol med en lang historie af fejl, problemer, sĂ„rbarheder, dĂ„rlig planlĂŠgning, kompleksitet og mangler (dog gĂŠlder dette i meget lidt for TLS 1.3). Men vi overvejer ikke den pĂ„ grund af overkompleksitet.
  • IPsec med IKE — har ikke alvorlige kryptografiske problemer, selvom de heller ikke er enkle. Hvis man lĂŠser om IKEv1 og IKEv2, er deres oprindelse STS, ISO/IEC IS 9798-3 og SIGMA (SIGn-and-MAc) protokoller — der er enkle nok til at implementere pĂ„ en aften.

Hvad gĂžr SIGMA, som det sidste led i udviklingen af STS/ISO protokoller, godt? Det opfylder alle vores krav (inklusive "skjule" modparters identifikatorer), har ikke kendte kryptografiske problemer. Det er minimalistisk — fjernelse af bare Ă©t element fra protokolens besked vil gĂžre den usikker.

Lad os bevÊge os fra den simpleste hjemmelavede protokol til SIGMA. Den mest grundlÊggende operation, der interesserer os, er nÞgleudveksling: en funktion, hvormed begge parter fÄr den samme vÊrdi, der kan bruges som en symmetrisk nÞgle. Uden at gÄ i detaljer: hver side genererer et ephemeral (kun brugt inden for en enkelt session) nÞglepar (offentlig og privat nÞgle), udveksler offentlige nÞgler, kalder en udvekslingsfunktion, hvor de sender deres private nÞgle og den offentlige nÞgle fra modparten som input.

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

Enhver kan blande sig ind og erstatte de offentlige nþgler med sine egne — i denne protokol er der ingen autentificering af samtalepartnere. Lad os tilfþje en signatur med langtidsholdbare nþgler.

┌─────┐                            ┌─────┐
│PeerA│                            │PeerB│
└──┬──┘                            └──┬──┘
   │IdA, PubA, sign(SignPrvA, (PubA)) │ ╔═══════════════════════════╗
   │─────────────────────────────────│ ║SignPrvA, SignPubA = load()║
   │                                  │ ║PrvA, PubA = DHgen()       ║
   │                                  │ ╚═══════════════════════════╝
   │IdB, PubB, sign(SignPrvB, (PubB)) │ ╔═══════════════════════════╗
   │─────────────────────────────────│ ║SignPrvB, SignPubB = load()║
   │                                  │ ║PrvB, PubB = DHgen()       ║
   │                                  │ ╚═══════════════════════════╝
   ────┐    ╔═════════════════════╗   │
       │    ║verify(SignPubB, ...)║   │
   ───┘    ║Key = DH(PrvA, PubB) ║   │
   │        ╚═════════════════════╝   │
   │                                  │

En sÄdan signatur vil ikke vÊre tilstrÊkkelig, da den ikke er knyttet til en specifik session. SÄdanne meddelelser vil vÊre "tilstrÊkkelige" for sessioner med andre deltagere. Hele konteksten skal underskrives. Dette tvinger ogsÄ til at tilfÞje en ny meddelelse fra A.

Desuden er det kritisk at tilfÞje sin egen identifikator til signaturen, da vi ellers kan Êndre IdXXX og genskrive meddelelsen med en anden kendt samtaleparts nÞgle. For at forhindre reflection angreb, det er nÞdvendigt, at elementerne under underskriften er placeret prÊcist i henhold til deres betydning: hvis A underskriver (PubA, PubB), sÄ skal B underskrive (PubB, PubA). Dette understreger ogsÄ vigtigheden af at vÊlge strukturen og formatet af de serialiserede data. For eksempel bliver mÊngder i ASN.1 DER-kodning sorteret: SET OF(PubA, PubB) vil vÊre identisk med SET OF(PubB, PubA).

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

Men vi har stadig ikke "bevist", at vi har udarbejdet den samme fĂŠlles nĂžgle til denne session. Man kan teknisk set undvĂŠre dette trin — den fĂžrste transportmeddelelse ville vĂŠre ugyldig, men vi Ăžnsker, at nĂ„r handshake er afsluttet, er vi sikre pĂ„, at alt faktisk er blevet aftalt. I Ăžjeblikket har vi ISO/IEC IS 9798-3 protokollen i vores hĂŠnder.

Vi kunne ogsÄ underskrive den genererede nÞgle. Dette er farligt, da det ikke er udelukket, at den anvendte underskriftsalgoritme kan have lÊkager (selvom det er bits-til-underskrift, men stadig lÊkager). Man kan underskrive hashen af den genererede nÞgle, men lÊkagen af selv hashen fra den genererede nÞgle kan have vÊrdi i en brute-force attack pÄ generate-funktionen. SIGMA bruger en MAC-funktion, der autentificerer afsenderens identitet.

┌─────┐                                            ┌─────┐
│PeerA│                                            │PeerB│
└──┬──┘                                            └──┬──┘
   │                    IdA, PubA                     │ ╔═══════════════════════════╗
   │─────────────────────────────────────────────────â€ș│ ║SignPrvA, SignPubA = load()║
   │                                                  │ ║PrvA, PubA = DHgen()       ║
   │                                                  │ ╚═══════════════════════════╝
   │IdB, PubB, sign(SignPrvB, (PubA, PubB)), MAC(IdB) │ ╔═══════════════════════════╗
   │â€č─────────────────────────────────────────────────│ ║SignPrvB, SignPubB = load()║
   │                                                  │ ║PrvB, PubB = DHgen()       ║
   │                                                  │ ╚═══════════════════════════╝
   │                                                  │ ╔═════════════════════╗
   │     sign(SignPrvA, (PubB, PubA)), MAC(IdA)       │ ║Key = DH(PrvA, PubB) ║
   │─────────────────────────────────────────────────â€ș│ ║verify(Key, IdB)     ║
   │                                                  │ ║verify(SignPubB, ...)║
   │                                                  │ ╚═════════════════════╝
   │                                                  │

Som nogle kan have interesse i at genbruge deres ephemeral nĂžgler som en optimering (hvilket selvfĂžlgelig er skadelig for PFS). For eksempel har vi genereret et nĂžglepar, forsĂžgt at oprette forbindelse, men TCP var ikke tilgĂŠngelig eller afbrudt et sted midt i protokollen. Det er en skam at spilde den brugte entropi og CPU-ressourcer pĂ„ et nyt par. Derfor introducerer vi et sĂ„kaldt cookie — en pseudorandom vĂŠrdi, der beskytter mod mulige tilfĂŠldige angreb ved gentagen brug af ephemeral offentlige nĂžgler. PĂ„ grund af bindingen mellem cookie og den ephemeral offentlige nĂžgle, kan den offentlige nĂžgle fra den modsatte deltager fjernes fra signaturen uden behov.

┌─────┐                                                                 ┌─────┐
│PeerA│                                                                 │PeerB│
└──┬──┘                                                                 └──┬──┘
   │                          IdA, PubA, CookieA                           │ ╔═══════════════════════════╗
   │──────────────────────────────────────────────────────────────────────>│ ║SignPrvA, SignPubA = load()║
   │                                                                       │ ║PrvA, PubA = DHgen()       ║
   │                                                                       │ ╚═══════════════════════════╝
   │IdB, PubB, CookieB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) │ ╔═══════════════════════════╗
   ││ ║verify(Key, IdB)     ║
   │                                                                       │ ║verify(SignPubB, ...)║
   │                                                                       │ ╚═════════════════════╝
   │                                                                       │

Endelig Þnsker vi at beskytte vores identiteters privatliv mod passive observatÞrer. For dette foreslÄr SIGMA fÞrst at udveksle ephemeral nÞgler, skabe en fÊlles nÞgle, som kan kryptere autentificerende og identificerende meddelelser. SIGMA beskriver to muligheder:

  • SIGMA-I — beskytter initiatoren mod aktive angreb, og respondenten mod passive: initiatoren autentificerer respondenten, og hvis der er noget galt, videregiver han ikke sin identifikation. Respondenten videregiver sin identifikation, hvis der startes et aktivt protokol. Passive observatĂžrer vil ikke lĂŠre noget;
    SIGMA-R — beskytter respondenten mod aktive angreb, initiatoren mod passive. Alt er omvendt, men i denne protokol overfĂžres der allerede fire beskeder under hĂ„ndtrykket.

    Vi vÊlger SIGMA-I, da det minder mere om, hvad vi forventer af klient-server-ordninger: klienten kender kun den autentificerede server, mens serveren kender alt. Derudover er det lettere at implementere pÄ grund af det lavere antal hÄndtrykmeddelelser. Alt, hvad vi tilfÞjer til protokollen, er kryptering af dele af meddelelsen og overfÞrsel af identifikatoren A til den krypterede del af den sidste meddelelse:

    ┌─────┐                                                                        ┌─────┐
    │PeerA│                                                                        │PeerB│
    └──┬──┘                                                                        └──┬──┘
       │                                PubA, CookieA                                 │ ╔═══════════════════════════╗
       │─────────────────────────────────────────────────────────────────────────────>| ║SignPrvA, SignPubA = load()║
       │                                                                              │ ║PrvA, PubA = DHgen()       ║
       │                                                                              │ ╚═══════════════════════════╝
       │PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB))) │ ╔═══════════════════════════╗
       │| ║verify(Key, IdB)     ║
       │                                                                              │ ║verify(SignPubB, ...)║
       │                                                                              │ ╚═════════════════════╝
       │                                                                              │
    
    • Til signering anvendes GOST R 34.10-2012 algoritmen med 256-bit nĂžgler.
    • Til generering af den fĂŠlles nĂžgle anvendes 34.10-2012 VKO.
    • Som MAC anvendes CMAC. Tekniskt set er dette en sĂŠrlig driftstilstand af en blokchiffer, beskrevet i GOST R 34.13-2015. Som krypteringsfunktion for denne tilstand — GrĂŠshoppen (34.12-2015).
    • Som identificering af modparten anvendes en hash af hans offentlige nĂžgle. Hashen anvendes som Stribog-256 (34.11-2012 256 bit).

    Efter hÄndtrykket vil vi have en aftalt fÊlles nÞgle. Denne kan vi bruge til autentificeret kryptering af transportsignaler. Denne del er ganske enkel og svÊr at fÄ galt: vi inkrementerer meddelelses tÊlleren, krypterer meddelelsen, autentificerer (MAC) tÊlleren og krypteringen, og sender. Ved modtagelse af meddelelsen tjekker vi, at tÊlleren har den forventede vÊrdi, autentificerer krypteringen med tÊlleren, og dekrypterer. Hvilken nÞgle skal vi bruge til at kryptere hÄndtryksmeddelelser, transportmeddelelser og hvilken til autentificering? At bruge én nÞgle til alle disse opgaver er farligt og irrationelt. Det er nÞdvendigt at generere nÞgler med specialiserede funktioner KDF (key derivation function). Igen, lad os ikke gÞre det kompliceret eller finde pÄ noget: HKDF er lÊnge kendt, godt undersÞgt og har ingen kendte problemer. DesvÊrre findes denne funktion ikke i den indbyggede Python-bibliotek, sÄ vi bruger hkdf pakken. HKDF bruger internt HMAC, som igen bruger hash-funktionen. Et eksempel pÄ implementering i Python pÄ Wikipedia's side fylder kun fÄ linjer kode. Ligesom i tilfÊlde med 34.10-2012 vil vi bruge Stribog-256 som hash-funktion. Udbuddet af vores nÞgleaftalefunktion vil blive kaldt sessionsnÞglen, fra hvilken de manglende symmetriske nÞgler vil blive udledt:

    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/skejer

    Lad os se, hvilke ASN.1 strukturer vi nu har fÄet til transmission af 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 — det, der skal underskrives (to be signed). HandshakeTBE — det, der skal krypteres (to be encrypted). Jeg vil gerne pĂ„pege feltet ukm i MsgHandshake1. 34.10 VKO, for at Ăžge randomiseringen af de genererede nĂžgler, omfatter parameteren UKM (user keying material) — bare ekstra entropi.

    TilsĂŠtning af kryptografi til koden

    Lad os kun overveje de ĂŠndringer, der er foretaget i den originale kode, da rammen forblev den samme (faktisk blev den endelige implementering skrevet fĂžrst, og derefter blev al kryptografi fjernet fra den).

    Da autentificering og identifikation af kommunikationspartnere vil blive udfĂžrt via offentlige nĂžgler, skal de nu opbevares et langt stykke tid et sted. For enkelhedens skyld bruger vi JSON af fĂžlgende type:

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

    our — vores nþglepar, hexadecimale private og offentlige nþgler. their — kommunikatorernes navne og deres offentlige nþgler. Lad os éndre kommandolinjeargumenterne og tilfþje postbehandling 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="Generer JSON med vores nye nĂžglepar",
    )
    parser.add_argument(
        "--keys",
        default="keys.json",
        required=False,
        help="JSON med vores og deres nĂžgler",
    )
    parser.add_argument(
        "--bind",
        default="::1",
        help="Adresse at lytte pÄ",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=6666,
        help="Port at lytte pÄ",
    )
    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 og afmonter vores og deres nĂžgler {{{
    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 for algoritmen 34.10 er et tilfÊldigt tal. Den har en stÞrrelse pÄ 256 bit for 256-bit elliptiske kurver. PyGOST arbejder ikke med et sÊt bytes, men med store tal, derfor skal vores private nÞgle (urandom(32)) konverteres til et tal ved hjÊlp af gost3410.prv_unmarshal(). Den offentlige nÞgle beregnes deterministisk fra den private nÞgle ved hjÊlp af gost3410.public_key(). Den offentlige nÞgle 34.10 bestÄr af to store tal, som ogsÄ skal konverteres til en byte-sekvens for nem opbevaring og overfÞrsel ved hjÊlp af gost3410.pub_marshal().

    Efter lÊsning af JSON-filen skal de offentlige nÞgler konverteres tilbage ved hjÊlp af gost3410.pub_unmarshal(). Da vi vil modtage identifikatorer for samtalepartnere i form af hash fra den offentlige nÞgle, kan de beregnes pÄ forhÄnd og placeres i et ordforrÄd for hurtig sÞgning. Streebog-256 hash er gost34112012256.GOST34112012256(), som fuldt ud opfylder hashlib interface for hash-funktioner.

    Hvordan er initiatorkoroutinen Êndret? Alt, som i hÄndtryksordningen: generer cookie (128-bit er helt tilstrÊkkeligt), en efemÊr nÞglepar 34.10, som vil blive brugt til VKO-funktionen for nÞgleaftale.

     395 async def initiator(host, port):
     396     _id = repr((host, port))
     397     logging.info("%s: dialer", _id)
     398     reader, writer = await asyncio.open_connection(host, port)
     399     # Generer vores ephemerale offentlige nĂžgle og cookie, send Handshake 0 besked {{{
     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()
    

    • venter pĂ„ svar og dekoder den ankomne Msg besked;
    • sikrer os om, at vi har modtaget handshake1;
    • dekoder den ephemerale offentlige nĂžgle fra modparten og beregner sessionnĂžglen;
    • genererer de symmetriske nĂžgler, der er nĂždvendige for at behandle TBE delen af beskeden.

     423     logging.info("%s: fik %s besked", _id, msg.choice)
     424     if msg.choice != "handshake1":
     425         logging.warning("%s: uventet besked, afbryder", _id)
     426         writer.close()
     427         return
     428     # }}}
     429     msg_handshake1 = msg.value
     430     # Valider Handshake besked {{{
     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 tal (urandom(8)), som ogsĂ„ krĂŠver deserialisering fra byte-reprĂŠsentationen ved hjĂŠlp af gost3410_vko.ukm_unmarshal(). VKO funktionen for 34.10-2012 256-bit er gost3410_vko.kek_34102012256() (KEK — nĂžglekrypteringsnĂžgle).

    Den genererede sessionnÞgle er allerede en 256-bit byte pseudotilfÊldig sekvens. SÄ den kan straks bruges i HKDF funktionen. Da GOST34112012256 opfylder hashlib interface, kan den bruges direkte i Hkdf klassen. Salt (den fÞrste argument til Hkdf) angiver vi ikke, da den genererede nÞgle pÄ grund af de ephemerale nÞgler vil vÊre forskellig for hver session, og den har allerede tilstrÊkkelig entropi. kdf.expand() giver som standard allerede nÞgler af 256-bit lÊngde, der er nÞdvendige for Kuznetsov i det fÞlgende.

    DernĂŠst kontrolleres TBE og TBS dele af den modtagne besked:

    • beregner og kontrollerer MAC over den ankomne ciphertext;
    • dekripterer ciphertext;
    • dekoder TBE strukturen;
    • der tages et identifikationsnummer fra samtalepartneren og kontrolleres, om det er kendt for os;
    • beregner og kontrollerer MAC over dette identifikationsnummer;
    • signaturen over TBS-strukturen kontrolleres, som omfatter cookies fra begge parter og den offentlige, midlertidige nĂžgle fra den modsatte part. Signaturen verificeres med modpartens langsigtede 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 nĂŠvnt tidligere, beskriver 34.13-2015 forskellige driftstilstande for blokke af krypteringsalgoritmer fra 34.12-2015. Blandt dem er der tilstand for generering af MAC, beregning af MAC. I PyGOST er det gost3413.mac(). Denne tilstand krĂŠver overfĂžrsel af krypteringsfunktionen (der modtager og returnerer Ă©n blok data), blockstĂžrrelsen og selve dataene. Hvorfor kan man ikke hardcode blockstĂžrrelsen? 34.12-2015 beskriver ikke kun den 128-bit krypteringsalgoritme Kuznechik, men ogsĂ„ den 64-bit Magma — en let ĂŠndret version af GOST 28147-89, skabt i KGB og stadig med en af de hĂžjeste sikkerhedsspecialiteter.

    Grashopper initialiseres gost.3412.GOST3412Kuznechik(key) ved at kalde det og returnerer et objekt med .encrypt()/ .decrypt() metoder, der er egnet til at blive sendt til 34.13 funktioner. MAC beregnes som fÞlger: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). For at sammenligne den beregnede MAC med den modtagne kan man ikke bruge almindelig sammenligning (==) af byte-strenge, da denne operation giver tidslÊkningsproblemer, som generelt kan fÞre til fatale sÄrbarheder af typen BEAST angreb pÄ TLS. I Python findes der en sÊrlig hmac.compare_digest funktion til dette.

    En blokkeringskrypteringsfunktion kan kun kryptere Ă©n blok data. For et stĂžrre antal og ikke-kvart lĂŠngde er det nĂždvendigt at anvende en krypteringsmetode. I 34.13-2015 er fĂžlgende beskrevet: ECB, CTR, OFB, CBC, CFB. Hver har sine egne anvendelsesomrĂ„der og egenskaber. Til vores store beklagelse har vi stadig ikke standardiserede autentificerede krypteringsmetoder (sĂ„som CCM, OCB, GCM og lignende) — vi er tvunget til selv at tilfĂžje en MAC. Jeg vĂŠlger tĂŠllermodus (CTR): den krĂŠver ikke udfyldning til blokstĂžrrelse, kan paralleliseres, bruger kun krypteringsfunktionen, kan sikkert bruges til at kryptere et stort antal beskeder (i modsĂŠtning til CBC, hvor der relativt hurtigt opstĂ„r kollisioner).

    Som .mac(), tager .ctr() lignende data som input: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Det krÊver at initialiseringsvektoren er prÊcist halv stÞrrelse af krypteringsblokken. Hvis vores krypteringsnÞgle kun bruges til at kryptere én besked (selvom den bestÄr af flere blokke), kan en nul initialiseringsvektor sikkert angives. For kryptering af handshake beskeder bruges der hver gang en separat nÞgle.

    Verifikationen af signaturen gost3410.verify() er triviel: vi sender den elliptiske kurve inden for hvilken vi arbejder (den noterer vi blot i vores GOSTIM protokol), den offentlige nĂžgle til signaturen (husk at dette skal vĂŠre et tuppel af to store tal, ikke en byte-streng), 34.11-2012 hash og den modtagne signatur.

    Herefter forbereder vi og sender et handshake2 meddelelse i initiatoren, idet vi udfÞrer de samme handlinger som vi gjorde ved verifikationen, blot symmetrisk: signatur pÄ vores nÞgler i stedet for verifikation, osv


     456     # Forbered og send Handshake 2 besked {{{
     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 etableret: %s", _id, peer_name)
     

    NÄr sessionen er etableret, genereres transportnÞgler (en separat nÞgle til kryptering, en til autentifikation, for hver af parterne), og Kuznetsov initialiseres til dekryptering og kontrol af MAC:

     499     # KĂžr tekstbesked sender, initialiser transportdekoder {{{
     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     # Vent pÄ testbeskeder {{{
     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-korutinen krypterer nu beskederne, inden de sendes over TCP-forbindelsen. Hver besked har en monoton stigende nonce, som ogsÄ fungerer som initialiseringsvektor ved kryptering i tÊllermodus. Hver besked og blok af beskeder vil garanteret have forskellige tÊllevÊrdier.

    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
    

    Indkommende beskeder behandles af koroutinen msg_receiver, som stÄr for autentificering 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("uventet nonce vĂŠrdi")
        mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode())
        if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])):
            raise ValueError("ugyldig 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 kun beregnet til uddannelsesmÊssige formÄl (da det ikke er dÊkket af test, i hvert fald)! Kildekoden til programmet kan downloades her (Stribog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Ligesom alle mine projekter, sÄsom GoGOST, PyDERASN, NNCP, GoVPN, GOSTIM er helt gratis software, der distribueres under betingelserne for GPLv3+.

    Sergey Matveev, kryptonþrd, medlem af Stiftelsen for fri software, Python/Go-udvikler, hovedspecialist FGUП „NTC „Atlas“.

Kilde: habr.com

KĂžb pĂ„lidelig webhosting med DDoS-beskyttelse, VPS VDS-servere đŸ”„ KĂžb pĂ„lidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster