GOSTIM: GOST криптографиясымен бір кеште P2P F2F E2EE IM

Әзірлеуші ​​болу PyGOST кітапханалар (таза Python-дағы ГОСТ криптографиялық примитивтер), мен тізедегі ең қарапайым қауіпсіз хабар алмасуды қалай жүзеге асыру туралы сұрақтарды жиі аламын. Көптеген адамдар қолданбалы криптографияны өте қарапайым деп санайды және блоктық шифрде .encrypt() шақыру оны байланыс арнасы арқылы қауіпсіз жіберу үшін жеткілікті болады. Басқалары қолданбалы криптография аз адамның тағдыры деп санайды және бай компаниялардың Telegram-математиктердің олимпиадалары бар болуы қолайлы. жүзеге асыра алмайды қауіпсіз протокол.

Осының бәрі криптографиялық хаттамаларды және қауіпсіз жедел хабар алмасуды енгізу соншалықты қиын міндет емес екенін көрсету үшін осы мақаланы жазуыма итермеледі. Дегенмен, өзіңіздің аутентификация мен негізгі келісім хаттамаларыңызды ойлап табудың қажеті жоқ.

GOSTIM: GOST криптографиясымен бір кеште P2P F2F E2EE IM
Мақала жазылады пиринг жүйесі, достан досқа, басынан аяғына дейін шифрланған көмегімен жедел хабаршы SIGMA-I аутентификация және негізгі келісім хаттамасы (оның негізінде жүзеге асырылады). IPsec IKE), тек ГОСТ криптографиялық алгоритмдерін қолдану арқылы PyGOST кітапханасы және ASN.1 хабарлама кодтау кітапханасы PyDERASN (бұл туралы мен қазірдің өзінде бұрын жазған). Алғы шарт: ол бір кеште (немесе жұмыс күнінде) нөлден бастап жазылатындай қарапайым болуы керек, әйтпесе ол енді қарапайым бағдарлама емес. Оның қателері, қажетсіз қиындықтары, кемшіліктері болуы мүмкін, сонымен қатар бұл менің асинцио кітапханасын пайдаланатын бірінші бағдарламам.

IM дизайны

Біріншіден, біз IM қандай болатынын түсінуіміз керек. Қарапайымдылық үшін, бұл қатысушыларды ашпай-ақ, тең дәрежелі желі болсын. Әңгімелесушімен байланысу үшін қандай мекен-жай: портқа қосылу керек екенін жеке көрсетеміз.

Мен түсінемін, қазіргі уақытта екі ерікті компьютерлер арасында тікелей байланыс бар деген болжам IM-ды практикада қолдану мүмкіндігін айтарлықтай шектеу болып табылады. Бірақ әзірлеушілер NAT-траверсальды балдақтардың барлық түрлерін неғұрлым көп енгізсе, біз IPv4 Интернетте соғұрлым ұзақ қаламыз, ерікті компьютерлер арасындағы байланыстың ықтималдығы төмендейді. Үйде және жұмыста IPv6 жоқтығына қанша уақыт шыдай аласыз?

Бізде достан досқа желі болады: барлық ықтимал әңгімелесушілер алдын ала белгілі болуы керек. Біріншіден, бұл бәрін айтарлықтай жеңілдетеді: біз өзімізді таныстырдық, атауды/кілтті таптық немесе таппадық, ажыратылды немесе сұхбаттасушыны біле отырып, жұмысты жалғастырдық. Екіншіден, жалпы алғанда, ол қауіпсіз және көптеген шабуылдарды жояды.

IM интерфейсі классикалық шешімдерге жақын болады түкке тұрғысыз жобалар, бұл маған олардың минимализмі мен Unix-жол философиясы үшін қатты ұнайды. IM бағдарламасы әр сұхбаттасушы үшін үш Unix домен ұялары бар каталог жасайды:

  • in — әңгімелесушіге жіберілген хабарламалар оған жазылады;
  • out - сұхбаттасушыдан алынған хабарламалар одан оқылады;
  • күй – одан оқу арқылы біз әңгімелесушінің қазіргі уақытта қосылғанын, қосылу мекенжайын/портын анықтаймыз.

Сонымен қатар, қашықтағы сұхбаттасушыға қосылуды бастайтын хост портын жазу арқылы қосылым ұясы жасалады.

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

Бұл тәсіл IM тасымалдау мен пайдаланушы интерфейсін тәуелсіз жүзеге асыруға мүмкіндік береді, өйткені дос жоқ, сіз бәріне ұнай алмайсыз. Қолдану tmux және / немесе көпқұйрық, синтаксисті бөлектеу арқылы көп терезелі интерфейсті алуға болады. Және көмегімен орау GNU Readline-үйлесімді хабарлама енгізу жолын алуға болады.

