GOSTIM: P2P F2F E2EE IM sa isang gabi na may GOST cryptography

Ang pagiging isang developer PyGOST mga aklatan (GOST cryptographic primitives sa purong Python), madalas akong makatanggap ng mga tanong tungkol sa kung paano ipatupad ang pinakasimpleng secure na pagmemensahe sa tuhod. Itinuturing ng maraming tao na ang inilapat na cryptography ay medyo simple, at ang pagtawag sa .encrypt() sa isang block cipher ay sapat na upang maipadala ito nang ligtas sa isang channel ng komunikasyon. Ang iba ay naniniwala na ang inilapat na cryptography ay ang kapalaran ng iilan, at ito ay katanggap-tanggap na ang mayayamang kumpanya tulad ng Telegram na may mga olympiad-mathematician hindi maipatupad secure na protocol.

Ang lahat ng ito ay nag-udyok sa akin na isulat ang artikulong ito upang ipakita na ang pagpapatupad ng mga cryptographic na protocol at secure na IM ay hindi napakahirap na gawain. Gayunpaman, hindi sulit ang pag-imbento ng iyong sariling pagpapatotoo at mga pangunahing protocol ng kasunduan.

GOSTIM: P2P F2F E2EE IM sa isang gabi na may GOST cryptography
Magsusulat ang artikulo aninagin-to-peer, kaibigan-sa-kaibigan, end-to-end na naka-encrypt instant messenger na may SIGMA-I pagpapatunay at pangunahing protocol ng kasunduan (batay sa kung saan ito ipinatupad IPsec IKE), gamit ang eksklusibong GOST cryptographic algorithm PyGOST library at ASN.1 message encoding library PyDERASN (tungkol sa kung saan ako na nagsulat noon). Isang paunang kinakailangan: ito ay dapat na napakasimple na maaari itong isulat mula sa simula sa isang gabi (o araw ng trabaho), kung hindi, ito ay hindi na isang simpleng programa. Marahil ito ay may mga error, hindi kinakailangang mga komplikasyon, mga pagkukulang, at ito ang aking unang programa gamit ang asyncio library.

disenyo ng IM

Una, kailangan nating maunawaan kung ano ang magiging hitsura ng ating IM. Para sa pagiging simple, hayaan itong maging isang peer-to-peer network, nang walang anumang pagtuklas ng mga kalahok. Personal naming ipahiwatig kung aling address: port upang kumonekta upang makipag-usap sa kausap.

Naiintindihan ko na, sa oras na ito, ang pagpapalagay na ang direktang komunikasyon ay magagamit sa pagitan ng dalawang di-makatwirang mga computer ay isang makabuluhang limitasyon sa applicability ng IM sa pagsasanay. Ngunit kung mas maraming mga developer ang nagpapatupad ng lahat ng uri ng NAT-traversal crutches, mas mananatili tayo sa IPv4 Internet, na may nakababahalang posibilidad ng komunikasyon sa pagitan ng mga arbitrary na computer. Gaano katagal mo matitiis ang kakulangan ng IPv6 sa bahay at sa trabaho?

Magkakaroon tayo ng network ng kaibigan-sa-kaibigan: lahat ng posibleng kausap ay dapat na malaman nang maaga. Una, lubos nitong pinapasimple ang lahat: ipinakilala namin ang aming sarili, natagpuan o hindi nakita ang pangalan/susi, nadiskonekta o patuloy na nagtatrabaho, alam ang kausap. Pangalawa, sa pangkalahatan, ito ay ligtas at inaalis ang maraming pag-atake.

Ang interface ng IM ay magiging malapit sa mga klasikong solusyon walang kabuluhang mga proyekto, na talagang gusto ko para sa kanilang minimalism at Unix-way na pilosopiya. Ang IM program ay lumilikha ng isang direktoryo na may tatlong Unix domain socket para sa bawat interlocutor:

  • saβ€”mga mensaheng ipinadala sa kausap ay nakatala dito;
  • out - ang mga mensahe na natanggap mula sa interlocutor ay binabasa mula dito;
  • estado - sa pamamagitan ng pagbabasa mula dito, malalaman natin kung kasalukuyang konektado ang interlocutor, ang address/port ng koneksyon.

Bilang karagdagan, ang isang conn socket ay nilikha, sa pamamagitan ng pagsusulat ng host port kung saan kami magsisimula ng isang koneksyon sa remote interlocutor.

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

Binibigyang-daan ka ng diskarteng ito na gumawa ng mga independiyenteng pagpapatupad ng IM transport at user interface, dahil walang kaibigan, hindi mo mapasaya ang lahat. Gamit tmux at / o multitail, maaari kang makakuha ng multi-window interface na may pag-highlight ng syntax. At sa tulong rlwrap maaari kang makakuha ng linya ng input ng mensahe na katugma sa GNU Readline.

Sa katunayan, ang mga walang sipsip na proyekto ay gumagamit ng FIFO file. Sa personal, hindi ko maintindihan kung paano magtrabaho sa mga file nang mapagkumpitensya sa asyncio nang walang sulat-kamay na background mula sa mga nakalaang thread (ginagamit ko ang wika para sa mga ganoong bagay sa mahabang panahon Go). Samakatuwid, nagpasya akong gawin ang mga socket ng domain ng Unix. Sa kasamaang palad, ginagawa nitong imposibleng gawin ang echo 2001:470:dead::babe 6666 > conn. Nalutas ko ang problemang ito gamit ang socat: echo 2001:470:dead::babe 6666 | socat - UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.

