GOSTIM: P2P F2F E2EE IM jioni moja na kriptografia ya GOST

Kuwa msanidi programu PyGOST maktaba (GOST cryptographic primitives katika Python safi), mara nyingi mimi hupokea maswali kuhusu jinsi ya kutekeleza ujumbe salama zaidi kwenye goti. Watu wengi huchukulia kriptografia iliyotumika kuwa rahisi sana, na kupiga simu .encrypt() kwenye nambari ya kuzuia kutatosha kuituma kwa usalama kupitia njia ya mawasiliano. Wengine wanaamini kuwa maandishi ya maandishi yaliyotumika ndio hatima ya wachache, na inakubalika kuwa kampuni tajiri kama Telegraph na wanahisabati wa olympiad. haiwezi kutekeleza itifaki salama.

Yote hii ilinisukuma kuandika nakala hii ili kuonyesha kuwa kutekeleza itifaki za kriptografia na usalama wa IM sio kazi ngumu sana. Hata hivyo, haifai kuvumbua uthibitishaji wako mwenyewe na itifaki kuu za makubaliano.

GOSTIM: P2P F2F E2EE IM jioni moja na kriptografia ya GOST
Makala itaandika rika-kwa-rika, rafiki kwa rafiki, umesimbwa kwa njia fiche kutoka mwisho hadi mwisho mjumbe wa papo hapo na SIGMA-I uthibitishaji na itifaki muhimu ya makubaliano (kwa misingi ambayo inatekelezwa IPsec IKE), kwa kutumia algoriti za kriptografia za GOST pekee maktaba ya PyGOST na maktaba ya usimbaji ya ujumbe wa ASN.1 PyDERASN (ambayo mimi tayari aliandika kabla) Sharti: lazima iwe rahisi sana kwamba inaweza kuandikwa kutoka mwanzo jioni moja (au siku ya kazi), vinginevyo sio programu rahisi tena. Pengine ina makosa, matatizo yasiyo ya lazima, mapungufu, pamoja na hii ni programu yangu ya kwanza kutumia maktaba ya asyncio.

Ubunifu wa IM

Kwanza, tunahitaji kuelewa jinsi IM yetu itaonekana. Kwa urahisi, iwe mtandao wa rika-kwa-rika, bila ugunduzi wowote wa washiriki. Sisi binafsi tutaonyesha anwani gani: bandari ya kuunganisha ili kuwasiliana na interlocutor.

Ninaelewa kuwa, kwa wakati huu, dhana kwamba mawasiliano ya moja kwa moja yanapatikana kati ya kompyuta mbili za kiholela ni kizuizi kikubwa cha utumiaji wa IM katika mazoezi. Lakini kadiri watengenezaji wanavyozidi kutekeleza kila aina ya mikongojo ya NAT-traversal, ndivyo tutakavyobaki kwenye Mtandao wa IPv4 kwa muda mrefu, kukiwa na uwezekano wa kukandamiza wa mawasiliano kati ya kompyuta kiholela. Je, ni muda gani unaweza kuvumilia ukosefu wa IPv6 nyumbani na kazini?

Tutakuwa na mtandao wa marafiki-kwa-rafiki: waingiliaji wote wanaowezekana lazima wajulikane mapema. Kwanza, hii hurahisisha kila kitu sana: tulijitambulisha, kupatikana au hatukupata jina / ufunguo, kukatwa au kuendelea kufanya kazi, tukijua mpatanishi. Pili, kwa ujumla, ni salama na huondoa mashambulizi mengi.

Kiolesura cha IM kitakuwa karibu na suluhisho za kawaida miradi isiyo na maana, ambayo ninaipenda sana kwa udogo wao na falsafa ya njia ya Unix. Mpango wa IM huunda saraka na soketi tatu za kikoa cha Unix kwa kila mpatanishi:

  • katika-ujumbe uliotumwa kwa interlocutor umeandikwa ndani yake;
  • nje - ujumbe uliopokelewa kutoka kwa mpatanishi unasomwa kutoka kwake;
  • hali - kwa kusoma kutoka kwake, tunapata ikiwa interlocutor imeunganishwa kwa sasa, anwani ya uunganisho / bandari.

Kwa kuongeza, tundu la conn linaundwa, kwa kuandika bandari ya mwenyeji ambayo tunaanzisha uunganisho kwa interlocutor ya mbali.

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

Njia hii inakuwezesha kufanya utekelezaji wa kujitegemea wa usafiri wa IM na kiolesura cha mtumiaji, kwa sababu hakuna rafiki, huwezi kumpendeza kila mtu. Kutumia tmux na / au mikia mingi, unaweza kupata kiolesura cha madirisha mengi kwa kuangazia sintaksia. Na kwa msaada rlwrap unaweza kupata laini ya ingizo ya ujumbe inayooana na GNU.

Kwa kweli, miradi isiyofaa hutumia faili za FIFO. Binafsi, sikuweza kuelewa jinsi ya kufanya kazi na faili kwa ushindani katika asyncio bila msingi ulioandikwa kwa mkono kutoka kwa nyuzi zilizojitolea (nimekuwa nikitumia lugha kwa vitu kama hivyo kwa muda mrefu. Go) Kwa hivyo, niliamua kufanya na soketi za kikoa cha Unix. Kwa bahati mbaya, hii inafanya kuwa haiwezekani kufanya echo 2001:470:dead::babe 6666 > conn. Nilitatua shida hii kwa kutumia socat: echo 2001:470:wafu::babe 6666 | socat - UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.

