GOSTIM: P2P F2F E2EE IM për një mbrëmje me kriptografi GOST

Si zhvillues PyGOST bibliotekë (atëherë kriptografike GOST në Python të pastër), shpesh marr pyetje se si të realizoj një exchanged të sigurt mesazhesh në mënyrë të thjeshtë. Shumë mendojnë se kriptografia aplikative është një gjë e lehtë, dhe thjesht thirrja e .encrypt() do të jetë e mjaftueshme për dërgimin e sigurt nëpër një kanal komunikimi. Të tjerë mendojnë se kriptografia aplikative është një fushë e disa të paktëve, dhe është në rregull që kompanitë e pasura si Telegram të mos kenë matematicienë të olimpiadave, nuk mund të realizojnë një protokoll të sigurt.

E gjithë kjo më shtyu të shkruaj këtë artikull, për të treguar se realizimi i protokolleve kriptografike dhe mesazheve të sigurta IM nuk është një detyrë kaq e vështirë. Megjithatë, nuk ia vlen të shpikni protokolle të vetat të autentifikimit dhe shkëmbimit të çelësave.

GOSTIM: P2P F2F E2EE IM për një mbrëmje me kriptografi GOST
Në artikull do të përshkruhet peer-to-peer, friend-to-friend, end-to-end të enkriptuar instant messenger me protokollin SIGMA-I të autentifikimit dhe shkëmbimit të çelësave (në bazë të cilit është realizuar IPsec IKE IPsec IKE), duke përdorimi ekskluzivisht algoritme kriptografike GOST nga biblioteka PyGOST dhe kodimin ASN.1 të mesazheve nga biblioteka PyDERASN (për të cilën tashmë kam shkruar më parë). Kushti i nevojshëm: ai duhet të jetë aq i thjeshtë, saqë të mund të shkruhet nga fillimi për një mbrëmje (ose një ditë pune), përndryshe nuk është një program i thjeshtë. Patjetër që ka gabime, komplikanime të tepruara, dhe mangësi, përveç se kjo është programi im i parë që përdor bibliotekën asyncio.

Dizajni i IM

Për të filluar, duhet të kuptojmë se si do të duket IM ynë. Për thjeshtësi, le të jetë një rrjet peer-to-peer, pa asnjë zbulim të pjesëmarrësve. Do të tregojmë vetë se në cilin adresë: port do të lidhemi për të komunikuar me biseduesin.

E kuptoj se, për momentin, supozimi i disponueshmërisë së lidhjes direkte midis dy kompjuterëve të rastësishëm është një kufizim i rëndësishëm i aplikueshmërisë së IM në praktikë. Por sa më shumë zhvillues të implementojnë ndihmesa për kalimin përmes NAT, aq më gjatë do të mbetemi në Internetin IPv4, me një probabilitet të mërzitshëm për lidhjen midis kompjuterëve të rastësishëm. Sa kohë mund ta durojmë mungesën e IPv6 në shtëpi dhe në punë?

Ne do të kemi një rrjet miqësor: të gjithë partnerët e mundshëm të bisedës duhet të jenë të njohur paraprakisht. Së pari, kjo e thjeshton shumë gjithçka: paraqituni, gjeni ose mos e gjeni emrin/çelësin, shk disconnect ose vazhdoni punën, duke e ditur partnerin. Së dyti, në përgjithësi, kjo është e sigurt dhe përjashton shumë sulme.

Interfata e IM do të jetë afër zgjidhjeve klasike projekti suckless, të cilat më pëlqejnë shumë për minimalizmin e tyre dhe filozofinë Unix-way. Programi IM krijon një direktori për çdo partner bisedë me tre soket të domenit Unix:

  • in — nĂ« tĂ« shkruhen mesazhet e dĂ«rguara pĂ«r partnerin;
  • out — nga tĂ« cilat lexohen mesazhet e pranuara nga partneri;
  • status — duke lehtem, ne kuptojmĂ« nĂ«se bashkĂ«biseduesi Ă«shtĂ« aktualisht i lidhur, adresa/porta e lidhjes.

Për më tepër, krijohet një soket conn, duke regjistruar në të hostin e portës, ne iniciatojmë lidhjen me bashkëbiseduesin e largët.

