GOSTIM: P2P F2F E2EE IM bir gecə ərzində GOST-kriptoqrafiya ilə

İnkişaf etdirici olaraq PyGOST kitabxanası (GOST kriptoqrafik primitivləri saf Python-da), tez-tez sadə təhlükəsiz mesajlaşma tətbiqi necə həyata keçirəcəyimlə bağlı suallar alıram. Bir çoxu tətbiqi kriptoqrafiyanı olduqca asan bir şey hesab edir və .encrypt() çağırışı blok şifrəsi üçün təhlükəsiz ötürmə üçün kifayətdir. Digərləri isə tətbiqi kriptoqrafiyanın yalnız bir neçə nəfərin işi olduğunu düşünür və zəngin şirkətlərin, məsələn, Telegram-ın matəmatiqa üzrə olimpiada iştirakçıları ilə bunu həyata keçirməsi qəbuledilməzdir. təhlükəsiz protokol həyata keçirə bilmir.

Bütün bunlar məni bu məqaləni yazmağa təşviq etdi ki, kriptoqrafik protokolları və təhlükəsiz IM yaratmağın o qədər də çətin bir iş olmadığını göstərim. Lakin, öz autentifikasiya və açar mübadiləsi protokollarını icad etmək olmaz.

GOSTIM: P2P F2F E2EE IM bir gecə ərzində GOST-kriptoqrafiya ilə
Məqalədə yazılacaq peer-to-peer, friend-to-friend, end-to-end şifrələnmiş instant messenger ilə SIGMA-I autentifikasiya və açar mübadiləsi protokolunun (hansının əsasında IPsec IKE) yalnızca GOST kriptoqrafiya alqoritmləri PyGOST kitabxanası və ASN.1 mesajlarının kodlanması üçün PyDERASN (haqqında artıq əvvəlki yazılarımda) danışmışdım. Zəruri şərt: o qədər sadə olmalıdır ki, sıfırdan bir gecədə (və ya iş günündə) yazıla bilsin, əks halda bu artıq sadə proqram deyil. İçində mütləq səhvlər, artıq çətinliklər, çatışmazlıqlar var, həmçinin bu mənim asyncio kitabxanasından istifadə edən ilk proqramımdır.

IM dizaynı

İlk öncə, bizim IM-ın necə görünəcəyini anlamaq lazımdır. Sadəlik üçün, bu peer-to-peer şəbəkə olsun, iştirakçıların aşkar edilməsi olmadan. Özümüz biliklə əlaqə qurmaq üçün hansı ünvana: porta qoşulacağımızı göstərəcəyik.

Mən başa düşürəm ki, hazırda, iki təsadüfi kompüter arasında birbaşa əlaqənin mövcudluğunu fərz etmək - IM-nin praktik tətbiqinin əhəmiyyətli məhdudiyyətidir. Ancaq daha çox inkişaf etdiricilər NAT-traversal köməkçiləri tətbiq etdikcə, biz IPv4 İnternetində daha uzun müddət qalacağıq, iki təsadüfi kompüter arasında əlaqə qurma ehtimalı azaltacağıq. İv6-nın evdə və işdə olmaması nə qədər dözülməzdir?

Bizim friend-to-friend şəbəkəmiz olacaq: mümkün olan bütün söhbətçilər əvvəldən məlum olmalıdır. Birincisi, bu hər şeyi xeyli asanlaşdırır: tanışdılar, ad/şifrəni tapdılar ya da tapa bilmədilər, əlaqəni kəsdilər ya da işə davam etdilər, tanışan bilərək. İkincisi, ümumi olaraq, bu təhlükəsizdir və bir çox hücumları istisna edir.

IM-nın interfeysi klassik həllərə yaxın olacaq suckless-proyektləri, minimalistliyi və Unix yolu fəlsəfəsi ilə çox bəyəndiyim.

  • in — göndərilən mesajların yazıldığı yerdir;
  • out — qəbul edilən mesajların oxunduğu yerdir;
  • state — ondan oxuyarkən, indi müqaviləçinin qoşulub- qoşulmadığını, ünvan / port qoşulmasını öyrənirik.

Həmçinin, conn soketi yaradılır, içərisinə host port yazılır, biz uzaq müqaviləçiyə qoşulmağı başlatırıq.

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

Bu yanaşma, IM transportunun və istifadəçi interfeysinin müstəqil icralarını yaratmağa imkan tanıyır, çünki zövq və rəng uyğun deyil, hər kəsə məmnuniyyət vermək mümkün deyil. İstifadə edərək tmux və / ya multitail, sintaksis təhlilatı ilə pəncərəli interfeysi ala bilərsiniz. Və rlwrap ilə mesajların daxil edilməsi üçün GNU Readline uyğun xətti əldə edə bilərsiniz.