Itifaki ya asili isiyo salama

TCP inatumika kama usafiri: inahakikisha utoaji na utaratibu wake. UDP haihakikishii (ambayo inaweza kuwa muhimu wakati cryptography inatumiwa), lakini msaada SCTP Python haitoki nje ya boksi.

Kwa bahati mbaya, katika TCP hakuna dhana ya ujumbe, tu mkondo wa ka. Kwa hiyo, ni muhimu kuja na muundo wa ujumbe ili waweze kushirikiwa kati yao wenyewe katika thread hii. Tunaweza kukubali kutumia herufi ya mlisho wa laini. Ni sawa kwa wanaoanza, lakini mara tu tunapoanza kusimba barua pepe zetu, herufi hii inaweza kuonekana popote katika maandishi ya siri. Katika mitandao, kwa hiyo, itifaki zinazotuma kwanza urefu wa ujumbe katika byte ni maarufu. Kwa mfano, nje ya kisanduku Python ina xdrlib, ambayo hukuruhusu kufanya kazi na umbizo sawa XDR.

Hatutafanya kazi kwa usahihi na kwa ufanisi na usomaji wa TCP - tutarahisisha msimbo. Tunasoma data kutoka kwa soketi kwa kitanzi kisicho na mwisho hadi tusimbue ujumbe kamili. JSON iliyo na XML pia inaweza kutumika kama umbizo la mbinu hii. Lakini kriptografia inapoongezwa, data italazimika kutiwa saini na kuthibitishwa - na hii itahitaji uwakilishi sawa wa vitu, ambao JSON/XML haitoi (matokeo ya utupaji yanaweza kutofautiana).

XDR inafaa kwa kazi hii, hata hivyo ninachagua ASN.1 iliyo na usimbaji wa DER na PyDERASN maktaba, kwa kuwa tutakuwa na vitu vya hali ya juu ambavyo mara nyingi ni vya kupendeza na rahisi kufanya kazi. Tofauti na schemaless msimbo, MessagePack au CBOR, ASN.1 itaangalia data kiotomatiki dhidi ya schema yenye msimbo mgumu.

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

Ujumbe uliopokelewa utakuwa Msg: ama maandishi ya MsgText (pamoja na sehemu moja ya maandishi kwa sasa) au ujumbe wa kupeana mkono wa MsgHandshake (ulio na jina la mpatanishi). Sasa inaonekana kuwa ngumu zaidi, lakini hii ni msingi wa siku zijazo.

     β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ └───s─s─sβ”€β”€β”€β”€β”¬β”€β”€β”˜ └──s A) β”‚ │──────── ────────>β”‚ β”‚ β”‚ β”‚MsgHandshake(IdB) β”‚ β”‚<──────────────── β”‚ β”‚ MsgText() β”‚ │──── MsgText() β”‚ β”‚ β”‚

IM bila kriptografia

Kama nilivyosema tayari, maktaba ya asyncio itatumika kwa shughuli zote za soketi. Hebu tutangaze kile tunachotarajia wakati wa uzinduzi:

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

Weka jina lako mwenyewe (--jina letu alice). Wapatanishi wote wanaotarajiwa wameorodheshwa wakitenganishwa na koma (-majina-yao bob, eve). Kwa kila moja ya waingiliaji, saraka iliyo na soketi za Unix huundwa, na vile vile utaratibu wa kila ndani, nje, hali:

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

Ujumbe unaotoka kwa mtumiaji kutoka kwenye soketi hutumwa kwenye foleni ya 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"))

Ujumbe unaotoka kwa waingiliaji hutumwa kwa foleni OUT_QUEUES, ambapo data huandikwa hadi kwenye soketi ya nje:

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

Unaposoma kutoka kwa soketi ya serikali, programu hutafuta anwani ya mpatanishi katika kamusi ya PEER_ALIVE. Ikiwa hakuna uhusiano na interlocutor bado, basi mstari tupu umeandikwa.

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

Wakati wa kuandika anwani kwenye tundu la conn, kitendakazi cha "kianzisha" cha unganisho kinazinduliwa:

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

Hebu fikiria aliyeanzisha. Kwanza ni wazi inafungua muunganisho kwa mwenyeji/bandari maalum na kutuma ujumbe wa kupeana mkono na jina lake:

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

Kisha, inasubiri jibu kutoka kwa chama cha mbali. Inajaribu kusimbua jibu linaloingia kwa kutumia mpango wa Msg ASN.1. Tunadhani kwamba ujumbe wote utatumwa katika sehemu moja ya TCP na tutaupokea kwa njia ya atomi tunapopiga simu .soma(). Tunaangalia kama tumepokea ujumbe wa kupeana mkono.

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

