GOSTIM: P2P F2F E2EE IM za jeden večer s kryptografií GOST

BĂœt vĂœvojáƙem PyGOST knihovny (GOST kryptografickĂĄ primitiva v čistĂ©m Pythonu), často dostĂĄvĂĄm otĂĄzky, jak implementovat nejjednoduĆĄĆĄĂ­ bezpečnĂ© zasĂ­lĂĄnĂ­ zprĂĄv na koleni. Mnoho lidĂ­ povaĆŸuje aplikovanou kryptografii za docela jednoduchou a volĂĄnĂ­ .encrypt() na blokovou ĆĄifru bude stačit k jejĂ­mu bezpečnĂ©mu odeslĂĄnĂ­ pƙes komunikačnĂ­ kanĂĄl. JinĂ­ věƙí, ĆŸe aplikovanĂĄ kryptografie je Ășdělem několika mĂĄlo lidĂ­, a je pƙijatelnĂ©, aby bohatĂ© společnosti jako Telegram s olympijskĂœmi matematiky nelze realizovat zabezpečenĂœ protokol.

To vĆĄe mě pƙimělo napsat tento člĂĄnek, abych ukĂĄzal, ĆŸe implementace kryptografickĂœch protokolĆŻ a bezpečnĂœch IM nenĂ­ tak obtĂ­ĆŸnĂœ Ășkol. NevyplatĂ­ se vĆĄak vymĂœĆĄlet vlastnĂ­ protokoly pro ověƙovĂĄnĂ­ a dohody o klíčích.

GOSTIM: P2P F2F E2EE IM za jeden večer s kryptografií GOST
ČlĂĄnek napĂ­ĆĄe peer-to-peer, pƙítel-ka-pƙítel, end-to-end ĆĄifrovanĂ© instant messenger s SIGMA-I autentizačnĂ­ a klíčovĂœ protokol (na jehoĆŸ zĂĄkladě je implementovĂĄn IPsec IKE), pouĆŸĂ­vajĂ­cĂ­ vĂœhradně kryptografickĂ© algoritmy GOST knihovnu PyGOST a knihovnu kĂłdovĂĄnĂ­ zprĂĄv ASN.1 PyDERASN (o kterĂ©m uĆŸ jsem napsal dƙíve). Pƙedpoklad: musĂ­ bĂœt tak jednoduchĂœ, aby se dal napsat od nuly za jeden večer (nebo pracovnĂ­ den), jinak uĆŸ to nenĂ­ jednoduchĂœ program. Asi mĂĄ chyby, zbytečnĂ© komplikace, nedostatky, navĂ­c je to mĆŻj prvnĂ­ program vyuĆŸĂ­vajĂ­cĂ­ knihovnu asyncio.

IM design

Nejprve musĂ­me pochopit, jak bude naĆĄe IM vypadat. Pro zjednoduĆĄenĂ­ nechĆ„ je to sĂ­Ć„ peer-to-peer, bez jakĂ©hokoli objevovĂĄnĂ­ ĂșčastnĂ­kĆŻ. Osobně uvedeme, na kterou adresu: port se pƙipojit pro komunikaci s ĂșčastnĂ­kem rozhovoru.

ChĂĄpu, ĆŸe v současnĂ© době je pƙedpoklad dostupnosti pƙímĂ© komunikace mezi dvěma libovolnĂœmi počítači vĂœznamnĂœm omezenĂ­m pouĆŸitelnosti IM v praxi. Čím vĂ­ce vĆĄak vĂœvojáƙi implementujĂ­ nejrĆŻznějĆĄĂ­ berličky pro pƙechod pƙes NAT, tĂ­m dĂ©le zĆŻstaneme na internetu IPv4 s deprimujĂ­cĂ­ pravděpodobnostĂ­ komunikace mezi libovolnĂœmi počítači. Jak dlouho dokĂĄĆŸete tolerovat nedostatek IPv6 doma a v prĂĄci?

Budeme mĂ­t sĂ­Ć„ pƙátel: vĆĄichni moĆŸnĂ­ partneƙi musĂ­ bĂœt znĂĄmi pƙedem. Za prvĂ©, toto vĆĄe vĂœrazně zjednoduĆĄuje: pƙedstavili jsme se, naĆĄli nebo nenaĆĄli jmĂ©no/klíč, odpojili jsme se nebo pokračovali v prĂĄci, kdyĆŸ jsme znali partnera. Za druhĂ©, obecně je bezpečnĂœ a eliminuje mnoho ĂștokĆŻ.

RozhranĂ­ IM se bude blĂ­ĆŸit klasickĂœm ƙeĆĄenĂ­m neĆĄĆ„astnĂ© projekty, kterĂ© se mi velmi lĂ­bĂ­ pro jejich minimalismus a filozofii Unix-way. Program IM vytvoƙí adresáƙ se tƙemi sokety domĂ©ny Unix pro kaĆŸdĂ©ho partnera:

  • in—zprĂĄvy odeslanĂ© partnerovi jsou v něm zaznamenĂĄny;
  • out - zprĂĄvy pƙijatĂ© od partnera jsou z něj čteny;
  • stav - čtenĂ­m z něj zjistĂ­me, zda je ĂșčastnĂ­k prĂĄvě pƙipojen, adresu spojenĂ­/port.

Kromě toho se vytvoƙí conn soket zapsĂĄnĂ­m hostitelskĂ©ho portu, do kterĂ©ho iniciujeme spojenĂ­ se vzdĂĄlenĂœm ĂșčastnĂ­kem.

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

Tento pƙístup vĂĄm umoĆŸĆˆuje provĂĄdět nezĂĄvislĂ© implementace pƙenosu IM a uĆŸivatelskĂ©ho rozhranĂ­, protoĆŸe neexistuje ĆŸĂĄdnĂœ pƙítel, nemĆŻĆŸete se zavděčit vĆĄem. PouĆŸitĂ­m tmux Đž / nebo vĂ­cenĂĄsobnĂ©, mĆŻĆŸete zĂ­skat rozhranĂ­ pro vĂ­ce oken se zvĂœrazněnĂ­m syntaxe. A s pomocĂ­ rlwrap mĆŻĆŸete zĂ­skat vstupnĂ­ ƙádek zprĂĄv kompatibilnĂ­ s GNU Readline.