Əslində, suckless layihələri FIFO fayllarından istifadə edir. şəxsi olaraq, asyncio ilə fayllarla necə eyni anda işləməyimi başa düşə bilmədim, ayrı ipliklərdən ibarət köməkçi təbəqə olmadan (bu cür şeylər üçün uzun zamandır Go) istifadə edirəm. Buna görə Unix domain soketləri ilə kifayətlənməyə qərar verdim. Təsüf ki, bu, echo 2001:470:dead::babe 6666 > conn etmək imkanı yaradır. Bu problemi həll etmək üçün istifadə etdim socat: echo 2001:470:dead::babe 6666 | socat — UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.

İlkin təhlükəsiz olmayan protokol

Nəqliyyat olaraq TCP istifadə olunur: bu, çatdırılmanı və onun sırasını təmin edir. UDP bunun ikisini də təmin etmir (bu, kriptoqrafiya tətbiq olunurken faydalı olardı), və SCTP Python-da daxili olaraq yoxdur.

Təəssüf ki, TCP-də mesaj anlayışı yoxdur, yalnız bayt axını var. Buna görə, bu axında bir-birindən ayırabilmək üçün mesajlar üçün bir format düşünmək lazımdır. Biz xətti tərcüməsi olan simvoldan istifadə etməyi razılaşa bilərik. Başlanğıc üçün bu yaxşıdır, lakin mesajlarımızı şifrələməyə başladığımızda, bu simvol şifrə mətni içində hər yerdə ola bilər. Bu səbəbdən şəbəkələrdə mesajların uzunluğunu baytlarda göndərən protokollar məşhurdur. Məsələn, Python-da daxili olaraq xdrlib var ki, belə formatla işləməyə imkan tanıyır XDR.

Biz TCP oxumaqla düzgün və effektiv işləməyəcəyik — kodu sadələşdirəcəyik. Soketdən sonsuz dövr ilə məlumatları oxuyuruq, tam mesajı deşifrə etmədiyimiz müddətcə. Bu yanaşma üçün bir format olaraq JSON və XML istifadə edə bilərik. Amma kriptoqrafiya əlavə olunduqda, məlumatları imzalayıb təsdiqləmək lazım olacaq — bu isə obyektlərin bayt-bayt uyğun olması tələb edir, bu isə JSON / XML ilə təmin edilmir (dumpsın nəticəsi fərqlənə bilər).

XDR bu cür tapşırıq üçün uyğundur, lakin mən ASN.1 ilə DER kodlamasını seçirəm və PyDERASN kitabxanadır, çünki əlimizdə yüksək səviyyəli obyektlər olacaq ki, onlarla işləmək daha rahatdır. Schemaless-dən fərqli olaraq bencode, MessagePackCBOR, ASN.1 avtomatik olaraq verilənləri sərt şəkildə təyin edilmiş sxemə qarşı yoxlayacaq.

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

Gələn mesaj Msg olacaq: ya MsgText (bir mətn sahəsi ilə), ya da MsgHandshake (burada danışıq tərəfinin adı ötürülür). Hazırda komplike görünür, lakin bu gələcək üçün bir hazırlıqdır.

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

IM şifrələmə olmadan

Daha əvvəl də dediyim kimi, socket-lərlə bütün əməliyyatlar asyncio kitabxanası istifadə edilərək həyata keçiriləcək. İşə salındıqda gözlədiyimiz şeyləri elan edək:

parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
    "--our-name",
    required=True,
    help="Bizim peer adı",
)
parser.add_argument(
    "--their-names",
    required=True,
    help="Onların peer adları, vergül ilə ayrılmış",
)
parser.add_argument(
    "--bind",
    default="::1",
    help="Dinləyəcəyimiz ünvan",
)
parser.add_argument(
    "--port",
    type=int,
    default=6666,
    help="Dinləyəcəyimiz port",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))

Öz adını təyin edirsiniz (—our-name alice). Vergüllə ayrılmış gözlənilən bütün danışıq tərəflərini sadalayın (—their-names bob,eve). Hər bir danışıq tərəfi üçün Unix socket-lərlə bir qovluq yaradılır və hər biri üçün bir coroutine yaradılır: in, out, state:

for peer_name in THEIR_NAMES:
    makedirs(peer_name, mode=0o700, exist_ok=True)
    out_queue = asyncio.Queue()
    OUT_QUEUES[peer_name] = out_queue
    asyncio.ensure_future(asyncio.start_unix_server(
        partial(unixsock_out_processor, out_queue=out_queue),
        path.join(peer_name, "out"),
    ))
    in_queue = asyncio.Queue()
    IN_QUEUES[peer_name] = in_queue
    asyncio.ensure_future(asyncio.start_unix_server(
        partial(unixsock_in_processor, in_queue=in_queue),
        path.join(peer_name, "in"),
    ))
    asyncio.ensure_future(asyncio.start_unix_server(
        partial(unixsock_state_processor, peer_name=peer_name),
        path.join(peer_name, "state"),
    ))