Ang orihinal na hindi secure na protocol

Ginagamit ang TCP bilang transportasyon: ginagarantiyahan nito ang paghahatid at pagkakasunud-sunod nito. Hindi ginagarantiyahan ng UDP ang alinman (na magiging kapaki-pakinabang kapag ginamit ang cryptography), ngunit suporta SCTP Ang Python ay hindi lumalabas sa kahon.

Sa kasamaang palad, sa TCP ay walang konsepto ng isang mensahe, isang stream lamang ng mga byte. Samakatuwid, kinakailangang makabuo ng isang format para sa mga mensahe upang maibahagi ang mga ito sa kanilang sarili sa thread na ito. Maaari kaming sumang-ayon na gamitin ang line feed character. Mabuti para sa mga nagsisimula, ngunit kapag sinimulan na naming i-encrypt ang aming mga mensahe, maaaring lumitaw ang character na ito kahit saan sa ciphertext. Sa mga network, samakatuwid, ang mga sikat na protocol ay ang mga unang nagpapadala ng haba ng mensahe sa mga byte. Halimbawa, out of the box ang Python ay may xdrlib, na nagbibigay-daan sa iyo upang gumana sa isang katulad na format XDR.

Hindi kami gagana nang tama at mahusay sa pagbabasa ng TCP - pasimplehin namin ang code. Binabasa namin ang data mula sa socket sa isang walang katapusang loop hanggang sa ma-decode namin ang kumpletong mensahe. Ang JSON na may XML ay maaari ding gamitin bilang isang format para sa diskarteng ito. Ngunit kapag idinagdag ang cryptography, ang data ay kailangang malagdaan at ma-authenticate - at mangangailangan ito ng isang byte-for-byte na magkaparehong representasyon ng mga bagay, na hindi ibinibigay ng JSON/XML (maaaring mag-iba ang mga resulta ng dump).

Ang XDR ay angkop para sa gawaing ito, gayunpaman pinili ko ang ASN.1 na may DER encoding at PyDERASN library, dahil magkakaroon tayo ng mga bagay na may mataas na antas na kadalasang mas kaaya-aya at maginhawang magtrabaho. Hindi tulad ng schemaless bencode, MessagePack o CBOR, awtomatikong susuriin ng ASN.1 ang data laban sa isang hard-coded na schema.

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

Ang natanggap na mensahe ay magiging Msg: alinman sa isang text MsgText (na may isang text field sa ngayon) o isang MsgHandshake handshake message (na naglalaman ng pangalan ng kausap). Ngayon mukhang sobrang kumplikado, ngunit ito ay isang pundasyon para sa hinaharap.

     β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ ┬──D ) β”‚ │───────── ────────>β”‚ β”‚ β”‚ β”‚MsgHandshake(IdB) β”‚ β”‚<─────────│─────── β”‚ β”‚ MsgText() β”‚ │──── MsgText() β”‚ β”‚ β”‚

IM na walang cryptography

Gaya ng nasabi ko na, ang asyncio library ay gagamitin para sa lahat ng operasyon ng socket. Ipahayag natin kung ano ang inaasahan natin sa paglulunsad:

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

Itakda ang iyong sariling pangalan (--our-name alice). Ang lahat ng inaasahang kausap ay nakalista na pinaghihiwalay ng mga kuwit (β€”their-name bob,eve). Para sa bawat isa sa mga interlocutor, isang direktoryo na may mga Unix socket ay nilikha, pati na rin isang coroutine para sa bawat 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"))

Ang mga mensaheng nagmumula sa user mula sa in socket ay ipinapadala sa IN_QUEUES queue:

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

Ang mga mensaheng nagmumula sa mga kausap ay ipinapadala sa OUT_QUEUES na mga pila, kung saan isinusulat ang data sa out socket:

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

Kapag nagbabasa mula sa isang state socket, hinahanap ng programa ang address ng kausap sa PEER_ALIVE na diksyunaryo. Kung wala pang koneksyon sa interlocutor, pagkatapos ay isang walang laman na linya ang nakasulat.

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

Kapag nagsusulat ng isang address sa isang conn socket, ang function na "initiator" ng koneksyon ay inilunsad:

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

Isaalang-alang natin ang nagpasimula. Una ay malinaw na nagbubukas ito ng koneksyon sa tinukoy na host/port at nagpapadala ng mensahe ng handshake na may pangalan nito:

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

Pagkatapos, naghihintay ito ng tugon mula sa malayong partido. Sinusubukang i-decode ang papasok na tugon gamit ang Msg ASN.1 scheme. Ipinapalagay namin na ang buong mensahe ay ipapadala sa isang TCP segment at matatanggap namin ito nang atomically kapag tumatawag sa .read(). Tinitingnan namin kung natanggap namin ang mensahe ng pagkakamay.

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