Шын мәнінде, сорғыш жобалар FIFO файлдарын пайдаланады. Өз басым, мен арнайы ағындардың қолмен жазылған фонынсыз асинцио режимінде бәсекеге қабілетті файлдармен қалай жұмыс істеу керектігін түсіне алмадым (мен мұндай нәрселер үшін тілді ұзақ уақыт бойы қолданамын) Go). Сондықтан мен Unix домен ұяшықтарымен айналысуды шештім. Өкінішке орай, бұл echo 2001:470:dead::babe 6666 > conn орындау мүмкін емес. Мен бұл мәселені пайдаланып шештім сокат: жаңғырық 2001:470:өлген::балапан 6666 | socat - UNIX-CONNECT:conn, socat READLINE UNIX-CONNECT:alice/in.

Бастапқы қауіпті протокол

TCP көлік ретінде пайдаланылады: ол жеткізуге және оның тәртібіне кепілдік береді. UDP екеуіне де кепілдік бермейді (бұл криптографияны пайдаланғанда пайдалы болар еді), бірақ қолдау көрсетеді SCTP Python қораптан шықпайды.

Өкінішке орай, TCP-де хабарлама ұғымы жоқ, тек байт ағыны ғана. Сондықтан, олар осы ағында бір-бірімен бөлісе алатындай етіп, хабарламалардың пішімін ойлап табу керек. Жол арнасының таңбасын пайдалануға келісе аламыз. Бұл жаңадан бастағандар үшін жақсы, бірақ біз хабарларды шифрлай бастағаннан кейін бұл таңба шифрленген мәтіннің кез келген жерінде пайда болуы мүмкін. Сондықтан желілерде танымал хаттамалар хабардың ұзындығын байтпен жіберетін протоколдар болып табылады. Мысалы, Python-да ұқсас пішіммен жұмыс істеуге мүмкіндік беретін xdrlib бар XDR.

Біз TCP оқуымен дұрыс және тиімді жұмыс істей алмаймыз - кодты жеңілдетеміз. Біз толық хабарламаны декодтаудан бұрын розеткадан деректерді шексіз циклде оқимыз. XML бар JSON осы тәсіл үшін пішім ретінде де пайдаланылуы мүмкін. Бірақ криптография қосылғанда, деректерге қол қойылып, аутентификациялануы керек - бұл JSON/XML қамтамасыз етпейтін нысандардың байт-байт бірдей көрінісін қажет етеді (дамп нәтижелері әртүрлі болуы мүмкін).

XDR бұл тапсырма үшін қолайлы, бірақ мен DER кодтауы бар ASN.1 таңдаймын және PyDERASN кітапхана, өйткені бізде жоғары деңгейлі нысандар болады, олармен жұмыс істеу жиі жағымды және ыңғайлы. Схемасыздан айырмашылығы бенкод, MessagePack немесе CBOR, ASN.1 деректерді қатаң кодталған схемаға қарсы автоматты түрде тексереді.

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

Алынған хабарлама Хабар болады: не мәтіндік MsgText (әзірше бір мәтіндік өрісі бар) немесе MsgHandshake қол алысу хабары (онда әңгімелесушінің аты бар). Қазір бұл өте күрделі болып көрінеді, бірақ бұл болашақтың негізі.

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

Криптографиясыз IM

Жоғарыда айтқанымдай, асинцио кітапхана барлық розетка операциялары үшін пайдаланылады. Іске қосу кезінде не күтетінімізді жария етейік:

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

Өз атыңызды қойыңыз (--біздің-атымыз Алиса). Барлық күтілетін әңгімелесушілер үтірмен бөлінген тізімде (— олардың аттары bob,eve). Әңгімелесушілердің әрқайсысы үшін Unix ұялары бар каталог жасалады, сондай-ақ әрбір кіру, шығу, күй үшін корутин жасалады:

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

Пайдаланушыдан кіріс ұясынан келетін хабарлар 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"))

Әңгімелесушілерден келетін хабарламалар OUT_QUEUES кезекке жіберіледі, олардан деректер шығыс ұяшығына жазылады:

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

Күй ұясынан оқу кезінде бағдарлама әңгімелесушінің мекенжайын PEER_ALIVE сөздігінде іздейді. Егер әңгімелесушімен әлі байланыс болмаса, онда бос жол жазылады.

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

Розеткаға мекенжай жазу кезінде қосылымның «инициаторы» функциясы іске қосылады:

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

Бастамашыны қарастырайық. Алдымен ол көрсетілген хост/портқа қосылымды ашады және оның атымен қол алысу хабарын жібереді:

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

Содан кейін ол қашықтағы тараптың жауабын күтеді. Msg ASN.1 схемасын пайдаланып кіріс жауапты декодтауға тырысады. Бүкіл хабарлама бір TCP сегментінде жіберіледі деп есептейміз және .read() қызметіне қоңырау шалған кезде оны атомдық түрде аламыз. Қол алысу хабарын алғанымызды тексереміз.

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

