Третий лишний: мастерим свой криптографический протокол

С криптографией всё не так просто, как может показаться!


Скажу честно, раньше я думал, будто бы проблемы криптографии лежат только в плоскости проектирования и реализации криптографически стойких функций. Стоило копнуть чуть глубже, как оказалось — чёрта с два, криптографические функции ещё и использовать нужно очень продумано!

Моя бакалаврская работа в университете — это разработка программы обмена защищёнными мгновенными сообщениями. Сначала я реализовал сетевую часть, потом подумал о том, что неплохо бы прикрутить шифрование. Мой выбор пал на хорошую со всех сторон криптобиблиотеку TweetNaCl, про которую я подробно рассказал в своём предыдущем посте. Итак, у нас есть установленное TCP-соединение между двумя пользователями, так давайте же шифровать!

Первая версия моего криптографического протокола была проста до неприличия. Сначала пользователи генерировали две пары ключей: для электронной подписи (далее ЭП) и для шифрования, после чего обменивались открытыми ключами между собой. Затем при отправке сообщения оно сначала подписывалось с помощью секретного ключа ЭП, после чего результат шифровался на основе открытого ключа шифрования собеседника и своего секретного ключа шифрования. При получении происходил обратный процесс: расшифровка шифротекста при помощи открытого ключа шифрования собеседника и своего секретного ключа шифрования, затем проверка электронной подписи расшифрованного текста, и в случае успеха обоих операций вывод сообщения на экран.

Всё здорово, если предположить, что авторы библиотеки TweetNaCl подобрали хорошие криптоалгоритмы и грамотно их реализовали, не правда ли? Авторы библиотеки — мировые эксперты в области криптографии, и они говорят, что их детище сделано добротно: (якобы) криптоаналитику Еве ничего не дадут публичные ключи ЭП и шифрования, которыми обменялись Алиса и Боб, равно как и зашифрованные сообщения. Они приводят в пользу своего мнения конкретные факты и суждения: обзорно, подробнее, с углублением в детали программной реализации, с точки зрения математики. Окей, давайте предположим, что всё сделано хорошо — мне не попадались доказательства обратного.

Допустим, хакер Меллори имеет полный доступ к коммутатору на пути передачи данных между Бобом и Алисой, которые в каждой книге и статье по криптографии пытаются общаться между собой по сети. Полный доступ означает, что Меллори помимо прослушивания канала (которое стойкая криптография делает практически бесполезным) может модифицировать и/или подделывать пакеты между Бобом и Алисой. В этом случае она может осуществить классическую атаку типа «Человек посередине» (она же — man in the middle, MiTM), представившись Бобу Алисой, а Алисе — Бобом сперва при обмене ключами (она выдаёт им свои ключи вместо ключей собеседника), а затем и при последующем обмене сообщениями. Теперь при посылке сообщения кем-либо из сторон она расшифровывает его имеющимися у неё ключами, шифрует другими ключами и передаёт адресату. Помимо чтения сообщений она может также без проблем изменять, удалять любые сообщения или даже подделывать сообщения целиком. Таким образом, при использовании полностью правильно работающих и стойких криптографических функций без применения более сложного криптографического протокола вся забота о приватности обесценивается на 99%.

Я спросил у своего преподавателя по защите информации, что же со всем этим делать, и получил ответ: «Первый вариант — передать ключи по защищённому каналу, второй — строить сеть доверия a la PGP». Второй вариант для моего небольшого приложения мне показался слишком сложным, так что я пошёл по первому пути, внедрив в свою программу 2 функции:

— генерация ключей ЭП и шифрования для записи их в файлы;

— загрузка ключей из файлов вместо обмена ими по сети при установлении соединения.

Ну теперь-то всё клёво, а? Поменялись файлами с ключами при личной встрече или через надёжный файлообменник (это похуже прошлого варианта, но часто вполне допустимо), установили соединение, ведёте беседу. Но кто докажет, что ключи не были подменены на компьютере в ваше отсутствие или при передаче через какую-нибудь социальную сеть? Конечно же хеши! К счастью, внутри TweetNaCl есть функция crypto_hash, реализующая надёжный алгоритм SHA-2 (конкретная модификация — SHA-512). Если собеседники будут сверять хеши ключей, то ими и прямо по сети можно спокойно поменяться (как в самой первой версии моего криптографического протокола), что несомненно будет самым удобным способом установления шифрованного канала.

Сначала я отдельно хешировал публичные ключи данной стороны и ключи собеседника, но потом начал хешировать как одно целое набор из публичных ключей сервера и публичных ключей клиента (сначала сервер, потом клиент). За счёт этого на обоих сторонах удалось получить один идентичный хеш вместо двух, который можно сверить, например, по телефону. Хеш выводится на экран в шестнадцатеричном коде — хотя и получается целых 128 символов, их можно передать по другому защищённому электронному каналу (через какой-нибудь доверенный сайт в Tor, скажем) либо сверить по тому же телефону (возможно, лишь некоторые байты, а не все).