Ve skutečnosti, bezstarostnĂ© projekty pouĆŸĂ­vajĂ­ soubory FIFO. Osobně jsem nedokĂĄzal pochopit, jak se soubory v asyncio kompetitivně pracovat bez ručně psanĂ©ho pozadĂ­ z vyhrazenĂœch vlĂĄken (jazyk na takovĂ© věci pouĆŸĂ­vĂĄm uĆŸ dlouho Go). Proto jsem se rozhodl vystačit s unixovĂœmi domĂ©novĂœmi sockety. BohuĆŸel to znemoĆŸĆˆuje provĂ©st echo 2001:470:dead::babe 6666 > conn. Tento problĂ©m jsem vyƙeĆĄil pomocĂ­ socat: echo 2001:470:dead::babe 6666 | socat - UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.

PĆŻvodnĂ­ nezabezpečenĂœ protokol

TCP se pouĆŸĂ­vĂĄ jako transport: garantuje doručenĂ­ a jeho objednĂĄvku. UDP nezaručuje ani jedno (coĆŸ by bylo uĆŸitečnĂ© pƙi pouĆŸitĂ­ kryptografie), ale podporu SCTP Python nevychĂĄzĂ­ z krabice.

BohuĆŸel v TCP neexistuje ĆŸĂĄdnĂœ koncept zprĂĄvy, pouze proud bajtĆŻ. Proto je potƙeba vymyslet formĂĄt zprĂĄv, aby je bylo moĆŸnĂ© sdĂ­let mezi sebou v tomto vlĂĄknu. MĆŻĆŸeme se dohodnout na pouĆŸitĂ­ znaku pro odƙádkovĂĄnĂ­. Pro začátek je to v poƙádku, ale jakmile začneme ĆĄifrovat naĆĄe zprĂĄvy, mĆŻĆŸe se tento znak objevit kdekoli v ĆĄifrovanĂ©m textu. V sĂ­tĂ­ch jsou proto oblĂ­benĂ© protokoly, kterĂ© nejprve odesĂ­lajĂ­ dĂ©lku zprĂĄvy v bajtech. Napƙíklad Python mĂĄ z krabice xdrlib, kterĂœ umoĆŸĆˆuje pracovat s podobnĂœm formĂĄtem XDR.

Se čtenĂ­m TCP nebudeme pracovat sprĂĄvně a efektivně – zjednoduĆĄĂ­me kĂłd. Čteme data ze zĂĄsuvky v nekonečnĂ© smyčce, dokud nedekĂłdujeme kompletnĂ­ zprĂĄvu. JSON s XML lze takĂ© pouĆŸĂ­t jako formĂĄt pro tento pƙístup. Ale kdyĆŸ se pƙidĂĄ kryptografie, data budou muset bĂœt podepsĂĄna a ověƙena - a to bude vyĆŸadovat bajt po bajtu identickou reprezentaci objektĆŻ, coĆŸ JSON/XML neposkytuje (vĂœsledky vĂœpisĆŻ se mohou liĆĄit).

XDR je pro tento Ășkol vhodnĂœ, nicmĂ©ně volĂ­m ASN.1 s kĂłdovĂĄnĂ­m DER a PyDERASN knihovny, protoĆŸe budeme mĂ­t po ruce pƙedměty na vysokĂ© Ășrovni, se kterĂœmi je často pƙíjemnějĆĄĂ­ a pohodlnějĆĄĂ­ pracovat. Na rozdĂ­l od bez schĂ©matu bencode, MessagePack nebo CBOR, ASN.1 automaticky porovnĂĄ data s pevně zakĂłdovanĂœm schĂ©matem.

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

PƙijatĂĄ zprĂĄva bude Msg: buď textovĂĄ MsgText (zatĂ­m s jednĂ­m textovĂœm polem) nebo MsgHandshake handshake zprĂĄva (kterĂĄ obsahuje jmĂ©no partnera). Teď to vypadĂĄ pƙekomplikovaně, ale tohle je zĂĄklad do budoucna.

     ┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┬──┘ └──┘┘──s IdA) │ │───────── - │ │ MsgText() │ │──── MsgText() │ │ │

IM bez kryptografie

Jak jsem jiĆŸ ƙekl, pro vĆĄechny operace soketĆŻ bude pouĆŸita knihovna asyncio. Pojďme oznĂĄmit, co očekĂĄvĂĄme pƙi spuĆĄtěnĂ­:

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

Nastavte si vlastnĂ­ jmĂ©no (--naĆĄe-jmĂ©no Alice). VĆĄichni očekĂĄvanĂ­ ĂșčastnĂ­ci rozhovoru jsou uvedeni odděleni čárkami (—jejich jmĂ©na bob, eve). Pro kaĆŸdĂ©ho z ĂșčastnĂ­kĆŻ dialogu je vytvoƙen adresáƙ s unixovĂœmi sokety a takĂ© korutina pro kaĆŸdĂœ vstupnĂ­ a vĂœstupnĂ­ stav:

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

ZprĂĄvy pƙichĂĄzejĂ­cĂ­ od uĆŸivatele ze vstupnĂ­ho soketu jsou odesĂ­lĂĄny do fronty IN_QUEUES:

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

ZprĂĄvy pƙichĂĄzejĂ­cĂ­ od ĂșčastnĂ­kĆŻ rozhovoru jsou odesĂ­lĂĄny do front OUT_QUEUES, ze kterĂœch jsou data zapisovĂĄna do vĂœstupnĂ­ho soketu:

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

Pƙi čtenĂ­ ze stavovĂ©ho soketu program hledĂĄ adresu ĂșčastnĂ­ka ve slovnĂ­ku PEER_ALIVE. Pokud jeĆĄtě nenĂ­ spojenĂ­ s ĂșčastnĂ­kem komunikace, zapĂ­ĆĄe se prĂĄzdnĂœ ƙádek.

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

Pƙi zápisu adresy do conn soketu se spustí funkce „iniciátor“ pƙipojení:

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

PodĂ­vejme se na iniciĂĄtora. Nejprve zjevně otevƙe pƙipojenĂ­ k určenĂ©mu hostiteli/portu a odeĆĄle zprĂĄvu handshake s jeho nĂĄzvem:

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

PotĂ© čekĂĄ na odpověď od vzdĂĄlenĂ© strany. PokusĂ­ se dekĂłdovat pƙíchozĂ­ odpověď pomocĂ­ schĂ©matu Msg ASN.1. PƙedpoklĂĄdĂĄme, ĆŸe celĂĄ zprĂĄva bude odeslĂĄna v jednom TCP segmentu a pƙi volĂĄnĂ­ .read() ji pƙijmeme atomicky. Zkontrolujeme, ĆŸe jsme obdrĆŸeli zprĂĄvu o podĂĄnĂ­ ruky.

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