Біз әңгімелесушінің аты-жөні бізге белгілі екенін тексереміз. Егер жоқ болса, онда біз байланысты үземіз. Біз онымен байланыс орнатқанымызды тексереміз (әңгімелесуші қайтадан бізге қосылуға бұйрық берді) және оны жабамыз. IN_QUEUES кезегі хабар мәтіні бар Python жолдарын ұстайды, бірақ бұрынғы TCP қосылымымен байланыстырылған жазушы туралы ұмытып қалуы үшін msg_sender корутиніне жұмысын тоқтатуға сигнал беретін арнайы None мәні бар.

 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 шығыс хабарларды қабылдайды (кіріс ұясынан кезекке тұрады), оларды MsgText хабарламасына сериялайды және TCP қосылымы арқылы жібереді. Ол кез келген уақытта бұзылуы мүмкін - біз мұны анық ұстаймыз.

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

Соңында инициатор розеткадан хабарламаларды оқудың шексіз цикліне кіреді. Бұл хабарламалардың мәтіндік хабарламалар екенін тексереді және оларды сәйкес сұхбаттасушының шығыс ұясына жіберілетін OUT_QUEUES кезегіне қояды. Неліктен сіз жай ғана .read() функциясын орындай алмайсыз және хабарды декодтай алмайсыз? Өйткені, пайдаланушының бірнеше хабарламалары операциялық жүйе буферінде біріктіріліп, бір TCP сегментінде жіберілуі мүмкін. Біз біріншісін декодтай аламыз, содан кейін келесінің бір бөлігі буферде қалуы мүмкін. Кез келген қалыптан тыс жағдай болған жағдайда, TCP қосылымын жабамыз және msg_sender корутинін тоқтатамыз (OUT_QUEUES кезегіне None жіберу арқылы).

 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)

Негізгі кодқа оралайық. Бағдарлама басталған кезде барлық корутиндерді жасағаннан кейін TCP серверін іске қосамыз. Әрбір орнатылған қосылым үшін ол жауап берушінің корутинін жасайды.

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

жауап беруші бастамашыға ұқсас және барлық бірдей әрекеттерді көрсетеді, бірақ қарапайымдылық үшін хабарларды оқудың шексіз циклі бірден басталады. Қазіргі уақытта қол алысу хаттамасы әр жағынан бір хабарлама жібереді, бірақ болашақта қосылым бастаушысынан екеуі болады, содан кейін мәтіндік хабарламаларды дереу жіберуге болады.

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

Қауіпсіз протокол

Біздің байланысымызды қауіпсіздендіру уақыты келді. Қауіпсіздік дегенді қалай түсінеміз және біз нені қалаймыз:

  • жіберілген хабарламалардың құпиялылығы;
  • жіберілген хабарламалардың түпнұсқалығы мен тұтастығы – олардың өзгерістері анықталуы керек;
  • қайталау шабуылдарынан қорғау - хабарлардың жоқтығы немесе қайталану фактісі анықталуы керек (және біз қосылымды тоқтатуды шешеміз);
  • алдын ала енгізілген ашық кілттердің көмегімен сұхбаттасушыларды сәйкестендіру және аутентификациялау - біз бұрыннан достан досқа желі құру туралы шешім қабылдадық. Аутентификациядан кейін ғана біз кіммен сөйлесіп жатқанымызды түсінеміз;
  • қолжетімділігі тамаша алға құпиялық қасиеттер (PFS) - біздің ұзақ өмір сүретін қол қою кілтін бұзу алдыңғы барлық хат-хабарларды оқу мүмкіндігіне әкелмеуі керек. Ұсталған трафикті жазу пайдасыз болады;
  • хабарлардың жарамдылығы/жарамдылығы (тасымалдау және қол алысу) тек бір TCP сеансы ішінде. Басқа сеанстан дұрыс қол қойылған/аутентификацияланған хабарламаларды (тіпті бір сұхбаттасушы болса да) енгізу мүмкін болмауы керек;
  • пассивті бақылаушы пайдаланушы идентификаторларын да, берілген ұзақ мерзімді ашық кілттерді де, олардан жасалған хэштерді де көрмеуі керек. Пассивті бақылаушыдан белгілі бір анонимдік.

Бір қызығы, барлығы дерлік кез келген қол алысу хаттамасында бұл минимумға ие болғысы келеді және жоғарыда айтылғандардың өте азы, сайып келгенде, «үйдегі» протоколдар үшін орындалады. Енді біз жаңа ештеңе ойлап таппаймыз. Мен міндетті түрде қолдануға кеңес берер едім Шу шеңбері хаттамаларды құру үшін, бірақ қарапайымырақ нәрсені таңдайық.

Ең танымал екі протокол:

  • TLS - қателердің, кептелістердің, осалдықтардың, нашар ойлаудың, күрделілік пен кемшіліктердің ұзақ тарихы бар өте күрделі хаттама (бірақ, бұл TLS 1.3-ке қатысы жоқ). Бірақ біз оны қарастырмаймыз, өйткені ол өте күрделі.
  • IPsec с IKE — күрделі криптографиялық проблемалар жоқ, бірақ олар да қарапайым емес. IKEv1 және IKEv2 туралы оқысаңыз, олардың көзі болып табылады STS, ISO/IEC IS 9798-3 және SIGMA (SIGn-and-MAc) протоколдары – бір кеште іске асыруға жеткілікті қарапайым.