Tunaangalia kwamba jina lililopokelewa la interlocutor linajulikana kwetu. Ikiwa sio, basi tunavunja uunganisho. Tunaangalia ikiwa tayari tumeanzisha unganisho naye (mingiliaji tena alitoa amri ya kuungana nasi) na kuifunga. Foleni ya IN_QUEUES hushikilia nyuzi za Chatu na maandishi ya ujumbe, lakini ina thamani maalum ya None inayoashiria utaratibu wa msg_sender kuacha kufanya kazi ili isahau kuhusu mwandishi wake anayehusishwa na muunganisho wa TCP uliopitwa na wakati.

 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 hupokea ujumbe unaotoka (uliowekwa kwenye foleni kutoka kwenye tundu), huisasisha hadi ujumbe wa MsgText na kuzituma kupitia muunganisho wa TCP. Inaweza kuvunja wakati wowote - tunakataza wazi hii.

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

Mwishoni, mwanzilishi huingia kwenye kitanzi kisicho na kikomo cha kusoma ujumbe kutoka kwa tundu. Hukagua kama ujumbe huu ni ujumbe wa maandishi na kuziweka kwenye foleni ya OUT_QUEUES, ambapo zitatumwa kwenye soketi ya nje ya mpatanishi husika. Kwa nini huwezi kufanya .read() na kusimbua ujumbe? Kwa sababu inawezekana kwamba ujumbe kadhaa kutoka kwa mtumiaji utaunganishwa katika bafa ya mfumo wa uendeshaji na kutumwa katika sehemu moja ya TCP. Tunaweza kusimbua ya kwanza, na kisha sehemu ya inayofuata inaweza kubaki kwenye bafa. Katika hali yoyote isiyo ya kawaida, tunafunga muunganisho wa TCP na kusimamisha utaratibu wa msg_sender (kwa kutuma Hakuna kwenye foleni ya 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)

Wacha turudi kwenye nambari kuu. Baada ya kuunda coroutines zote wakati programu inapoanza, tunaanza seva ya TCP. Kwa kila muunganisho ulioanzishwa, huunda utaratibu wa kiitikio.

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

kijibu ni sawa na kianzisha na huakisi vitendo vyote sawa, lakini kitanzi kisicho na kikomo cha kusoma ujumbe huanza mara moja, kwa urahisi. Hivi sasa, itifaki ya kushikana mikono hutuma ujumbe mmoja kutoka kwa kila upande, lakini katika siku zijazo kutakuwa na mbili kutoka kwa mwanzilishi wa uunganisho, baada ya hapo ujumbe wa maandishi unaweza kutumwa mara moja.

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

Itifaki salama

Ni wakati wa kulinda mawasiliano yetu. Tunamaanisha nini kwa usalama na tunataka nini:

  • usiri wa ujumbe unaotumwa;
  • uhalisi na uadilifu wa ujumbe unaopitishwa - mabadiliko yao lazima yatambuliwe;
  • ulinzi dhidi ya mashambulizi ya replay - ukweli wa kukosa au kurudiwa ujumbe lazima kugunduliwa (na sisi kuamua kusitisha uhusiano);
  • utambulisho na uthibitishaji wa waingiliaji kwa kutumia funguo za umma zilizoingizwa awali - tayari tuliamua mapema kwamba tulikuwa tukifanya mtandao wa kirafiki-kwa-rafiki. Ni baada tu ya uthibitishaji ndipo tutaelewa tunawasiliana na nani;
  • upatikanaji usiri kamili wa mbele properties (PFS) - kuhatarisha ufunguo wetu wa kutia sahihi wa muda mrefu haufai kusababisha uwezo wa kusoma barua zote za awali. Kurekodi trafiki iliyozuiliwa inakuwa kazi bure;
  • uhalali/uhalali wa ujumbe (usafiri na kupeana mkono) ndani ya kipindi kimoja cha TCP pekee. Kuingiza ujumbe uliotiwa sahihi/kuidhinishwa kwa usahihi kutoka kwa kipindi kingine (hata kwa mpatanishi sawa) haufai kuwezekana;
  • mtazamaji tu hapaswi kuona vitambulishi vya mtumiaji, funguo za umma zinazotumika kwa muda mrefu, au heshi kutoka kwao. Kutokujulikana fulani kutoka kwa mtazamaji tu.

Kwa kushangaza, karibu kila mtu anataka kuwa na kiwango cha chini hiki katika itifaki yoyote ya kupeana mkono, na kidogo sana ya hapo juu hatimaye hufikiwa kwa itifaki za "nyumbani". Sasa hatutabuni chochote kipya. Ningependa kupendekeza kutumia Mfumo wa kelele kwa itifaki za ujenzi, lakini wacha tuchague kitu rahisi zaidi.

Itifaki mbili maarufu zaidi ni:

  • TLS - itifaki ngumu sana na historia ndefu ya mende, jambs, udhaifu, mawazo duni, utata na mapungufu (hata hivyo, hii haina uhusiano mdogo na TLS 1.3). Lakini hatuzingatii kwa sababu ni ngumu zaidi.
  • IPsec с Ike - usiwe na shida kubwa za kriptografia, ingawa pia sio rahisi. Ikiwa unasoma kuhusu IKEv1 na IKEv2, basi chanzo chao ni STS, Itifaki za ISO/IEC IS 9798-3 na SIGMA (SIGn-and-MAc) - rahisi kutosha kutekeleza jioni moja.

