İnkişaf etdirici olaraq 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. 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.

Məqalədə yazılacaq , , instant messenger ilə autentifikasiya və açar mübadiləsi protokolunun (hansının əsasında ) yalnızca GOST kriptoqrafiya alqoritmləri PyGOST kitabxanası və ASN.1 mesajlarının kodlanması üçün (haqqında artıq ) 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 , 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 və / ya , sintaksis təhlilatı ilə pəncərəli interfeysi ala bilərsiniz. Və 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 ) 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 : 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ə 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 .
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ə 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 , və , 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 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 protokolların qurulması üçün istifadə etməyi tövsiyə edərdim, amma daha sadə bir şey seçək.
İki protokol ən populyardır:
- — 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.
- c — ciddi kriptoqrafik problemləri yoxdur, baxmayaraq ki, onlar da sadə deyil. IKEv1 və IKEv2 haqqında oxuduqda, onların mənşəyinin , 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 : 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 , 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 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ı — (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 (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. (açarın əldə edilməsi funksiyası). Yenə də bir şey icad etmədən çalışaq: 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 paket. HKDF daxilində , 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ə , 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 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 — 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. 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. (CCM, OCB, GCM və bənzərləri tipli) — MAC əlavə etməyə məcburuq. Mən seçirəm (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 += 1Gə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 (Stribog-256 hash: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Öz layihələrim kimi, bu tip , , , , GOSTIM tamamilə , aşağıdakı şərtlərlə paylanılır .
, , iştirakçı , Python/Go inkişafçısı, baş mütəxəssis .
Mənbə: habr.com