Sinusuri namin na ang natanggap na pangalan ng kausap ay kilala sa amin. Kung hindi, pagkatapos ay masira namin ang koneksyon. Sinusuri namin kung nakagawa na kami ng isang koneksyon sa kanya (muling nagbigay ng utos ang kausap na kumonekta sa amin) at isara ito. Ang IN_QUEUES queue ay nagtataglay ng mga string ng Python na may text ng mensahe, ngunit may espesyal na halaga na Wala na nagsenyas sa msg_sender coroutine na huminto sa paggana upang makalimutan nito ang tungkol sa manunulat nito na nauugnay sa legacy na koneksyon sa 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     # }}}

tumatanggap ang msg_sender ng mga papalabas na mensahe (nakapila mula sa isang in socket), ini-serialize ang mga ito sa isang mensaheng MsgText at ipinapadala ang mga ito sa isang koneksyon sa TCP. Maaari itong masira anumang sandali - malinaw nating naharang ito.

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

Sa dulo, ang initiator ay pumapasok sa isang walang katapusang loop ng pagbabasa ng mga mensahe mula sa socket. Sinusuri kung ang mga mensaheng ito ay mga text message at inilalagay ang mga ito sa OUT_QUEUES queue, kung saan ipapadala ang mga ito sa out socket ng kaukulang kausap. Bakit hindi mo na lang gawin ang .read() at i-decode ang mensahe? Dahil posibleng maraming mensahe mula sa user ang pagsasama-samahin sa buffer ng operating system at ipapadala sa isang TCP segment. Maaari naming i-decode ang una, at pagkatapos ay ang bahagi ng kasunod na isa ay maaaring manatili sa buffer. Sa kaso ng anumang abnormal na sitwasyon, isinasara namin ang koneksyon ng TCP at ihihinto ang msg_sender coroutine (sa pamamagitan ng pagpapadala ng Wala sa OUT_QUEUES queue).

 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)

Bumalik tayo sa pangunahing code. Matapos gawin ang lahat ng coroutine sa oras na magsimula ang programa, sisimulan namin ang TCP server. Para sa bawat naitatag na koneksyon, lumilikha ito ng coroutine ng responder.

logging.basicConfig(
    level=logging.INFO,
    format="%(levelname)s %(asctime)s: %(funcName)s: %(message)s",
)
loop = asyncio.get_event_loop()
server = loop.run_until_complete(asyncio.start_server(responder, args.bind, args.port))
logging.info("Listening on: %s", server.sockets[0].getsockname())
loop.run_forever()

Ang responder ay katulad ng initiator at sinasalamin ang lahat ng parehong mga aksyon, ngunit ang walang katapusang loop ng pagbabasa ng mga mensahe ay nagsisimula kaagad, para sa pagiging simple. Sa kasalukuyan, ang protocol ng handshake ay nagpapadala ng isang mensahe mula sa bawat panig, ngunit sa hinaharap ay magkakaroon ng dalawa mula sa initiator ng koneksyon, pagkatapos ay maipapadala kaagad ang mga text message.

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

Secure na protocol

Oras na para i-secure ang ating mga komunikasyon. Ano ang ibig nating sabihin sa seguridad at ano ang gusto natin:

  • pagiging kompidensiyal ng mga ipinadalang mensahe;
  • pagiging tunay at integridad ng mga ipinadalang mensahe - dapat makita ang kanilang mga pagbabago;
  • proteksyon laban sa mga pag-atake ng replay - ang katotohanan ng nawawala o paulit-ulit na mga mensahe ay dapat makita (at nagpasya kaming wakasan ang koneksyon);
  • pagkakakilanlan at pagpapatunay ng mga kausap gamit ang paunang inilagay na mga pampublikong key - napagpasyahan na namin kanina na gagawa kami ng network ng kaibigan-sa-kaibigan. Pagkatapos lamang ng pagpapatunay ay mauunawaan natin kung kanino tayo nakikipag-usap;
  • availability perpektong pasulong na lihim properties (PFS) - ang pagkompromiso sa aming matagal nang signing key ay hindi dapat humantong sa kakayahang basahin ang lahat ng nakaraang sulat. Ang pagtatala ng naharang na trapiko ay nagiging walang silbi;
  • validity/validity ng mga mensahe (transport at handshake) sa loob lang ng isang TCP session. Ang pagpasok ng wastong nilagdaan/na-authenticate na mga mensahe mula sa isa pang session (kahit na may parehong kausap) ay hindi dapat posible;
  • hindi dapat makita ng passive observer ang alinman sa mga identifier ng user, ipinadala ang pangmatagalang public key, o mga hash mula sa kanila. Isang tiyak na anonymity mula sa isang passive observer.

Nakapagtataka, halos lahat ay gustong magkaroon ng pinakamababang ito sa anumang protocol ng handshake, at napakakaunti sa nabanggit sa huli ay natutugunan para sa "homegrown" na mga protocol. Ngayon hindi kami mag-iimbento ng bago. Talagang inirerekumenda ko ang paggamit balangkas ng ingay para sa pagbuo ng mga protocol, ngunit pumili tayo ng mas simple.

Ang dalawang pinakasikat na protocol ay:

  • TLS - isang napakakomplikadong protocol na may mahabang kasaysayan ng mga bug, jamb, mga kahinaan, hindi magandang pag-iisip, pagiging kumplikado at mga pagkukulang (gayunpaman, ito ay walang gaanong kinalaman sa TLS 1.3). Ngunit hindi namin ito isinasaalang-alang dahil ito ay sobrang kumplikado.
  • IPsec с IKE β€” walang malubhang problema sa cryptographic, bagama't hindi rin sila simple. Kung nabasa mo ang tungkol sa IKEv1 at IKEv2, kung gayon ang kanilang pinagmulan ay STS, ISO/IEC IS 9798-3 at SIGMA (SIGn-and-MAc) na mga protocol - sapat na simple upang ipatupad sa isang gabi.

Ano ang mabuti sa SIGMA, bilang pinakabagong link sa pagbuo ng mga protocol ng STS/ISO? Natutugunan nito ang lahat ng aming kinakailangan (kabilang ang "pagtatago" ng mga identifier ng interlocutor) at walang alam na mga problema sa cryptographic. Ito ay minimalistic - ang pag-alis ng hindi bababa sa isang elemento mula sa mensahe ng protocol ay hahantong sa kawalan ng seguridad nito.

Pumunta tayo mula sa pinakasimpleng home-grown protocol hanggang sa SIGMA. Ang pinakapangunahing operasyon na interesado kami ay pangunahing kasunduan: Isang function na naglalabas ng parehong halaga sa parehong kalahok, na maaaring magamit bilang isang simetriko na key. Nang walang mga detalye: bawat isa sa mga partido ay bumubuo ng isang ephemeral (ginamit lamang sa loob ng isang session) key pair (pampubliko at pribadong key), pagpapalitan ng mga pampublikong susi, tawagan ang function ng kasunduan, sa input kung saan ipinapasa nila ang kanilang pribadong key at ang publiko susi ng kausap.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ ─── │┬── β”‚ ╔══════════ ══════════╗ │───────────────>β”‚ ───────────────>β”‚ β•‘PrvA, Pu───>β”‚ β•‘PrvA ─║║PrvA, Pu ═ ════════ ═══════════╝ β”‚ IdB, PubB β”‚ ╔═══════════␐␕═══␐══ β”‚<───────── ──────│ β•‘PrvB, PubB = DHgen()β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•ββ•β•β•βββ••β•β•ββ•β•” ───┐ ╔════ ═══╧════════════╗ β”‚ β•‘Key = DH(PrvA, PubB)β•‘ <β”€β”€β”€β”ββ•šβ••β”ββ•šβ••β”β˜ ═══════ ════╝ β”‚ β”‚ β”‚ β”‚

Kahit sino ay maaaring tumalon sa gitna at palitan ang mga pampublikong susi ng kanilang sarili - walang pagpapatunay ng mga kausap sa protocol na ito. Magdagdag tayo ng pirma na may matagal nang mga susi.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ ─── │┬── SignPrvA, (PubA)) β”‚ ╔═ │──────────── ────────── ─────────────────────────── Sign ubA = load()β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β• ═══════ ══════════ ═══════ ═══════════␐═══␐═B, PubA , sign(SignPrvB, (PubB)) β”‚ ╔══════════════ ═══════ ══════════ ═══════ ══════‐— ─────────── ───────────── ──│ β•‘SignPrvB, SignPubB = load( )β•‘ β”‚ β”‚ β•‘PrvB, PubB ─│ β•‘ ═════════ . ═════╗ β”‚ β”‚ β•‘verify( SignPubB, ...)β•‘ β”‚ <β”€β”€β”€β”˜ β•‘Key = DH(Pr vA, PubB) β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•ββ•β•β•βββ•β••β•β•ββ•β••β•β•ββ•β••β•β•ββ•β••β• ═╝ β”‚ β”‚ β”‚

Ang gayong pirma ay hindi gagana, dahil hindi ito nakatali sa isang partikular na sesyon. Ang ganitong mga mensahe ay "angkop" din para sa mga sesyon kasama ang ibang mga kalahok. Ang buong konteksto ay dapat mag-subscribe. Pinipilit kami nitong magdagdag ng isa pang mensahe mula kay A.

Bilang karagdagan, mahalagang idagdag ang iyong sariling identifier sa ilalim ng lagda, dahil kung hindi, maaari naming palitan ang IdXXX at muling lagdaan ang mensahe gamit ang susi ng isa pang kilalang kausap. Iwasan pag-atake ng pagmuni-muni, kinakailangan na ang mga elemento sa ilalim ng lagda ay nasa malinaw na tinukoy na mga lugar ayon sa kanilang kahulugan: kung A sign (PubA, PubB), dapat lagdaan ng B (PubB, PubA). Ito rin ay nagsasalita sa kahalagahan ng pagpili ng istraktura at format ng serialized na data. Halimbawa, ang mga set sa ASN.1 DER encoding ay pinagbukod-bukod: SET OF(PubA, PubB) ay magiging magkapareho sa SET OF(PubB, PubA).

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ ─── │┬── β”‚ ╔══════════ ═════════════════╗ │───────────——──—──——──— ────────── ─────────────>β”‚ β•‘SignPrvA, SignPubA = load()β•‘ β”‚ β”‚ β•‘PrvA, PubA = β‘‚ ┐ β•• ═ ═══════ ═══════════════╝ β”‚IdB, PubB, sign(SignPrvB, (IdB, PubA, PubB)) β”‚ ␔␕═╕╕ ═════ ════════════╗ β”‚<──────────────── ─── ─ ────────── ─────────│ β•‘SignPrvB, SignPubB = load()β•‘ β”‚ β”‚ β•‘PrvB, PubB = DHgen() β•‘ β”‚ β”β•β•šβ•β•β•š ═ ════════ ══════════╝ β”‚ sign(SignPrvA, (IdA, PubB, PubA)) β”‚ ╔═══════␐═══␐═══␐══ ════╗ │─ ───────────────────────────────—───—── ───>β”‚ β•‘verify(SignPubB, ...) β•‘ β”‚ β”‚ β•‘key = dh (prva, PUBB) β•‘ β”‚ β”‚ β”‚