Je, ni nini kizuri kuhusu SIGMA, kama kiungo cha hivi punde katika uundaji wa itifaki za STS/ISO? Inakidhi mahitaji yetu yote (ikiwa ni pamoja na vitambulishi vya "kuficha" interlocutor) na haina matatizo ya kriptografia inayojulikana. Ni minimalistic - kuondoa angalau kipengele kimoja kutoka kwa ujumbe wa itifaki itasababisha ukosefu wake wa usalama.

Hebu tutoke kwenye itifaki rahisi zaidi ya watu wazima nyumbani hadi SIGMA. Operesheni ya kimsingi ambayo tunavutiwa nayo ni makubaliano muhimu: Chaguo za kukokotoa zinazotoa washiriki wote wawili thamani sawa, ambayo inaweza kutumika kama kitufe cha ulinganifu. Bila kuingia katika maelezo: kila moja ya wahusika hutoa ephemeral (inayotumiwa tu ndani ya kikao kimoja) jozi muhimu (funguo za umma na za kibinafsi), kubadilishana funguo za umma, piga kazi ya makubaliano, kwa ingizo ambalo hupitisha ufunguo wao wa kibinafsi na umma. ufunguo wa interlocutor.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ └──└──d β”‚ ╔══════════ ══════════╗ │──────────────>β”‚ β•‘PrvA,β‚• ═ ════════ ═══════════╝ β”‚ IdB, PubB β”‚ ╔═════════════════════════════════ β”‚<──────── ──────│ β•‘PrvB, PubB = DHgen()β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ───┐ ╔════ ═══╧════════════╗ β”‚ β•‘Ufunguo = DH(PrvA, PubB)β•‘ <β”€β”€β”€β”˜β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ═══════ ════╝ β”‚ β”‚ β”‚ β”‚

Mtu yeyote anaweza kuruka katikati na kuchukua nafasi ya funguo za umma na wao wenyewe - hakuna uthibitishaji wa interlocutors katika itifaki hii. Hebu tuongeze saini na funguo za muda mrefu.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ └── └──(Ab) SignPrvA, (PubA)) β”‚ ╔═ │─────────── ────────── ─────────Shili bA = mzigo()β•‘ β”‚ β•‘PrvA, PubA = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β• ═══════ ══════════════════════════ , ishara(SignPrvB, (PubB)) β”‚ ╔══════════════ ═══════ ════════════════ ────────── ────────────- ═════════ ══════════════ ══╝ ────┐ β•” ══════════════════════════════ ═════╗ β”‚ β”‚ β•‘thibitisha( SignPub, ...)β•‘ β”‚ <β”€β”€β”€β”˜ β•‘Ufunguo = DH(Pr vA, PubB) β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ═╝ β”‚ β”‚ β”‚

Saini kama hiyo haitafanya kazi, kwani haijafungwa kwenye kikao maalum. Ujumbe kama huo pia "unafaa" kwa vipindi na washiriki wengine. Muktadha mzima lazima ujisajili. Hii inatulazimisha pia kuongeza ujumbe mwingine kutoka kwa A.

Kwa kuongeza, ni muhimu kuongeza kitambulisho chako mwenyewe chini ya sahihi, kwa kuwa vinginevyo tunaweza kuchukua nafasi ya IdXXX na kutia sahihi tena ujumbe kwa ufunguo wa mpatanishi mwingine anayejulikana. Ili kuzuia mashambulizi ya kutafakari, ni muhimu kwamba vipengele vilivyo chini ya saini viwe katika maeneo yaliyofafanuliwa wazi kulingana na maana yao: ikiwa ishara A (PubA, PubB), basi B lazima isaini (PubB, PubA). Hii pia inazungumza juu ya umuhimu wa kuchagua muundo na muundo wa data ya mfululizo. Kwa mfano, seti katika usimbaji wa ASN.1 DER zimepangwa: SET OF(PubA, PubB) itakuwa sawa na SET OF(PubB, PubA).

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ └──└──d β”‚ ╔══════════ ═══════════════╗ │──────────——───── ───────── ────────────>β”‚ β•‘SignPrvA, SignPubA = mzigo()β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() β••β••β••β•• ═ ═══════ ═══════════════╝ β”‚IdB, PubB, ishara(SignPrvB, (IdB, PubA, PubB)) β”‚ ═══════════════════ ═════ ═══════════╗ β”‚<───────────────────────── ───────── ─────────│ β•‘SignPrvB, SignPubB = mzigo()β•‘ β”‚ β”‚ β•‘PrvB, PubB = DHgen() β•‘ β”‚ β”‚ ═══════│ ════════ ══════════╝ β”‚ ishara(SignPrvA, (IdA, PubB, PubA)) β”‚ ╔══════════════════════════ ════╗ │─ ─────────────────────────────────────── ───>β”‚ β•‘thibitisha(SignPubB, ...) β•‘ β”‚ β”‚ β•‘ufunguo = dh (prva, PUBB) β•‘ β”‚ β”‚ β”‚

Hata hivyo, bado "hatujathibitisha" kwamba tumetoa ufunguo sawa wa pamoja wa kipindi hiki. Kimsingi, tunaweza kufanya bila hatua hii - kiunganisho cha kwanza cha usafiri kitakuwa batili, lakini tunataka kwamba wakati kushikana mikono kukamilika, tutakuwa na uhakika kwamba kila kitu kinakubaliwa. Kwa sasa tuna itifaki ya ISO/IEC IS 9798-3 mkononi.