efbe7516 085fa709 5dedd496 048c92e8 3f32fc31 e33c489a c16438fa c325ec95

249df584 28f1cb86 2d89c746 080622f0 a7ddfaf4 b8c72fd2 03870a67 d4cd2353

Я думал насчёт использования чего-то попроще типа SHA-256 с 64 результирующими символами, но решил, что никто ведь не помешает пользователям сверить лишь 64 или 32 символа этого хеша, а для по-настоящему хардкорной безопасности 128 символов лишними не будут. Также думал насчёт использования более информативной кодировки, например Base64, но только представьте себе сверку по телефону хеша в таком виде:

ZWZiZTc1 MTYwODVm YTcwOTVk ZWRkNDk2 MDQ4Yzky ZTgzZjMy

ZmMzMWUz M2M0ODlh YzE2NDM4 ZmFjMzI1 ZWM5NQ==

Нет уж, с шестнадцатеричными данными будет куда удобнее, хоть их и больше! Использование только цифр и прописных латинских букв (каждые 5 бит хеша — одна буква или цифра, 2^5 = 32) — тоже не панацея, ибо хеш будет иметь длину целых 103 символа при заметном увеличении сложности. В общем, ничего лучше изначального варианта с HEX-кодами я не придумал.

Стоит начать интересоваться криптографией, как становится сложно остановиться — эта наука затягивает. Я решил, что не стоит полагаться на один и тот же набор ключей — стоит при установлении соединения использовать те старые долговременные ключи, но сразу же меняться временными сеансовыми ключами. За счёт использования ранее согласованных неподменённых долговременных ключей Мелори не сможет подменить сеансовые ключи, но в чём же профит, спросите вы? В том, что мы получаем очень крутое свойство нашего криптопротокола под названием «совершенная прямая секретность» (оно же — perfect forward secrecy). Даже если в руках Мелори окажутся долговременные ключи и сохранённый ранее шифрованный трафик, то расшифровать его она не сможет, ибо он шифровался временными сеансовыми ключами, нигде не сохранёнными после беседы. (Также Мелори не будут бесконечно собирать зашифрованные одними и теми же ключами сообщения, ведь теоретически большое их количество может привести к компрометации ключей, но вопрос реальности такой атаки — это вопрос к математикам, не ко мне.)

У меня была мысль пойти ещё дальше и после обмена сеансовыми ключами шифровать каждое сообщение временным ключом (открытым сеансовым ключом собеседника и секретным временным своим), передавая временный открытый ключ собеседнику для расшифровки, но… Как выяснилось, это уже слишком. Дэниел Юлиус Бернштейн, автор NaCl и TweetNaCl, написал обстоятельную статью о правильном экономном использовании псевдослучайных чисел, и я остановился. Действительно, будет ли лучше, если я буду постоянно вытягивать из линуксовского ГПСЧ новые нонсы для функции шифрования и новые ключи? Честно говоря, у меня есть определённые сомнения в этом. Пожалуй, пока что я остановлюсь на сеансовых ключах без временных ключей, а также использованию хеша предыдущего зашифрованного сообщения в качестве нонса, что заодно уменьшит нагрузку на сеть и защитит мою программу от атак типа «повтор» (replay). Если генерировать нонс случайным образом, то для Мелори нет никаких проблем в том, чтобы сохранить нонс плюс зашифрованное сообщение Боба и без расшифровки послать данный набор в этом сеансе связи Алисе 100 раз, и 100 раз Алиса примет эти дубликаты как правильные сообщения — они ведь правильно зашифрованы и подписаны! При генерации нонс из хеша предыдущего сообщения Алиса не сможет расшифровать ни один дубликат, ведь при расшифровке она будет использовать не тот нонс, с которым было зашифровано сообщение.

Что-то из этого я уже реализовал, что-то пока есть только на бумаге. С благодарностью выслушаю любую критику и советы, как сделать мой криптографический протокол лучше.

Криптография интересна именно потому, что она сложна — простые объекты быстро перестают удивлять и манить. Жаль, что сейчас я знаю о ней очень мало, но я буду стараться поднять свой уровень, ибо это стоящая задача. Делая свою бакалаврскую работу, я в очередной раз пришёл к выводу, что обеспечить информационную безопасность на все 100% просто невозможно, можно лишь бесконечно приближаться к идеалу, и это придаёт особую пикантность данной интереснейшей области знаний.

P. S. Моя работа опубликована на Github.