Gayunpaman, hindi pa rin namin "napatunayan" na nakabuo kami ng parehong nakabahaging key para sa session na ito. Sa prinsipyo, magagawa natin nang wala ang hakbang na ito - ang pinakaunang koneksyon sa transportasyon ay magiging di-wasto, ngunit gusto namin na kapag natapos na ang pakikipagkamay, sigurado kami na ang lahat ay talagang napagkasunduan. Sa ngayon mayroon kaming ISO/IEC IS 9798-3 na protocol sa kamay.

Maaari naming lagdaan ang nabuong susi mismo. Delikado ito, dahil posibleng may mga leaks sa signature algorithm na ginamit (kahit bits-per-signature, ngunit tumutulo pa rin). Posibleng pumirma ng hash ng derivation key, ngunit ang pagtagas kahit na ang hash ng derivated key ay maaaring maging mahalaga sa isang brute-force na pag-atake sa derivation function. Gumagamit ang SIGMA ng MAC function na nagpapatunay sa sender ID.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ ─── │┬── β”‚ ╔══════════ ═════════════════╗ │───────────——──—──——──— ────────── ──────────────────>β”‚ β•‘SignPrvA, SignPubA = load()β•‘ β”‚ β”‚ β•‘ β”‚ β”‚ β•‘PrvA, SignPubA = load()β•‘ β”‚ β”‚ β•‘PrvA β•š ═══════ ════════════════════╝ β”‚IdB, PubB, sign(SignPrvB, (PubA, PubB) β•”(IdB) β•”(IdB) β•”(IdB) ═ ═══ β”‚<───────────────── ────────── ——─ ────────── ─│ β•‘SignPrvB, SignPubB = load()β•‘ β”‚ β”‚ β•‘PrvB, PubB = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β• ══␐════␐═╕══␐═╕ ═════════ Ang β•‘Susi = DH( PrvA, PubB) β•‘ │───────────────────── ──  ───── ────────── ─────>β”‚ β•‘verify(Key, IdB) β•‘ β”‚ β”‚ β•‘verify(SignPubB, ...)β•‘ β”‚ β”‚ β•šβ•β•β•β•β•βββ•β••β•βββ•β••β•βββ•β••β•βββ•β••β•βββ•β•• ════ ═╝ β”‚ β”‚

Bilang isang pag-optimize, maaaring gusto ng ilan na gamitin muli ang kanilang mga ephemeral key (na, siyempre, kapus-palad para sa PFS). Halimbawa, nakabuo kami ng key pair, sinubukang kumonekta, ngunit hindi available ang TCP o naantala sa isang lugar sa gitna ng protocol. Nakakahiya na sayangin ang nasayang na entropy at mga mapagkukunan ng processor sa isang bagong pares. Samakatuwid, ipapakilala namin ang tinatawag na cookie - isang pseudo-random na halaga na magpoprotekta laban sa mga posibleng random na pag-atake ng replay kapag muling gumagamit ng mga ephemeral na pampublikong key. Dahil sa pagbubuklod sa pagitan ng cookie at ng panandaliang pampublikong susi, ang pampublikong susi ng kabaligtaran na partido ay maaaring alisin sa lagda bilang hindi kinakailangan.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ ‬─ │──, Pubookie A β”‚ ╔════════ ═══════════════════╗ │─────────—───—──—─—— ────────── ───────────────────────────────—───—── ─>β”‚ β•‘SignPrvA, SignPubA = load( )β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•ββ•β•β•βββ••β•β•ββ•β•• ══╝ β”‚IdB, PubB, CookieB , sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) β”‚ ╔═════════════════␐═══␐═══␐═══␐══ β•— β”‚< . ────────── ────────────────────│ β•‘SignPrvB, SignPubB = load()()β•‘ β”‚ β”‚ = ,β”‚ β”‚ β”‚ β”‚ β•šβ•β•β•β•β•β• ═════════════════════╝ β”‚ β”‚ ╔════␐═══␐␐╕══␐═╕ ═══════╗ β”‚ sign( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA) β”‚ β•‘Key = DH(PrvA, PubB) β•‘ │─────────────── ─── ── ── . ───────>β”‚ β•‘ verify(Key, IdB) β•‘ β”‚ β”‚ β•‘verify(SignPubB, ...)β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•ββ•β•β•ββ•β•β•ββ•β• β”‚