Zkontrolujeme, zda je nĂĄm znĂĄmĂ© pƙijatĂ© jmĂ©no partnera. Pokud ne, pƙeruĆĄĂ­me spojenĂ­. Zkontrolujeme, zda jsme s nĂ­m jiĆŸ navĂĄzali spojenĂ­ (ĂșčastnĂ­k opět dal pƙíkaz ke spojenĂ­ s nĂĄmi) a uzavƙeme jej. Fronta IN_QUEUES obsahuje pythonovskĂ© ƙetězce s textem zprĂĄvy, ale mĂĄ speciĂĄlnĂ­ hodnotu None, kterĂĄ signalizuje korutině msg_sender, aby pƙestala pracovat, takĆŸe zapomene na svĆŻj zapisovač spojenĂœ se starĆĄĂ­m pƙipojenĂ­m TCP.

 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 pƙijĂ­mĂĄ odchozĂ­ zprĂĄvy (ve frontě z in socketu), serializuje je do zprĂĄvy MsgText a odesĂ­lĂĄ je pƙes TCP spojenĂ­. MĆŻĆŸe se kdykoli zlomit - jasně to zachytĂ­me.

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

Na konci vstoupĂ­ iniciĂĄtor do nekonečnĂ© smyčky čtenĂ­ zprĂĄv ze zĂĄsuvky. Zkontroluje, zda se jednĂĄ o textovĂ© zprĂĄvy, a zaƙadĂ­ je do fronty OUT_QUEUES, ze kterĂ© budou odeslĂĄny do vĂœstupnĂ­ho soketu odpovĂ­dajĂ­cĂ­ho partnera. Proč prostě nemĆŻĆŸete udělat .read() a dekĂłdovat zprĂĄvu? ProtoĆŸe je moĆŸnĂ©, ĆŸe několik zprĂĄv od uĆŸivatele bude agregovĂĄno ve vyrovnĂĄvacĂ­ paměti operačnĂ­ho systĂ©mu a odeslĂĄno v jednom segmentu TCP. MĆŻĆŸeme dekĂłdovat prvnĂ­ a část nĂĄsledujĂ­cĂ­ho pak mĆŻĆŸe zĆŻstat ve vyrovnĂĄvacĂ­ paměti. V pƙípadě jakĂ©koli abnormĂĄlnĂ­ situace uzavƙeme TCP spojenĂ­ a zastavĂ­me korutinu msg_sender (zaslĂĄnĂ­m None do fronty OUT_QUEUES).

 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)

VraĆ„me se k hlavnĂ­mu kĂłdu. Po vytvoƙenĂ­ vĆĄech korutin v době spuĆĄtěnĂ­ programu spustĂ­me TCP server. Pro kaĆŸdĂ© navĂĄzanĂ© spojenĂ­ vytvoƙí korutinu respondĂ©ru.

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

respondĂ©r je podobnĂœ iniciĂĄtoru a zrcadlĂ­ vĆĄechny stejnĂ© akce, ale nekonečnĂĄ smyčka čtenĂ­ zprĂĄv se pro jednoduchost spustĂ­ okamĆŸitě. V současnĂ© době protokol handshake odesĂ­lĂĄ jednu zprĂĄvu z kaĆŸdĂ© strany, ale v budoucnu budou dvě od iniciĂĄtora pƙipojenĂ­, po kterĂœch lze okamĆŸitě odesĂ­lat textovĂ© zprĂĄvy.

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

ZabezpečenĂœ protokol

Je čas zabezpečit naơi komunikaci. Co myslíme bezpečností a co chceme:

  • dĆŻvěrnost pƙenĂĄĆĄenĂœch zprĂĄv;
  • autentičnost a integrita pƙenĂĄĆĄenĂœch zprĂĄv – jejich změny musĂ­ bĂœt detekovĂĄny;
  • ochrana proti replay ĂștokĆŻm - musĂ­ bĂœt detekovĂĄna skutečnost chybějĂ­cĂ­ch nebo opakovanĂœch zprĂĄv (a my se rozhodneme ukončit spojenĂ­);
  • identifikace a autentizace ĂșčastnĂ­kĆŻ komunikace pomocĂ­ pƙedem zadanĂœch veƙejnĂœch klíčƯ – jiĆŸ dƙíve jsme se rozhodli, ĆŸe vytvoƙíme sĂ­Ć„ pƙátel. Teprve po autentizaci pochopĂ­me, s kĂœm komunikujeme;
  • dostupnost dokonalĂ© dopƙednĂ© utajenĂ­ vlastnosti (PFS) – kompromitace naĆĄeho dlouhodobĂ©ho podpisovĂ©ho klíče by neměla vĂ©st ke schopnosti číst veĆĄkerou pƙedchozĂ­ korespondenci. NahrĂĄvĂĄnĂ­ zachycenĂ©ho provozu se stĂĄvĂĄ zbytečnĂœm;
  • platnost/platnost zprĂĄv (transport a handshake) pouze v rĂĄmci jednĂ© TCP relace. VklĂĄdĂĄnĂ­ sprĂĄvně podepsanĂœch/ověƙenĂœch zprĂĄv z jinĂ© relace (ani se stejnĂœm partnerem) by nemělo bĂœt moĆŸnĂ©;
  • pasivnĂ­ pozorovatel by neměl vidět ani uĆŸivatelskĂ© identifikĂĄtory, pƙenĂĄĆĄenĂ© dlouhodobĂ© veƙejnĂ© klíče ani hashe z nich. JistĂĄ anonymita od pasivnĂ­ho pozorovatele.

Pƙekvapivě tĂ©měƙ kaĆŸdĂœ chce mĂ­t toto minimum v jakĂ©mkoli protokolu handshake a jen velmi mĂĄlo z vĂœĆĄe uvedenĂ©ho je nakonec splněno pro „domĂĄcí“ protokoly. Teď nebudeme vymĂœĆĄlet nic novĂ©ho. Určitě bych doporučil pouĆŸĂ­vat Noise framework pro stavebnĂ­ protokoly, ale zvolme něco jednoduĆĄĆĄĂ­ho.

Dva nejoblíbenějơí protokoly jsou:

  • TLS - velmi sloĆŸitĂœ protokol s dlouhou historiĂ­ chyb, zĂĄsekĆŻ, zranitelnostĂ­, ĆĄpatnĂ©ho myĆĄlenĂ­, sloĆŸitosti a nedostatkĆŻ (to vĆĄak nemĂĄ mnoho společnĂ©ho s TLS 1.3). Ale neuvaĆŸujeme o tom, protoĆŸe je to pƙíliĆĄ komplikovanĂ©.
  • IPsec с IKE — nemajĂ­ vĂĄĆŸnĂ© kryptografickĂ© problĂ©my, i kdyĆŸ takĂ© nejsou jednoduchĂ©. Pokud čtete o IKEv1 a IKEv2, tak jejich zdroj je STS, ISO/IEC IS 9798-3 a protokoly SIGMA (SIGN-and-MAc) – dostatečně jednoduchĂ© na implementaci za jeden večer.

Co je dobrĂ©ho na SIGMA, jako nejnovějĆĄĂ­ člĂĄnek ve vĂœvoji STS/ISO protokolĆŻ? Splƈuje vĆĄechny naĆĄe poĆŸadavky (včetně „skrytí“ identifikĂĄtorĆŻ partnera) a nemĂĄ ĆŸĂĄdnĂ© znĂĄmĂ© kryptografickĂ© problĂ©my. Je minimalistickĂĄ – odstraněnĂ­ alespoƈ jednoho prvku ze zprĂĄvy protokolu povede k jejĂ­ nejistotě.

Pojďme od nejjednoduĆĄĆĄĂ­ho domĂĄcĂ­ho protokolu k SIGMA. NejzĂĄkladnějĆĄĂ­ operace, kterĂĄ nĂĄs zajĂ­mĂĄ, je klíčovĂĄ dohoda: Funkce, kterĂĄ dĂĄvĂĄ oběma ĂșčastnĂ­kĆŻm stejnou hodnotu, kterou lze pouĆŸĂ­t jako symetrickĂœ klíč. AniĆŸ bychom zachĂĄzeli do podrobnostĂ­: kaĆŸdĂĄ ze stran vygeneruje pomĂ­jivĂœ (pouĆŸitĂœ pouze v rĂĄmci jednĂ© relace) pĂĄr klíčƯ (veƙejnĂœ a soukromĂœ klíč), vyměnĂ­ si veƙejnĂ© klíče, zavolĂĄ funkci dohody, na jejĂ­mĆŸ vstupu pƙedĂĄ svĆŻj soukromĂœ klíč a veƙejnĂœ klíč. klíč partnera.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └,─┘ ┘── I┘ ┘── I┘ ┘─ I─ Ad │ ╔══════════ ══════════╗ │───────────────>─ ──>│ ╚DDHA, Hospoda A ════════ ═══════════╝ │ IdB, PubB │ ╔══════════════╕════╕═══════ │<───────── ──────│ ║PrvB, PubB = DHgen()║ │ │ ╚════════════════‐‐ ───┐ ╔════ ═══╧════════════╗ │ ║Klíč = DH(PrvA, PubB)║ <──────┕╕╀ ═══════ ════╝ │ │ │ │

Kdokoli mĆŻĆŸe skočit doprostƙed a nahradit veƙejnĂ© klíče svĂœmi vlastnĂ­mi - v tomto protokolu neexistuje ĆŸĂĄdnĂĄ autentizace ĂșčastnĂ­kĆŻ rozhovoru. Pƙidejme podpis s dlouhověkĂœmi klíči.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┬└,─┘ └,─┘ ┘──┘ ┘── A znak(SignPrvA, (PubA)) │ ╔═ Podepiơte se = load()║ │ │ ║PrvA, PubA = DHgen() ║ │ │ ╚═══════ ═══════ ══════════╕╂══╕╂ ═══╂ ═══════════╕╂══╕╂ , podepsat (SignPrvB, (PubB)) - ─────────── ───────────── ──│ ║SignPrvB, SignPubB = load( )║ │ │ ║PrvB, PubB = DHgen() ║─ ════════ – ═════╗ │ │ ║ověƙit( SignPubB, ...)║ │ <───┘ ║Klíč = DH(Pr vA, PubB) ║ │ │ ╚═══════╕════╕════╕══════ ═╝ │ │ │

TakovĂœ podpis nebude fungovat, protoĆŸe nenĂ­ vĂĄzĂĄn na konkrĂ©tnĂ­ relaci. TakovĂ© zprĂĄvy jsou takĂ© „vhodnĂ©â€œ pro sezenĂ­ s ostatnĂ­mi ĂșčastnĂ­ky. CelĂœ kontext se musĂ­ pƙihlĂĄsit. To nĂĄs nutĂ­ pƙidat takĂ© dalĆĄĂ­ zprĂĄvu od A.

Kromě toho je dĆŻleĆŸitĂ© pƙidat svĆŻj vlastnĂ­ identifikĂĄtor pod podpis, protoĆŸe jinak mĆŻĆŸeme nahradit IdXXX a znovu podepsat zprĂĄvu klíčem jinĂ©ho znĂĄmĂ©ho partnera. Aby se zabrĂĄnilo reflexnĂ­ Ăștoky, je nutnĂ©, aby prvky pod podpisem byly na jasně definovanĂœch mĂ­stech podle jejich vĂœznamu: pokud A značí (PubA, PubB), musĂ­ se podepisovat B (PubB, PubA). To takĂ© vypovĂ­dĂĄ o dĆŻleĆŸitosti vĂœběru struktury a formĂĄtu serializovanĂœch dat. Napƙíklad sady v kĂłdovĂĄnĂ­ ASN.1 DER jsou seƙazeny: SET OF(PubA, PubB) bude totoĆŸnĂ© s SET OF(PubB, PubA).

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └,─┘ ┘── I┘ ┘── I┘ ┘─ I─ Ad │ ╔══════════ ═════════════════╗ │───────────└└└———└—— — ────────── ─────────────>│ ║SignPrvA, SignPubA = load()║ │ │ ║PrvA, PubA = DHgen() ──│ ┕║─│ ═══════ ═══════════════╝ │IdB, PubB, sign(SignPrvB, (IdB, PubA, PubB)) ││␕╕╕═␕╕╕═␕╕╕══╕╕═══╕ ═════ ════════════╗ │<─────────────────└———└—— ────────── ─────────│ ║SignPrvB, SignPubB = load()║ │ │ ║PrvB, PubB = DHgen() ║ │ │ │ ═╕╕═ ═╕╕══║╕═╕-║ ═══════ ══════════╝ │ znak (SignPrvA, (IdA, PubB, PubA)) │ ╔═════╕═══╕════╕════╕══ ═══╗ │─ - ───>│ ║ověƙit (SignPubB, ...) ║ │ │ ║klíč = dh (prva, PUBB) ║ │ │ │