STS/ISO хаттамаларын әзірлеудегі соңғы сілтеме ретінде SIGMA не жақсы? Ол біздің барлық талаптарымызға жауап береді (соның ішінде әңгімелесушінің идентификаторларын «жасыру») және белгілі криптографиялық проблемалары жоқ. Бұл минималистік - хаттама хабарламасынан кем дегенде бір элементті жою оның сенімсіздігіне әкеледі.

Ең қарапайым үйде өсірілетін хаттамадан SIGMA-ға көшейік. Бізді қызықтыратын ең негізгі операция негізгі келісім: Екі қатысушыға бірдей мәнді шығаратын функция, оны симметриялық кілт ретінде пайдалануға болады. Егжей-тегжейлерге тоқталмай: тараптардың әрқайсысы эфемерлі (бір сеанс ішінде ғана пайдаланылады) кілттер жұбын (ашық және жабық кілттер) жасайды, ашық кілттермен алмасады, келісім функциясын шақырады, оның енгізуіне өздерінің жеке кілтін және ашық кілтін береді. әңгімелесушінің кілті.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └──d, I───b │ ╔══════════ ══════════╗ │───────────────>│ ║AprvA =(║PrvA) ═ ════════ ═══════════╝ │ IdB, PubB │ ╔══════════════════ │<───────── ──────│ ║PrvB, PubB = DHgen()║ │ │ ╚═══════════════════════ ───┐ ╔════ ═══╧════════════╗ │ ║Кілт = DH(PrvA, PubB)║ <───┘ ╚──┘ ╚══ ═══════ ════╝ │ │ │ │

Кез келген адам ортаға секіріп, ашық кілттерді өз кілтімен алмастыра алады - бұл хаттамада әңгімелесушілердің аутентификациясы жоқ. Ұзақ өмір сүретін кілттері бар қолтаңбаны қосайық.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └──d, Pub,───, белгісі(SignPrvA, (PubA)) │ ╔═ │──────────── ────────── ──────r─────r──P, P, ubA = жүктеме()║ │ │ ║PrvA, PubA = DHgen() ║ │ │ ╚═══════ ═══════ ════════════════B, Pub═══B , белгісі(SignPrvB, (PubB)) │ ╔══════════════ ═══════ ══════════════╔─────╔ ─────────── ───────────── ──│ ║SignPrvB, SignPubB = жүктелу( )║ │ │ ║PrvB, PubB = │ │ ║PrvB, PubB ┑ ┕ ┕ ┕ ─ SignPrvB ═════════ ══════════════ ══╝ ────┐ ╔ ═════════════ ═════╗ │ │ ║тексеру( SignPubB, ...)║ │ <───┘ ║Кілт = DH(Pr vA, PubB) ║ │ │ ╚════════════════════ ═╝ │ │ │

Мұндай қолтаңба жұмыс істемейді, өйткені ол белгілі бір сессияға байланысты емес. Мұндай хабарламалар басқа қатысушылармен сеанстар үшін де «қолайлы». Бүкіл контекст жазылуы керек. Бұл бізді А-ның тағы бір хабарламасын қосуға мәжбүр етеді.

Бұған қоса, қолтаңбаның астына өзіңіздің идентификаторыңызды қосу өте маңызды, өйткені әйтпесе IdXXX ауыстырып, хабарға басқа белгілі әңгімелесушінің кілтімен қайта қол қоюға болады. Алдын алу рефлексия шабуылдары, қолтаңбаның астындағы элементтер мағынасына қарай нақты белгіленген орындарда болуы қажет: егер А (PubA, PubB) белгісін қойса, онда В қол қоюы керек (PubB, PubA). Бұл сонымен қатар серияланған деректердің құрылымы мен пішімін таңдаудың маңыздылығын көрсетеді. Мысалы, ASN.1 DER кодтауындағы жиындар сұрыпталады: SET OF(PubA, PubB) SET OF(PubB, PubA) бірдей болады.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └──d, I───b │ ╔══════════ ═════════════════╗ │────────────────────── ────────── ─────────────>│ ║SignPrvA, SignPubA = жүктеу()║ │ │ ║PrvA, PubA = DHgen() ═┕ ═┕ ═ ═ ═══════ ═══════════════╝ │IdB, PubB, белгі(SignPrvB, (IdB, PubA, PubB)) │ ╔═════ ═════ ════════════╗ │<────────────────────────────── ────────── ─────────│ ║SignPrvB, SignPubB = жүктеу()║ │ │ ║PrvB, PubB = DHgen() ║ │ ────│ │ ══════ ═ ════════ ══════════╝ │ белгісі(SignPrvA, (IdA, PubB, PubA)) │ ╔═══════════════ ════╗ │─ ──────────────────────────────────────────── ───>│ ║тексеру(SignPubB, ...) ║ │ │ ║кілт = dh (prva, PUBB) ║ │ │ │