Sa wakas, gusto naming makuha ang privacy ng aming mga kasosyo sa pag-uusap mula sa isang passive observer. Upang gawin ito, iminumungkahi ng SIGMA na makipagpalitan muna ng mga ephemeral key at bumuo ng isang karaniwang key kung saan ie-encrypt ang pagpapatotoo at pagtukoy ng mga mensahe. Inilalarawan ng SIGMA ang dalawang opsyon:

  • SIGMA-I - pinoprotektahan ang nagpasimula mula sa mga aktibong pag-atake, ang tumutugon mula sa mga pasibo: pinapatotohanan ng initiator ang tumutugon at kung may hindi tumutugma, hindi nito ibinibigay ang pagkakakilanlan nito. Ang nasasakdal ay nagbibigay ng kanyang pagkakakilanlan kung ang isang aktibong protocol ay nagsimula sa kanya. Ang passive observer ay walang natutunan;
    SIGMA-R - pinoprotektahan ang tumutugon mula sa mga aktibong pag-atake, ang pasimuno mula sa mga pasibo. Ang lahat ay eksaktong kabaligtaran, ngunit sa protocol na ito apat na mensahe ng pagkakamay ang naipadala na.

    Pinipili namin ang SIGMA-I dahil mas katulad ito sa inaasahan namin mula sa mga pamilyar na bagay ng client-server: ang client ay kinikilala lamang ng authenticated server, at alam na ng lahat ang server. Dagdag pa, mas madaling ipatupad dahil sa mas kaunting mga mensahe ng handshake. Ang idinagdag lang namin sa protocol ay i-encrypt ang bahagi ng mensahe at ilipat ang identifier A sa naka-encrypt na bahagi ng huling mensahe:

    PubA, CookieA β”‚ ╔══════════ ═════════════════ ═══════════════‗ ══‐‐══‗ ────────── ───── ────────── ───────────────—───—─ ─────────── ───── ──────>β”‚ β•‘SignPrvA , SignPubA = load()β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() ␑ ════ ═════════ ═════════ ════╝ β”‚ PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) ╔┐)β••) ═══════════════ ═══════╗ β”‚<────— ─ ── ────────── ───── ────────── β•‘SignP rvB, SignPubB = load()β•‘ β”‚ β”‚ β•‘ PrvB, PubB = ␑gen() β•‘ ════════ ════════════════╝ β”‚ β”‚ ╔════════ ═␐╕══␐══ ══╗ β”‚ Enc((IdA, sign( SignPrva, (Cookieb, Cookiea, Puba)), Mac (IDA))) β”‚ β•‘Key = DH (PRVA, PubB) β•‘ β”‚ β”‚ β”‚ XNUMX ────────────────── ────────── ─——─ ─────────── ──────>β”‚ β•‘verify(Key, IdB) β•‘ β”‚ β”‚ β•‘verify( SignPubB, ...)β•‘ β”‚ β”‚ β•šβ•β•β•β•β•ββ•β••β•βββ••β•β•ββ•β•• ════ ══╝ β”‚ β”‚
    
    • GOST R ay ginagamit para sa lagda 34.10-2012 algorithm na may 256-bit key.
    • Upang buuin ang pampublikong key, 34.10/2012/XNUMX VKO ang ginagamit.
    • Ginagamit ang CMAC bilang MAC. Sa teknikal, ito ay isang espesyal na mode ng pagpapatakbo ng isang block cipher, na inilarawan sa GOST R 34.13-2015. Bilang isang function ng pag-encrypt para sa mode na ito βˆ’ Grasshopper (34.12-2015).
    • Ang hash ng kanyang pampublikong key ay ginagamit bilang identifier ng kausap. Ginamit bilang hash Stribog-256 (34.11/2012/256 XNUMX bits).

    Pagkatapos ng handshake, magkakasundo kami sa isang shared key. Magagamit namin ito para sa napatotohanang pag-encrypt ng mga mensahe sa transportasyon. Ang bahaging ito ay napakasimple at mahirap magkamali: dinadagdagan namin ang counter ng mensahe, ine-encrypt ang mensahe, pinapatotohanan (MAC) ang counter at ciphertext, ipinapadala. Kapag tumatanggap ng mensahe, tinitingnan namin na ang counter ay may inaasahang halaga, pinapatotohanan ang ciphertext gamit ang counter, at i-decrypt ito. Anong key ang dapat kong gamitin upang i-encrypt ang mga mensahe ng handshake, maghatid ng mga mensahe, at kung paano patotohanan ang mga ito? Ang paggamit ng isang susi para sa lahat ng mga gawaing ito ay mapanganib at hindi matalino. Ito ay kinakailangan upang makabuo ng mga susi gamit ang mga dalubhasang pag-andar Ang KDF (key derivation function). Muli, huwag nating hatiin ang buhok at mag-imbento ng isang bagay: HKDF matagal nang kilala, mahusay na sinaliksik at walang alam na mga problema. Sa kasamaang palad, ang katutubong Python library ay walang ganitong function, kaya ginagamit namin hkdf plastik na bag. Panloob na ginagamit ng HKDF HMAC, na gumagamit naman ng hash function. Ang isang halimbawang pagpapatupad sa Python sa pahina ng Wikipedia ay tumatagal lamang ng ilang linya ng code. Tulad ng sa kaso ng 34.10/2012/256, gagamitin namin ang Stribog-XNUMX bilang hash function. Ang output ng aming key agreement function ay tatawaging session key, kung saan bubuo ang mga nawawalang simetriko:

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

    Mga Istraktura/Skema

    Tingnan natin kung anong mga istruktura ng ASN.1 ang mayroon tayo ngayon para sa pagpapadala ng lahat ng data na ito:

    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 ang pipirmahan. HandshakeTBE - kung ano ang ie-encrypt. Iginuhit ko ang iyong pansin sa larangan ng ukm sa MsgHandshake1. 34.10 VKO, para sa mas malaking randomization ng mga nabuong key, kasama ang UKM (user keying material) parameter - karagdagang entropy lang.

    Pagdaragdag ng Cryptography sa Code

    Isaalang-alang lamang natin ang mga pagbabagong ginawa sa orihinal na code, dahil ang balangkas ay nanatiling pareho (sa katunayan, ang pangwakas na pagpapatupad ay isinulat muna, at pagkatapos ay ang lahat ng cryptography ay pinutol dito).

    Dahil ang pagpapatotoo at pagkilala sa mga kausap ay isasagawa gamit ang mga pampublikong susi, kailangan na silang maiimbak sa isang lugar nang mahabang panahon. Para sa pagiging simple, ginagamit namin ang JSON tulad nito:

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

    aming - aming key pair, hexadecimal pribado at pampublikong key. kanilang β€” mga pangalan ng mga kausap at kanilang mga pampublikong susi. Baguhin natin ang mga argumento ng command line at magdagdag ng post-processing ng data ng 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),
        }
    # }}}
    

    Ang pribadong key ng 34.10 algorithm ay isang random na numero. 256-bit na laki para sa 256-bit elliptic curves. Ang PyGOST ay hindi gumagana sa isang hanay ng mga byte, ngunit sa malalaking numero, kaya ang aming pribadong key (urandom(32)) ay kailangang ma-convert sa isang numero gamit ang gost3410.prv_unmarshal(). Ang pampublikong susi ay deterministikong tinutukoy mula sa pribadong susi gamit ang gost3410.public_key(). Ang pampublikong key 34.10 ay dalawang malalaking numero na kailangan ding i-convert sa isang byte sequence para sa kadalian ng pag-imbak at paghahatid gamit ang gost3410.pub_marshal().

    Pagkatapos basahin ang JSON file, ang mga pampublikong key ay kailangang i-convert pabalik gamit ang gost3410.pub_unmarshal(). Dahil matatanggap namin ang mga identifier ng mga interlocutor sa anyo ng hash mula sa pampublikong key, maaari silang agad na kalkulahin nang maaga at ilagay sa isang diksyunaryo para sa mabilis na paghahanap. Ang hash ng Stribog-256 ay gost34112012256.GOST34112012256(), na ganap na nakakatugon sa interface ng hashlib ng mga function ng hash.

    Paano nagbago ang initiator coroutine? Ang lahat ay ayon sa scheme ng handshake: bumubuo kami ng cookie (128-bit ay marami), isang ephemeral key pair na 34.10, na gagamitin para sa VKO key agreement function.

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

    • naghihintay kami ng tugon at nagde-decode ng papasok na mensahe ng Msg;
    • siguraduhing makakakuha ka ng pakikipagkamay1;
    • i-decode ang ephemeral public key ng kabaligtaran na partido at kalkulahin ang session key;
    • Bumubuo kami ng mga simetriko na key na kinakailangan para sa pagproseso ng TBE na bahagi ng mensahe.

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

    Ang UKM ay isang 64-bit na numero (urandom(8)), na nangangailangan din ng deserialization mula sa byte na representasyon nito gamit ang gost3410_vko.ukm_unmarshal(). Ang VKO function para sa 34.10/2012/256 3410-bit ay gost34102012256_vko.kek_XNUMX() (KEK - encryption key).

    Ang nabuong session key ay isa nang 256-bit pseudo-random byte sequence. Samakatuwid, maaari itong agad na magamit sa mga function ng HKDF. Dahil natutugunan ng GOST34112012256 ang interface ng hashlib, maaari itong agad na magamit sa klase ng Hkdf. Hindi namin tinukoy ang asin (ang unang argumento ng Hkdf), dahil ang nabuong key, dahil sa ephemerality ng mga kalahok na pares ng key, ay magkakaiba para sa bawat session at naglalaman na ng sapat na entropy. Ang kdf.expand() bilang default ay gumagawa na ng mga 256-bit na key na kinakailangan para sa Grasshopper sa susunod.

    Susunod, ang TBE at TBS na bahagi ng papasok na mensahe ay sinusuri:

    • ang MAC sa papasok na ciphertext ay kinakalkula at sinusuri;
    • ang ciphertext ay decrypted;
    • Na-decode ang istraktura ng TBE;
    • ang identifier ng kausap ay kinuha mula dito at ito ay nasuri kung siya ay kilala sa amin sa lahat;
    • Ang MAC sa ibabaw ng identifier na ito ay kinakalkula at sinusuri;
    • na-verify ang lagda sa istruktura ng TBS, na kinabibilangan ng cookie ng parehong partido at ang pampublikong pansamantalang key ng kabaligtaran na partido. Ang pirma ay napatunayan ng matagal nang signature key ng kausap.

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

    Tulad ng isinulat ko sa itaas, 34.13/2015/XNUMX ay naglalarawan ng iba't ibang harangan ang mga mode ng pagpapatakbo ng cipher mula 34.12/2015/3413. Kabilang sa mga ito ay mayroong isang mode para sa pagbuo ng mga pagsingit ng imitasyon at mga kalkulasyon ng MAC. Sa PyGOST ito ay gost34.12.mac(). Ang mode na ito ay nangangailangan ng pagpasa sa encryption function (pagtanggap at pagbabalik ng isang bloke ng data), ang laki ng encryption block at, sa katunayan, ang data mismo. Bakit hindi mo ma-hardcode ang laki ng encryption block? Inilalarawan ng 2015/128/64 hindi lamang ang XNUMX-bit Grasshopper cipher, kundi pati na rin ang XNUMX-bit Magma - isang bahagyang binagong GOST 28147-89, na nilikha pabalik sa KGB at mayroon pa ring isa sa mga pinakamataas na threshold ng kaligtasan.

    Sinisimulan ang Kuznechik sa pamamagitan ng pagtawag sa gost.3412.GOST3412Kuznechik(key) at ibinabalik ang isang bagay na may mga pamamaraang .encrypt()/.decrypt() na angkop para sa pagpasa sa 34.13 function. Ang MAC ay kinakalkula tulad ng sumusunod: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). Upang ihambing ang kinakalkula at natanggap na MAC, hindi mo maaaring gamitin ang karaniwang paghahambing (==) ng mga byte string, dahil ang operasyong ito ay naglalabas ng oras ng paghahambing, na, sa pangkalahatang kaso, ay maaaring humantong sa nakamamatay na mga kahinaan tulad ng BEAST pag-atake sa TLS. May espesyal na function ang Python, hmac.compare_digest, para dito.

    Ang block cipher function ay maaari lamang mag-encrypt ng isang bloke ng data. Para sa mas malaking numero, at kahit na hindi isang multiple ng haba, kinakailangan na gamitin ang encryption mode. Inilalarawan ng 34.13-2015 ang sumusunod: ECB, CTR, OFB, CBC, CFB. Ang bawat isa ay may sariling katanggap-tanggap na mga lugar ng aplikasyon at mga katangian. Sa kasamaang palad, wala pa rin tayong standardized na-authenticate na mga mode ng pag-encrypt (tulad ng CCM, OCB, GCM at mga katulad nito) - napipilitan tayong idagdag man lang ang MAC mismo. pinili ko counter mode (CTR): hindi ito nangangailangan ng padding sa laki ng block, maaaring iparallelize, ginagamit lamang ang encryption function, maaaring ligtas na magamit upang i-encrypt ang malaking bilang ng mga mensahe (hindi tulad ng CBC, na may mga banggaan na medyo mabilis).

    Tulad ng .mac(), .ctr() ay kumukuha ng katulad na input: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Kinakailangang tumukoy ng initialization vector na eksaktong kalahati ng haba ng encryption block. Kung ang aming encryption key ay ginagamit lamang upang i-encrypt ang isang mensahe (kahit na mula sa ilang mga bloke), pagkatapos ay ligtas na magtakda ng zero initialization vector. Upang i-encrypt ang mga mensahe ng handshake, gumagamit kami ng hiwalay na key sa bawat oras.

    Ang pag-verify sa signature na gost3410.verify() ay trivial: ipinapasa namin ang elliptic curve kung saan kami nagtatrabaho (itinatala lang namin ito sa aming GOSTIM protocol), ang pampublikong susi ng pumirma (huwag kalimutan na dapat itong isang tuple ng dalawa malalaking numero, at hindi isang byte string), 34.11/2012/XNUMX hash at ang mismong lagda.

    Susunod, sa initiator naghahanda kami at nagpapadala ng mensahe ng handshake sa handshake2, nagsasagawa ng parehong mga aksyon tulad ng ginawa namin sa panahon ng pag-verify, simetriko lang: pag-sign sa aming mga susi sa halip na suriin, atbp...

     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)
     

    Kapag naitatag ang session, ang mga transport key ay nabuo (isang hiwalay na key para sa pag-encrypt, para sa pagpapatunay, para sa bawat isa sa mga partido), at ang Grasshopper ay sinisimulan upang i-decrypt at suriin ang 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     # }}}
    

    Ine-encrypt na ngayon ng msg_sender coroutine ang mga mensahe bago ipadala ang mga ito sa isang koneksyon sa TCP. Ang bawat mensahe ay may monotonically increase na nonce, na siya ring initialization vector kapag naka-encrypt sa counter mode. Ang bawat mensahe at bloke ng mensahe ay ginagarantiyahan na may iba't ibang halaga ng counter.

    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
    

    Ang mga papasok na mensahe ay pinoproseso ng msg_receiver coroutine, na humahawak sa pagpapatunay at pag-decryption:

    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)
    

    Konklusyon

    Ang GOSTIM ay nilayon na gamitin nang eksklusibo para sa mga layuning pang-edukasyon (dahil hindi ito saklaw ng mga pagsusulit, hindi bababa sa)! Maaaring ma-download ang source code ng programa dito (Π‘Ρ‚Ρ€ΠΈΠ±ΠΎΠ³-256 Ρ…ΡΡˆ: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Как ΠΈ всС ΠΌΠΎΠΈ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Ρ‹, Ρ‚ΠΈΠΏΠ° GoGOST, PyDERASN, NCCP, GoVPN, GOSTIM ay ganap libreng software, ibinahagi sa ilalim ng mga tuntunin GPLv3 +.

    Sergey Matveev, cypherpunk, miyembro SPO Foundation, developer ng Python/Go, punong espesyalista FSUE "STC "Atlas".

Pinagmulan: www.habr.com

Magdagdag ng komento