StĂĄle jsme vĆĄak „neprokĂĄzali“, ĆŸe jsme pro tuto relaci vygenerovali stejnĂœ sdĂ­lenĂœ klíč. V zĂĄsadě se bez tohoto kroku obejdeme - hned prvnĂ­ dopravnĂ­ spojenĂ­ bude neplatnĂœ, ale chceme, abychom po dokončenĂ­ handshaku měli jistotu, ĆŸe je opravdu vĆĄe domluveno. V tuto chvĂ­li mĂĄme k dispozici protokol ISO/IEC IS 9798-3.

Mohli bychom podepsat samotnĂœ vygenerovanĂœ klíč. To je nebezpečnĂ©, protoĆŸe je moĆŸnĂ©, ĆŸe v pouĆŸitĂ©m podpisovĂ©m algoritmu mĆŻĆŸe dochĂĄzet k ĂșnikĆŻm (sice bitĆŻ na podpis, ale stĂĄle dochĂĄzĂ­ k ĂșnikĆŻm). Je moĆŸnĂ© podepsat hash derivačnĂ­ho klíče, ale Ășnik dokonce i hashe odvozenĂ©ho klíče mĆŻĆŸe bĂœt cennĂœ pƙi Ăștoku hrubou silou na derivačnĂ­ funkci. SIGMA pouĆŸĂ­vĂĄ funkci MAC, kterĂĄ ověƙuje ID odesĂ­latele.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └,─┘ ┘── I┘ ┘── I┘ ┘─ I─ Ad │ ╔══════════ ═════════════════╗ │───────────└└└———└—— — ────────── ──────────────────>│ ║SignPrvA, SignPubA = load()║ │ │ =║DHA│ ║DHA, PubA ═══════ ════════════════════╝ │IdB, PubB, podepsat (SignPrvB, (PubA, PubB)╕ₕₕ ═══ │<───────────────── ─────────────└──└─ —└─ — ────────── ─│ ║SignPrvB, SignPubB = načíst()║ │ │ ║PrvB, PubB = DHgen() ║ │ │ ╚════ ═╕═══╕═══════ ═══════ ══╝ │ │ ╔════════════ ═════════, Publikum A) │ (Ib(Pub)A)Prv. ║Klíč = DH( PrvA, PubB) ║ │───────────────────── ─────—└─——└─— ────────── ─────>│ ║ověƙit (klíč, IdB) ║ │ │ ║ověƙit (SignPubB, ...)║ │ │ ╚══╕════╕═␕␕═╕═␕════ ═════ ═╝ │ │

Jako optimalizaci mohou někteƙí chtĂ­t znovu pouĆŸĂ­t svĂ© pomĂ­jivĂ© klíče (coĆŸ je samozƙejmě pro PFS neĆĄĆ„astnĂ©). Napƙíklad jsme vygenerovali pĂĄr klíčƯ, pokusili se pƙipojit, ale TCP nebyl dostupnĂœ nebo byl někde uprostƙed protokolu pƙeruĆĄen. Je ĆĄkoda plĂœtvat plĂœtvĂĄnĂ­m entropiĂ­ a zdroji procesoru na novĂœ pĂĄr. Proto zavedeme tzv. cookie – pseudonĂĄhodnou hodnotu, kterĂĄ bude chrĂĄnit pƙed pƙípadnĂœmi Ăștoky nĂĄhodnĂ©ho pƙehrĂĄnĂ­ pƙi opětovnĂ©m pouĆŸitĂ­ efemĂ©rnĂ­ch veƙejnĂœch klíčƯ. Vzhledem k vazbě mezi cookie a efemĂ©rnĂ­m veƙejnĂœm klíčem mĆŻĆŸe bĂœt veƙejnĂœ klíč protějĆĄĂ­ strany z podpisu odstraněn jako nepotƙebnĂœ.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┬└,─┘ └,─┘ └,── A┘ ┘── A CookieA │ ╔════════ ═══════════════════╗ │──────└└└———└—— — ────────── - ─>│ ║SignPrvA, SignPubA = load( )║ │ │ ║PrvA, PubA = DHgen() ║ │ │ ╚══════════════════════════════════════════════════ ══╝ │IdB, PubB, CookieB , sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) │ ╔═══════════════════╕════╕════╕ ╗ │< - ────────── ────────────────────│ ║SignPrvB, SignPubB = načíst()║ │ │ ┕, ║ │ │ DH, ║ │ Prv BDH, │B) B) ╚══════ - ═══════╗ │ podepsat( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA) │ ║Klíč = DH(PrvA, PubB) ║ │───────────└——└└—— ─ - ───────>│ ║ ověƙit (Klíč, IdB) ║ │ │ ║ověƙit (SignPubB, ...)║ │ │ ╚══════════╕═══╕════╕═ₕ═╕═ₕ═╕═ │

