BĂœt vĂœvojĂĄĆem knihovny (GOST kryptografickĂĄ primitiva v ÄistĂ©m Pythonu), Äasto dostĂĄvĂĄm otĂĄzky, jak implementovat nejjednoduĆĄĆĄĂ bezpeÄnĂ© zasĂlĂĄnĂ zprĂĄv na koleni. Mnoho lidĂ povaĆŸuje aplikovanou kryptografii za docela jednoduchou a volĂĄnĂ .encrypt() na blokovou ĆĄifru bude staÄit k jejĂmu bezpeÄnĂ©mu odeslĂĄnĂ pĆes komunikaÄnĂ kanĂĄl. JinĂ vÄĆĂ, ĆŸe aplikovanĂĄ kryptografie je ĂșdÄlem nÄkolika mĂĄlo lidĂ, a je pĆijatelnĂ©, aby bohatĂ© spoleÄnosti jako Telegram s olympijskĂœmi matematiky zabezpeÄenĂœ protokol.
To vĆĄe mÄ pĆimÄlo napsat tento ÄlĂĄnek, abych ukĂĄzal, ĆŸe implementace kryptografickĂœch protokolĆŻ a bezpeÄnĂœch IM nenĂ tak obtĂĆŸnĂœ Ășkol. NevyplatĂ se vĆĄak vymĂœĆĄlet vlastnĂ protokoly pro ovÄĆovĂĄnĂ a dohody o klĂÄĂch.