Дегенмен, біз осы сеанс үшін бірдей ортақ кілтті жасағанымызды әлі «дәлелдеген жоқпыз». Негізінде, біз бұл қадамсыз жасай аламыз - ең бірінші көлік байланысы жарамсыз болады, бірақ біз қол алысу аяқталғаннан кейін бәрі шынымен келісілгеніне сенімді болғымыз келеді. Қазіргі уақытта бізде ISO/IEC IS 9798-3 протоколы бар.

Біз жасалған кілттің өзіне қол қоя аламыз. Бұл қауіпті, өйткені қолданылған қолтаңба алгоритмінде ағып кетулер болуы мүмкін (әрбір қолтаңба үшін бит болса да, әлі де ағып кетеді). Туынды кілттің хэшіне қол қоюға болады, бірақ туынды кілттің хэшінің де ағып кетуі туынды функциясына өрескел күш шабуылында құнды болуы мүмкін. SIGMA жіберуші идентификаторының түпнұсқалығын растайтын MAC функциясын пайдаланады.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └──d, I───b │ ╔══════════ ═════════════════╗ │────────────────────── ────────── ──────────────────>│ ║SignPrvA, SignPubA = жүктеме()║ │ │ ────│ │ ║D│A,Prb) ╚ ═══════ ════════════════════╝ │IdB, PubB, белгі(SignPrvB, (PubA, PubB)═B), MAC(┕d)═ ═══ │<───────────────── ────────── ──────────── ────────── ─│ ║SignPrvB, SignPubB = жүктеу()║ │ │ ║PrvB, PubB = DHgen() ║ │ │ ╚════ ══════════ ═════════ ══╝ │ │ ╔════════════ ══════════════╗ │ ═══╗ │ белгісі(IPAb,PAb) │ ║Кілт = DH( PrvA, PubB) ║ │───────────────────── ── ──────────── ────────── ─────>│ ║тексеру(Кілт, IdB) ║ │ │ ║тексеру(SignPubB, ...)║ │ │ ╚══════════════ ═════ ═╝ │ │

Оңтайландыру ретінде кейбіреулер өздерінің эфемерлі кілттерін қайта пайдаланғысы келуі мүмкін (бұл, әрине, PFS үшін өкінішті). Мысалы, біз кілттер жұбын жасадық, қосылуға тырыстық, бірақ TCP қол жетімді болмады немесе хаттаманың ортасында бір жерде үзілді. Босқа кеткен энтропия мен процессор ресурстарын жаңа жұпқа жұмсау ұят. Сондықтан біз «cookie» деп аталатын файлды - эфемерлі ашық кілттерді қайта пайдалану кезінде ықтимал кездейсоқ қайталау шабуылдарынан қорғайтын псевдокездейсоқ мәнді енгіземіз. Cookie файлы мен эфемерлі ашық кілт арасындағы байланыстыруға байланысты қарсы тараптың ашық кілтін қажетсіз деп қолтаңбадан алып тастауға болады.

┌─────┐ ┌─────┐ │PeerA│ │PeerB│ └──┬──┘ └──d, I───, CookieA │ ╔════════ ═══════════════════╗ │───────────────────— ────────── ──────────────────────────────────────────── ─>│ ║SignPrvA, SignPubA = жүктеу( )║ │ │ ║PrvA, PubA = DHgen() ║ │ │ ╚═════════════════════════ ══╝ │IdB, PubB, CookieB , белгісі(SignPrvB, (CookieA, CookieB, PubB)), MAC(IdB) │ ╔═════════════════════════ ═ ╗ │< ──────────────────────────────────────────── ────────── ────────────────────│ ║SignPrvB, SignPubB = жүк()║ │ ──────────│ │ ╚══════ ═════════════════════╝ │ │ ╔════════════ ═══════╗ │ белгісі( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA) │ ║Кілт = DH(PrvA, PubB) ║ │───────────────────────── ─ ── ──────────────────────────────────────────── ───────>│ ║ тексеру(Кілт, IdB) ║ │ │ ║тексеру(SignPubB, ...)║ │ │ ╚═══════════════════════════ │