Nakonec chceme zĂ­skat soukromĂ­ naĆĄich konverzačnĂ­ch partnerĆŻ od pasivnĂ­ho pozorovatele. Za tĂ­mto Ășčelem SIGMA navrhuje nejprve vyměƈovat pomĂ­jivĂ© klíče a vyvinout společnĂœ klíč, na kterĂ©m se budou ĆĄifrovat autentizačnĂ­ a identifikačnĂ­ zprĂĄvy. SIGMA popisuje dvě moĆŸnosti:

  • SIGMA-I - chrĂĄnĂ­ iniciĂĄtora pƙed aktivnĂ­mi Ăștoky, respondĂ©ra pƙed pasivnĂ­mi: iniciĂĄtor autentizuje respondĂ©ra a pokud něco neodpovĂ­dĂĄ, neprozradĂ­ svou identifikaci. ObĆŸalovanĂœ pƙedĂĄ svou identifikaci, pokud je s nĂ­m zahĂĄjen aktivnĂ­ protokol. PasivnĂ­ pozorovatel se nic nedozvĂ­;
    SIGMA-R - chrĂĄnĂ­ respondĂ©ra pƙed aktivnĂ­mi Ăștoky, iniciĂĄtora pƙed pasivnĂ­mi. VĆĄechno je pƙesně naopak, ale v tomto protokolu jsou jiĆŸ pƙenĂĄĆĄeny čtyƙi zprĂĄvy o podĂĄnĂ­ ruky.

    Vybrali jsme SIGMA-I, protoĆŸe se vĂ­ce podobĂĄ tomu, co očekĂĄvĂĄme od znĂĄmĂœch věcĂ­ typu klient-server: klienta rozpoznĂĄ pouze ověƙenĂœ server a server uĆŸ znĂĄ kaĆŸdĂœ. NavĂ­c se snĂĄze implementuje dĂ­ky menĆĄĂ­mu počtu zprĂĄv typu handshake. VĆĄe, co do protokolu pƙidĂĄvĂĄme, je zaĆĄifrovat část zprĂĄvy a pƙenĂ©st identifikĂĄtor A do zaĆĄifrovanĂ© části poslednĂ­ zprĂĄvy:

    PubA, CookieA │ ╔══════════ ═════════════――└―――└―――└――― ────────── ───── ────────── ─────────────—————— ─────────── – ═══════ ═════════ ════╝ │ PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB)╕═␐)╕)═)╕) ═════════ ═══════════════ ═══════╗ │<───—└———└— — ────────── ───── ────────── ║SignP rvB, SignPubB = load()║ │ │ ═ PrvB, PubB = DHgen() ║ DHgen() ║ ═══════ - ══╗ │ Enc((IdA, sign( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA))) │ ║Klíč = DH(PrvA, PubB) ║ │──────────└─—─└─—— - ─────────── ──────>│ ║ověƙit (klíč, IdB) ║ │ │ ║ověƙit ( SignPubB, ...)║ │ │ ╚═╕═══╕════════════ ═════ ══╝ │ │
    
    • GOST R se pouĆŸĂ­vĂĄ pro podpis 34.10-2012 algoritmus s 256bitovĂœmi klíči.
    • Pro vygenerovĂĄnĂ­ veƙejnĂ©ho klíče se pouĆŸĂ­vĂĄ 34.10 VKO.
    • CMAC se pouĆŸĂ­vĂĄ jako MAC. Technicky se jednĂĄ o speciĂĄlnĂ­ reĆŸim provozu blokovĂ© ĆĄifry, popsanĂœ v GOST R 34.13-2015. Jako ĆĄifrovacĂ­ funkce pro tento reĆŸim − Kobylka (34.12-2015).
    • Hash jeho veƙejnĂ©ho klíče se pouĆŸĂ­vĂĄ jako identifikĂĄtor partnera. PouĆŸĂ­vĂĄ se jako hash Stribog-256 (34.11. 2012. 256 XNUMX bitĆŻ).

    Po podĂĄnĂ­ ruky se dohodneme na sdĂ­lenĂ©m klíči. MĆŻĆŸeme jej pouĆŸĂ­t pro autentizovanĂ© ĆĄifrovĂĄnĂ­ transportnĂ­ch zprĂĄv. Tato část je velmi jednoduchĂĄ a tÄ›ĆŸko udělĂĄme chybu: inkrementujeme počítadlo zprĂĄv, zaĆĄifrujeme zprĂĄvu, ověƙíme (MAC) počítadlo a ĆĄifrovanĂœ text, odeĆĄleme. Pƙi pƙíjmu zprĂĄvy zkontrolujeme, ĆŸe počítadlo mĂĄ očekĂĄvanou hodnotu, ĆĄifrovanĂœ text ověƙíme počítadlem a deĆĄifrujeme. JakĂœ klíč bych měl pouĆŸĂ­t k ĆĄifrovĂĄnĂ­ zprĂĄv handshake, pƙenosu zprĂĄv a jak je ověƙit? PouĆŸĂ­vĂĄnĂ­ jednoho klíče pro vĆĄechny tyto Ășkoly je nebezpečnĂ© a nerozumnĂ©. Klíče je nutnĂ© generovat pomocĂ­ specializovanĂœch funkcĂ­ KDF (funkce odvozenĂ­ klíče). Opět si netƙepeme vlasy a něco vymyslĂ­me: HKDF je dlouho znĂĄmĂĄ, dobƙe prozkoumanĂĄ a nemĂĄ ĆŸĂĄdnĂ© znĂĄmĂ© problĂ©my. BohuĆŸel nativnĂ­ knihovna Pythonu tuto funkci nemĂĄ, takĆŸe pouĆŸĂ­vĂĄme hkdf IgelitovĂĄ taĆĄka. HKDF interně pouĆŸĂ­vĂĄ HMAC, kterĂœ zase pouĆŸĂ­vĂĄ hashovacĂ­ funkci. Pƙíklad implementace v Pythonu na strĂĄnce Wikipedie zabere jen pĂĄr ƙádkĆŻ kĂłdu. Stejně jako v pƙípadě 34.10 pouĆŸijeme jako hashovacĂ­ funkci Stribog-2012. VĂœstup naĆĄĂ­ funkce shody klíčƯ se bude nazĂœvat klíč relace, ze kterĂ©ho se vygenerujĂ­ chybějĂ­cĂ­ symetrickĂ©:

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

    Struktury/schéma

    PodĂ­vejme se, jakĂ© struktury ASN.1 nynĂ­ mĂĄme pro pƙenos vĆĄech těchto dat:

    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 je to, co bude podepsĂĄno. HandshakeTBE - co bude zaĆĄifrovĂĄno. Upozorƈuji na pole ukm v MsgHandshake1. 34.10 VKO, pro jeĆĄtě větĆĄĂ­ randomizaci generovanĂœch klíčƯ, obsahuje parametr UKM (uĆŸivatelskĂœ klíčovacĂ­ materiĂĄl) - jen dalĆĄĂ­ entropie.

    Pƙidání kryptografie do kódu

    UvaĆŸujme pouze změny provedenĂ© v pĆŻvodnĂ­m kĂłdu, protoĆŸe framework zĆŻstal stejnĂœ (ve skutečnosti byla nejprve napsĂĄna konečnĂĄ implementace a potĂ© z nĂ­ byla vyƙíznuta veĆĄkerĂĄ kryptografie).

    Vzhledem k tomu, ĆŸe autentizace a identifikace ĂșčastnĂ­kĆŻ budou probĂ­hat pomocĂ­ veƙejnĂœch klíčƯ, je nynĂ­ tƙeba je někde uklĂĄdat na dlouhou dobu. Pro jednoduchost pouĆŸĂ­vĂĄme JSON takto:

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

    nĂĄĆĄ - nĂĄĆĄ pĂĄr klíčƯ, hexadecimĂĄlnĂ­ soukromĂœ a veƙejnĂœ klíč. jejich — jmĂ©na ĂșčastnĂ­kĆŻ rozhovoru a jejich veƙejnĂ© klíče. Pojďme změnit argumenty pƙíkazovĂ©ho ƙádku a pƙidat nĂĄslednĂ© zpracovĂĄnĂ­ dat JSON:

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

    SoukromĂœ klíč algoritmu 34.10 je nĂĄhodnĂ© číslo. 256bitovĂĄ velikost pro 256bitovĂ© eliptickĂ© kƙivky. PyGOST nepracuje se sadou bajtĆŻ, ale s vysokĂĄ čísla, takĆŸe nĂĄĆĄ soukromĂœ klíč (urandom(32)) je tƙeba pƙevĂ©st na číslo pomocĂ­ gost3410.prv_unmarshal(). VeƙejnĂœ klíč je určen deterministicky ze soukromĂ©ho klíče pomocĂ­ gost3410.public_key(). VeƙejnĂœ klíč 34.10 jsou dvě velkĂĄ čísla, kterĂĄ je takĂ© potƙeba pƙevĂ©st na bajtovou sekvenci pro snadnĂ© uklĂĄdĂĄnĂ­ a pƙenos pomocĂ­ gost3410.pub_marshal().

    Po pƙečtenĂ­ souboru JSON je tƙeba veƙejnĂ© klíče pƙevĂ©st zpět pomocĂ­ gost3410.pub_unmarshal(). Vzhledem k tomu, ĆŸe z veƙejnĂ©ho klíče obdrĆŸĂ­me identifikĂĄtory ĂșčastnĂ­kĆŻ ve formě hashe, lze je okamĆŸitě pƙedem vypočítat a umĂ­stit do slovnĂ­ku pro rychlĂ© vyhledĂĄvĂĄnĂ­. Hash Stribog-256 je gost34112012256.GOST34112012256(), kterĂœ plně vyhovuje rozhranĂ­ hashlib hashovacĂ­ch funkcĂ­.

    Jak se změnil korutin iniciĂĄtoru? VĆĄe je podle schĂ©matu handshake: vygenerujeme soubor cookie (128 bitĆŻ je dostatek), pomĂ­jivĂœ pĂĄr klíčƯ 34.10, kterĂœ bude pouĆŸit pro funkci dohody klíčƯ VKO.

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

    • čekĂĄme na odpověď a dekĂłdujeme pƙíchozĂ­ zprĂĄvu Msg;
    • ujistěte se, ĆŸe dostanete handshake1;
    • dekĂłdovat pomĂ­jivĂœ veƙejnĂœ klíč protějĆĄĂ­ strany a vypočítat klíč relace;
    • Generujeme symetrickĂ© klíče potƙebnĂ© pro zpracovĂĄnĂ­ TBE části zprĂĄvy.

     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 je 64bitovĂ© číslo (urandom(8)), kterĂ© takĂ© vyĆŸaduje deserializaci z jeho bytovĂ© reprezentace pomocĂ­ gost3410_vko.ukm_unmarshal(). Funkce VKO pro 34.10 2012-bit je gost256_vko.kek_3410() (KEK - ĆĄifrovacĂ­ klíč).

    VygenerovanĂœ klíč relace je jiĆŸ 256bitovĂĄ pseudonĂĄhodnĂĄ sekvence bajtĆŻ. Proto mĆŻĆŸe bĂœt okamĆŸitě pouĆŸit ve funkcĂ­ch HKDF. ProtoĆŸe GOST34112012256 vyhovuje rozhranĂ­ hashlib, lze jej okamĆŸitě pouĆŸĂ­t ve tƙídě Hkdf. Salt (prvnĂ­ argument Hkdf) neuvĂĄdĂ­me, protoĆŸe vygenerovanĂœ klíč bude kvĆŻli pomĂ­jivosti zĂșčastněnĂœch pĂĄrĆŻ klíčƯ pro kaĆŸdou relaci jinĂœ a jiĆŸ obsahuje dostatek entropie. kdf.expand() ve vĂœchozĂ­m nastavenĂ­ jiĆŸ vytváƙí 256bitovĂ© klíče poĆŸadovanĂ© pro Grasshopper později.

    Dále se zkontrolují části TBE a TBS pƙíchozí zprávy:

    • MAC pƙes pƙíchozĂ­ ĆĄifrovĂœ text se vypočítĂĄ a zkontroluje;
    • ĆĄifrovanĂœ text je deĆĄifrovĂĄn;
    • Struktura TBE je dekĂłdovĂĄna;
    • odebere se z něj identifikĂĄtor partnera a zkontroluje se, zda je nĂĄm vĆŻbec znĂĄm;
    • MAC pƙes tento identifikĂĄtor se vypočítĂĄ a zkontroluje;
    • je ověƙen podpis nad strukturou TBS, kterĂĄ zahrnuje cookie obou stran a veƙejnĂœ efemĂ©rnĂ­ klíč protějĆĄĂ­ strany. Podpis je ověƙen trvanlivĂœm podpisovĂœm klíčem ĂșčastnĂ­ka jednĂĄnĂ­.

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

    Jak jsem psal vĂœĆĄe, 34.13 popisuje rĆŻznĂ© provoznĂ­ reĆŸimy blokovĂ© ĆĄifry ze dne 34.12. Mezi nimi je reĆŸim pro generovĂĄnĂ­ imitacĂ­ vloĆŸek a vĂœpočty MAC. V PyGOST je to gost2015.mac(). Tento reĆŸim vyĆŸaduje pƙedĂĄnĂ­ ĆĄifrovacĂ­ funkce (pƙíjem a vrĂĄcenĂ­ jednoho bloku dat), velikost ĆĄifrovacĂ­ho bloku a vlastně i data samotnĂĄ. Proč nemĆŻĆŸete pevně zakĂłdovat velikost ĆĄifrovacĂ­ho bloku? 3413 popisuje nejen 34.12bitovou ĆĄifru Grasshopper, ale takĂ© 2015bitovou Magma - mĂ­rně upravenĂœ GOST 28147-89, vytvoƙenĂœ zpět v KGB a stĂĄle mĂĄ jeden z nejvyĆĄĆĄĂ­ch bezpečnostnĂ­ch prahĆŻ.

    Kuznechik je inicializovĂĄn volĂĄnĂ­m gost.3412.GOST3412Kuznechik(key) a vracĂ­ objekt s metodami .encrypt()/.decrypt() vhodnĂœmi pro pƙedĂĄnĂ­ funkcĂ­m 34.13. MAC se vypočítĂĄ nĂĄsledovně: gost3413.mac(GOST3412Kuznechik(klíč).ĆĄifrovat, KUZNECHIK_BLOCKSIZE, ĆĄifrovĂœ text). Pro porovnĂĄnĂ­ vypočtenĂ© a pƙijatĂ© MAC nemĆŻĆŸete pouĆŸĂ­t obvyklĂ© srovnĂĄnĂ­ (==) bajtovĂœch ƙetězcĆŻ, protoĆŸe tato operace unikĂĄ porovnĂĄvacĂ­ čas, coĆŸ v obecnĂ©m pƙípadě mĆŻĆŸe vĂ©st k fatĂĄlnĂ­m zranitelnostem, jako je BESTIE Ăștoky na TLS. Python mĂĄ k tomu speciĂĄlnĂ­ funkci hmac.compare_digest.

    Funkce blokovĂ© ĆĄifry dokĂĄĆŸe zaĆĄifrovat pouze jeden blok dat. Pro větĆĄĂ­ počet, a to ani ne nĂĄsobek dĂ©lky, je nutnĂ© pouĆŸĂ­t reĆŸim ĆĄifrovĂĄnĂ­. 34.13-2015 popisuje nĂĄsledujĂ­cĂ­: ECB, CTR, OFB, CBC, CFB. KaĆŸdĂœ mĂĄ svĂ© vlastnĂ­ pƙijatelnĂ© oblasti pouĆŸitĂ­ a vlastnosti. BohuĆŸel stĂĄle nemĂĄme standardizovanĂ© ověƙenĂ© reĆŸimy ĆĄifrovĂĄnĂ­ (jako CCM, OCB, GCM a podobně) - jsme nuceni si MAC alespoƈ pƙidat sami. vybĂ­rĂĄm si reĆŸim čítače (CTR): nevyĆŸaduje vĂœplƈ do velikosti bloku, lze paralelizovat, pouĆŸĂ­vĂĄ pouze funkci ĆĄifrovĂĄnĂ­, lze jej bezpečně pouĆŸĂ­t k ĆĄifrovĂĄnĂ­ velkĂ©ho mnoĆŸstvĂ­ zprĂĄv (na rozdĂ­l od CBC, kterĂ© mĂĄ kolize poměrně rychle).

    Stejně jako .mac() pƙijĂ­mĂĄ .ctr() podobnĂœ vstup: ĆĄifrovĂœ text = gost3413.ctr(GOST3412Kuznechik(klíč).encrypt, KUZNECHIK_BLOCKSIZE, prostĂœ text, iv). Je nutnĂ© zadat inicializačnĂ­ vektor, kterĂœ je pƙesně polovičnĂ­ neĆŸ dĂ©lka ĆĄifrovacĂ­ho bloku. Pokud je nĂĄĆĄ ĆĄifrovacĂ­ klíč pouĆŸit pouze k zaĆĄifrovĂĄnĂ­ jednĂ© zprĂĄvy (i kdyĆŸ z vĂ­ce blokĆŻ), pak je bezpečnĂ© nastavit nulovĂœ inicializačnĂ­ vektor. K ĆĄifrovĂĄnĂ­ zprĂĄv typu handshake pouĆŸĂ­vĂĄme pokaĆŸdĂ© samostatnĂœ klíč.

    OvěƙenĂ­ podpisu gost3410.verify() je triviĂĄlnĂ­: pƙedĂĄme eliptickou kƙivku, ve kterĂ© pracujeme (prostě ji zaznamenĂĄme v naĆĄem protokolu GOSTIM), veƙejnĂœ klíč podepisujĂ­cĂ­ho (nezapomeƈte, ĆŸe by to měla bĂœt n-tice dvou velkĂĄ čísla, nikoli bajtovĂœ ƙetězec), hash 34.11. 2012. XNUMX a samotnĂœ podpis.

    DĂĄle v iniciĂĄtoru pƙipravĂ­me a odeĆĄleme handshake zprĂĄvu do handshake2, pƙičemĆŸ provedeme stejnĂ© akce jako pƙi ověƙovĂĄnĂ­, pouze symetricky: podepisovĂĄnĂ­ naĆĄich klíčƯ mĂ­sto kontroly atd...

     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)
     

    Po navĂĄzĂĄnĂ­ relace se vygenerujĂ­ pƙenosovĂ© klíče (samostatnĂœ klíč pro ĆĄifrovĂĄnĂ­, pro ověƙovĂĄnĂ­, pro kaĆŸdou ze stran) a inicializuje se Grasshopper, aby deĆĄifroval a zkontroloval MAC:

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

    Korutina msg_sender nynĂ­ ĆĄifruje zprĂĄvy pƙed jejich odeslĂĄnĂ­m na TCP spojenĂ­. KaĆŸdĂĄ zprĂĄva mĂĄ monotĂłnně rostoucĂ­ nonce, coĆŸ je takĂ© inicializačnĂ­ vektor pƙi zaĆĄifrovĂĄnĂ­ v reĆŸimu čítače. Je zaručeno, ĆŸe kaĆŸdĂĄ zprĂĄva a blok zprĂĄv bude mĂ­t jinou hodnotu čítače.

    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
    

    PƙíchozĂ­ zprĂĄvy jsou zpracovĂĄvĂĄny rutinou msg_receiver, kterĂĄ zajiĆĄĆ„uje ověƙovĂĄnĂ­ a deĆĄifrovĂĄnĂ­:

    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)
    

    Závěr

    GOSTIM je určen vĂœhradně pro vzdělĂĄvacĂ­ Ășčely (protoĆŸe se na něj nevztahujĂ­ alespoƈ testy)! ZdrojovĂœ kĂłd programu lze stĂĄhnout zde (ĐĄŃ‚Ń€ĐžĐ±ĐŸĐł-256 хэш: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). КаĐș Đž ĐČсД ĐŒĐŸĐž ĐżŃ€ĐŸĐ”Đșты, топа GoGOST, PyDERASN, NCCP, GoVPN, GOSTIM je zcela svobodnĂœ softwaredistribuovĂĄn za podmĂ­nek GPLv3 +.

    Sergej Matvejev, cypherpunk, člen Nadace SPO, Python/Go-vĂœvojáƙ, hlavnĂ­ specialista Federal State Unitary Enterprise "STC "Atlas".

Zdroj: www.habr.com

Kupte si spolehlivĂœ hosting pro strĂĄnky s DDoS ochranou, VPS VDS servery đŸ”„ Kupte si spolehlivĂœ webhosting s ochranou DDoS, VPS VDS servery | ProHoster