ÄlĂĄnek napĂĆĄe , , instant messenger s autentizaÄnĂ a klĂÄovĂœ protokol (na jehoĆŸ zĂĄkladÄ je implementovĂĄn ), pouĆŸĂvajĂcĂ vĂœhradnÄ kryptografickĂ© algoritmy GOST knihovnu PyGOST a knihovnu kĂłdovĂĄnĂ zprĂĄv ASN.1 (o kterĂ©m uĆŸ jsem ). PĆedpoklad: musĂ bĂœt tak jednoduchĂœ, aby se dal napsat od nuly za jeden veÄer (nebo pracovnĂ den), jinak uĆŸ to nenĂ jednoduchĂœ program. Asi mĂĄ chyby, zbyteÄnĂ© komplikace, nedostatky, navĂc je to mĆŻj prvnĂ program vyuĆŸĂvajĂcĂ knihovnu asyncio.
IM design
Nejprve musĂme pochopit, jak bude naĆĄe IM vypadat. Pro zjednoduĆĄenĂ nechĆ„ je to sĂĆ„ peer-to-peer, bez jakĂ©hokoli objevovĂĄnĂ ĂșÄastnĂkĆŻ. OsobnÄ uvedeme, na kterou adresu: port se pĆipojit pro komunikaci s ĂșÄastnĂkem rozhovoru.
ChĂĄpu, ĆŸe v souÄasnĂ© dobÄ je pĆedpoklad dostupnosti pĆĂmĂ© komunikace mezi dvÄma libovolnĂœmi poÄĂtaÄi vĂœznamnĂœm omezenĂm pouĆŸitelnosti IM v praxi. ÄĂm vĂce vĆĄak vĂœvojĂĄĆi implementujĂ nejrĆŻznÄjĆĄĂ berliÄky pro pĆechod pĆes NAT, tĂm dĂ©le zĆŻstaneme na internetu IPv4 s deprimujĂcĂ pravdÄpodobnostĂ komunikace mezi libovolnĂœmi poÄĂtaÄi. Jak dlouho dokĂĄĆŸete tolerovat nedostatek IPv6 doma a v prĂĄci?
Budeme mĂt sĂĆ„ pĆĂĄtel: vĆĄichni moĆŸnĂ partneĆi musĂ bĂœt znĂĄmi pĆedem. Za prvĂ©, toto vĆĄe vĂœraznÄ zjednoduĆĄuje: pĆedstavili jsme se, naĆĄli nebo nenaĆĄli jmĂ©no/klĂÄ, odpojili jsme se nebo pokraÄovali v prĂĄci, kdyĆŸ jsme znali partnera. Za druhĂ©, obecnÄ je bezpeÄnĂœ a eliminuje mnoho ĂștokĆŻ.
RozhranĂ IM se bude blĂĆŸit klasickĂœm ĆeĆĄenĂm , kterĂ© se mi velmi lĂbĂ pro jejich minimalismus a filozofii Unix-way. Program IM vytvoĆĂ adresĂĄĆ se tĆemi sokety domĂ©ny Unix pro kaĆŸdĂ©ho partnera:
- inâzprĂĄvy odeslanĂ© partnerovi jsou v nÄm zaznamenĂĄny;
- out - zprĂĄvy pĆijatĂ© od partnera jsou z nÄj Äteny;
- stav - ÄtenĂm z nÄj zjistĂme, zda je ĂșÄastnĂk prĂĄvÄ pĆipojen, adresu spojenĂ/port.
KromÄ toho se vytvoĆĂ conn soket zapsĂĄnĂm hostitelskĂ©ho portu, do kterĂ©ho iniciujeme spojenĂ se vzdĂĄlenĂœm ĂșÄastnĂkem.
|-- alice
| |-- in
| |-- out
| `-- state
|-- bob
| |-- in
| |-- out
| `-- state
`- conn
Tento pĆĂstup vĂĄm umoĆŸĆuje provĂĄdÄt nezĂĄvislĂ© implementace pĆenosu IM a uĆŸivatelskĂ©ho rozhranĂ, protoĆŸe neexistuje ĆŸĂĄdnĂœ pĆĂtel, nemĆŻĆŸete se zavdÄÄit vĆĄem. PouĆŸitĂm Đž / nebo , mĆŻĆŸete zĂskat rozhranĂ pro vĂce oken se zvĂœraznÄnĂm syntaxe. A s pomocĂ mĆŻĆŸete zĂskat vstupnĂ ĆĂĄdek zprĂĄv kompatibilnĂ s GNU Readline.
Ve skuteÄnosti, bezstarostnĂ© projekty pouĆŸĂvajĂ soubory FIFO. OsobnÄ jsem nedokĂĄzal pochopit, jak se soubory v asyncio kompetitivnÄ pracovat bez ruÄnÄ psanĂ©ho pozadĂ z vyhrazenĂœch vlĂĄken (jazyk na takovĂ© vÄci pouĆŸĂvĂĄm uĆŸ dlouho ). Proto jsem se rozhodl vystaÄit s unixovĂœmi domĂ©novĂœmi sockety. BohuĆŸel to znemoĆŸĆuje provĂ©st echo 2001:470:dead::babe 6666 > conn. Tento problĂ©m jsem vyĆeĆĄil pomocĂ : echo 2001:470:dead::babe 6666 | socat - UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.
PĆŻvodnĂ nezabezpeÄenĂœ protokol
TCP se pouĆŸĂvĂĄ jako transport: garantuje doruÄenĂ a jeho objednĂĄvku. UDP nezaruÄuje ani jedno (coĆŸ by bylo uĆŸiteÄnĂ© pĆi pouĆŸitĂ kryptografie), ale podporu Python nevychĂĄzĂ z krabice.
BohuĆŸel v TCP neexistuje ĆŸĂĄdnĂœ koncept zprĂĄvy, pouze proud bajtĆŻ. Proto je potĆeba vymyslet formĂĄt zprĂĄv, aby je bylo moĆŸnĂ© sdĂlet mezi sebou v tomto vlĂĄknu. MĆŻĆŸeme se dohodnout na pouĆŸitĂ znaku pro odĆĂĄdkovĂĄnĂ. Pro zaÄĂĄtek je to v poĆĂĄdku, ale jakmile zaÄneme ĆĄifrovat naĆĄe zprĂĄvy, mĆŻĆŸe se tento znak objevit kdekoli v ĆĄifrovanĂ©m textu. V sĂtĂch jsou proto oblĂbenĂ© protokoly, kterĂ© nejprve odesĂlajĂ dĂ©lku zprĂĄvy v bajtech. NapĆĂklad Python mĂĄ z krabice xdrlib, kterĂœ umoĆŸĆuje pracovat s podobnĂœm formĂĄtem .
Se ÄtenĂm TCP nebudeme pracovat sprĂĄvnÄ a efektivnÄ â zjednoduĆĄĂme kĂłd. Äteme data ze zĂĄsuvky v nekoneÄnĂ© smyÄce, dokud nedekĂłdujeme kompletnĂ zprĂĄvu. JSON s XML lze takĂ© pouĆŸĂt jako formĂĄt pro tento pĆĂstup. Ale kdyĆŸ se pĆidĂĄ kryptografie, data budou muset bĂœt podepsĂĄna a ovÄĆena - a to bude vyĆŸadovat bajt po bajtu identickou reprezentaci objektĆŻ, coĆŸ JSON/XML neposkytuje (vĂœsledky vĂœpisĆŻ se mohou liĆĄit).
XDR je pro tento Ășkol vhodnĂœ, nicmĂ©nÄ volĂm ASN.1 s kĂłdovĂĄnĂm DER a knihovny, protoĆŸe budeme mĂt po ruce pĆedmÄty na vysokĂ© Ășrovni, se kterĂœmi je Äasto pĆĂjemnÄjĆĄĂ a pohodlnÄjĆĄĂ pracovat. Na rozdĂl od bez schĂ©matu , nebo , ASN.1 automaticky porovnĂĄ data s pevnÄ zakĂłdovanĂœm schĂ©matem.
# Msg ::= CHOICE {
# text MsgText,
# handshake [0] EXPLICIT MsgHandshake }
class Msg(Choice):
schema = ((
("text", MsgText()),
("handshake", MsgHandshake(expl=tag_ctxc(0))),
))
# MsgText ::= SEQUENCE {
# text UTF8String (SIZE(1..MaxTextLen))}
class MsgText(Sequence):
schema = ((
("text", UTF8String(bounds=(1, MaxTextLen))),
))
# MsgHandshake ::= SEQUENCE {
# peerName UTF8String (SIZE(1..256)) }
class MsgHandshake(Sequence):
schema = ((
("peerName", UTF8String(bounds=(1, 256))),
))
PĆijatĂĄ zprĂĄva bude Msg: buÄ textovĂĄ MsgText (zatĂm s jednĂm textovĂœm polem) nebo MsgHandshake handshake zprĂĄva (kterĂĄ obsahuje jmĂ©no partnera). TeÄ to vypadĂĄ pĆekomplikovanÄ, ale tohle je zĂĄklad do budoucna.
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââŹâââ âââââââs IdA) â ââââââââââ - â â MsgText() â âââââ MsgText() â â â
IM bez kryptografie
Jak jsem jiĆŸ Ćekl, pro vĆĄechny operace soketĆŻ bude pouĆŸita knihovna asyncio. PojÄme oznĂĄmit, co oÄekĂĄvĂĄme pĆi spuĆĄtÄnĂ:
parser = argparse.ArgumentParser(description="GOSTIM")
parser.add_argument(
"--our-name",
required=True,
help="Our peer name",
)
parser.add_argument(
"--their-names",
required=True,
help="Their peer names, comma-separated",
)
parser.add_argument(
"--bind",
default="::1",
help="Address to listen on",
)
parser.add_argument(
"--port",
type=int,
default=6666,
help="Port to listen on",
)
args = parser.parse_args()
OUR_NAME = UTF8String(args.our_name)
THEIR_NAMES = set(args.their_names.split(","))
Nastavte si vlastnĂ jmĂ©no (--naĆĄe-jmĂ©no Alice). VĆĄichni oÄekĂĄvanĂ ĂșÄastnĂci rozhovoru jsou uvedeni oddÄleni ÄĂĄrkami (âjejich jmĂ©na bob, eve). Pro kaĆŸdĂ©ho z ĂșÄastnĂkĆŻ dialogu je vytvoĆen adresĂĄĆ s unixovĂœmi sokety a takĂ© korutina pro kaĆŸdĂœ vstupnĂ a vĂœstupnĂ stav:
for peer_name in THEIR_NAMES:
makedirs(peer_name, mode=0o700, exist_ok=True)
out_queue = asyncio.Queue()
OUT_QUEUES[peer_name] = out_queue
asyncio.ensure_future(asyncio.start_unix_server(
partial(unixsock_out_processor, out_queue=out_queue),
path.join(peer_name, "out"),
))
in_queue = asyncio.Queue()
IN_QUEUES[peer_name] = in_queue
asyncio.ensure_future(asyncio.start_unix_server(
partial(unixsock_in_processor, in_queue=in_queue),
path.join(peer_name, "in"),
))
asyncio.ensure_future(asyncio.start_unix_server(
partial(unixsock_state_processor, peer_name=peer_name),
path.join(peer_name, "state"),
))
asyncio.ensure_future(asyncio.start_unix_server(unixsock_conn_processor, "conn"))
ZprĂĄvy pĆichĂĄzejĂcĂ od uĆŸivatele ze vstupnĂho soketu jsou odesĂlĂĄny do fronty IN_QUEUES:
async def unixsock_in_processor(reader, writer, in_queue: asyncio.Queue) -> None:
while True:
text = await reader.read(MaxTextLen)
if text == b"":
break
await in_queue.put(text.decode("utf-8"))
ZprĂĄvy pĆichĂĄzejĂcĂ od ĂșÄastnĂkĆŻ rozhovoru jsou odesĂlĂĄny do front OUT_QUEUES, ze kterĂœch jsou data zapisovĂĄna do vĂœstupnĂho soketu:
async def unixsock_out_processor(reader, writer, out_queue: asyncio.Queue) -> None:
while True:
text = await out_queue.get()
writer.write(("[%s] %s" % (datetime.now(), text)).encode("utf-8"))
await writer.drain()
PĆi ÄtenĂ ze stavovĂ©ho soketu program hledĂĄ adresu ĂșÄastnĂka ve slovnĂku PEER_ALIVE. Pokud jeĆĄtÄ nenĂ spojenĂ s ĂșÄastnĂkem komunikace, zapĂĆĄe se prĂĄzdnĂœ ĆĂĄdek.
async def unixsock_state_processor(reader, writer, peer_name: str) -> None:
peer_writer = PEER_ALIVES.get(peer_name)
writer.write(
b"" if peer_writer is None else (" ".join([
str(i) for i in peer_writer.get_extra_info("peername")[:2]
]).encode("utf-8") + b"n")
)
await writer.drain()
writer.close()
PĆi zĂĄpisu adresy do conn soketu se spustĂ funkce âiniciĂĄtorâ pĆipojenĂ:
async def unixsock_conn_processor(reader, writer) -> None:
data = await reader.read(256)
writer.close()
host, port = data.decode("utf-8").split(" ")
await initiator(host=host, port=int(port))
PodĂvejme se na iniciĂĄtora. Nejprve zjevnÄ otevĆe pĆipojenĂ k urÄenĂ©mu hostiteli/portu a odeĆĄle zprĂĄvu handshake s jeho nĂĄzvem:
130 async def initiator(host, port):
131 _id = repr((host, port))
132 logging.info("%s: dialing", _id)
133 reader, writer = await asyncio.open_connection(host, port)
134 # Handshake message {{{
135 writer.write(Msg(("handshake", MsgHandshake((
136 ("peerName", OUR_NAME),
137 )))).encode())
138 # }}}
139 await writer.drain()
PotĂ© ÄekĂĄ na odpovÄÄ od vzdĂĄlenĂ© strany. PokusĂ se dekĂłdovat pĆĂchozĂ odpovÄÄ pomocĂ schĂ©matu Msg ASN.1. PĆedpoklĂĄdĂĄme, ĆŸe celĂĄ zprĂĄva bude odeslĂĄna v jednom TCP segmentu a pĆi volĂĄnĂ .read() ji pĆijmeme atomicky. Zkontrolujeme, ĆŸe jsme obdrĆŸeli zprĂĄvu o podĂĄnĂ ruky.
141 # Wait for Handshake message {{{
142 data = await reader.read(256)
143 if data == b"":
144 logging.warning("%s: no answer, disconnecting", _id)
145 writer.close()
146 return
147 try:
148 msg, _ = Msg().decode(data)
149 except ASN1Error:
150 logging.warning("%s: undecodable answer, disconnecting", _id)
151 writer.close()
152 return
153 logging.info("%s: got %s message", _id, msg.choice)
154 if msg.choice != "handshake":
155 logging.warning("%s: unexpected message, disconnecting", _id)
156 writer.close()
157 return
158 # }}}
Zkontrolujeme, zda je nĂĄm znĂĄmĂ© pĆijatĂ© jmĂ©no partnera. Pokud ne, pĆeruĆĄĂme spojenĂ. Zkontrolujeme, zda jsme s nĂm jiĆŸ navĂĄzali spojenĂ (ĂșÄastnĂk opÄt dal pĆĂkaz ke spojenĂ s nĂĄmi) a uzavĆeme jej. Fronta IN_QUEUES obsahuje pythonovskĂ© ĆetÄzce s textem zprĂĄvy, ale mĂĄ speciĂĄlnĂ hodnotu None, kterĂĄ signalizuje korutinÄ msg_sender, aby pĆestala pracovat, takĆŸe zapomene na svĆŻj zapisovaÄ spojenĂœ se starĆĄĂm pĆipojenĂm TCP.
159 msg_handshake = msg.value
160 peer_name = str(msg_handshake["peerName"])
161 if peer_name not in THEIR_NAMES:
162 logging.warning("unknown peer name: %s", peer_name)
163 writer.close()
164 return
165 logging.info("%s: session established: %s", _id, peer_name)
166 # Run text message sender, initialize transport decoder {{{
167 peer_alive = PEER_ALIVES.pop(peer_name, None)
168 if peer_alive is not None:
169 peer_alive.close()
170 await IN_QUEUES[peer_name].put(None)
171 PEER_ALIVES[peer_name] = writer
172 asyncio.ensure_future(msg_sender(peer_name, writer))
173 # }}}
msg_sender pĆijĂmĂĄ odchozĂ zprĂĄvy (ve frontÄ z in socketu), serializuje je do zprĂĄvy MsgText a odesĂlĂĄ je pĆes TCP spojenĂ. MĆŻĆŸe se kdykoli zlomit - jasnÄ to zachytĂme.
async def msg_sender(peer_name: str, writer) -> None:
in_queue = IN_QUEUES[peer_name]
while True:
text = await in_queue.get()
if text is None:
break
writer.write(Msg(("text", MsgText((
("text", UTF8String(text)),
)))).encode())
try:
await writer.drain()
except ConnectionResetError:
del PEER_ALIVES[peer_name]
return
logging.info("%s: sent %d characters message", peer_name, len(text))
Na konci vstoupĂ iniciĂĄtor do nekoneÄnĂ© smyÄky ÄtenĂ zprĂĄv ze zĂĄsuvky. Zkontroluje, zda se jednĂĄ o textovĂ© zprĂĄvy, a zaĆadĂ je do fronty OUT_QUEUES, ze kterĂ© budou odeslĂĄny do vĂœstupnĂho soketu odpovĂdajĂcĂho partnera. ProÄ prostÄ nemĆŻĆŸete udÄlat .read() a dekĂłdovat zprĂĄvu? ProtoĆŸe je moĆŸnĂ©, ĆŸe nÄkolik zprĂĄv od uĆŸivatele bude agregovĂĄno ve vyrovnĂĄvacĂ pamÄti operaÄnĂho systĂ©mu a odeslĂĄno v jednom segmentu TCP. MĆŻĆŸeme dekĂłdovat prvnĂ a ÄĂĄst nĂĄsledujĂcĂho pak mĆŻĆŸe zĆŻstat ve vyrovnĂĄvacĂ pamÄti. V pĆĂpadÄ jakĂ©koli abnormĂĄlnĂ situace uzavĆeme TCP spojenĂ a zastavĂme korutinu msg_sender (zaslĂĄnĂm None do fronty OUT_QUEUES).
174 buf = b""
175 # Wait for test messages {{{
176 while True:
177 data = await reader.read(MaxMsgLen)
178 if data == b"":
179 break
180 buf += data
181 if len(buf) > MaxMsgLen:
182 logging.warning("%s: max buffer size exceeded", _id)
183 break
184 try:
185 msg, tail = Msg().decode(buf)
186 except ASN1Error:
187 continue
188 buf = tail
189 if msg.choice != "text":
190 logging.warning("%s: unexpected %s message", _id, msg.choice)
191 break
192 try:
193 await msg_receiver(msg.value, peer_name)
194 except ValueError as err:
195 logging.warning("%s: %s", err)
196 break
197 # }}}
198 logging.info("%s: disconnecting: %s", _id, peer_name)
199 IN_QUEUES[peer_name].put(None)
200 writer.close()
66 async def msg_receiver(msg_text: MsgText, peer_name: str) -> None:
67 text = str(msg_text["text"])
68 logging.info("%s: received %d characters message", peer_name, len(text))
69 await OUT_QUEUES[peer_name].put(text)
VraĆ„me se k hlavnĂmu kĂłdu. Po vytvoĆenĂ vĆĄech korutin v dobÄ spuĆĄtÄnĂ programu spustĂme TCP server. Pro kaĆŸdĂ© navĂĄzanĂ© spojenĂ vytvoĆĂ korutinu respondĂ©ru.
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s %(asctime)s: %(funcName)s: %(message)s",
)
loop = asyncio.get_event_loop()
server = loop.run_until_complete(asyncio.start_server(responder, args.bind, args.port))
logging.info("Listening on: %s", server.sockets[0].getsockname())
loop.run_forever()
respondĂ©r je podobnĂœ iniciĂĄtoru a zrcadlĂ vĆĄechny stejnĂ© akce, ale nekoneÄnĂĄ smyÄka ÄtenĂ zprĂĄv se pro jednoduchost spustĂ okamĆŸitÄ. V souÄasnĂ© dobÄ protokol handshake odesĂlĂĄ jednu zprĂĄvu z kaĆŸdĂ© strany, ale v budoucnu budou dvÄ od iniciĂĄtora pĆipojenĂ, po kterĂœch lze okamĆŸitÄ odesĂlat textovĂ© zprĂĄvy.
72 async def responder(reader, writer):
73 _id = writer.get_extra_info("peername")
74 logging.info("%s: connected", _id)
75 buf = b""
76 msg_expected = "handshake"
77 peer_name = None
78 while True:
79 # Read until we get Msg message {{{
80 data = await reader.read(MaxMsgLen)
81 if data == b"":
82 logging.info("%s: closed connection", _id)
83 break
84 buf += data
85 if len(buf) > MaxMsgLen:
86 logging.warning("%s: max buffer size exceeded", _id)
87 break
88 try:
89 msg, tail = Msg().decode(buf)
90 except ASN1Error:
91 continue
92 buf = tail
93 # }}}
94 if msg.choice != msg_expected:
95 logging.warning("%s: unexpected %s message", _id, msg.choice)
96 break
97 if msg_expected == "text":
98 try:
99 await msg_receiver(msg.value, peer_name)
100 except ValueError as err:
101 logging.warning("%s: %s", err)
102 break
103 # Process Handshake message {{{
104 elif msg_expected == "handshake":
105 logging.info("%s: got %s message", _id, msg_expected)
106 msg_handshake = msg.value
107 peer_name = str(msg_handshake["peerName"])
108 if peer_name not in THEIR_NAMES:
109 logging.warning("unknown peer name: %s", peer_name)
110 break
111 writer.write(Msg(("handshake", MsgHandshake((
112 ("peerName", OUR_NAME),
113 )))).encode())
114 await writer.drain()
115 logging.info("%s: session established: %s", _id, peer_name)
116 peer_alive = PEER_ALIVES.pop(peer_name, None)
117 if peer_alive is not None:
118 peer_alive.close()
119 await IN_QUEUES[peer_name].put(None)
120 PEER_ALIVES[peer_name] = writer
121 asyncio.ensure_future(msg_sender(peer_name, writer))
122 msg_expected = "text"
123 # }}}
124 logging.info("%s: disconnecting", _id)
125 if msg_expected == "text":
126 IN_QUEUES[peer_name].put(None)
127 writer.close()
ZabezpeÄenĂœ protokol
Je Äas zabezpeÄit naĆĄi komunikaci. Co myslĂme bezpeÄnostĂ a co chceme:
- dĆŻvÄrnost pĆenĂĄĆĄenĂœch zprĂĄv;
- autentiÄnost a integrita pĆenĂĄĆĄenĂœch zprĂĄv â jejich zmÄny musĂ bĂœt detekovĂĄny;
- ochrana proti replay ĂștokĆŻm - musĂ bĂœt detekovĂĄna skuteÄnost chybÄjĂcĂch nebo opakovanĂœch zprĂĄv (a my se rozhodneme ukonÄit spojenĂ);
- identifikace a autentizace ĂșÄastnĂkĆŻ komunikace pomocĂ pĆedem zadanĂœch veĆejnĂœch klĂÄĆŻ â jiĆŸ dĆĂve jsme se rozhodli, ĆŸe vytvoĆĂme sĂĆ„ pĆĂĄtel. Teprve po autentizaci pochopĂme, s kĂœm komunikujeme;
- dostupnost vlastnosti (PFS) â kompromitace naĆĄeho dlouhodobĂ©ho podpisovĂ©ho klĂÄe by nemÄla vĂ©st ke schopnosti ÄĂst veĆĄkerou pĆedchozĂ korespondenci. NahrĂĄvĂĄnĂ zachycenĂ©ho provozu se stĂĄvĂĄ zbyteÄnĂœm;
- platnost/platnost zprĂĄv (transport a handshake) pouze v rĂĄmci jednĂ© TCP relace. VklĂĄdĂĄnĂ sprĂĄvnÄ podepsanĂœch/ovÄĆenĂœch zprĂĄv z jinĂ© relace (ani se stejnĂœm partnerem) by nemÄlo bĂœt moĆŸnĂ©;
- pasivnĂ pozorovatel by nemÄl vidÄt ani uĆŸivatelskĂ© identifikĂĄtory, pĆenĂĄĆĄenĂ© dlouhodobĂ© veĆejnĂ© klĂÄe ani hashe z nich. JistĂĄ anonymita od pasivnĂho pozorovatele.
PĆekvapivÄ tĂ©mÄĆ kaĆŸdĂœ chce mĂt toto minimum v jakĂ©mkoli protokolu handshake a jen velmi mĂĄlo z vĂœĆĄe uvedenĂ©ho je nakonec splnÄno pro âdomĂĄcĂâ protokoly. TeÄ nebudeme vymĂœĆĄlet nic novĂ©ho. UrÄitÄ bych doporuÄil pouĆŸĂvat pro stavebnĂ protokoly, ale zvolme nÄco jednoduĆĄĆĄĂho.
Dva nejoblĂbenÄjĆĄĂ protokoly jsou:
- - velmi sloĆŸitĂœ protokol s dlouhou historiĂ chyb, zĂĄsekĆŻ, zranitelnostĂ, ĆĄpatnĂ©ho myĆĄlenĂ, sloĆŸitosti a nedostatkĆŻ (to vĆĄak nemĂĄ mnoho spoleÄnĂ©ho s TLS 1.3). Ale neuvaĆŸujeme o tom, protoĆŸe je to pĆĂliĆĄ komplikovanĂ©.
- Ń â nemajĂ vĂĄĆŸnĂ© kryptografickĂ© problĂ©my, i kdyĆŸ takĂ© nejsou jednoduchĂ©. Pokud Ätete o IKEv1 a IKEv2, tak jejich zdroj je , ISO/IEC IS 9798-3 a protokoly SIGMA (SIGN-and-MAc) â dostateÄnÄ jednoduchĂ© na implementaci za jeden veÄer.
Co je dobrĂ©ho na SIGMA, jako nejnovÄjĆĄĂ ÄlĂĄnek ve vĂœvoji STS/ISO protokolĆŻ? SplĆuje vĆĄechny naĆĄe poĆŸadavky (vÄetnÄ âskrytĂâ identifikĂĄtorĆŻ partnera) a nemĂĄ ĆŸĂĄdnĂ© znĂĄmĂ© kryptografickĂ© problĂ©my. Je minimalistickĂĄ â odstranÄnĂ alespoĆ jednoho prvku ze zprĂĄvy protokolu povede k jejĂ nejistotÄ.
PojÄme od nejjednoduĆĄĆĄĂho domĂĄcĂho protokolu k SIGMA. NejzĂĄkladnÄjĆĄĂ operace, kterĂĄ nĂĄs zajĂmĂĄ, je : Funkce, kterĂĄ dĂĄvĂĄ obÄma ĂșÄastnĂkĆŻm stejnou hodnotu, kterou lze pouĆŸĂt jako symetrickĂœ klĂÄ. AniĆŸ bychom zachĂĄzeli do podrobnostĂ: kaĆŸdĂĄ ze stran vygeneruje pomĂjivĂœ (pouĆŸitĂœ pouze v rĂĄmci jednĂ© relace) pĂĄr klĂÄĆŻ (veĆejnĂœ a soukromĂœ klĂÄ), vymÄnĂ si veĆejnĂ© klĂÄe, zavolĂĄ funkci dohody, na jejĂmĆŸ vstupu pĆedĂĄ svĆŻj soukromĂœ klĂÄ a veĆejnĂœ klĂÄ. klĂÄ partnera.
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââ â,ââ âââ Iâ âââ Iâ ââ Iâ Ad â âââââââââââ âââââââââââ ââââââââââââââââ>â ââ>â âDDHA, Hospoda A ââââââââ ââââââââââââ â IdB, PubB â ââââââââââââââââââââââââââââ â<âââââââââ âââââââ âPrvB, PubB = DHgen()â â â âââââââââââââââââââ ââââ âââââ ââââ§âââââââââââââ â âKlĂÄ = DH(PrvA, PubB)â <ââââââââ†âââââââ âââââ â â â â
Kdokoli mĆŻĆŸe skoÄit doprostĆed a nahradit veĆejnĂ© klĂÄe svĂœmi vlastnĂmi - v tomto protokolu neexistuje ĆŸĂĄdnĂĄ autentizace ĂșÄastnĂkĆŻ rozhovoru. PĆidejme podpis s dlouhovÄkĂœmi klĂÄi.
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââŹâ,ââ â,ââ ââââ âââ A znak(SignPrvA, (PubA)) â ââ PodepiĆĄte se = load()â â â âPrvA, PubA = DHgen() â â â ââââââââ âââââââ ââââââââââââââââ ââââ âââââââââââââââââ , podepsat (SignPrvB, (PubB)) - âââââââââââ âââââââââââââ âââ âSignPrvB, SignPubB = load( )â â â âPrvB, PubB = DHgen() ââ ââââââââ â ââââââ â â âovÄĆit( SignPubB, ...)â â <ââââ âKlĂÄ = DH(Pr vA, PubB) â â â âââââââââââââââââââââââââ ââ â â â
TakovĂœ podpis nebude fungovat, protoĆŸe nenĂ vĂĄzĂĄn na konkrĂ©tnĂ relaci. TakovĂ© zprĂĄvy jsou takĂ© âvhodnĂ©â pro sezenĂ s ostatnĂmi ĂșÄastnĂky. CelĂœ kontext se musĂ pĆihlĂĄsit. To nĂĄs nutĂ pĆidat takĂ© dalĆĄĂ zprĂĄvu od A.
KromÄ toho je dĆŻleĆŸitĂ© pĆidat svĆŻj vlastnĂ identifikĂĄtor pod podpis, protoĆŸe jinak mĆŻĆŸeme nahradit IdXXX a znovu podepsat zprĂĄvu klĂÄem jinĂ©ho znĂĄmĂ©ho partnera. Aby se zabrĂĄnilo , je nutnĂ©, aby prvky pod podpisem byly na jasnÄ definovanĂœch mĂstech podle jejich vĂœznamu: pokud A znaÄĂ (PubA, PubB), musĂ se podepisovat B (PubB, PubA). To takĂ© vypovĂdĂĄ o dĆŻleĆŸitosti vĂœbÄru struktury a formĂĄtu serializovanĂœch dat. NapĆĂklad sady v kĂłdovĂĄnĂ ASN.1 DER jsou seĆazeny: SET OF(PubA, PubB) bude totoĆŸnĂ© s SET OF(PubB, PubA).
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââ â,ââ âââ Iâ âââ Iâ ââ Iâ Ad â âââââââââââ ââââââââââââââââââ âââââââââââââââââââââââ ââââââââââ âââââââââââââ>â âSignPrvA, SignPubA = load()â â â âPrvA, PubA = DHgen() âââ ââââ âââââââ ââââââââââââââââ âIdB, PubB, sign(SignPrvB, (IdB, PubA, PubB)) âââââââââââââââââââââ âââââ âââââââââââââ â<ââââââââââââââââââââââââ ââââââââââ ââââââââââ âSignPrvB, SignPubB = load()â â â âPrvB, PubB = DHgen() â â â â ââââ âââââââââ-â âââââââ âââââââââââ â znak (SignPrvA, (IdA, PubB, PubA)) â âââââââââââââââââââââââ ââââ ââ - âââ>â âovÄĆit (SignPubB, ...) â â â âklĂÄ = dh (prva, PUBB) â â â â
StĂĄle jsme vĆĄak âneprokĂĄzaliâ, ĆŸe jsme pro tuto relaci vygenerovali stejnĂœ sdĂlenĂœ klĂÄ. V zĂĄsadÄ se bez tohoto kroku obejdeme - hned prvnĂ dopravnĂ spojenĂ bude neplatnĂœ, ale chceme, abychom po dokonÄenĂ handshaku mÄli jistotu, ĆŸe je opravdu vĆĄe domluveno. V tuto chvĂli mĂĄme k dispozici protokol ISO/IEC IS 9798-3.
Mohli bychom podepsat samotnĂœ vygenerovanĂœ klĂÄ. To je nebezpeÄnĂ©, protoĆŸe je moĆŸnĂ©, ĆŸe v pouĆŸitĂ©m podpisovĂ©m algoritmu mĆŻĆŸe dochĂĄzet k ĂșnikĆŻm (sice bitĆŻ na podpis, ale stĂĄle dochĂĄzĂ k ĂșnikĆŻm). Je moĆŸnĂ© podepsat hash derivaÄnĂho klĂÄe, ale Ășnik dokonce i hashe odvozenĂ©ho klĂÄe mĆŻĆŸe bĂœt cennĂœ pĆi Ăștoku hrubou silou na derivaÄnĂ funkci. SIGMA pouĆŸĂvĂĄ funkci MAC, kterĂĄ ovÄĆuje ID odesĂlatele.
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââ â,ââ âââ Iâ âââ Iâ ââ Iâ Ad â âââââââââââ ââââââââââââââââââ âââââââââââââââââââââââ ââââââââââ ââââââââââââââââââ>â âSignPrvA, SignPubA = load()â â â =âDHAâ âDHA, PubA âââââââ âââââââââââââââââââââ âIdB, PubB, podepsat (SignPrvB, (PubA, PubB)âââ âââ â<âââââââââââââââââ ââââââââââââââââââââââââ ââââââââââ ââ âSignPrvB, SignPubB = naÄĂst()â â â âPrvB, PubB = DHgen() â â â âââââ âââââââââââââ âââââââ âââ â â âââââââââââââ âââââââââ, Publikum A) â (Ib(Pub)A)Prv. âKlĂÄ = DH( PrvA, PubB) â ââââââââââââââââââââââ âââââââââââââ ââââââââââ âââââ>â âovÄĆit (klĂÄ, IdB) â â â âovÄĆit (SignPubB, ...)â â â ââââââââââââââââââââ âââââ ââ â â
Jako optimalizaci mohou nÄkteĆĂ chtĂt znovu pouĆŸĂt svĂ© pomĂjivĂ© klĂÄe (coĆŸ je samozĆejmÄ pro PFS neĆĄĆ„astnĂ©). NapĆĂklad jsme vygenerovali pĂĄr klĂÄĆŻ, pokusili se pĆipojit, ale TCP nebyl dostupnĂœ nebo byl nÄkde uprostĆed protokolu pĆeruĆĄen. Je ĆĄkoda plĂœtvat plĂœtvĂĄnĂm entropiĂ a zdroji procesoru na novĂœ pĂĄr. Proto zavedeme tzv. cookie â pseudonĂĄhodnou hodnotu, kterĂĄ bude chrĂĄnit pĆed pĆĂpadnĂœmi Ăștoky nĂĄhodnĂ©ho pĆehrĂĄnĂ pĆi opÄtovnĂ©m pouĆŸitĂ efemĂ©rnĂch veĆejnĂœch klĂÄĆŻ. Vzhledem k vazbÄ mezi cookie a efemĂ©rnĂm veĆejnĂœm klĂÄem mĆŻĆŸe bĂœt veĆejnĂœ klĂÄ protÄjĆĄĂ strany z podpisu odstranÄn jako nepotĆebnĂœ.
âââââââ âââââââ âPeerAâ âPeerBâ ââââŹâââŹâ,ââ â,ââ â,ââ Aâ âââ A CookieA â âââââââââ ââââââââââââââââââââ ââââââââââââââââââ ââââââââââ - â>â âSignPrvA, SignPubA = load( )â â â âPrvA, PubA = DHgen() â â â âââââââââââââââââââââââââââââââââââââââââââââââââââ âââ âIdB, PubB, CookieB , sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) â âââââââââââââââââââââââââââââââ â â< - ââââââââââ âââââââââââââââââââââ âSignPrvB, SignPubB = naÄĂst()â â â â, â â â DH, â â Prv BDH, âB) B) âââââââ - ââââââââ â podepsat( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA) â âKlĂÄ = DH(PrvA, PubB) â âââââââââââââââââââ â - âââââââ>â â ovÄĆit (KlĂÄ, IdB) â â â âovÄĆit (SignPubB, ...)â â â ââââââââââââââââââââââââââââââ â
Nakonec chceme zĂskat soukromĂ naĆĄich konverzaÄnĂch partnerĆŻ od pasivnĂho pozorovatele. Za tĂmto ĂșÄelem SIGMA navrhuje nejprve vymÄĆovat pomĂjivĂ© klĂÄe a vyvinout spoleÄnĂœ klĂÄ, na kterĂ©m se budou ĆĄifrovat autentizaÄnĂ a identifikaÄnĂ zprĂĄvy. SIGMA popisuje dvÄ moĆŸnosti:
- SIGMA-I - chrĂĄnĂ iniciĂĄtora pĆed aktivnĂmi Ăștoky, respondĂ©ra pĆed pasivnĂmi: iniciĂĄtor autentizuje respondĂ©ra a pokud nÄco neodpovĂdĂĄ, neprozradĂ svou identifikaci. ObĆŸalovanĂœ pĆedĂĄ svou identifikaci, pokud je s nĂm zahĂĄjen aktivnĂ protokol. PasivnĂ pozorovatel se nic nedozvĂ;
SIGMA-R - chrĂĄnĂ respondĂ©ra pĆed aktivnĂmi Ăștoky, iniciĂĄtora pĆed pasivnĂmi. VĆĄechno je pĆesnÄ naopak, ale v tomto protokolu jsou jiĆŸ pĆenĂĄĆĄeny ÄtyĆi zprĂĄvy o podĂĄnĂ ruky.Vybrali jsme SIGMA-I, protoĆŸe se vĂce podobĂĄ tomu, co oÄekĂĄvĂĄme od znĂĄmĂœch vÄcĂ typu klient-server: klienta rozpoznĂĄ pouze ovÄĆenĂœ server a server uĆŸ znĂĄ kaĆŸdĂœ. NavĂc se snĂĄze implementuje dĂky menĆĄĂmu poÄtu zprĂĄv typu handshake. VĆĄe, co do protokolu pĆidĂĄvĂĄme, je zaĆĄifrovat ÄĂĄst zprĂĄvy a pĆenĂ©st identifikĂĄtor A do zaĆĄifrovanĂ© ÄĂĄsti poslednĂ zprĂĄvy:
PubA, CookieA â âââââââââââ âââââââââââââââââââââââââââ ââââââââââ âââââ ââââââââââ âââââââââââââââââââ âââââââââââ â âââââââ âââââââââ âââââ â PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB)âââ)â)â)â) âââââââââ âââââââââââââââ ââââââââ â<ââââââââââââ ââââââââââ âââââ ââââââââââ âSignP rvB, SignPubB = load()â â â â PrvB, PubB = DHgen() â DHgen() â âââââââ - âââ â Enc((IdA, sign( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA))) â âKlĂÄ = DH(PrvA, PubB) â âââââââââââââââââââ - âââââââââââ ââââââ>â âovÄĆit (klĂÄ, IdB) â â â âovÄĆit ( SignPubB, ...)â â â âââââââââââââââââââ âââââ âââ â â
- GOST R se pouĆŸĂvĂĄ pro podpis algoritmus s 256bitovĂœmi klĂÄi.
- Pro vygenerovĂĄnĂ veĆejnĂ©ho klĂÄe se pouĆŸĂvĂĄ 34.10 VKO.
- CMAC se pouĆŸĂvĂĄ jako MAC. Technicky se jednĂĄ o speciĂĄlnĂ reĆŸim provozu blokovĂ© ĆĄifry, popsanĂœ v GOST R 34.13-2015. Jako ĆĄifrovacĂ funkce pro tento reĆŸim â (34.12-2015).
- Hash jeho veĆejnĂ©ho klĂÄe se pouĆŸĂvĂĄ jako identifikĂĄtor partnera. PouĆŸĂvĂĄ se jako hash (34.11. 2012. 256 XNUMX bitĆŻ).
Po podĂĄnĂ ruky se dohodneme na sdĂlenĂ©m klĂÄi. MĆŻĆŸeme jej pouĆŸĂt pro autentizovanĂ© ĆĄifrovĂĄnĂ transportnĂch zprĂĄv. Tato ÄĂĄst je velmi jednoduchĂĄ a tÄĆŸko udÄlĂĄme chybu: inkrementujeme poÄĂtadlo zprĂĄv, zaĆĄifrujeme zprĂĄvu, ovÄĆĂme (MAC) poÄĂtadlo a ĆĄifrovanĂœ text, odeĆĄleme. PĆi pĆĂjmu zprĂĄvy zkontrolujeme, ĆŸe poÄĂtadlo mĂĄ oÄekĂĄvanou hodnotu, ĆĄifrovanĂœ text ovÄĆĂme poÄĂtadlem a deĆĄifrujeme. JakĂœ klĂÄ bych mÄl pouĆŸĂt k ĆĄifrovĂĄnĂ zprĂĄv handshake, pĆenosu zprĂĄv a jak je ovÄĆit? PouĆŸĂvĂĄnĂ jednoho klĂÄe pro vĆĄechny tyto Ășkoly je nebezpeÄnĂ© a nerozumnĂ©. KlĂÄe je nutnĂ© generovat pomocĂ specializovanĂœch funkcĂ (funkce odvozenĂ klĂÄe). OpÄt si netĆepeme vlasy a nÄco vymyslĂme: je dlouho znĂĄmĂĄ, dobĆe prozkoumanĂĄ a nemĂĄ ĆŸĂĄdnĂ© znĂĄmĂ© problĂ©my. BohuĆŸel nativnĂ knihovna Pythonu tuto funkci nemĂĄ, takĆŸe pouĆŸĂvĂĄme IgelitovĂĄ taĆĄka. HKDF internÄ pouĆŸĂvĂĄ , kterĂœ zase pouĆŸĂvĂĄ hashovacĂ funkci. PĆĂklad implementace v Pythonu na strĂĄnce Wikipedie zabere jen pĂĄr ĆĂĄdkĆŻ kĂłdu. StejnÄ jako v pĆĂpadÄ 34.10 pouĆŸijeme jako hashovacĂ funkci Stribog-2012. VĂœstup naĆĄĂ funkce shody klĂÄĆŻ se bude nazĂœvat klĂÄ relace, ze kterĂ©ho se vygenerujĂ chybÄjĂcĂ symetrickĂ©:
kdf = Hkdf(None, key_session, hash=GOST34112012256) kdf.expand(b"handshake1-mac-identity") kdf.expand(b"handshake1-enc") kdf.expand(b"handshake1-mac") kdf.expand(b"handshake2-mac-identity") kdf.expand(b"handshake2-enc") kdf.expand(b"handshake2-mac") kdf.expand(b"transport-initiator-enc") kdf.expand(b"transport-initiator-mac") kdf.expand(b"transport-responder-enc") kdf.expand(b"transport-responder-mac")Struktury/schéma
PodĂvejme se, jakĂ© struktury ASN.1 nynĂ mĂĄme pro pĆenos vĆĄech tÄchto dat:
class Msg(Choice): schema = (( ("text", MsgText()), ("handshake0", MsgHandshake0(expl=tag_ctxc(0))), ("handshake1", MsgHandshake1(expl=tag_ctxc(1))), ("handshake2", MsgHandshake2(expl=tag_ctxc(2))), )) class MsgText(Sequence): schema = (( ("payload", MsgTextPayload()), ("payloadMac", MAC()), )) class MsgTextPayload(Sequence): schema = (( ("nonce", Integer(bounds=(0, float("+inf")))), ("ciphertext", OctetString(bounds=(1, MaxTextLen))), )) class MsgHandshake0(Sequence): schema = (( ("cookieInitiator", Cookie()), ("pubKeyInitiator", PubKey()), )) class MsgHandshake1(Sequence): schema = (( ("cookieResponder", Cookie()), ("pubKeyResponder", PubKey()), ("ukm", OctetString(bounds=(8, 8))), ("ciphertext", OctetString()), ("ciphertextMac", MAC()), )) class MsgHandshake2(Sequence): schema = (( ("ciphertext", OctetString()), ("ciphertextMac", MAC()), )) class HandshakeTBE(Sequence): schema = (( ("identity", OctetString(bounds=(32, 32))), ("signature", OctetString(bounds=(64, 64))), ("identityMac", MAC()), )) class HandshakeTBS(Sequence): schema = (( ("cookieTheir", Cookie()), ("cookieOur", Cookie()), ("pubKeyOur", PubKey()), )) class Cookie(OctetString): bounds = (16, 16) class PubKey(OctetString): bounds = (64, 64) class MAC(OctetString): bounds = (16, 16)HandshakeTBS je to, co bude podepsĂĄno. HandshakeTBE - co bude zaĆĄifrovĂĄno. UpozorĆuji na pole ukm v MsgHandshake1. 34.10 VKO, pro jeĆĄtÄ vÄtĆĄĂ randomizaci generovanĂœch klĂÄĆŻ, obsahuje parametr UKM (uĆŸivatelskĂœ klĂÄovacĂ materiĂĄl) - jen dalĆĄĂ entropie.
PĆidĂĄnĂ kryptografie do kĂłdu
UvaĆŸujme pouze zmÄny provedenĂ© v pĆŻvodnĂm kĂłdu, protoĆŸe framework zĆŻstal stejnĂœ (ve skuteÄnosti byla nejprve napsĂĄna koneÄnĂĄ implementace a potĂ© z nĂ byla vyĆĂznuta veĆĄkerĂĄ kryptografie).
Vzhledem k tomu, ĆŸe autentizace a identifikace ĂșÄastnĂkĆŻ budou probĂhat pomocĂ veĆejnĂœch klĂÄĆŻ, je nynĂ tĆeba je nÄkde uklĂĄdat na dlouhou dobu. Pro jednoduchost pouĆŸĂvĂĄme JSON takto:
{ "our": { "prv": "21254cf66c15e0226ef2669ceee46c87b575f37f9000272f408d0c9283355f98", "pub": "938c87da5c55b27b7f332d91b202dbef2540979d6ceaa4c35f1b5bfca6df47df0bdae0d3d82beac83cec3e353939489d9981b7eb7a3c58b71df2212d556312a1" }, "their": { "alice": "d361a59c25d2ca5a05d21f31168609deeec100570ac98f540416778c93b2c7402fd92640731a707ec67b5410a0feae5b78aeec93c4a455a17570a84f2bc21fce", "bob": "aade1207dd85ecd283272e7b69c078d5fae75b6e141f7649ad21962042d643512c28a2dbdc12c7ba40eb704af920919511180c18f4d17e07d7f5acd49787224a" } }nĂĄĆĄ - nĂĄĆĄ pĂĄr klĂÄĆŻ, hexadecimĂĄlnĂ soukromĂœ a veĆejnĂœ klĂÄ. jejich â jmĂ©na ĂșÄastnĂkĆŻ rozhovoru a jejich veĆejnĂ© klĂÄe. PojÄme zmÄnit argumenty pĆĂkazovĂ©ho ĆĂĄdku a pĆidat nĂĄslednĂ© zpracovĂĄnĂ dat JSON:
from pygost import gost3410 from pygost.gost34112012256 import GOST34112012256 CURVE = gost3410.GOST3410Curve( *gost3410.CURVE_PARAMS["GostR3410_2001_CryptoPro_A_ParamSet"] ) parser = argparse.ArgumentParser(description="GOSTIM") parser.add_argument( "--keys-gen", action="store_true", help="Generate JSON with our new keypair", ) parser.add_argument( "--keys", default="keys.json", required=False, help="JSON with our and their keys", ) parser.add_argument( "--bind", default="::1", help="Address to listen on", ) parser.add_argument( "--port", type=int, default=6666, help="Port to listen on", ) args = parser.parse_args() if args.keys_gen: prv_raw = urandom(32) pub = gost3410.public_key(CURVE, gost3410.prv_unmarshal(prv_raw)) pub_raw = gost3410.pub_marshal(pub) print(json.dumps({ "our": {"prv": hexenc(prv_raw), "pub": hexenc(pub_raw)}, "their": {}, })) exit(0) # Parse and unmarshal our and their keys {{{ with open(args.keys, "rb") as fd: _keys = json.loads(fd.read().decode("utf-8")) KEY_OUR_SIGN_PRV = gost3410.prv_unmarshal(hexdec(_keys["our"]["prv"])) _pub = hexdec(_keys["our"]["pub"]) KEY_OUR_SIGN_PUB = gost3410.pub_unmarshal(_pub) KEY_OUR_SIGN_PUB_HASH = OctetString(GOST34112012256(_pub).digest()) for peer_name, pub_raw in _keys["their"].items(): _pub = hexdec(pub_raw) KEYS[GOST34112012256(_pub).digest()] = { "name": peer_name, "pub": gost3410.pub_unmarshal(_pub), } # }}}SoukromĂœ klĂÄ algoritmu 34.10 je nĂĄhodnĂ© ÄĂslo. 256bitovĂĄ velikost pro 256bitovĂ© eliptickĂ© kĆivky. PyGOST nepracuje se sadou bajtĆŻ, ale s , takĆŸe nĂĄĆĄ soukromĂœ klĂÄ (urandom(32)) je tĆeba pĆevĂ©st na ÄĂslo pomocĂ gost3410.prv_unmarshal(). VeĆejnĂœ klĂÄ je urÄen deterministicky ze soukromĂ©ho klĂÄe pomocĂ gost3410.public_key(). VeĆejnĂœ klĂÄ 34.10 jsou dvÄ velkĂĄ ÄĂsla, kterĂĄ je takĂ© potĆeba pĆevĂ©st na bajtovou sekvenci pro snadnĂ© uklĂĄdĂĄnĂ a pĆenos pomocĂ gost3410.pub_marshal().
Po pĆeÄtenĂ souboru JSON je tĆeba veĆejnĂ© klĂÄe pĆevĂ©st zpÄt pomocĂ gost3410.pub_unmarshal(). Vzhledem k tomu, ĆŸe z veĆejnĂ©ho klĂÄe obdrĆŸĂme identifikĂĄtory ĂșÄastnĂkĆŻ ve formÄ hashe, lze je okamĆŸitÄ pĆedem vypoÄĂtat a umĂstit do slovnĂku pro rychlĂ© vyhledĂĄvĂĄnĂ. Hash Stribog-256 je gost34112012256.GOST34112012256(), kterĂœ plnÄ vyhovuje rozhranĂ hashlib hashovacĂch funkcĂ.
Jak se zmÄnil korutin iniciĂĄtoru? VĆĄe je podle schĂ©matu handshake: vygenerujeme soubor cookie (128 bitĆŻ je dostatek), pomĂjivĂœ pĂĄr klĂÄĆŻ 34.10, kterĂœ bude pouĆŸit pro funkci dohody klĂÄĆŻ VKO.
395 async def initiator(host, port): 396 _id = repr((host, port)) 397 logging.info("%s: dialing", _id) 398 reader, writer = await asyncio.open_connection(host, port) 399 # Generate our ephemeral public key and cookie, send Handshake 0 message {{{ 400 cookie_our = Cookie(urandom(16)) 401 prv = gost3410.prv_unmarshal(urandom(32)) 402 pub_our = gost3410.public_key(CURVE, prv) 403 pub_our_raw = PubKey(gost3410.pub_marshal(pub_our)) 404 writer.write(Msg(("handshake0", MsgHandshake0(( 405 ("cookieInitiator", cookie_our), 406 ("pubKeyInitiator", pub_our_raw), 407 )))).encode()) 408 # }}} 409 await writer.drain()- ÄekĂĄme na odpovÄÄ a dekĂłdujeme pĆĂchozĂ zprĂĄvu Msg;
- ujistÄte se, ĆŸe dostanete handshake1;
- dekĂłdovat pomĂjivĂœ veĆejnĂœ klĂÄ protÄjĆĄĂ strany a vypoÄĂtat klĂÄ relace;
- Generujeme symetrickĂ© klĂÄe potĆebnĂ© pro zpracovĂĄnĂ TBE ÄĂĄsti zprĂĄvy.
423 logging.info("%s: got %s message", _id, msg.choice) 424 if msg.choice != "handshake1": 425 logging.warning("%s: unexpected message, disconnecting", _id) 426 writer.close() 427 return 428 # }}} 429 msg_handshake1 = msg.value 430 # Validate Handshake message {{{ 431 cookie_their = msg_handshake1["cookieResponder"] 432 pub_their_raw = msg_handshake1["pubKeyResponder"] 433 pub_their = gost3410.pub_unmarshal(bytes(pub_their_raw)) 434 ukm_raw = bytes(msg_handshake1["ukm"]) 435 ukm = ukm_unmarshal(ukm_raw) 436 key_session = kek_34102012256(CURVE, prv, pub_their, ukm, mode=2001) 437 kdf = Hkdf(None, key_session, hash=GOST34112012256) 438 key_handshake1_mac_identity = kdf.expand(b"handshake1-mac-identity") 439 key_handshake1_enc = kdf.expand(b"handshake1-enc") 440 key_handshake1_mac = kdf.expand(b"handshake1-mac")UKM je 64bitovĂ© ÄĂslo (urandom(8)), kterĂ© takĂ© vyĆŸaduje deserializaci z jeho bytovĂ© reprezentace pomocĂ gost3410_vko.ukm_unmarshal(). Funkce VKO pro 34.10 2012-bit je gost256_vko.kek_3410() (KEK - ĆĄifrovacĂ klĂÄ).
VygenerovanĂœ klĂÄ relace je jiĆŸ 256bitovĂĄ pseudonĂĄhodnĂĄ sekvence bajtĆŻ. Proto mĆŻĆŸe bĂœt okamĆŸitÄ pouĆŸit ve funkcĂch HKDF. ProtoĆŸe GOST34112012256 vyhovuje rozhranĂ hashlib, lze jej okamĆŸitÄ pouĆŸĂt ve tĆĂdÄ Hkdf. Salt (prvnĂ argument Hkdf) neuvĂĄdĂme, protoĆŸe vygenerovanĂœ klĂÄ bude kvĆŻli pomĂjivosti zĂșÄastnÄnĂœch pĂĄrĆŻ klĂÄĆŻ pro kaĆŸdou relaci jinĂœ a jiĆŸ obsahuje dostatek entropie. kdf.expand() ve vĂœchozĂm nastavenĂ jiĆŸ vytvĂĄĆĂ 256bitovĂ© klĂÄe poĆŸadovanĂ© pro Grasshopper pozdÄji.
DĂĄle se zkontrolujĂ ÄĂĄsti TBE a TBS pĆĂchozĂ zprĂĄvy:
- MAC pĆes pĆĂchozĂ ĆĄifrovĂœ text se vypoÄĂtĂĄ a zkontroluje;
- ĆĄifrovanĂœ text je deĆĄifrovĂĄn;
- Struktura TBE je dekĂłdovĂĄna;
- odebere se z nÄj identifikĂĄtor partnera a zkontroluje se, zda je nĂĄm vĆŻbec znĂĄm;
- MAC pĆes tento identifikĂĄtor se vypoÄĂtĂĄ a zkontroluje;
- je ovÄĆen podpis nad strukturou TBS, kterĂĄ zahrnuje cookie obou stran a veĆejnĂœ efemĂ©rnĂ klĂÄ protÄjĆĄĂ strany. Podpis je ovÄĆen trvanlivĂœm podpisovĂœm klĂÄem ĂșÄastnĂka jednĂĄnĂ.
441 try: 442 peer_name = validate_tbe( 443 msg_handshake1, 444 key_handshake1_mac_identity, 445 key_handshake1_enc, 446 key_handshake1_mac, 447 cookie_our, 448 cookie_their, 449 pub_their_raw, 450 ) 451 except ValueError as err: 452 logging.warning("%s: %s, disconnecting", _id, err) 453 writer.close() 454 return 455 # }}} 128 def validate_tbe( 129 msg_handshake: Union[MsgHandshake1, MsgHandshake2], 130 key_mac_identity: bytes, 131 key_enc: bytes, 132 key_mac: bytes, 133 cookie_their: Cookie, 134 cookie_our: Cookie, 135 pub_key_our: PubKey, 136 ) -> str: 137 ciphertext = bytes(msg_handshake["ciphertext"]) 138 mac_tag = mac(GOST3412Kuznechik(key_mac).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext) 139 if not compare_digest(mac_tag, bytes(msg_handshake["ciphertextMac"])): 140 raise ValueError("invalid MAC") 141 plaintext = ctr( 142 GOST3412Kuznechik(key_enc).encrypt, 143 KUZNECHIK_BLOCKSIZE, 144 ciphertext, 145 8 * b"x00", 146 ) 147 try: 148 tbe, _ = HandshakeTBE().decode(plaintext) 149 except ASN1Error: 150 raise ValueError("can not decode TBE") 151 key_sign_pub_hash = bytes(tbe["identity"]) 152 peer = KEYS.get(key_sign_pub_hash) 153 if peer is None: 154 raise ValueError("unknown identity") 155 mac_tag = mac( 156 GOST3412Kuznechik(key_mac_identity).encrypt, 157 KUZNECHIK_BLOCKSIZE, 158 key_sign_pub_hash, 159 ) 160 if not compare_digest(mac_tag, bytes(tbe["identityMac"])): 161 raise ValueError("invalid identity MAC") 162 tbs = HandshakeTBS(( 163 ("cookieTheir", cookie_their), 164 ("cookieOur", cookie_our), 165 ("pubKeyOur", pub_key_our), 166 )) 167 if not gost3410.verify( 168 CURVE, 169 peer["pub"], 170 GOST34112012256(tbs.encode()).digest(), 171 bytes(tbe["signature"]), 172 ): 173 raise ValueError("invalid signature") 174 return peer["name"]Jak jsem psal vĂœĆĄe, 34.13 popisuje rĆŻznĂ© ze dne 34.12. Mezi nimi je reĆŸim pro generovĂĄnĂ imitacĂ vloĆŸek a vĂœpoÄty MAC. V PyGOST je to gost2015.mac(). Tento reĆŸim vyĆŸaduje pĆedĂĄnĂ ĆĄifrovacĂ funkce (pĆĂjem a vrĂĄcenĂ jednoho bloku dat), velikost ĆĄifrovacĂho bloku a vlastnÄ i data samotnĂĄ. ProÄ nemĆŻĆŸete pevnÄ zakĂłdovat velikost ĆĄifrovacĂho bloku? 3413 popisuje nejen 34.12bitovou ĆĄifru Grasshopper, ale takĂ© 2015bitovou - mĂrnÄ upravenĂœ GOST 28147-89, vytvoĆenĂœ zpÄt v KGB a stĂĄle mĂĄ jeden z nejvyĆĄĆĄĂch bezpeÄnostnĂch prahĆŻ.
Kuznechik je inicializovĂĄn volĂĄnĂm gost.3412.GOST3412Kuznechik(key) a vracĂ objekt s metodami .encrypt()/.decrypt() vhodnĂœmi pro pĆedĂĄnĂ funkcĂm 34.13. MAC se vypoÄĂtĂĄ nĂĄsledovnÄ: gost3413.mac(GOST3412Kuznechik(klĂÄ).ĆĄifrovat, KUZNECHIK_BLOCKSIZE, ĆĄifrovĂœ text). Pro porovnĂĄnĂ vypoÄtenĂ© a pĆijatĂ© MAC nemĆŻĆŸete pouĆŸĂt obvyklĂ© srovnĂĄnĂ (==) bajtovĂœch ĆetÄzcĆŻ, protoĆŸe tato operace unikĂĄ porovnĂĄvacĂ Äas, coĆŸ v obecnĂ©m pĆĂpadÄ mĆŻĆŸe vĂ©st k fatĂĄlnĂm zranitelnostem, jako je Ăștoky na TLS. Python mĂĄ k tomu speciĂĄlnĂ funkci hmac.compare_digest.
Funkce blokovĂ© ĆĄifry dokĂĄĆŸe zaĆĄifrovat pouze jeden blok dat. Pro vÄtĆĄĂ poÄet, a to ani ne nĂĄsobek dĂ©lky, je nutnĂ© pouĆŸĂt reĆŸim ĆĄifrovĂĄnĂ. 34.13-2015 popisuje nĂĄsledujĂcĂ: ECB, CTR, OFB, CBC, CFB. KaĆŸdĂœ mĂĄ svĂ© vlastnĂ pĆijatelnĂ© oblasti pouĆŸitĂ a vlastnosti. BohuĆŸel stĂĄle nemĂĄme standardizovanĂ© (jako CCM, OCB, GCM a podobnÄ) - jsme nuceni si MAC alespoĆ pĆidat sami. vybĂrĂĄm si (CTR): nevyĆŸaduje vĂœplĆ do velikosti bloku, lze paralelizovat, pouĆŸĂvĂĄ pouze funkci ĆĄifrovĂĄnĂ, lze jej bezpeÄnÄ pouĆŸĂt k ĆĄifrovĂĄnĂ velkĂ©ho mnoĆŸstvĂ zprĂĄv (na rozdĂl od CBC, kterĂ© mĂĄ kolize pomÄrnÄ rychle).
StejnÄ jako .mac() pĆijĂmĂĄ .ctr() podobnĂœ vstup: ĆĄifrovĂœ text = gost3413.ctr(GOST3412Kuznechik(klĂÄ).encrypt, KUZNECHIK_BLOCKSIZE, prostĂœ text, iv). Je nutnĂ© zadat inicializaÄnĂ vektor, kterĂœ je pĆesnÄ poloviÄnĂ neĆŸ dĂ©lka ĆĄifrovacĂho bloku. Pokud je nĂĄĆĄ ĆĄifrovacĂ klĂÄ pouĆŸit pouze k zaĆĄifrovĂĄnĂ jednĂ© zprĂĄvy (i kdyĆŸ z vĂce blokĆŻ), pak je bezpeÄnĂ© nastavit nulovĂœ inicializaÄnĂ vektor. K ĆĄifrovĂĄnĂ zprĂĄv typu handshake pouĆŸĂvĂĄme pokaĆŸdĂ© samostatnĂœ klĂÄ.
OvÄĆenĂ podpisu gost3410.verify() je triviĂĄlnĂ: pĆedĂĄme eliptickou kĆivku, ve kterĂ© pracujeme (prostÄ ji zaznamenĂĄme v naĆĄem protokolu GOSTIM), veĆejnĂœ klĂÄ podepisujĂcĂho (nezapomeĆte, ĆŸe by to mÄla bĂœt n-tice dvou velkĂĄ ÄĂsla, nikoli bajtovĂœ ĆetÄzec), hash 34.11. 2012. XNUMX a samotnĂœ podpis.
DĂĄle v iniciĂĄtoru pĆipravĂme a odeĆĄleme handshake zprĂĄvu do handshake2, pĆiÄemĆŸ provedeme stejnĂ© akce jako pĆi ovÄĆovĂĄnĂ, pouze symetricky: podepisovĂĄnĂ naĆĄich klĂÄĆŻ mĂsto kontroly atd...
456 # Prepare and send Handshake 2 message {{{ 457 tbs = HandshakeTBS(( 458 ("cookieTheir", cookie_their), 459 ("cookieOur", cookie_our), 460 ("pubKeyOur", pub_our_raw), 461 )) 462 signature = gost3410.sign( 463 CURVE, 464 KEY_OUR_SIGN_PRV, 465 GOST34112012256(tbs.encode()).digest(), 466 ) 467 key_handshake2_mac_identity = kdf.expand(b"handshake2-mac-identity") 468 mac_tag = mac( 469 GOST3412Kuznechik(key_handshake2_mac_identity).encrypt, 470 KUZNECHIK_BLOCKSIZE, 471 bytes(KEY_OUR_SIGN_PUB_HASH), 472 ) 473 tbe = HandshakeTBE(( 474 ("identity", KEY_OUR_SIGN_PUB_HASH), 475 ("signature", OctetString(signature)), 476 ("identityMac", MAC(mac_tag)), 477 )) 478 tbe_raw = tbe.encode() 479 key_handshake2_enc = kdf.expand(b"handshake2-enc") 480 key_handshake2_mac = kdf.expand(b"handshake2-mac") 481 ciphertext = ctr( 482 GOST3412Kuznechik(key_handshake2_enc).encrypt, 483 KUZNECHIK_BLOCKSIZE, 484 tbe_raw, 485 8 * b"x00", 486 ) 487 mac_tag = mac( 488 GOST3412Kuznechik(key_handshake2_mac).encrypt, 489 KUZNECHIK_BLOCKSIZE, 490 ciphertext, 491 ) 492 writer.write(Msg(("handshake2", MsgHandshake2(( 493 ("ciphertext", OctetString(ciphertext)), 494 ("ciphertextMac", MAC(mac_tag)), 495 )))).encode()) 496 # }}} 497 await writer.drain() 498 logging.info("%s: session established: %s", _id, peer_name)Po navĂĄzĂĄnĂ relace se vygenerujĂ pĆenosovĂ© klĂÄe (samostatnĂœ klĂÄ pro ĆĄifrovĂĄnĂ, pro ovÄĆovĂĄnĂ, pro kaĆŸdou ze stran) a inicializuje se Grasshopper, aby deĆĄifroval a zkontroloval MAC:
499 # Run text message sender, initialize transport decoder {{{ 500 key_initiator_enc = kdf.expand(b"transport-initiator-enc") 501 key_initiator_mac = kdf.expand(b"transport-initiator-mac") 502 key_responder_enc = kdf.expand(b"transport-responder-enc") 503 key_responder_mac = kdf.expand(b"transport-responder-mac") ... 509 asyncio.ensure_future(msg_sender( 510 peer_name, 511 key_initiator_enc, 512 key_initiator_mac, 513 writer, 514 )) 515 encrypter = GOST3412Kuznechik(key_responder_enc).encrypt 516 macer = GOST3412Kuznechik(key_responder_mac).encrypt 517 # }}} 519 nonce_expected = 0 520 # Wait for test messages {{{ 521 while True: 522 data = await reader.read(MaxMsgLen) ... 530 msg, tail = Msg().decode(buf) ... 537 try: 538 await msg_receiver( 539 msg.value, 540 nonce_expected, 541 macer, 542 encrypter, 543 peer_name, 544 ) 545 except ValueError as err: 546 logging.warning("%s: %s", err) 547 break 548 nonce_expected += 1 549 # }}}Korutina msg_sender nynĂ ĆĄifruje zprĂĄvy pĆed jejich odeslĂĄnĂm na TCP spojenĂ. KaĆŸdĂĄ zprĂĄva mĂĄ monotĂłnnÄ rostoucĂ nonce, coĆŸ je takĂ© inicializaÄnĂ vektor pĆi zaĆĄifrovĂĄnĂ v reĆŸimu ÄĂtaÄe. Je zaruÄeno, ĆŸe kaĆŸdĂĄ zprĂĄva a blok zprĂĄv bude mĂt jinou hodnotu ÄĂtaÄe.
async def msg_sender(peer_name: str, key_enc: bytes, key_mac: bytes, writer) -> None: nonce = 0 encrypter = GOST3412Kuznechik(key_enc).encrypt macer = GOST3412Kuznechik(key_mac).encrypt in_queue = IN_QUEUES[peer_name] while True: text = await in_queue.get() if text is None: break ciphertext = ctr( encrypter, KUZNECHIK_BLOCKSIZE, text.encode("utf-8"), long2bytes(nonce, 8), ) payload = MsgTextPayload(( ("nonce", Integer(nonce)), ("ciphertext", OctetString(ciphertext)), )) mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) writer.write(Msg(("text", MsgText(( ("payload", payload), ("payloadMac", MAC(mac_tag)), )))).encode()) nonce += 1PĆĂchozĂ zprĂĄvy jsou zpracovĂĄvĂĄny rutinou msg_receiver, kterĂĄ zajiĆĄĆ„uje ovÄĆovĂĄnĂ a deĆĄifrovĂĄnĂ:
async def msg_receiver( msg_text: MsgText, nonce_expected: int, macer, encrypter, peer_name: str, ) -> None: payload = msg_text["payload"] if int(payload["nonce"]) != nonce_expected: raise ValueError("unexpected nonce value") mac_tag = mac(macer, KUZNECHIK_BLOCKSIZE, payload.encode()) if not compare_digest(mac_tag, bytes(msg_text["payloadMac"])): raise ValueError("invalid MAC") plaintext = ctr( encrypter, KUZNECHIK_BLOCKSIZE, bytes(payload["ciphertext"]), long2bytes(nonce_expected, 8), ) text = plaintext.decode("utf-8") await OUT_QUEUES[peer_name].put(text)ZĂĄvÄr
GOSTIM je urÄen vĂœhradnÄ pro vzdÄlĂĄvacĂ ĂșÄely (protoĆŸe se na nÄj nevztahujĂ alespoĆ testy)! ZdrojovĂœ kĂłd programu lze stĂĄhnout (ĐĄŃŃĐžĐ±ĐŸĐł-256 Ń ŃŃ: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). ĐаĐș Đž ĐČŃĐ” ĐŒĐŸĐž ĐżŃĐŸĐ”ĐșŃŃ, ŃОпа , , , , GOSTIM je zcela distribuovĂĄn za podmĂnek .
, , Älen , Python/Go-vĂœvojĂĄĆ, hlavnĂ specialista .
Zdroj: www.habr.com