Tunaweza kusaini ufunguo uliotengenezwa wenyewe. Hii ni hatari, kwani kuna uwezekano kwamba kunaweza kuwa na uvujaji katika algorithm ya saini inayotumiwa (ingawa bits-per-signature, lakini bado inavuja). Inawezekana kutia saini heshi ya ufunguo wa utokaji, lakini kuvuja hata heshi ya ufunguo uliotolewa kunaweza kuwa muhimu katika shambulio la nguvu-katili kwenye chaguo la kukokotoa. SIGMA hutumia chaguo la kukokotoa la MAC ambalo huthibitisha kitambulisho cha mtumaji.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ └──└──d β”‚ ╔══════════ ═══════════════╗ │──────────——───── ───────── ────────────────- β•š ═══════ ════════════════════╝ β”‚IdB, PubB, saini(SignPrvB, (PubA, PubIdB)β••), MAC (PubB)═│══ ═══ β”‚<──────────────── ────────── ──—────── ───────── ─│ β•‘SignPrvB, SignPubB = mzigo()β•‘ β”‚ β”‚ β•‘PrvB, PubB = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β• ═════════════════ ═════════ ══╝ β”‚ β”‚ ╔════════════ ═════════╗ │═════ ═════════╗ │══╗ β”‚ sign),(Pub)(Pub)(Publ),(Pub) β”‚ β•‘Ufunguo = DH( PrvA, PubB) β•‘ │──────────────────── ── ─────────── ───────── ─────>β”‚ β•‘thibitisha(Ufunguo, IdB) β•‘ β”‚ β”‚ β•‘thibitisha(SignPubB, ...)β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ═════ ═╝ β”‚ β”‚

Kama uboreshaji, wengine wanaweza kutaka kutumia tena vitufe vyao vya muda mfupi (ambayo, bila shaka, ni bahati mbaya kwa PFS). Kwa mfano, tulizalisha jozi muhimu, tulijaribu kuunganisha, lakini TCP haipatikani au iliingiliwa mahali fulani katikati ya itifaki. Ni aibu kupoteza rasilimali za entropy na processor kwenye jozi mpya. Kwa hivyo, tutaanzisha kinachojulikana kama kuki - thamani ya uwongo ya nasibu ambayo italinda dhidi ya mashambulio ya uchezaji wa nasibu yanayoweza kutokea wakati wa kutumia tena funguo za muda mfupi za umma. Kwa sababu ya kufungana kati ya kidakuzi na ufunguo wa umma wa muda mfupi, ufunguo wa umma wa upande mwingine unaweza kuondolewa kutoka kwa sahihi kama si lazima.

β”Œβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β” β”‚PeerAβ”‚ β”‚PeerBβ”‚ β””β”€β”€β”¬β”€β”€β”˜ └── └── └──Cook A β”‚ ╔════════ ═════════════════╗ │────────——─── ───────── ─────────────────────────────────────── ─>β”‚ β•‘SignPrvA, SignPubA = mzigo( )β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ══╝ β”‚IdB, PubB, CookieB , ishara(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) β”‚ ╔══════════════════════════════════════ ═ β•— β”‚< ────────────────────────────────────── ───────── ──────────────────- β”‚ β•šβ•β•β•β•β•β• ═════════════════════╝ β”‚ β”‚ ╔═══════════════════ ═══════╗ β”‚ ishara( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA) β”‚ β•‘Ufunguo = DH(PrvA, PubB) β•‘ │───────────────── ─ ── ────────────────────────────────────── ───────>β”‚ β•‘ Thibitisha (ufunguo, IDB) β•‘ β”‚ β”‚ β•‘Vitolea (SignPubb, ...) β•‘ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚

Hatimaye, tunataka kupata ufaragha wa washirika wetu wa mazungumzo kutoka kwa mtazamaji tu. Ili kufanya hivyo, SIGMA inapendekeza kwanza kubadilishana vitufe vya muda mfupi na kuunda ufunguo wa kawaida ambao unaweza kusimba kwa njia fiche uthibitishaji na kutambua ujumbe. SIGMA inaelezea chaguzi mbili:

  • SIGMA-I - inalinda mwanzilishi kutokana na mashambulizi ya kazi, jibu kutoka kwa wale watazamaji: mwanzilishi huthibitisha jibu na ikiwa kitu hailingani, basi haitoi kitambulisho chake. Mshtakiwa anatoa kitambulisho chake ikiwa itifaki hai imeanza naye. Mtazamaji tu hajifunzi chochote;
    SIGMA-R - inalinda jibu kutokana na mashambulizi ya kazi, mwanzilishi kutoka kwa wale watazamaji. Kila kitu ni kinyume kabisa, lakini katika itifaki hii jumbe nne za kupeana mikono tayari zimetumwa.

    Tunachagua SIGMA-I kwa kuwa inafanana zaidi na kile tunachotarajia kutoka kwa vitu vinavyojulikana vya seva ya mteja: mteja anatambuliwa tu na seva iliyoidhinishwa, na kila mtu tayari anaijua seva. Pamoja na hayo ni rahisi kutekeleza kutokana na ujumbe mdogo wa kupeana mkono. Tunachoongeza kwenye itifaki ni kusimba sehemu ya ujumbe kwa njia fiche na kuhamisha kitambulisho A hadi sehemu iliyosimbwa ya ujumbe wa mwisho:

    PubA, CookieA β”‚ ╔═════════ ═══════════════════════════╀ ───────── ───── ───────── ─────────────────────── ────────── ───── ──────>β”‚ β•‘SignPrvA , SignPubA = mzigo()β•‘ β”‚ β”‚ β•‘PrvA, PubA = DHgen() ════┕ ════════ ═════════ ════╝ β”‚ PubB, CookieB, Enc((IdB, sign(SignPrvB, (CookieA, CookieB, PubB)), MAC(Id════) ══════════════ ═══════╗ β”‚<───════ ═══════╗ β”‚<─────——— ───────── ───── ────────── β•‘SignP rvB, SignPubB = mzigo()β•‘ β”‚ β”‚ β•‘ PrvB, PubB = DHgen β”• ₐ ════════ ════════════════╝ β”‚ β”‚ ╔══════════════════════════ ══╗ β”‚ Enc((IdA, ishara( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA))) β”‚ β•‘Ufunguo = DH(PrvA, PubB) β•‘ │───────────────── ─ ───────────────── ────────── ──────────── ────────── ──────>β”‚ β•‘thibitisha(Ufunguo, IdB) β•‘ β”‚ β”‚ β•‘thibitisha( SignPubB, ...)β•‘ β”‚ β”‚ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• ═════ ══╝ β”‚ β”‚
    
    • GOST R hutumiwa kwa saini 34.10-2012 algorithm na funguo 256-bit.
    • Ili kuzalisha ufunguo wa umma, 34.10/2012/XNUMX VKO hutumiwa.
    • CMAC inatumika kama MAC. Kitaalam, hii ni njia maalum ya uendeshaji wa block cipher, iliyoelezwa katika GOST R 34.13-2015. Kama kazi ya usimbaji fiche kwa modi hii - Nyasi (34.12-2015).
    • Heshi ya ufunguo wake wa umma hutumiwa kama kitambulisho cha mpatanishi. Inatumika kama heshi Stribog-256 (34.11/2012/256 XNUMX bits).

    Baada ya kushikana mkono, tutakubaliana juu ya ufunguo ulioshirikiwa. Tunaweza kuitumia kwa usimbaji fiche ulioidhinishwa wa ujumbe wa usafiri. Sehemu hii ni rahisi sana na ni ngumu kufanya makosa: tunaongeza kihesabu ujumbe, tunasimba ujumbe kwa njia fiche, thibitisha (MAC) kihesabu na maandishi ya siri, tuma. Tunapopokea ujumbe, tunaangalia kuwa kaunta ina thamani inayotarajiwa, thibitisha maandishi ya siri kwa kaunta, na usimbue. Je, ni ufunguo gani ninaopaswa kutumia kusimba ujumbe wa kupeana mkono kwa njia fiche, ujumbe wa usafiri, na jinsi ya kuzithibitisha? Kutumia ufunguo mmoja kwa kazi hizi zote ni hatari na sio busara. Ni muhimu kuzalisha funguo kwa kutumia kazi maalum KDF (kazi muhimu ya derivation). Tena, tusigawanye nywele na kubuni kitu: HKDF inajulikana kwa muda mrefu, imetafitiwa vyema na haina matatizo yanayojulikana. Kwa bahati mbaya, maktaba ya asili ya Python haina kazi hii, kwa hivyo tunatumia hkdf mfuko wa plastiki. HKDF hutumia ndani HMAC, ambayo kwa upande wake hutumia kazi ya hashi. Utekelezaji wa mfano katika Python kwenye ukurasa wa Wikipedia huchukua mistari michache tu ya nambari. Kama ilivyo kwa 34.10/2012/256, tutatumia Stribog-XNUMX kama chaguo la kukokotoa la heshi. Toleo la chaguo la kukokotoa la makubaliano yetu litaitwa ufunguo wa kikao, ambapo zile zinazokosekana za ulinganifu zitatolewa:

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

    Miundo/Mipango

    Hebu tuangalie ni miundo gani ya ASN.1 tuliyo nayo sasa ya kusambaza data hii yote:

    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 ndiyo itatiwa saini. HandshakeTBE - ni nini kitakachosimbwa. Ninakuvutia kwa uga wa ukm katika MsgHandshake1. 34.10 VKO, kwa ujanibishaji mkubwa zaidi wa funguo zinazozalishwa, inajumuisha parameta ya UKM (nyenzo za ufunguo wa mtumiaji) - entropy ya ziada tu.

    Kuongeza Cryptography kwa Kanuni

    Wacha tuchunguze mabadiliko tu yaliyofanywa kwa nambari ya asili, kwani mfumo ulibaki sawa (kwa kweli, utekelezaji wa mwisho uliandikwa kwanza, na kisha maandishi yote yamekatwa kutoka kwake).

    Kwa kuwa uthibitishaji na utambulisho wa interlocutors utafanyika kwa kutumia funguo za umma, sasa zinahitaji kuhifadhiwa mahali fulani kwa muda mrefu. Kwa unyenyekevu, tunatumia JSON kama hii:

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

    zetu - jozi zetu muhimu, funguo za hexadecimal za kibinafsi na za umma. yao - majina ya interlocutors na funguo zao za umma. Wacha tubadilishe hoja za safu ya amri na tuongeze usindikaji wa baada ya data ya 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),
        }
    # }}}
    

    Ufunguo wa kibinafsi wa algoriti ya 34.10 ni nambari ya nasibu. Ukubwa wa biti 256 kwa mikondo ya duaradufu ya biti 256. PyGOST haifanyi kazi na seti ya ka, lakini kwa idadi kubwa, kwa hivyo ufunguo wetu wa faragha (urandom(32)) unahitaji kubadilishwa kuwa nambari kwa kutumia gost3410.prv_unmarshal(). Ufunguo wa umma hubainishwa kwa kubainisha kutoka kwa ufunguo wa faragha kwa kutumia gost3410.public_key(). Ufunguo wa umma 34.10 ni nambari mbili kubwa ambazo pia zinahitaji kubadilishwa kuwa mfuatano wa baiti kwa urahisi wa kuhifadhi na uwasilishaji kwa kutumia gost3410.pub_marshal().

    Baada ya kusoma faili ya JSON, funguo za umma ipasavyo zinahitaji kubadilishwa kwa kutumia gost3410.pub_unmarshal(). Kwa kuwa tutapokea vitambulisho vya interlocutors kwa namna ya hash kutoka kwa ufunguo wa umma, wanaweza kuhesabiwa mara moja mapema na kuwekwa kwenye kamusi kwa utafutaji wa haraka. Stribog-256 heshi ni gost34112012256.GOST34112012256(), ambayo inakidhi kikamilifu kiolesura cha hash cha vitendaji vya heshi.

    Je, utaratibu wa mwanzilishi umebadilikaje? Kila kitu ni kulingana na mpango wa kupeana mikono: tunatengeneza kuki (128-bit ni nyingi), jozi ya ufunguo wa muda mfupi 34.10, ambayo itatumika kwa kazi ya makubaliano muhimu ya 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()
    

    • tunasubiri jibu na kusimbua ujumbe unaoingia wa Msg;
    • hakikisha unapata handshake1;
    • simbua ufunguo wa umma wa muda mfupi wa upande mwingine na uhesabu ufunguo wa kikao;
    • Tunatengeneza vitufe vya ulinganifu vinavyohitajika ili kuchakata sehemu ya TBE ya ujumbe.

     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 ni nambari ya biti 64 (urandom(8)), ambayo pia inahitaji kuondolewa kutoka kwa uwakilishi wake kwa kutumia gost3410_vko.ukm_unmarshal(). Kazi ya VKO kwa 34.10/2012/256 3410-bit ni gost34102012256_vko.kek_XNUMX () (KEK - ufunguo wa encryption).

    Kitufe cha kipindi kilichoundwa tayari ni mlolongo wa baiti wa 256-bit pseudo-random. Kwa hiyo, inaweza kutumika mara moja katika kazi za HKDF. Kwa kuwa GOST34112012256 inakidhi interface ya hahlib, inaweza kutumika mara moja katika darasa la Hkdf. Hatuelezi chumvi (hoja ya kwanza ya Hkdf), kwa kuwa ufunguo unaozalishwa, kutokana na ephemerality ya jozi muhimu zinazoshiriki, itakuwa tofauti kwa kila kikao na tayari ina entropy ya kutosha. kdf.expand() kwa chaguo-msingi tayari hutoa vitufe vya 256-bit vinavyohitajika kwa Grasshopper baadaye.

    Ifuatayo, sehemu za TBE na TBS za ujumbe unaoingia huangaliwa:

    • MAC juu ya maandishi yanayoingia huhesabiwa na kuangaliwa;
    • maandishi ya siri yamesimbwa;
    • Muundo wa TBE umeamuliwa;
    • kitambulisho cha interlocutor kinachukuliwa kutoka humo na kinaangaliwa ikiwa anajulikana kwetu kabisa;
    • MAC juu ya kitambulisho hiki huhesabiwa na kuangaliwa;
    • sahihi juu ya muundo wa TBS imethibitishwa, ambayo inajumuisha kidakuzi cha pande zote mbili na ufunguo wa muda wa umma wa upande mwingine. Sahihi inathibitishwa na ufunguo wa saini wa muda mrefu wa interlocutor.

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

    Kama nilivyoandika hapo juu, tarehe 34.13/2015/XNUMX inaeleza mambo mbalimbali zuia njia za uendeshaji za cipher kuanzia tarehe 34.12/2015/3413. Miongoni mwao kuna mode ya kuzalisha kuingiza kuiga na mahesabu ya MAC. Katika PyGOST hii ni gost34.12.mac(). Hali hii inahitaji kupitisha kazi ya usimbuaji (kupokea na kurudisha kizuizi kimoja cha data), saizi ya kizuizi cha usimbuaji na, kwa kweli, data yenyewe. Kwa nini huwezi kuweka msimbo mkali wa saizi ya usimbaji fiche? Tarehe 2015/128/64 inaeleza si tu XNUMX-bit Grasshopper cipher, lakini pia XNUMX-bit. Magma - GOST 28147-89 iliyobadilishwa kidogo, iliyoundwa nyuma katika KGB na bado ina moja ya vizingiti vya juu zaidi vya usalama.

    Kuznechik huanzishwa kwa kupiga simu gost.3412.GOST3412Kuznechik(ufunguo) na hurejesha kitu chenye mbinu za .encrypt()/.decrypt() zinazofaa kupitisha vipengele 34.13. MAC imekokotolewa kama ifuatavyo: gost3413.mac(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, ciphertext). Ili kulinganisha MAC iliyohesabiwa na kupokea, huwezi kutumia ulinganisho wa kawaida (==) wa kamba za baiti, kwani operesheni hii huvuja wakati wa kulinganisha, ambayo, kwa ujumla, inaweza kusababisha udhaifu mbaya kama vile. BORA mashambulizi dhidi ya TLS. Python ina kazi maalum, hmac.compare_digest, kwa hili.

    Chaguo la kukokotoa la msimbo wa kuzuia linaweza tu kusimba kizuizi kimoja cha data. Kwa nambari kubwa, na hata sio nyingi ya urefu, ni muhimu kutumia hali ya usimbuaji. 34.13-2015 inaeleza yafuatayo: ECB, CTR, OFB, CBC, CFB. Kila mmoja ana maeneo yake ya kukubalika ya maombi na sifa. Kwa bahati mbaya, bado hatuna viwango njia za usimbaji zilizothibitishwa (kama vile CCM, OCB, GCM na kadhalika) - tunalazimika angalau kuongeza MAC sisi wenyewe. na chagua hali ya kukabiliana (CTR): haihitaji pedi kwa saizi ya kizuizi, inaweza kusawazishwa, hutumia tu kazi ya usimbaji fiche, inaweza kutumika kwa usalama kusimba idadi kubwa ya ujumbe (tofauti na CBC, ambayo ina migongano kwa haraka).

    Kama .mac(), .ctr() huchukua ingizo sawa: ciphertext = gost3413.ctr(GOST3412Kuznechik(key).encrypt, KUZNECHIK_BLOCKSIZE, plaintext, iv). Inahitajika kutaja vekta ya uanzishaji ambayo ni nusu ya urefu wa kizuizi cha usimbaji. Ikiwa ufunguo wetu wa usimbuaji unatumiwa tu kusimba ujumbe mmoja (ingawa kutoka kwa vizuizi kadhaa), basi ni salama kuweka vekta ya uanzishaji sifuri. Ili kusimba ujumbe wa kupeana mikono kwa njia fiche, tunatumia kitufe tofauti kila wakati.

    Kuthibitisha sahihi ya gost3410.verify() ni jambo dogo: tunapitisha mduara wa duaradufu ambamo tunafanya kazi (tunarekodi tu katika itifaki yetu ya GOSTIM), ufunguo wa umma wa aliyetia sahihi (usisahau kwamba hii inapaswa kuwa nakala mbili. idadi kubwa, na sio kamba ya baiti), 34.11/2012/XNUMX hashi na saini yenyewe.

    Kisha, katika kianzisha tunatayarisha na kutuma ujumbe wa kupeana mkono kwa kupeana mkono2, tukifanya vitendo sawa na tulivyofanya wakati wa uthibitishaji, kwa ulinganifu tu: kusaini kwenye funguo zetu badala ya kuangalia, nk...

     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)
     

    Kipindi kinapoanzishwa, funguo za usafiri hutolewa (ufunguo tofauti wa usimbaji fiche, kwa uthibitishaji, kwa kila wahusika), na Panzi inaanzishwa ili kusimbua na kuangalia 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     # }}}
    

    Utaratibu wa msg_sender sasa husimba barua pepe kabla ya kuzituma kwenye muunganisho wa TCP. Kila ujumbe una nonce inayoongezeka mara moja, ambayo pia ni vekta ya uanzishaji inaposimbwa kwa njia fiche katika hali ya kaunta. Kila uzuiaji wa ujumbe na ujumbe umehakikishiwa kuwa na thamani tofauti ya kaunta.

    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
    

    Barua pepe zinazoingia huchakatwa na utaratibu wa msg_receiver, ambao hushughulikia uthibitishaji na usimbuaji:

    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)
    

    Hitimisho

    GOSTIM imekusudiwa kutumiwa pekee kwa madhumuni ya kielimu (kwani haijashughulikiwa na vipimo, angalau)! Nambari ya chanzo ya programu inaweza kupakuliwa hapa (Π‘Ρ‚Ρ€ΠΈΠ±ΠΎΠ³-256 Ρ…ΡΡˆ: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Как ΠΈ всС ΠΌΠΎΠΈ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Ρ‹, Ρ‚ΠΈΠΏΠ° GoGOST, PyDERASN, NCCP, GoVPN, GOSTIM ni kabisa programu ya bure, kusambazwa chini ya masharti GPLv3 +.

    Sergey Matveev, cypherpunk, mwanachama Msingi wa SPO, Python/Go msanidi programu, mtaalamu mkuu FSUE "STC" Atlas.

Chanzo: mapenzi.com

Kuongeza maoni