Соңында, біз әңгімелесушілеріміздің құпиялылығын пассивті бақылаушыдан алғымыз келеді. Бұл үшін SIGMA алдымен эфемерлік кілттермен алмасуды және аутентификациялық және идентификациялық хабарламаларды шифрлайтын ортақ кілтті әзірлеуді ұсынады. SIGMA екі опцияны сипаттайды:

  • SIGMA-I - инициаторды белсенді шабуылдардан, жауап берушіні пассивті шабуылдардан қорғайды: бастамашы жауап берушіні аутентификациялайды, ал егер бірдеңе сәйкес келмесе, ол өзінің идентификациясын бермейді. Егер онымен белсенді хаттама басталса, сотталушы өзінің жеке басын куәландырады. Пассивті бақылаушы ештеңе үйренбейді;
    SIGMA-R - жауап берушіні белсенді шабуылдардан, бастамашыны пассивті шабуылдардан қорғайды. Барлығы керісінше, бірақ бұл хаттамада төрт қол алысу хабарламасы жіберілген.

    Біз SIGMA-I-ді таңдаймыз, өйткені ол клиент-серверден күтетін нәрсеге көбірек ұқсайды: клиентті тек аутентификацияланған сервер ғана таниды және серверді барлығы біледі. Оған қоса, қол алысу хабарларының аз болуына байланысты оны жүзеге асыру оңайырақ. Протоколға қосатынымыз хабардың бір бөлігін шифрлау және соңғы хабарламаның шифрланған бөлігіне А идентификаторын тасымалдау:

    PubA, CookieA │ ╔══════════ ══════════════════════════╔═══╔═════════ ─────────── ───── ────────── ───────────────────────────────── ─────────── ───── ──────>│ ║SignPrvA , SignPubA = жүктеу()║ │ │ ║PrvA, PubA = DHgen() ╔ ─ ╔ ┐ ╔ ┐ ═════════ ═════════ ════╝ │ PubB, CookieB, Enc((IdB, белгі(SignPrvB, (CookieA, CookieB, PubB)), MAC (CookieA, CookieB, PubB)), MAC(IdB) ═══════════════ ═══════╗ │<─────────────── ────────── ───── ────────── ║SignP rvB, SignPubB = жүктеу()║ │ │ ║ PrvB, PubB = DHgen │ ─ ─ ┕ ┕ ┕ ┕ () ════════ ════════════════╝ │ │ ╔════════ ═══════ ══╗ │ Enc((IdA, белгісі( SignPrvA, (CookieB, CookieA, PubA)), MAC(IdA))) │ ║Кілт = DH(PrvA, PubB) ║ │─────────────────────────── ─ ────────────────── ────────── ──────────────── ─────────── ──────>│ ║тексеру(Кілт, IdB) ║ │ │ ║тексеру( SignPubB, ...)║ │ │ ╚══════════════ ═════ ══╝ │ │
    
    • Қол қою үшін ГОСТ R қолданылады 34.10-2012 256-биттік кілттері бар алгоритм.
    • Ашық кілтті жасау үшін 34.10 VKO пайдаланылады.
    • CMAC MAC ретінде пайдаланылады. Техникалық тұрғыдан бұл ГОСТ Р 34.13-2015 сипатталған блоктық шифрдің арнайы жұмыс режимі. Бұл режим үшін шифрлау функциясы ретінде − Шегіртке (34.12-2015).
    • Оның ашық кілтінің хэші әңгімелесушінің идентификаторы ретінде пайдаланылады. Хэш ретінде пайдаланылады Stribog-256 (34.11 2012 бит).

    Қол алысқаннан кейін біз ортақ кілт туралы келісеміз. Біз оны тасымалдау хабарламаларының аутентификацияланған шифрлауы үшін пайдалана аламыз. Бұл бөлік өте қарапайым және қателесу қиын: біз хабарлама санауышын көбейтеміз, хабарламаны шифрлаймыз, есептегіш пен шифрлық мәтінді аутентификациялаймыз (MAC), жібереміз. Хабарламаны алған кезде есептегіште күтілетін мән бар екенін тексереміз, шифрлы мәтінді санауышпен аутентификациялап, оның шифрын ашамыз. Қол алысу хабарларын, тасымалдау хабарларын шифрлау және олардың түпнұсқалығын қалай растау үшін қандай кілтті пайдалануым керек? Барлық осы тапсырмалар үшін бір кілтті пайдалану қауіпті және ақылсыз. Арнайы функцияларды пайдаланып кілттерді жасау қажет КДФ (кілт шығару функциясы). Тағы да, шашты бөліп, бірдеңе ойлап таппайық: HKDF бұрыннан белгілі, жақсы зерттелген және белгілі проблемалары жоқ. Өкінішке орай, жергілікті Python кітапханасында бұл функция жоқ, сондықтан біз пайдаланамыз hkdf пластик пакет. HKDF ішкі пайдаланады HMAC, ол өз кезегінде хэш функциясын пайдаланады. Уикипедия бетіндегі Python тіліндегі мысалды іске асыру кодтың бірнеше жолын алады. 34.10 жағдайындағыдай, хэш функциясы ретінде Stribog-2012 қолданамыз. Біздің негізгі келісім функциясының нәтижесі жетіспейтін симметриялылар жасалатын сеанс кілті деп аталады:

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

    Құрылымдар/схемалар

    Енді осы деректерді беру үшін бізде қандай ASN.1 құрылымдары бар екенін қарастырайық:

    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 қол қойылады. HandshakeTBE - не шифрланады. Мен сіздің назарыңызды MsgHandshake1 ішіндегі ukm өрісіне аударамын. 34.10 VKO, генерацияланған кілттерді бұдан да үлкен рандомизациялау үшін UKM (пайдаланушыны кілттеу материалы) параметрін қамтиды - жай ғана қосымша энтропия.

    Кодқа криптография қосу

    Түпнұсқа кодқа енгізілген өзгерістерді ғана қарастырайық, өйткені құрылым өзгеріссіз қалды (шын мәнінде, түпкілікті іске асыру алдымен жазылған, содан кейін барлық криптография одан кесілген).

    Әңгімелесушілерді аутентификациялау және сәйкестендіру ашық кілттер арқылы жүзеге асырылатындықтан, енді олар ұзақ уақыт бойы бір жерде сақталуы керек. Қарапайымдылық үшін біз JSON келесідей пайдаланамыз:

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

    біздің - біздің кілттер жұбы, он алтылық жеке және ашық кілттер. олардың — әңгімелесушілердің аттары және олардың ашық кілттері. Пәрмен жолы аргументтерін өзгертейік және 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),
        }
    # }}}
    

    34.10 алгоритмінің жабық кілті кездейсоқ сан болып табылады. 256-биттік эллиптикалық қисықтар үшін 256-биттік өлшем. PyGOST байттар жиынтығымен жұмыс істемейді, бірақ үлкен сандар, сондықтан біздің жеке кілтімізді (urandom(32)) gost3410.prv_unmarshal() арқылы санға түрлендіру қажет. Ашық кілт gost3410.public_key() арқылы жеке кілттен детерминирленген түрде анықталады. Ашық кілт 34.10 - бұл gost3410.pub_marshal() көмегімен сақтау және тасымалдау жеңілдігі үшін байт тізбегіне түрлендіру қажет екі үлкен сан.

    JSON файлын оқығаннан кейін жалпы кілттерді gost3410.pub_unmarshal() арқылы қайта түрлендіру қажет. Әңгімелесушілердің идентификаторларын хэш түрінде ашық кілттен алатындықтан, оларды дереу алдын ала есептеп, жылдам іздеу үшін сөздікке орналастыруға болады. Stribog-256 хэш - бұл gost34112012256.GOST34112012256(), ол хэш функцияларының хэшлиб интерфейсін толығымен қанағаттандырады.

    Бастамашы корутин қалай өзгерді? Барлығы қол алысу схемасына сәйкес: біз cookie файлын (128-бит көп), VKO кілт келісімі функциясы үшін пайдаланылатын эфемерлі кілттер жұбы 34.10 жасаймыз.

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

    • біз жауапты күтеміз және кіріс хабарламаны декодтаймыз;
    • қол алысуыңызға көз жеткізіңіз1;
    • қарсы жақтың эфемерлі ашық кілтін декодтау және сеанс кілтін есептеу;
    • Хабардың TBE бөлігін өңдеуге қажетті симметриялық кілттерді жасаймыз.

     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 — 64 биттік сан (urandom(8)), ол сондай-ақ gost3410_vko.ukm_unmarshal() арқылы байт көрсетілімінен сериядан шығаруды талап етеді. 34.10 2012-биттік VKO функциясы gost256_vko.kek_3410() (KEK - шифрлау кілті).

    Жасалған сеанс кілті әлдеқашан 256-биттік жалған кездейсоқ байт тізбегі болып табылады. Сондықтан оны HKDF функцияларында бірден қолдануға болады. ГОСТ34112012256 хэшлиб интерфейсін қанағаттандыратындықтан, оны бірден Hkdf класында қолдануға болады. Біз тұзды (Hkdf бірінші аргументі) көрсетпейміз, өйткені генерацияланған кілт қатысушы кілт жұптарының эфемерлілігіне байланысты әр сеанс үшін әртүрлі болады және жеткілікті энтропияны қамтиды. kdf.expand() әдепкі бойынша Grasshopper үшін қажет 256 биттік кілттерді кейінірек шығарады.

    Әрі қарай, кіріс хабарламаның TBE және TBS бөліктері тексеріледі:

    • кіріс шифрленген мәтін бойынша MAC есептеледі және тексеріледі;
    • шифрленген мәтіннің шифры шешілді;
    • TBE құрылымы декодталған;
    • одан әңгімелесушінің идентификаторы алынып, оның бізге мүлде таныс-танылмағаны тексеріледі;
    • Осы идентификатордың үстіндегі MAC есептеледі және тексеріледі;
    • TBS құрылымындағы қолтаңба тексерілді, ол екі тараптың cookie файлын және қарсы тараптың ашық эфемерлі кілтін қамтиды. Қолтаңба әңгімелесушінің ұзақ мерзімді қол қою кілтімен расталады.

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

    Мен жоғарыда жазғанымдай, 34.13 әртүрлі сипаттайды блоктық шифрдың жұмыс режимдері 34.12 ж. Олардың ішінде имитациялық кірістірулерді және MAC есептеулерін генерациялау режимі бар. PyGOST-те бұл gost2015.mac(). Бұл режим шифрлау функциясын (деректердің бір блогын қабылдау және қайтару), шифрлау блогының өлшемін және шын мәнінде деректердің өзін беруді талап етеді. Неліктен шифрлау блогының өлшемін қатты кодтай алмайсыз? 3413 34.12-биттік Grasshopper шифрін ғана емес, сонымен қатар 2015-битті де сипаттайды. Магма - сәл өзгертілген ГОСТ 28147-89, қайтадан КГБ-да жасалған және әлі де қауіпсіздіктің ең жоғары шектерінің біріне ие.

    Kuznechik gost.3412.GOST3412Kuznechik(кілт) шақыру арқылы инициализацияланады және 34.13 функцияларына өту үшін қолайлы .encrypt()/.decrypt() әдістерімен нысанды қайтарады. MAC келесідей есептеледі: gost3413.mac(ГОСТ3412Кузнечик(кілт).шифрлау, KUZNECHIK_BLOCKSIZE, шифрланған мәтін). Есептелген және алынған MAC салыстыру үшін байт жолдарының әдеттегі салыстыруын (==) пайдалана алмайсыз, өйткені бұл операция салыстыру уақытын ағып кетеді, бұл жалпы жағдайда өлімге әкелетін осалдықтарға әкелуі мүмкін BEAST TLS шабуылдары. Бұл үшін Python-да hmac.compare_digest арнайы функциясы бар.

    Блоктық шифр функциясы деректердің бір блогын ғана шифрлай алады. Ұзындықтың еселі емес, үлкенірек саны үшін шифрлау режимін пайдалану қажет. 34.13-2015 келесілерді сипаттайды: ECB, CTR, OFB, CBC, CFB. Әрқайсысының қолданудың қолайлы аймақтары мен сипаттамалары бар. Өкінішке орай, бізде әлі күнге дейін стандартталған жоқ аутентификацияланған шифрлау режимдері (CCM, OCB, GCM және т.б. сияқты) - біз кем дегенде MAC қосуға мәжбүрміз. Мен тандаймын қарсы режим (CTR): ол блок өлшеміне толтыруды қажет етпейді, параллельді болуы мүмкін, тек шифрлау функциясын пайдаланады, үлкен көлемдегі хабарламаларды шифрлау үшін қауіпсіз пайдаланылуы мүмкін (соқтығысуы салыстырмалы түрде жылдам болатын CBC-тен айырмашылығы).

    .mac() сияқты .ctr() ұқсас енгізуді қабылдайды: шифрланған мәтін = gost3413.ctr(GOST3412Kuznechik(кілт).шифрлау, KUZNECHIK_BLOCKSIZE, ашық мәтін, iv). Шифрлау блогының ұзындығының дәл жартысы болатын инициализация векторын көрсету қажет. Егер біздің шифрлау кілті тек бір хабарламаны (бірнеше блоктан болса да) шифрлау үшін пайдаланылса, онда нөлдік инициализация векторын орнату қауіпсіз. Қол алысу хабарларын шифрлау үшін біз әр жолы бөлек кілтті пайдаланамыз.

    gost3410.verify() қолтаңбасын тексеру маңызды емес: біз жұмыс істеп жатқан эллиптикалық қисық сызығын (біз оны жай ғана GOSTIM хаттамамызға жазамыз), қол қоюшының ашық кілтін (бұл екі кортеж болуы керек екенін ұмытпаңыз) өтеміз. байт жолы емес, үлкен сандар), 34.11 хэш және қолтаңбаның өзі.

    Әрі қарай, бастамашыда біз қол алысу хабарламасын дайындап, handshake2-ге жібереміз, тексеру кезіндегі әрекеттерді тек симметриялы түрде орындаймыз: тексерудің орнына кілттерімізге қол қою және т.

     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)
     

    Сеанс орнатылған кезде тасымалдау кілттері жасалады (шифрлау үшін, аутентификация үшін, тараптардың әрқайсысы үшін бөлек кілт) және MAC шифрын шешу және тексеру үшін Grasshopper инициализацияланады:

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

    msg_sender корутині енді хабарларды TCP қосылымында жібермес бұрын шифрлайды. Әрбір хабарламаның монотонды түрде ұлғаюы жоқ, ол да есептегіш режимде шифрланған кезде инициализация векторы болып табылады. Әрбір хабарлама мен хабарлама блогының басқа есептегіш мәні болуы кепілдендірілген.

    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
    

    Кіріс хабарламалар аутентификация мен шифрды шешуді өңдейтін msg_receiver корутинімен өңделеді:

    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)
    

    қорытынды

    GOSTIM тек білім беру мақсатында пайдалануға арналған (себебі ол тесттермен қамтылмаған, кем дегенде)! Бағдарламаның бастапқы кодын жүктеп алуға болады осында (Стрибог-256 хэш: 995bbd368c04e50a481d138c5fa2e43ec7c89bc77743ba8dbabee1fde45de120). Как и все мои проекты, типа GoGOST, PyDERASN, ҰЯО, GoVPN, ГОСТИМ толығымен тегін бағдарламалық қамтамасыз ету, шарттары бойынша таратылады GPLv3 +.

    Сергей Матвеев, киферпанк, мүше SPO қоры, Python/Go әзірлеушісі, бас маман «Атлас» СТК» КМК.

Ақпарат көзі: www.habr.com

пікір қалдыру