asyncio.ensure_future(asyncio.start_unix_server(unixsock_conn_processor, "conn"))

İstifadəçidən gələn mesajlar in socket-dən IN_QUEUES növbələrinə göndərilir:

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

Danışıq tərəflərindən gələn mesajlar OUT_QUEUES növbələrinə göndərilir, buradan verilənlər out socket-ə yazılır:

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

state socket-dən oxuyarkən, proqram PEER_ALIVE lüğətində danışıq tərəfinin ünvanını axtarır. Əgər danışıq tərəfi ilə hələ bağlantı yoxdursa, boş bir sətir yazılır.

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

conn socket-in ünvan yazıldıqda, bağlantının "inicinatçısı" funksiyası çağırılır:

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

İnişiyatoru nəzərdən keçirək. Əvvəlcə o, açıq-aşkar müəyyən edilmiş host/port ünvanına bağlantı açır və öz adı ilə handshake mesajı göndərir:

 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 mesajı {{{
 135     writer.write(Msg(("handshake", MsgHandshake((
 136         ("peerName", OUR_NAME),
 137     )))).encode())
 138     # }}}
 139     await writer.drain()

Daha sonra, uzaq tərəfdən cavab gözləyir. Gələn cavabı Msg ASN.1 sxemi ilə deşifrə etməyə çalışır. Bütün mesajın bir TCP seqmentində göndəriləcəyini və onu .read() çağırışında atomik olaraq alacağımızı qəbul edirik. Handshake mesajını aldığımızı yoxlayırıq.

 141     # Handshake mesajını gözləyin {{{
 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     # }}}

Gələn qarşı tərəfin adı ilə tanış olunmaığını yoxlayırıq. Əgər tanış deyilsə, bağlantını qırırıq. Onunla artıq əlaqə qurmadığımızı yoxlayırıq (qarşı tərəf yenidən bizə qoşulmaq əmrini verdikdə) və onu bağlayırıq. IN_QUEUES növbəsinə mesaj mətnini ifadə edən Python sətirləri yerləşdirilir, lakin None xüsusi dəyəri msg_sender korutinasının işini dayandırması üçün siqnal verir ki, o, köhnə TCP bağlantısı ilə bağlı writer-in unudulmasını bilsin.

 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     # Mətn mesajı göndərmək, nəqliyyat deşifrə edənini işə salın {{{
 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 çıxan mesajları (in socket-dan növbəyə qoyulmuş) qəbul edir, onları MsgText mesajına serialize edir və TCP bağlantısı vasitəsilə göndərir. O, istənilən vaxt kəsilə bilər — biz bunu aydın şəkildə yaxalayırıq.

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

Sonda sonunda initiator sonsuz bir döngüye girerek soketten gelen mesajları okumaya başlar. Mesajların metin mi yoksa başka bir tür mü olduğunu kontrol eder ve OUT_QUEUES kuyruğuna yerleştirir; buradan ilgili muhataba gönderilecektir. Neden sadece .read() yapıp mesajı çözümlemeden geçemiyoruz? Çünkü kullanıcılardan gelen birkaç mesajın işletim sistemi tamponunda bir araya getirilip tek bir TCP segmenti olarak gönderilme durumu imkansız değil. Çözümleyebildiğimiz ilk mesaj olur, ancak tamponda bir sonraki ile ilgili kısım kalabilir. Herhangi bir sorun durumunda TCP bağlantısını kapatıyor ve msg_sender korutiniyi durduruyoruz (OUT_QUEUES kuyruğuna None göndererek).

 174     buf = b""
 175     # Test mesajlarını bekle {{{
 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: maksimum tampon boyutu aşıldı", _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: beklenmedik %s mesajı", _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: bağlantıyı kesiyor: %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: %d karakterli mesaj alındı", peer_name, len(text))
  69     await OUT_QUEUES[peer_name].put(text)

Ana kodumuza geri dönelim. Program başlatıldığında tüm korutinler oluşturulduktan sonra, TCP sunucusunu başlatırız. Her kurulan bağlantı için bir responder (cevaplayıcı) korutini oluşturur.

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

responder, initiator ile benzerlik gösterir ve tüm aynı eylemleri tersle yapar, ancak sonsuz mesaj okuma döngüsü hemen başlatılır, basitlik için. Şu anda el sıkışma protokolü her taraftan birer mesaj gönderiyor, ancak gelecekte, bağlantıyı başlatan iki mesaj gönderecek ve ardından metin gönderimi hemen mümkün olacaktır.

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

Təhlükəsiz protokol

İndi komunikasiyamızı təhlükəsizləşdirməyin vaxtı gəldi. Təhlükəsizlik dedikdə nəyi nəzərdə tuturuq və nə istəyirik:

  • göndərilən mesajların məxfiliyi;
  • göndərilən mesajların autentikliyi və bütövlüyü — onların dəyişdirilməsi aşkar olunmalıdır;
  • təkraredici hücumlardan (replay attack) qorunma — mesajların itirilməsi və ya təkrarı aşkar olunmalı (və biz əlaqəni kəsməyə qərar veririk);
  • müqavilələrdə dəqiqlik və autentikliyə görə tərəflərin əvvəlcədən daxil edilmiş açarları ilə tanınması — artıq dost-dost şəbəkəsi qurmağı qərara almışıq. Ancaq autentifikasiyadan sonra kiminlə ünsiyyət qurduğumuzu anlayacağıq;
  • mövcudluğu perfect forward secrecy xüsusiyyətləri (PFS) — uzunmüddətli imza açarımızın kompromisi əvvəlki bütün yazışmaların oxunma imkanına səbəb olmamalıdır. İtirilən trafikin qeydi mənasız olur;
  • mesajların (nəqliyyat və əl sıxdığı) yalnız bir TCP seansının çərçivəsində etibarlılığı. Başqa bir seansdan (hətta eyni tərəfdaşla belə) düzgün imzalanmış/audited edilmiş mesajların daxil edilməsi mümkün olmamalıdır;
  • passiv müşahidəçi istifadəçi identifikasiyalarını, göndərilən uzunmüddətli açarları və onların üzrə olan hash-ları görməməlidir. Passiv müşahidəçidən müəyyən bir anonimlik.

Təəccüblüdür, amma bu minimum demək olar ki, hər kəsin istədiyi bir şeydir hər hansı bir imza protokolunda, və siyahıda olanların çox azı axırda "yerli" protokollar üçün yerinə yetirilir. İndi yeni bir şey icad etməyəcəyik. Mən mütləq Noise framework protokolların qurulması üçün istifadə etməyi tövsiyə edərdim, amma daha sadə bir şey seçək.

İki protokol ən populyardır:

  • TLS — uzun bir səhv, nöqsan, zəiflik, pis planlaşdırma, çətinlik və qüsurlar tarixinə malik olan mürəkkəb bir protokoldur (lakin TLS 1.3-ə bu çox aid deyil). Amma bunun mürəkkəbliyinə görə onu nəzərdən keçirməyəcəyik.
  • IPsec c IKE — ciddi kriptoqrafik problemləri yoxdur, baxmayaraq ki, onlar da sadə deyil. IKEv1 və IKEv2 haqqında oxuduqda, onların mənşəyinin STS, ISO/IEC IS 9798-3 və SIGMA (SIGn-and-MAc) protokolları — bir gecədə həyata keçirmək üçün kifayət qədər sadədir.

SIGMA, STS/ISO protokollarının son inkişaf mərhələsi olaraq nə ilə yaxşıdır? O, bütün tələblərimizi (o cümlədən tərəfdaşların identifikatorlarının "gizlənməsi") ödənir, tanınmış kriptoqrafik problemləri yoxdur. O, minimalistdir — protokol mesajından yalnız bir elementi çıxarıldıqda, o, təhlükəsiz olmayacaq.

Gəlin, ən sadə yerli protokoldan SIGMA-ya qədər irəliləyə bilək. Bizim üçün maraqlı olan ən əsas əməliyyat açarların razılaşdırılmasıdır: burada hər iki iştirakçı simmetrik açar olaraq istifadə edilə biləcək eyni dəyəri əldə edəcək. Detallara girmədən: hər tərəf efemeral (yalnız bir sessiya üçün istifadə olunan) açar cütü (ictimai və özəl açarlar) yaradır, ictimai açarları bir-biri ilə dəyişir, razılaşdırma funksiyasını çağırır, burada öz özəl açarını və tərəfdaşının ictimai açarını təqdim edir.

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

Hər kəs ortada ola və ictimai açarları öz açarları ilə əvəz edə bilər — bu protokolda tərəfdaşların kimliyinin doğrulanması yoxdur. Gəlin uzunömürlü açarlarla imza əlavə edək.

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

Bu imza uyğun deyil, çünki o, müəyyən bir sessiyaya bağlanmamışdır. Belə mesajlar "uyğun" olacaq və digər iştirakçılarla olan sessiyalar üçün də keçərlidir. Bütün kontekst imza edilməlidir. Bu, A-dan başqa bir mesaj göndərməyimizi tələb edir.

Bundan əlavə, imzanın altında öz identifikatorumuzu əlavə etmək kritikdir, çünki əks halda, biz IdXXX-i dəyişdirə bilərik və mesajı digər tanınmış söhbət iştirakçısının açarı ilə yenidən imzalaya bilərik. Bunun qarşısını almaq üçün reflection hücumları, imza altında olan elementlərin mənaca dəqiq təyin edilmiş yerlərdə olması vacibdir: əgər A (PubA, PubB) imzalayırsa, B (PubB, PubA) imzalamalıdır. Bu, həmçinin serialize edilmiş verilənlərin strukturu və formatının seçimindəki əhəmiyyətini vurğulayır. Məsələn, ASN.1 DER kodlaşdırmasında çoxluqlar sıralanır: SET OF(PubA, PubB) SET OF(PubB, PubA) ilə eynidir.

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

Ancaq hələ də bu sessiya üçün eyni ümumi açarın istehsal olunduğunu «sübut» etməmişik. Əslində, bu mərhəldən keçmədən də edə bilərik — ilk transport mesajı qeyri-valide olacaq, amma istəyirik ki, danışıq başa çatdıqda hər şeyin həqiqətən razılaşdırıldığından əmin olaq. Hazırda ISO/IEC IS 9798-3 protokoluna sahibik.

Biz istehsal olunan açarı da imzalaya bilərdik. Bu təhlükəlidir, çünki istifadə olunan imza algoritmində sızmalar ola bilər (həyatda bitlər imza üçün də olsa, amma hələ də sızma var). İstehsal olunan açarın hashını imzalamaq mümkündür, amma istehsal olunan açarın hashının sızması, istehsal funksiyasına brute-force hücum edilməsi zamanı dəyərli ola bilər. SIGMA, göndərən şəxsinin idendifikatorunu doğrulayan MAC funksiyasını istifadə edir.

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

Optimallaşdırma məqsədilə bəziləri efemer açarlarını yenidən istifadə etmək istəyir (bu, əlbəttə, PFS üçün fəlakətlidir). Məsələn, biz açar cütü yaratdıq, qoşulmağa çalışdıq, lakin TCP mövcud deyil və ya protokolun ortasında kəsildi. Yeni bir cüt üçün sərf olunan entropiya və prosessor resurslarını itirməkdə təəssüflənməməliyik. Buna görə, bir növ cookie — efemer ictimai açarların təkrar istifadəsi zamanı mümkün təsadüfi replay hücumlarından qoruyacaq saxta təsadüfi dəyər təqdim edirik. Cookie və efemer ictimai açar arasındakı bağlayıcılıq səbəbindən, əks tərəfin ictimai açarını imzadan kənarlaşdırmaq mümkündür.

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

Nəhayət, biz aktiv gözləyicidən qarşı tərəfimizin identifikasiyalarının gizliliyini qorumaq istəyirik. Bunun üçün SIGMA ilk olaraq müvəqqəti açarların mübadiləsini təklif edir, sonra ortaq açar istehsal edilir, bunun əsasında identifikasiyanı və autentifikasiya mesajlarını şifrələyir. SIGMA iki variantı təsvir edir:

  • SIGMA-I — başlatanı aktiv hücumlardan, cavab verəni isə passiv hücumlardan qoruyur: başlatan cavab verəni autentifikasiya edir və əgər bir şey səhvdirsə, öz identifikasiyasını açıqlamır. Cavab verən isə aktiv protokol başlandıqda öz identifikasiyasını açıqlayır. Passiv gözləyici heç bir məlumat əldə etmir;
    SIGMA-R — cavab verəni aktiv hücumlardan, başlatanı isə passiv hücumlardan qoruyur. Hər şey əksinədir, lakin bu protokolda artıq dörd mesaj mübadiləsi olur.

    SIGMA-I-ni seçirik, çünki bu, müştəri-server münasibətlərindən gözlədiyimizə daha çox oxşayır: müştəri yalnız autentifikasiya olunmuş serveri tanıyır, server isə hər şeyi bilir. Üstəlik, mesaj mübadiləsinin az olmasından dolayı icra etməyi daha asandır. Protokolda yalnız mesajın bir hissəsinin şifrələnməsi və A identifikatorunun son mesajın şifrələnmiş hissəsinə köçürülməsi var:

    ┌─────┐                                                                        ┌─────┐
    │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, ...)║
       │                                                                              │ ╚═════════════════════╝
       │                                                                              │
    
    • İmzalama üçün GOST R 34.10-2012 256-bit açar algoritmasından istifadə olunur.
    • Ümumi açarın əldə edilməsi üçün 34.10-2012 VKO istifadə olunur.
    • MAC olaraq CMAC istifadə olunur. Texniki olaraq bu, GOST R 34.13-2015-də təsvir olunan blok şifrəsinin xüsusi iş rejimidir. Bu rejimdə şifrləmə funksiyası — Küznəcik (34.12-2015).
    • Hər bir tərəfdaşın identifikasiyası olaraq onun publik açarının hash-dan istifadə olunur. Hash olaraq istifadə edilir Stribog-256 (34.11-2012 256 bit).

    Sözügedən razılaşmadan sonra bizim ümumi açarımız olacaq. Bunu biz autentifikasiya olunmuş transport mesajlarının şifrələnməsi üçün istifadə edə bilərik. Bu hissə tamamilə sadədir və burada səhv etmək çətindir: mesaj sayğacını artırırıq, mesajı şifrələyirik, sayğacı və şifrlənmiş mətni autentifikasiyalaşdırırıq (MAC) və göndəririk. Mesaj alınanda sayğacın gözlənilən dəyərinin olub-olmadığını yoxlayırıq, şifrlənmiş mətni sayğac ilə autentifikasiyalaşdırırıq və deşifrə edirik. Rəhbərlik mesajlarını, transportlarını şifrələmək üçün hansı açarı istifadə etmək lazımdır? Bütün bu tapşırıqlar üçün bir açar istifadə etmək təhlükəlidir və ağılsızdır. Xüsusi funksiyalardan istifadə edərək açarların yaradılması mütləqdir. KDF (açarın əldə edilməsi funksiyası). Yenə də bir şey icad etmədən çalışaq: HKDF uzun müddətdir tanınan, yaxşı araşdırılmışdır və bilinən problemləri yoxdur. Təəssüf ki, Python-un yerli kitabxanasında bu funksiya yoxdur, ona görə də istifadə edirik hkdf paket. HKDF daxilində Yeni fayl sistemi, hansı ki, öz növbəsində, bir hash funksiyasını istifadə edir. Wikipedia səhifəsindəki Python-da tətbiq nümunəsi bir neçə sətir kod götürür. 34.10-2012 kimi, hash funksiyası olaraq Stribog-256-dan istifadə edəcəyik. Açar uyğunluq funksiyamızın çıxışı seans açarı olacaq, onun əsasında əskik simmetrik açarlar hazırlanacaq:

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

    Strukturlar/sxemlər

    İndi bu məlumatların ötürülməsi üçün hansı ASN.1 strukturlarını əldə etdiyimizə nəzər salaq:

    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 — imzalanacaq olan (to be signed). HandshakeTBE — şifrələnəcək olan (to be encrypted). MsgHandshake1-dəki ukm sahəsi (user keying material) haqqında məlumat verir, daha çox təsadüfi açarların istehsalı üçün 34.10 VKO bu parametri əlavə edir — sadəcə əlavə entropiya.

    Koda kriptografiya əlavə etmək

    Sadəcə orijinal koda edilən dəyişiklikləri nəzərdən keçirəcəyik, çünki çərçivə eyni qalmışdır (əslində, əvvəlcə son versiya yazılmışdır, sonra kriptologiya silinmişdir).

    Söhbətçilərin autentifikasiyası və identifikasiyası açıq açarlar vasitəsilə keçiriləcək, ona görə də indi onları uzun müddət saxlamaq lazımdır. Asanlıq üçün belə bir JSON istifadə edək:

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

    bizim — açar cütlüklərinin (privat və publik açarların) altşıqı. onların — danışıq iştirakçılarının adları və onların publik açarları. Komanda xəttinin arqumentlərini dəyişək və JSON məlumatlarının post-emaləsini əlavə edək:

    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="Yeni açar cütlüyü ilə JSON yaratmaqdır",
    )
    parser.add_argument(
        "--keys",
        default="keys.json",
        required=False,
        help="Bizim və onların açarlarından ibarət JSON",
    )
    parser.add_argument(
        "--bind",
        default="::1",
        help="Dinləniləcək ünvan",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=6666,
        help="Dinləniləcək port",
    )
    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)
    
    # Bizim və onların açarlarını parsola və aç
    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),
        }
    # }}}
    

    34.10 algoritmının privat açarı — təsadüfi sayıdır. 256-bit həcmindədir, 256-bit elliptik əyrilər üçün. PyGOST byte dəstləri ilə işləmək yerinə böyüklü saylarla, buna görə də bizim privat açarımız (urandom(32)) ədədi formata çevrilməlidir, gost3410.prv_unmarshal() istifadə edilərək. Publik açar privatdan müəyyən edilmiş qaydada hesablanır, gost3410.public_key() istifadə edilərək. 34.10 publik açarı — iki böyük ədəd, hansı ki, rahatlıq üçün saxlanılması və ötürülməsi üçün byte ardıcıllığına çevrilməlidir, gost3410.pub_marshal() istifadə edilərək.

    JSON faylını oxuduqdan sonra publik açarlar müvafiq olaraq geri çevrilməlidir, gost3410.pub_unmarshal() istifadə edilərək. Çünki danışıq iştirakçılarından gələn identifikatorlar publik açarın hashı şəklində olacağı üçün, onları əvvəlcədən hesablayıb sürətli axtarış üçün sözlükdə yercə qoya bilərik. Stribog-256 hashı gost34112012256.GOST34112012256(), hashlib interfeysinə tam uyğundur.

    Başlatıcı korutinasında nə dəyişdi? Hər şey, əl sıxma sxeminə uyğun: cookie (128-bit tamamilə yetərlidir), 34.10 efemerik açar cütlüyü yaradırıq ki, bu da VKO açar razılaşma funksiyaı üçün istifadə ediləcək.

     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     # Ephemeral public key and cookie generation, sending 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()
    

    • gözləyirik cavabı və gələn Msg mesajını deşifrə edirik;
    • handshake1 aldığımıza əmin oluruq;
    • qarşı tərəfin ephemeral publik açarını deşifrə edir və sessiya açarını hesablayırıq;
    • mesajın TBE hissəsi üçün zəruri olan simmetrik açarları istehsal edirik.

     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     # Handshake mesajını doğrulama {{{
     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 64-bit bir ədəddir (urandom(8)), bu da gövdə görünümdən deserializasiyanı tələb edir, gost3410_vko.ukm_unmarshal() istifadə edərək. VKO funksiyası üçün 34.10-2012 256-bit bu gost3410_vko.kek_34102012256() dir (KEK — açar şifrələmə açarı).

    İstehsal olunan sessiya açarı artıq 256-bitlik bayt pseudorandom ardıcıllığıdır. Ona görə də, onu dərhal HKDF funksiyasında istifadə edə bilərik. GOST34112012256 hashlib interfeysini təmin etdiyinə görə, Hkdf sinifində birbaşa istifadə edilə bilər. Duz (Hkdf-nin birinci arqumenti) biz göstərmirik, çünki istehsal olunan açar, iştirak edən ephemeral açar cütlərinin səbəbi ilə, hər sessiya üçün fərqli olacaq və onlarda artıq kifayət qədər entropiya var. kdf.expand() standart olaraq 256-bit açar tələb etdiyi üçün hansıkiler Küzneçik üçün sonrakı mərhələdə tələb olunur.

    Daha sonra gələn mesajın TBE və TBS hissələri yoxlanılır:

    • gələn şifrələnmiş mətn üzərində MAC hesablanır və yoxlanılır;
    • şifrələnmiş mətn deşifrə olunur;
    • TBE strukturu deşifrə olunur;
    • ondan tərəfdaşın identifikatoru götürülür və onun bizim üçün məlum olub-olmadığı yoxlanılır;
    • bu identifikator üzərində MAC hesablanır və yoxlanılır;
    • TBS strukturunun imzası yoxlanılır, burada hər iki tərəfin cookie-ləri və qarşı tərəfin publik ephemeral açarı vardır. İmza tərəfdaşın uzun müddətli imza açarı ilə yoxlanılır.

     441     çalış:
     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, bağlantı kesiliyor", _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("geçersiz 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("TBE'yi çözmek mümkün değil")
     151     key_sign_pub_hash = bytes(tbe["identity"])
     152     peer = KEYS.get(key_sign_pub_hash)
     153     if peer is None:
     154         raise ValueError("bilinmeyen kimlik")
     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("geçersiz kimlik 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("geçersiz imza")
     174     return peer["name"]
    

    Daha önce de belirttiğim gibi, 34.13-2015, farklılarını tanımlıyor blok şifrelerinin çalışma modları 34.12-2015. Bunlar arasında, MAC hesabı ve hesaplama işlevi olan imitosyalama modu bulunmaktadır. PyGOST'ta bu, gost3413.mac(). Bu mod, şifreleme işlevinin (bir blok veri kabul eden ve döndüren) aktarımını, şifre blok boyutunu ve ilgili verileri gerektirir. Neden blok boyutunu hardcode edemeyiz? 34.12-2015 yalnızca 128 bitlik Kuznechik şifresini değil, ayrıca 64 bitlik Magma — KGB tarafından oluşturulmuş ve hâlâ en yüksek güvenlik eşiklerinden birine sahip olan, biraz değiştirilmiş GOST 28147-89'dır.

    Kuznechik, gost.3412.GOST3412Kuznechik(key) çağrısıyla başlatılır ve .encrypt()/.decrypt() yöntemleri olan bir nesne döndürür. MAC şu şekilde hesaplanır: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). Hesaplanan ve gelen MAC karşılaştırması için, salt byte dizeleri kullanarak standart karşılaştırma (==) kullanılamaz çünkü bu işlem, zaman sızdırma gerektirir, bu da genel olarak kritik güvenlik açığına yol açabilir olan bir durumu tetikleyebilir. BEAST TLS'ye yönelik saldırılar. Python'da bunun için özel bir hmac.compare_digest işlevi vardır.

    Bir blok şifreleme işlevi yalnızca bir veri bloğunu şifreleyebilir. Daha fazla veri için, ayrıca uzunluğu tam katlarıyla olmayan veriler için şifreleme modu kullanmak gerekir. 34.13-2015'te şu modlar tanımlanmıştır: ECB, CTR, OFB, CBC, CFB. Her birinin kendine özgü geçerli uygulama alanları ve özellikleri vardır. Ne yazık ki, hala standartlaştırılmış bir versiyonumuz yok. şifrləmə autentifikasiya rejimləri (CCM, OCB, GCM və bənzərləri tipli) — MAC əlavə etməyə məcburuq. Mən seçirəm sayğac rejimi (CTR): blok ölçüsünü tamamlamaz, paralel işləyə bilər, yalnız şifrləmə funksiyasından istifadə edir, çoxlu mesajları şifrləmək üçün təhlükəsiz şəkildə istifadə oluna bilər (CBC-dən fərqli olaraq, burada nisbətən sürətlə çarpazlama başlayır).

    mac() kimi, .ctr() da bənzər məlumatları qəbul edir: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). İnitialization vektoru dəqiq şifrləmə blokunun yarısı qədər olmalıdır. Əgər şifrləmə açarımız yalnız bir mesajı (bir neçə blokdan ibarət olsa belə) şifrləmək üçün istifadə edilirsə, sıfır başlanğıc vektoru müəyyən etmək təhlükəsizdir. Handshake mesajlarını şifrləmək üçün hər dəfə xüsusi bir açar istifadə edirik.

    gost3410.verify() imzasını yoxlamaq asandır: istifadə etdiyimiz ellipsik əyrini əlavə edirik (onu bizim GOSTIM protokolunda sadəcə olaraq sabitləşdiririk), imza edən şəxsin açıq açarını (bu, iki böyük ədəddən ibarət tuple olmalıdır, bytes string deyil), 34.11-2012 hashini və gələn imzayı göndəririk.

    Daha sonra, initiator-da handshake2 mesajını göndərmək üçün hazırlayırıq, yoxlama zamanı etdiyimiz eyni yolu izləyərək: öz açarlarımızda imza yaratmaq yerinə yoxlama və s…

     456     # Handshake 2 mesajını hazırlayıb göndərin {{{
     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)
     

    Sessiya qurulduqda, transport açarları hazırlanır (şifrələmə, autentifikasiya üçün ayrı hər iki tərəf üzrə bir açar), Küzneçik dekodlaşdırma və MAC yoxlamaq üçün başladılır:

     499     # Text mesaj göndəricisini çalışdırın, transport dekoderini başlatın {{{
     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     # Test mesajlarını gözləyin {{{
     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 korutinası mesajları TCP əlaqəsinə göndərməzdən əvvəl şifrələyir. Hər bir mesajın monoton olaraq artan nonce dəyəri var, bu da şifrləmə zamanı ilkinizasiya vektoru kimi fəaliyyət göstərir. Hər bir mesajda və mesaj bloklarında mütləq fərqli sayğac dəyərləri olacaq.

    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
    

    Gələn mesajlar msg_receiver korutinası tərəfindən, kimlik doğrulama və deşifrə etmə ilə məşğul olan bir korutina tərəfindən işlənir:

    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("gözlənilməz nonce dəyəri")
        mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode())
        if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])):
            raise ValueError("şəxsiyyət doğrulama kodu yanlışdır")
        plaintext = ctr(
            encrypter,
            KUZNECHIK_BLOCKSIZE,
            bytes(payload["ciphertext"]),
            long2bytes(nonce_expected, 8),
        )
        text = plaintext.decode("utf-8")
        await OUT_QUEUES[peer_name].put(text)
    

    Nəticə

    GOSTIM yalnız tədris məqsədləri üçün istifadə edilməlidir (çünki ən azından test edilməmişdir)! Proqramın qaynağı burada endirilə bilər burada (Stribog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Öz layihələrim kimi, bu tip GoGOST, PyDERASN, NNCP, GoVPN, GOSTIM tamamilə açıq mənbə proqramı, aşağıdakı şərtlərlə paylanılır GPLv3+.

    Sergey Matveyev, şifropank, iştirakçı SPF Fondu, Python/Go inkişafçısı, baş mütəxəssis FQUP "NTC 'Atlas'".

Mənbə: habr.com

DDoS qoruması olan saytlara etibarlı hosting satın alın, VPS VDS serverlər 🔥 DDoS qoruması olan saytlara etibarlı hosting satın alın, VPS VDS serverlər | ProHoster