|-- alice
|   |-- në
|   |-- jashtë
|   `-- status
|-- bob
|   |-- në
|   |-- jashtë
|   `-- status
`- conn

Ky qasje lejon krijimin e zbatimeve të pavarura të transportit IM dhe ndërfaqes së përdoruesit, sepse në shije dhe ngjyrë nuk ka shok, nuk i pëlqen të gjithëve. Duke përdorur tmux dhe/ose multitail, mund të merrni një ndërfaqe me shumë dritare me ndriçim sintaksor. Dhe me ndihmën e rlwrap mund të merrni një varg të përputhshëm me GNU Readline për të futur mesazhe.

NĂ« tĂ« vĂ«rtetĂ«, projektet suckless pĂ«rdorin skedarĂ« FIFO. Personalish nuk arrita tĂ« kuptoj se si tĂ« punoj me skedarĂ«t nĂ« asyncio nĂ« mĂ«nyrĂ« tĂ« konkurueshme pa njĂ« mbĂ«shtetje manuale prej thjeshtuarish (pĂ«r kĂ«to gjĂ«ra kam pĂ«rdorur prej kohĂ«sh gjuhĂ«n Go). Prandaj vendosa tĂ« kaloj me soketĂ«t Unix domain. FatkeqĂ«sisht, kjo heq mundĂ«sinĂ« pĂ«r tĂ« bĂ«rĂ« echo 2001:470:dead::babe 6666 > conn. E zgjodha kĂ«tĂ« problem, duke pĂ«rdorur socat: echo 2001:470:dead::babe 6666 | socat — UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/nĂ«.

Protocoli fillestar i pasigurt

Transporti përdor TCP: ai garanton dorëzimin dhe rendin e tij. UDP nuk garanton asnjërin prej tyre (çka do të ishte e dobishme kur aplikohet kriptografia), dhe mbështetje. SCTP në Python nuk ka mbështetje nga get.

Fatkeqësisht, në TCP nuk ekziston nocioni i mesazhit, vetëm ai i rrjedhës së bajtëve. Prandaj, është e nevojshme të imagjinojmë një format për mesazhet, në mënyrë që ato të mund të ndahen në këtë rrjedhë. Mund të biem dakord të përdorim simbolin e transferimit të rreshtit. Në fillim do të mjaftojë, megjithatë, kur të fillojmë të kodojmë mesazhet tona, ky simbol mund të shfaqet kudo në tekstin e koduar. Në rrjetet, prandaj, janë popullorë protokollet që dërgojnë më parë gjatë mesazhit në bajtë. Për shembull, në Python ka xdrlib që lejon të punojë me një format të tillë. XDR.

Ne do tĂ« punojmĂ« siç duhet dhe nĂ« mĂ«nyrĂ« efikase me leximin TCP — do ta thjeshtĂ«sojmĂ« kodin. LexojmĂ« tĂ« dhĂ«nat nga socketi nĂ« njĂ« cikĂ«l tĂ« pafund derisa tĂ« dekodojmĂ« mesazhin e plotĂ«. Si format pĂ«r kĂ«tĂ« qasje mund tĂ« pĂ«rdorim si JSON ashtu edhe XML. Por, kur tĂ« shtohet kriptografia, tĂ« dhĂ«nat do tĂ« duhet tĂ« nĂ«nshkruhen dhe tĂ« autentifikohen — dhe kjo do tĂ« kĂ«rkojĂ« njĂ« paraqitje identike byte-for-byte tĂ« objekteve, gjĂ« qĂ« JSON/XML nuk e sigurojnĂ« (rezultati i dumps mund tĂ« ndryshojĂ«).

XDR është i përshtatshëm për këtë detyrë, megjithatë unë zgjedh ASN.1 me kodimin DER dhe PyDERASN bibliotekën, pasi do të kemi objekte me nivele të larta me të cilat shpesh është më këndshëm dhe praktik të punojmë. Ndryshe nga schemaless bencode, MessagePack ose CBOR, ASN.1 automatikisht do të verifikojë të dhënat përballë një skeme të ngurtë të caktuar.

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

Mesazhi i pranuar do të jetë Msg: ose MsgText tekstual (ndërsa për tani me një fushë tekstuale), ose mesazhi i dorëzimit MsgHandshake (në të cilin dërgohet emri i bashkëbiseduesit). Tani duket e komplikuar, por kjo është një ndihmesë për të ardhmen.

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

Pa pa kriptografisë

Siç e thashë, për të gjitha operacionet me socket-et do të përdoret biblioteka asyncio. Le të shpallim atë që presim në momentin e startit:

parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
    "--our-name",
    required=True,
    help="Emri ynë i kolegut",
)
parser.add_argument(
    "--their-names",
    required=True,
    help="Emrat e tyre të kolegëve, të ndarë me presje",
)
parser.add_argument(
    "--bind",
    default="::1",
    help="Adresa për të dëgjuar",
)
parser.add_argument(
    "--port",
    type=int,
    default=6666,
    help="Porti për të dëgjuar",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))

Vendoset emri ynĂ« (—our-name alice). TĂ« gjithĂ« biseduesit e pritur njĂ«ri pas tjetrit me presje (—their-names bob,eve). PĂ«r secilin nga biseduesit, krijohet njĂ« dosje me socket-e Unix, si dhe njĂ« korutinĂ« pĂ«r çdo in, out, state:

për emri i peers në THEIR_NAMES:
    bëni mappat(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"))

Mesazhet që vijnë nga përdoruesi nga socketi in dërgohen në radhët IN_QUEUES:

async def unixsock_in_processor(reader, writer, in_queue: asyncio.Queue) -> None:
    në mënyrë që të përherë:
        tekst = await reader.read(MaxTextLen)
        nëse tekst == b"":
            break
        await in_queue.put(tekst.decode("utf-8"))

Mesazhet që vijnë nga bashkëbiseduesit dërgohen në radhët OUT_QUEUES, nga të cilat të dhënat shkruhen në socketin out:

async def unixsock_out_processor(reader, writer, out_queue: asyncio.Queue) -> None:
    në mënyrë që të përherë:
        tekst = await out_queue.get()
        writer.write(("[%s] %s" % (datetime.now(), tekst)).encode("utf-8"))
        await writer.drain()

Kur lexon nga socketi state, programi kërkon në fjalorin PEER_ALIVE adresën e bashkëbiseduesit. Nëse lidhja me bashkëbiseduesin nuk ekziston, atëherë shkruhet një varg i zbrazët.

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

Kur kursej adresa në soket, aktivizohet funksioni "nismëtar" i lidhjes:

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

Le të shqyrtojmë nismëtarin. Fillimisht, ai natyrisht hap një lidhje me host-in/port-in e caktuar dhe dërgon një mesazh handshake me emrin e tij:

 130 async def initiator(host, port):
 131     _id = repr((host, port))
 132     logging.info("%s: po dial", _id)
 133     reader, writer = await asyncio.open_connection(host, port)
 134     # Mesazhi i handshake-it {{{
 135     writer.write(Msg(("handshake", MsgHandshake((
 136         ("emri i palës", OUR_NAME),
 137     )))).encode())
 138     # }}}
 139     await writer.drain()

Dhe më pas, pret një përgjigje nga ana e largët. Mundohet të dekodojë përgjigjen e ardhur sipas skemës Msg ASN.1. Supozomë se e gjithë mesazhi do të dërgohet me një TCP-segment dhe ne do ta marrim atë në mënyrë atomike gjatë thirrjes .read(). Kontrollojmë që kemi marrë pikërisht mesazhin e handshake-it.

 Prisni mesazhin Handshake {{{
 142     data = await reader.read(256)
 143     nëse data == b"":
 144         logging.warning("%s: nuk ka përgjigje, po shkëputet", _id)
 145         writer.close()
 146         kthehu
 147     provo:
 148         msg, _ = Msg().decode(data)
 149     përjashto ASN1Error:
 150         logging.warning("%s: përgjigje e paqartë, po shkëputet", _id)
 151         writer.close()
 152         kthehu
 153     logging.info("%s: mori mesazhin %s", _id, msg.choice)
 154     nëse msg.choice != "handshake":
 155         logging.warning("%s: mesazh i papritur, po shkëputet", _id)
 156         writer.close()
 157         kthehu
 158     # }}}

Po kontrollojmë nëse emri i arsimtarit është i njohur për ne. Nëse jo, atëherë po shkëputemi. Po kontrollojmë nëse kemi pasur tashmë një lidhje me të (arsimtari sërish dha komandën për t'u lidhur me ne) dhe e mbyllim atë. Në IN_QUEUES vendosen vargje Python me tekstin e mesazhit, por ka një vlerë të veçantë None, që sinjalizon korutinën msg_sender të ndalojë punën, në mënyrë që ajo të harrojë për writer-in e saj, i lidhur me lidhjen TCP të vjetër.

 159     msg_handshake = msg.value
 160     peer_name = str(msg_handshake["peerName"])
 161     nëse emri i palës nuk është në EMRAT E TYRE:
 162         logging.warning("emër palë i panjohur: %s", peer_name)
 163         writer.close()
 164         return
 165     logging.info("%s: sesioni është krijuar: %s", _id, peer_name)
 166     # Ekzekuto dërguesin e mesazheve me tekst, inicializo dekoduesin e transportit {{{
 167     peer_alive = PEER_ALIVES.pop(peer_name, None)
 168     nëse peer_alive është jo 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 pranon mesazhet nĂ« dalje (tĂ« vendosura nĂ« radhĂ« nga in soketi), i serializon ato nĂ« njĂ« mesazh MsgText dhe i dĂ«rgon pĂ«rmes lidhjes TCP. Ajo mund tĂ« ndĂ«rpritet nĂ« çdo moment — kĂ«tĂ« ne e kapim qartazi.

async def msg_sender(peer_name: str, writer) -> None:
    in_queue = IN_QUEUES[peer_name]
    while True:
        text = await in_queue.get()
        nëse text është 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: dërguar %d karaktere mesazh", peer_name, len(text))

Në fund, inicuesi hyn në një cikël të pafund leximi të mesazheve nga socketi. Kontrollon nëse janë mesazhe tekstuale dhe i vendos ato në radhën OUT_QUEUES, nga e cila do të dërgohen në socketin out të biseduesit përkatës. Pse nuk mund të bëjmë thjesht .read() dhe të dekodojmë mesazhin? Sepse nuk përjashtohet mundësia që disa mesazhe nga përdoruesi të jenë agreguar në tamponin e sistemit operativ dhe të dërgohen në një TCP-segment. Mund të dekodojmë të parin, ndërsa në tampon mund të mbetet një pjesë nga e dyta. Në çdo situatë anormale, ne mbyllim TCP-në lidhje dhe ndalojmë korutinën msg_sender (me dërgimin e None në radhën OUT_QUEUES).

 174     buf = b""
 175     # Pritini mesazhet e testit {{{
 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: madhësia maksimale e tamponit është tejkaluar", _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: mesazh i papritur %s", _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: po shkëputet: %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: pranuar mesazhi me %d karaktere", peer_name, len(text))
  69     await OUT_QUEUES[peer_name].put(text)

Le ta kthejmë te kodi kryesor. Pas krijimit të të gjitha korutave në momentin e nisjes së programit, ne nisim serverin TCP. Për çdo lidhje të vendosur, ai krijon një korutë përgjigjëse (responder).

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("Dëgjojmë në: %s", server.sockets[0].getsockname())
loop.run_forever()

responder është i ngjashëm me initiator dhe kryen të njëjtat veprime, por cikli i pafund i leximit të mesazheve fillon menjëherë, për thjeshtësi. Tani protokolli i dorëzimit dërgon një mesazh nga secila anë, por më vonë do të dërgojë dy nga iniciatori i lidhjes, pas të cilave mund të dërgohen menjëherë mesazhet tekstual.

  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         # Lexoni derisa të marrim mesazhin Msg {{{
  80         data = await reader.read(MaxMsgLen)
  81         if data == b"":
  82             logging.info("%s: e mbyllur lidhja", _id)
  83             break
  84         buf += data
  85         if len(buf) > MaxMsgLen:
  86             logging.warning("%s: është tejkaluar madhësia maksimale e buffers", _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: mesazh i papritur %s", _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         # Procesoni mesazhin Handshake {{{
 104         elif msg_expected == "handshake":
 105             logging.info("%s: mori mesazhin %s", _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("emri i panjohur i partnerit: %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: seanca e krijuar: %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: po shkëputemi", _id)
 125     if msg_expected == "text":
 126         IN_QUEUES[peer_name].put(None)
 127     writer.close()

Protokolli i sigurtë

Ka ardhur koha pĂ«r tĂ« siguruar komunikimin tonĂ«. ÇfarĂ« do tĂ« thotĂ« siguria pĂ«r ne dhe çfarĂ« dĂ«shirojmĂ«:

  • privatĂ«sia e mesazheve tĂ« dĂ«rguara;
  • autenticiteti dhe integriteti i mesazheve tĂ« dĂ«rguara - ndryshimi i tyre duhet tĂ« zbulohet;
  • mbrojtja nga sulmet e riprodhimit (replay attack) - fakti i humbjes apo i pĂ«rsĂ«ritjes sĂ« mesazheve duhet tĂ« zbulohet (dhe ne vendosim tĂ« ndĂ«rpresim lidhjen);
  • identifikimi dhe autentifikimi i biseduesve nĂ«pĂ«rmjet çelĂ«save publikĂ« tĂ« futur mĂ« parĂ« - ne tashmĂ« kemi vendosur se krijojmĂ« njĂ« rrjet miqĂ«sor. VetĂ«m pas autentifikimit do tĂ« kuptojmĂ« me kĂ« po komunikojmĂ«;
  • praninĂ« siguri perfekte pĂ«rpara karakteristikat e pĂ«rmirĂ«suara (PFS) - komprometimi i çelĂ«sit tonĂ« tĂ« gjatĂ« tĂ« nĂ«nshkrimit nuk duhet tĂ« çojĂ« nĂ« mundĂ«sinĂ« e leximin tĂ« gjithĂ« bisedave tĂ« mĂ«parshme. Regjistrimi i trafikut tĂ« kapur bĂ«het i padobishĂ«m;
  • validiteti i mesazheve (transportit dhe dorĂ«heqjes) vetĂ«m brenda njĂ« seance TCP. Shtimi i mesazheve tĂ« nĂ«nshkruara/autentifikuara si duhet nga njĂ« seancĂ« tjetĂ«r (edhe me tĂ« njĂ«jtin bisedues) nuk duhet tĂ« jetĂ« e mundur;
  • njĂ« vĂ«zhgues pasiv nuk duhet tĂ« shohĂ« as identifikuesit e pĂ«rdoruesve, as çelĂ«sat publikĂ« tĂ« gjatĂ« qĂ« dĂ«rgohen, as hash-at e tyre. NjĂ«farĂ« anonimiteti nga vĂ«zhguesi pasiv.

ËshtĂ« e habitshme, por ky minimum dĂ«shirohet nga tĂ« gjithĂ« nĂ« çdo protokoll dorĂ«heqjeje, dhe shumĂ« pak nga ato qĂ« janĂ« pĂ«rmendur pĂ«rfundimisht zbatohen pĂ«r protokollet "tĂ« shtĂ«pisĂ«". Tani, le tĂ« mos shpikim diçka tĂ« re. UnĂ« do tĂ« rekomandoja me siguri tĂ« pĂ«rdorim Noise framework pĂ«r ndĂ«rtimin e protokolleve, por le tĂ« zgjidhim diçka mĂ« tĂ« thjeshtĂ«.

Dy protokollet më popullore janë:

  • TLS — njĂ« protokoll shumĂ« i komplikuar me njĂ« histori tĂ« gjatĂ« gabimesh, problemeve, dobĂ«sive, planifikimit tĂ« keq, kompleksitetit dhe mungesave (nĂ« tĂ« vĂ«rtetĂ«, kjo Ă«shtĂ« pak e lidhur me TLS 1.3). Por nuk e shqyrtojmĂ« atĂ« pĂ«r shkak tĂ« shumĂ« komplikuese.
  • IPsec me IKE — nuk kanĂ« probleme serioze kriptografike, megjithatĂ« gjithashtu nuk janĂ« tĂ« thjeshta. NĂ«se lexoni mbi IKEv1 dhe IKEv2, origjina e tyre Ă«shtĂ« STS, ISO/IEC IS 9798-3 dhe protokollet SIGMA (SIGn-and-MAc) — mjaft tĂ« thjeshta pĂ«r tu realizuar brenda njĂ« nate.

ÇfarĂ« e bĂ«n SIGMA, si nyja e fundit e zhvillimit tĂ« protokolleve STS/ISO, tĂ« shkĂ«lqyer? Ai plotĂ«son tĂ« gjitha kĂ«rkesat tona (pĂ«rfshirĂ« "fshehjen" e identifikuesve tĂ« biseduesve), dhe nuk ka probleme tĂ« njohura kriptografike. Ai Ă«shtĂ« minimalist — heqja e ndonjĂ« elementi nga mesazhi i protokollit do ta bĂ«jĂ« atĂ« tĂ« pasigurt.

Le të kalojmë nga protokolli më i thjeshtë i shkruar nga vetë ne deri te SIGMA. Operacioni më themelor që na intereson është konsensusi i çelësave: një funksion, në daljen e të cilit të dy pjesëmarrësit do të marrin njësoj vlerën, e cila mund të përdoret si një çelës simetrik. Pa hyrë në detaje: secila palë gjeneron një çift çelësh efemer (i cili përdoret vetëm brenda një seance), shkëmben çelësat publikë, dhe thërrasin funksionin e konsensusit, në të cilin kalojnë çelësin e tyre privat dhe çelësin publik të biseduesit.

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

Çdo kush mund tĂ« ndĂ«rhyjĂ« dhe tĂ« zĂ«vendĂ«sojĂ« çelĂ«sat publikĂ« me tĂ« tijat — ky protokoll nuk ka autentikim tĂ« bashkĂ«biseduesve. Le tĂ« shtojmĂ« njĂ« nĂ«nshkrim me çelĂ«sa tĂ« gjatĂ«.

┌─────┐                            ┌─────┐
│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) ║   │
   │        ╚═════════════════════╝   │
   │                                  │

Kyç kyç nuk do të jetë e përshtatshme, sepse nuk është e lidhur me një sesion specifik. Mesazhe të tilla "janë të përshtatshme" edhe për sesionet me pjesëmarrës të tjerë. Duhet të nënshkruhet i gjithë konteksti. Kjo gjithashtu detyron që të shtohet një mesazh tjetër nga A.

Për më tepër, është thelbësore të shtoni nënshkrimin dhe identifikuesin tuaj, sepse në të kundërt, ne mund të zëvendësojmë IdXXX dhe të nënshkruajmë mesazhin me çelësin e dikujt tjetër të njohur. Për të parandaluar sulmet reflection, është e nevojshme që elementët nënshkrues të ndodhen në vende të caktuara në përputhje me kuptimin e tyre: nëse A nënshkruan (PubA, PubB), atëherë B duhet të nënshkruajë (PubB, PubA). Kjo tregon gjithashtu rëndësinë e zgjedhjes së strukturës dhe formatit të të dhënave të serializuara. Për shembull, grupet në kodimin ASN.1 DER janë të renditura: SET OF(PubA, PubB) do të jetë identike me SET OF(PubB, PubA).

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

MegjithatĂ«, ne ende nuk e kemi "provuara" qĂ« kemi prodhuar njĂ« çelĂ«s tĂ« pĂ«rbashkĂ«t tĂ« njĂ«jtĂ« pĂ«r kĂ«tĂ« seancĂ«. NĂ« parim, mund tĂ« bĂ«jmĂ« pa kĂ«tĂ« hap — mesazhi i parĂ« tĂ« transportit do tĂ« jetĂ« i pavlefshĂ«m, por ne duam qĂ« kur dorĂ«zimi tĂ« pĂ«rfundojĂ«, tĂ« jemi tĂ« sigurt se gjithçka Ă«shtĂ« vĂ«rtet e konfirmuar. NĂ« kĂ«tĂ« moment, kemi nĂ« duar ISO/IEC IS 9798-3 protokollin.

Ne mund të nënshkruajmë edhe çelësin e prodhuar. Kjo është e rrezikshme, pasi nuk përjashtohet që në algoritmin e përdorur të nënshkrimit mund të ketë rrjedhje (madje edhe bitet për nënshkrim, por prapë rrjedhje). Mund të nënshkruajmë hash-in e çelësit të prodhuar, por rrjedhja e madhe edhe e hash-it të çelësit të prodhuar mund të ketë vlerë gjatë sulmeve brute-force ndaj funksionit të prodhimit. SIGMA përdor funksionin MAC, i cili autentifikon identifikuesin e dërguesit.

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

Si pĂ«r optimizim, disa mund tĂ« dĂ«shirojnĂ« tĂ« ri-pĂ«rdorin çelĂ«sat e tyre efemerĂ« (çfarĂ«, sigurisht, Ă«shtĂ« e dĂ«mshme pĂ«r PFS). PĂ«r shembull, ne gjeneruam njĂ« çift çelĂ«sash, pĂ«rpiqem tĂ« lidhemi, por TCP nuk ishte i disponueshĂ«m ose u ndĂ«rpre diku nĂ« mes tĂ« protokollit. ËshtĂ« njĂ« humbje e madhe tĂ« shpenzohet entropi dhe burime procesori pĂ«r njĂ« çift tĂ« ri. Prandaj, do tĂ« prezantojmĂ« njĂ« cookie — njĂ« vlerĂ« pseudonatyrale, e cila do tĂ« mbrojĂ« nga sulmet e mundshme tĂ« ri-pĂ«rdorimit nga rastĂ«sisht nĂ« ri-pĂ«rdorimin e çelĂ«save publikĂ« efemerĂ«. PĂ«r shkak tĂ« lidhjes mes cookie dhe çelĂ«sit publik efemer, çelĂ«si publik i palĂ«s tjetĂ«r mund tĂ« hiqet nga nĂ«nshkrimi si i panevojshĂ«m.

┌─────┐                                                                 ┌─────┐
│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, ...)║
   │                                                                       │ ╚═════════════════════╝
   │                                                                       │

Së fundi, ne duam të sigurojmë privatësinë e identifikuesve tanë nga vëzhguesit pasivë. Për këtë, SIGMA propozon që fillimisht të shkëmbejë çelësa efemerë, të krijojë një çelës të përbashkët, mbi të cilin të kodohen mesazhet autentikues dhe identifikues. SIGMA përshkruan dy mundësi:

  • SIGMA-I — mbron iniciatorin nga sulmet aktive, dhe pĂ«rgjigjĂ«sin nga ato pasive: iniciatori autentikon pĂ«rgjigjĂ«sin dhe, nĂ«se diçka nuk pĂ«rputhet, ai nuk e zbuluar identifikimin e tij. NdĂ«rsa pĂ«rgjigjĂ«si jep identifikimin e tij nĂ«se fillohet njĂ« protokoll aktiv. NjĂ« vĂ«zhgues pasiv nuk merr asnjĂ« informacion;
    SIGMA-R — mbron pĂ«rgjigjĂ«sin nga sulmet aktive, dhe iniciatorin nga ato pasive. TĂ« gjitha kĂ«to pĂ«rkundrazi, por nĂ« kĂ«tĂ« protokoll dĂ«rgohen katĂ«r mesazhe dorĂ«zimi.

    Zgjedhim SIGMA-I si më të ngjashëm me atë që presim nga gjërat tradicionale klient-server: klienti e njeh vetëm serverin e autentikuar, ndërsa serveri i di të gjitha. Gjithashtu, është më i thjeshtë për t'u implementuar për shkak të numrit më të vogël të mesazheve të dorëzimit. E gjithë ajo që ne e shtojmë në protokoll është enkriptimi i një pjese të mesazhit dhe transferimi i identifikuesit A në pjesën e enkriptuar të mesazhit të fundit:

    ┌─────┐                                                                        ┌─────┐
    │PeerA│                                                                        │PeerB│
    └──┬──┘                                                                        └──┬──┘
       │                                PubA, CookieA                                 │ ╔═══════════════════════════╗
       │─────────────────────────────────────────────────────────────────────────────│ ║SignPrvA, SignPubA = load()║
       │                                                                              │ ║PrvA, PubA = DHgen()       ║
       │                                                                              │ ╚═══════════════════════════╝
       │PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB))) │ ╔═══════════════════════════╗
       │<─────────────────────────────────────────────────────────────────────────────│ ║SignPrvB, SignPubB = load()║
       │                                                                              │ ║PrvB, PubB = DHgen()       ║
       │                                                                              │ ╚═══════════════════════════╝
       │                                                                              │ ╔═════════════════════╗
       │       Enc((IdA, sign(SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA)))         │ ║Key = DH(PrvA, PubB) ║
       │─────────────────────────────────────────────────────────────────────────────│ ║verify(Key, IdB)     ║
       │                                                                              │ ║verify(SignPubB, ...)║
       │                                                                              │ ╚═════════════════════╝
       │                                                                              │
    
    • PĂ«r nĂ«nshkrim pĂ«rdoret GOST R 34.10-2012 algoritmi me çelĂ«sa 256-bit.
    • PĂ«r gjenerimin e çelĂ«sit tĂ« pĂ«rbashkĂ«t pĂ«rdoret 34.10-2012 VKO.
    • Si MAC pĂ«rdoret CMAC. Teknikisht, kjo Ă«shtĂ« njĂ« aplikim i veçantĂ« i funksionit tĂ« bllokimit tĂ« enkriptimit, i pĂ«rshkruar nĂ« GOST R 34.13-2015. Si funksion enkriptimi pĂ«r kĂ«tĂ« mod Ă«shtĂ« Kuzhnik (34.12-2015).
    • Si identifikues i biseduesit pĂ«rdoret hash-i nga çelĂ«si i tij publik. Si hash pĂ«rdoret Stribog-256 (34.11-2012 256 bit).

    Pas dorĂ«zimit, do tĂ« kemi tĂ« rĂ«nĂ« dakord mbi njĂ« çelĂ«s tĂ« pĂ«rbashkĂ«t. Ky mund tĂ« pĂ«rdoret pĂ«r enkriptimin e autentikuar tĂ« mesazheve transportuese. Kjo pjesĂ« Ă«shtĂ« shumĂ« e thjeshtĂ« dhe Ă«shtĂ« e vĂ«shtirĂ« tĂ« gabosh: inkrementojmĂ« numĂ«ruesin e mesazheve, enkriptojmĂ« mesazhin, autentikojmĂ« (MAC) numĂ«ruesin dhe tekstin e enkriptuar, e dĂ«rgojmĂ«. Kur pranojmĂ« mesazhin kontrollojmĂ« qĂ« numĂ«ruesi tĂ« ketĂ« vlerĂ«n e pritur, autentikojmĂ« tekstin e enkriptuar me numĂ«ruesin, dekriptojmĂ«. ÇfarĂ« çelĂ«si duhet pĂ«rdorur pĂ«r enkriptimin e mesazheve, pĂ«r dorĂ«zimin, si duhet autentikuar? PĂ«rdorimi i njĂ« çelĂ«si tĂ« vetĂ«m pĂ«r kĂ«to detyra Ă«shtĂ« e rrezikshme dhe e paarsyeshme. Duhet gjeneruar çelĂ«sat duke pĂ«rdorur funksione tĂ« specializuara KDF (funksioni i derivimit tĂ« çelĂ«sit). NĂ«se do tĂ« ishim gjithmonĂ« duke ndĂ«rlikuar dhe shpikur diçka: HKDF nĂ« jetĂ« e njohur prej kohĂ«sh, e studiuar mirĂ« dhe nuk ka probleme tĂ« njohura. FatkeqĂ«sisht, nĂ« bibliotekĂ«n e natyrshme Python nuk ka kĂ«tĂ« funksion, prandaj pĂ«rdorim hkdf paketĂ«n. HKDF pĂ«rdor brenda HMAC, i cili, nga ana e tij, pĂ«rdor funksionin hash. NjĂ« shembull realizimi nĂ« Python nĂ« faqen Wikipedia merr vetĂ«m disa rreshta kodi. Ashtu siç ndodh me 34.10-2012, si funksion hash do tĂ« pĂ«rdorim Strboga-256. Dalja e funksionit tonĂ« tĂ« pajtimit tĂ« çelĂ«ve do tĂ« quhet çelĂ«si sesionar, nga i cili do tĂ« prodhohen simetrikĂ«t e munguar:

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

    Strukturat/skemat

    Le të shqyrtojmë se cilat struktura ASN.1 kemi marrë tani për të transferuar të gjitha këto të dhëna:

    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 — ajo qĂ« do tĂ« nĂ«nshkruhet (to be signed). HandshakeTBE — ajo qĂ« do tĂ« kriptohet (to be encrypted). DĂ«shiroj tĂ« tĂ«rheq vĂ«mendjen pĂ«r fushĂ«n ukm nĂ« MsgHandshake1. 34.10 VKO, pĂ«r njĂ« mĂ« shumĂ« rastĂ«sishmĂ«ri nĂ« çelĂ«sat e prodhuar, pĂ«rfshin parametrin UKM (materiali i çelĂ«sit tĂ« pĂ«rdoruesit) — thjesht njĂ« entropi shtesĂ«.

    Shtimi i kriptografisë në kod

    Do të shqyrtojmë vetëm ndryshimet e bëra në kodin origjinal, pasi skeleti ka mbetur i njëjtë (në të vërtetë, fillimisht është shkruar implementimi përfundimtar dhe më pas është ndarë çdo kriptografi).

    Pasi autentikimi dhe identifikimi i palëve do të bëhet përmes çelësave publikë, tani duhet t'i ruajmë diku për një kohë të gjatë. Për thjeshtësi do të përdorim një JSON të tillë:

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

    our — çifti ynĂ« i çelĂ«save, çelĂ«si privat dhe publik nĂ« formĂ«n heksadecimale. their — emrat e palĂ«ve dhe çelĂ«sat e tyre publikĂ«. Do tĂ« ndryshojmĂ« argumentet e komandĂ«s dhe do tĂ« shtojmĂ« post-pĂ«rpunimin e tĂ« dhĂ«nave 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="Generoni JSON me çiftin tonë të ri të çelësit",
    )
    parser.add_argument(
        "--keys",
        default="keys.json",
        required=False,
        help="JSON me çelësat tanë dhe të tyre",
    )
    parser.add_argument(
        "--bind",
        default="::1",
        help="Adresa për të dëgjuar",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=6666,
        help="Porti për të dëgjuar",
    )
    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)
    
    # Analizo dhe shpërndaj çelësat tanë dhe të tyre {{{
    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),
        }
    # }}}
    

    ÇelĂ«si privat i algoritmit 34.10 Ă«shtĂ« njĂ« numĂ«r i rastĂ«sishĂ«m. Ka njĂ« madhĂ«si prej 256 bit pĂ«r kurvat eliptike 256-bit. PyGOST punon jo me njĂ« grup bitĂ«sh, por me Numra tĂ« mĂ«dhenj, prandaj çelĂ«si ynĂ« privat (urandom(32)) duhet tĂ« konvertohet nĂ« numĂ«r, duke pĂ«rdorur gost3410.prv_unmarshal(). ÇelĂ«si publik llogaritet nĂ« mĂ«nyrĂ« deterministe nga çelĂ«si privat, duke pĂ«rdorur gost3410.public_key(). ÇelĂ«si publik 34.10 Ă«shtĂ« dy numra tĂ« mĂ«dhenj qĂ« gjithashtu duhet tĂ« kthehen nĂ« njĂ« sekuencĂ« bytesh pĂ«r ruajtje dhe transfertĂ« mĂ« tĂ« lehtĂ«, duke pĂ«rdorur gost3410.pub_marshal().

    Pasi të lexohet skedari JSON, çelësat publikë duhet të kthehen përsëri, duke përdorur gost3410.pub_unmarshal(). Duke qenë se do të marrim identifikuesit e bashkëbiseduesve si hash nga çelësi publik, mund t'i llogarisim ato paraprakisht dhe t'i vendosim në një fjalor për kërkimin e shpejtë. Hashi Stribog-256 është gost34112012256.GOST34112012256(), e cila plotëson plotësisht ndërfaqen e funksioneve hash nga hashlib.

    Si ka ndryshuar korutina e iniciatorit? Të gjitha, si sipas skemës së dorëzimit: gjenerojmë cookie (128-bit mjafton), çiftin efemer të çelësave 34.10, i cili do të përdoret për funksionin VKO të pajtimit të çelësave.

     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     # Generoni çelësin tonë publik ephemeral dhe cookie, dërgojmë mesazhin Handshake 0 {{{
     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()
    

    • po presim pĂ«rgjigjen dhe decodojmĂ« mesazhin Msg tĂ« arritur;
    • sigurohemi qĂ« morĂ«m handshake1;
    • decodojmĂ« çelĂ«sin publik ephemeral tĂ« palĂ«s tjetĂ«r dhe llogarisim çelĂ«sin e seancĂ«s;
    • krijojmĂ« çelĂ«sat simetrikĂ« tĂ« nevojshĂ«m pĂ«r tĂ« pĂ«rpunuar pjesĂ«n TBE tĂ« mesazhit.

     423     logging.info("%s: mori %s mesazh", _id, msg.choice)
     424     nëse msg.choice != "handshake1":
     425         logging.warning("%s: mesazh befasues, duke u shkëputur", _id)
     426         writer.close()
     427         kthehu
     428     # }}}
     429     msg_handshake1 = msg.value
     430     # Validoni mesazhin Handshake {{{
     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 Ă«shtĂ« njĂ« numĂ«r 64-bit (urandom(8)), i cili gjithashtu kĂ«rkon deserializim nga pĂ«rfaqĂ«simi nĂ« byte, duke pĂ«rdorur gost3410_vko.ukm_unmarshal(). Funksioni VKO pĂ«r 34.10-2012 256-bit Ă«shtĂ« gost3410_vko.kek_34102012256() (KEK — çelĂ«si i enkripcioni).

    ÇelĂ«si i sesionit i prodhuar tashmĂ« Ă«shtĂ« njĂ« sekuencĂ« pseudotrashe 256-bit. Prandaj, mund tĂ« pĂ«rdoret menjĂ«herĂ« nĂ« funksionin HKDF. Duke qenĂ« se GOST34112012256 pĂ«rputhet me ndĂ«rfaqen hashlib, mund tĂ« pĂ«rdoret menjĂ«herĂ« nĂ« klasĂ«n Hkdf. Ne nuk e specifikojmĂ« kripĂ«n (argumenti i parĂ« nĂ« Hkdf) sepse çelĂ«si i prodhuar pĂ«r shkak tĂ« efemeritetit tĂ« çifteve tĂ« çelĂ«save do tĂ« jetĂ« i ndryshĂ«m pĂ«r çdo sesion dhe ai tashmĂ« ka mjaft entropi. kdf.expand() pĂ«r default jep tashmĂ« çelĂ«sa me gjatĂ«si 256-bit, tĂ« nevojshme pĂ«r Kuzuçin nĂ« vazhdim.

    Më pas kontrollohen pjesët TBE dhe TBS të mesazhit të pranuar:

    • llogaritet dhe kontrollohet MAC mbi tekstin e enkriptuar tĂ« pranuar;
    • tekstin e enkriptuar dekriptohet;
    • struktura TBE dekodifikohet;
    • nga ajo merret identifikuesi i palĂ«s biseduese dhe kontrollohet a na Ă«shtĂ« i njohur;
    • llogaritet dhe kontrollohet MAC mbi kĂ«tĂ« identifikues;
    • kontrollohet nĂ«nshkrimi mbi strukturĂ«n TBS, e cila pĂ«rfshin skedat e tĂ« dy palĂ«ve dhe çelĂ«sin publik efemer tĂ« palĂ«s sĂ« kundĂ«rt. NĂ«nshkrimi kontrollohet me çelĂ«sin e nĂ«nshkrimit afatgjatĂ« tĂ« palĂ«s biseduese.

     441     provo:
     442         emri_peer = valide_tbe(
     443             mesazhi_handshake1,
     444             çelësi_handshake1_mac_identity,
     445             çelësi_handshake1_enc,
     446             çelësi_handshake1_mac,
     447             cookie_yne,
     448             cookie_tyre,
     449             pub_tyre_raw,
     450         )
     451     përjashto ValueError si gabim:
     452         logging.warning("%s: %s, duke u shkëputur", _id, gabim)
     453         shkruesi.mbyll()
     454         kthe
     455     # }}}
    
     128 def valide_tbe(
     129         mesazhi_handshake: Union[MsgHandshake1, MsgHandshake2],
     130         çelësi_mac_identity: bytes,
     131         çelësi_enc: bytes,
     132         çelësi_mac: bytes,
     133         cookie_tyre: Cookie,
     134         cookie_yne: Cookie,
     135         pub_çelësi_yne: PubKey,
     136 ) -> str:
     137     ciphertext = bytes(mesazhi_handshake["ciphertext"])
     138     mac_tag = mac(GOST3412Kuznechik(çelësi_mac).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext)
     139     nëse jo krahaso_digest(mac_tag, bytes(mesazhi_handshake["ciphertextMac"])):
     140         ngrit_valueError("MAC i pavlefshëm")
     141     plaintext = ctr(
     142         GOST3412Kuznechik(çelësi_enc).encrypt,
     143         KUZNECHIK_BLOCKSIZE,
     144         ciphertext,
     145         8 * b"x00",
     146     )
     147     provo:
     148         tbe, _ = HandshakeTBE().decode(plaintext)
     149     përjashto ASN1Error:
     150         ngrit_valueError("nuk mund të dekodoj TBE")
     151     çelësi_nënshkrim_pub_hash = bytes(tbe["identity"])
     152     peer = KEYSET.get(çelësi_nënshkrim_pub_hash)
     153     nëse peer është None:
     154         ngrit_valueError("identitet i panjohur")
     155     mac_tag = mac(
     156         GOST3412Kuznechik(çelësi_mac_identity).encrypt,
     157         KUZNECHIK_BLOCKSIZE,
     158         çelësi_nënshkrim_pub_hash,
     159     )
     160     nëse jo krahaso_digest(mac_tag, bytes(tbe["identityMac"])):
     161         ngrit_valueError("MAC i identitetit i pavlefshëm")
     162     tbs = HandshakeTBS((
     163         ("cookieTyre", cookie_tyre),
     164         ("cookieYne", cookie_yne),
     165         ("pubÇelĂ«siYne", pub_çelĂ«si_yne),
     166     ))
     167     nëse jo gost3410.verify(
     168         CURVE,
     169         peer["pub"],
     170         GOST34112012256(tbs.encode()).digest(),
     171         bytes(tbe["signature"]),
     172     ):
     173         ngrit_valueError("nënshkrim i pavlefshëm")
     174     kthe peer["emri"]
    

    Siç e pĂ«rmenda mĂ« lart, 34.13-2015 pĂ«rshkruan variacione tĂ« ndryshme tĂ« funksionimit tĂ« algoritmeve tĂ« bllokut nga 34.12-2015. Mes tyre ka njĂ« mĂ«nyrĂ« pĂ«r prodhimin e imitoshkallĂ«s, llogaritjes sĂ« MAC-it. NĂ« PyGOST kjo Ă«shtĂ« gost3413.mac(). Kjo mĂ«nyrĂ« kĂ«rkon kalimin e funksionit tĂ« enkriptimit (qĂ« merr dhe kthen njĂ« bllok tĂ« vetĂ«m tĂ« dhĂ«nash), madhĂ«sinĂ« e bllokut tĂ« enkriptimit dhe, nĂ« vetvete, tĂ« dhĂ«nat. Pse nuk mund tĂ« kodosh me fort madhĂ«sinĂ« e bllokut tĂ« enkriptimit? 34.12-2015 pĂ«rshkruan jo vetĂ«m enkriptimin 128-bit Kyznechik, por gjithashtu edhe 64-bitin Magma — njĂ« version pak tĂ« modifikuar i GOST 28147-89, i krijuar qĂ« nga KGB dhe ende ka njĂ« nga pragjet mĂ« tĂ« larta tĂ« sigurisĂ«.

    Kuznechik inicializohet duke thirrur gost.3412.GOST3412Kuznechik(key) dhe kthen një objekt me metodat .encrypt()/.decrypt() të përshtatshme për kalim në funksionin 34.13. MAC-u llogaritet si më poshtë: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). Për të krahasuar MAC-in e llogaritur me atë të ardhur nuk mund të përdoret krahasimi i zakonshëm (==) i string-ëve të bajtëve, pasi kjo operacion jep rrjedhje të kohës së krahasimit, e cila, në rastin më të përgjithshëm, mund të çojë në dobësi fatale si BEAST sulmet ndaj TLS. Në Python ka një funksion të veçantë hmac.compare_digest për këtë.

    Funksioni i algoritmit tĂ« enkriptimit mund tĂ« enkriptojĂ« vetĂ«m njĂ« bllok tĂ« dhĂ«nash. PĂ«r mĂ« shumĂ« se njĂ« bllok, dhe pĂ«r gjatĂ«si qĂ« nuk janĂ« shumĂ«fish, Ă«shtĂ« e nevojshme tĂ« pĂ«rdoret njĂ« mod i enkriptimit. NĂ« 34.13-2015 pĂ«rshkruhen: ECB, CTR, OFB, CBC, CFB. Secili ka fushat e veta tĂ« lejueshme tĂ« pĂ«rdorimit dhe karakteristikat. FatkeqĂ«sisht, akoma nuk kemi standardizuar modet e enkriptimit tĂ« autentikuara (si CCM, OCB, GCM dhe tĂ« ngjashme) — jemi tĂ« detyruar tĂ« shtojmĂ« sĂ« paku MAC. UnĂ« zgjedh modin e numĂ«rimit (CTR): ai nuk kĂ«rkon plotĂ«sim deri nĂ« madhĂ«sinĂ« e bllokut, mund tĂ« ekzekutohet paralelisht, pĂ«rdor vetĂ«m funksionin e enkriptimit, mund tĂ« pĂ«rdoret nĂ« mĂ«nyrĂ« tĂ« sigurt pĂ«r enkriptimin e njĂ« numri tĂ« madh mesazhesh (ndryshe nga CBC, ku relativisht shpejt fillojnĂ« kolizionet).

    Si .mac(), .ctr() merr të dhëna të ngjashme si input: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Kërkohet caktimi i vektorit të inicializimit, i gjatë saktësisht sa gjysma e bllokut të enkriptimit. Nëse çelësi ynë i enkriptimit përdoret vetëm për të encryptuar një mesazh (ndonjëherë edhe për disa blloqe), është e sigurt të caktohet një vektor inicializimi zero. Për enkriptimin e mesazheve handshake përdorim çdo herë një çelës të veçantë.

    Kontrolli i nënshkrimit gost3410.verify() është trivial: dërgojmë kurbën eliptike brenda së cilës punojmë (cila thjesht e fixojmë në protokollin tonë GOSTIM), çelësin publik të nënshkruesit (mos harroni se kjo duhet të jetë një tuple e dy numrave të mëdhenj, dhe jo një string bajtësh), hash 34.11-2012 dhe nënshkrimin që ka ardhur.

    Më pas, në iniciator ne përgatisim dhe dërgojmë mesazhin handshake2 të dorëzimit, duke kryer të njëjtat veprime që bëmë gjatë kontrollit, por në mënyrë simetrike: nënshkrimi me çelësat tanë në vend të kontrollit, etj...

     456     # Përgatit dhe dërgo Mesazhin Handshake 2 {{{
     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: sesioni u krijua: %s", _id, peer_name)
     

    Kur sesioni është krijuar, prodhohen çelësat e transportit (një çelës i veçuar për enkriptimin, për autentifikimin, për secilën nga palët), inicializohet Kuznechik për dekriptimin dhe verifikimin e MAC-it:

     499     # Funksioni i dërguesit të mesazheve, inicializo dekoderin e transportit {{{
     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     # Prit për mesazhet testuese {{{
     521     while True:
     522         data = await reader.read(MaxMsgLen)
     ...
     530             msg, tail = Msg().decode(buf)
     ...
     537         try:
     538             await msg_receiver(
     539                 msg.value,
     540                 nonce_expected,
     541                 macer,
     542                 encrypter,
     543                 peer_name,
     544             )
     545         except ValueError as err:
     546             logging.warning("%s: %s", err)
     547             break
     548         nonce_expected += 1
     549     # }}}
    

    msg_sender korutina tani enkripton mesazhet para dĂ«rgimit nĂ« lidhjen TCP. Çdo mesazh ka njĂ« nonce qĂ« rritet monotonikisht, gjithashtu duke shĂ«rbyer si vektor inicializimi gjatĂ« enkriptimit nĂ« modalitetin e numĂ«ruesit. Vlerat e numĂ«ruesit nĂ« çdo mesazh dhe bllok mesazhi do tĂ« garantohet tĂ« kenĂ« ndryshime.

    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
    

    Mesazhet që vijnë përpunohen nga korutina msg_receiver, e cila merret me autentifikimin dhe dekriptimin:

    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("vlera e papritur e nonce")
        mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode())
        if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])):
            raise ValueError("MAC e pavlefshme")
        plaintext = ctr(
            encrypter,
            KUZNECHIK_BLOCKSIZE,
            bytes(payload["ciphertext"]),
            long2bytes(nonce_expected, 8),
        )
        text = plaintext.decode("utf-8")
        await OUT_QUEUES[peer_name].put(text)
    

    Përfundimi

    GOSTIM parashikohet të përdoret ekskluzivisht për qëllime edukative (pasi nuk është testuar, si minimum)! Kodi burimor i programit mund të shkarkohet këtu (Stribog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Si sihtë të gjitha projektet e mia, lloji GoGOST, PyDERASN, NNCP, GoVPN, GOSTIM është plotësisht softuer i lirë, i përhapur sipas kushteve GPLv3+.

    Serguei Matveev, cryptopunk, anëtar i Fondit të Softuerëve të Lirë, zhvillues Python/Go, specialist kryesor FGUP "NTC Atlas".

Burimi: habr.com

Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster