libsodium: Public-key authenticated encryption или как я расшифровал сообщение без закрытого ключа

в 15:15, , рубрики: c++, криптография, криптография libsodium с++, Программирование, метки:

Эта статья является примером того, как недопонимание работы криптографических примитивов и излишняя самоуверенность могут привести к критическим ошибкам при реализации криптографической защиты. Надеюсь моя ошибка станет для кого-то полезным примером.

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

Реализация криптографических примитивов вручную дело неблагодарное и чреватое ошибками, поэтому было решено использовать какую-либо библиотеку с открытым исходным кодом для реализации указанной выше задумки. Так как в проекте уже использовались библиотеки ZeroMQ, ее обертка CZMQ, которые в свою очередь обеспечивали безопасность передачи данных на базе библиотеки libsodium, то выбор пал на нее. Действительно, зачем плодить зависимости, если в ней уже все есть.

О libsodium
Как сказано на официальном сайте, libsodium — это открытая, современная, простая библиотека для шифрования, электронной цифровой подписи, хеширования и др.
Там же приведен внушительный список проектов и компаний, которые используют libsodium, среди которых, например Tox

Итак, беглое чтение документации показало, что библиотека содержит реализацию асимметричного шифрования на эллиптических кривых Public-key authenticated encryption. Кроме того, есть возможность подтвердить подлинность сообщения посредством MAC. Шифрование и генерация MAC выполняется при помощи функции crypto_box_easy, обратная процедура (проверка и расшифровка) — при помощи crypto_box_open_easy.

Из документации:

Оригинал

Using public-key authenticated encryption, Bob can encrypt a confidential message specifically for Alice, using Alice's public key.
Using Bob's public key, Alice can verify that the encrypted message was actually created by Bob and was not tampered with, before eventually decrypting it.
Alice only needs Bob's public key, the nonce and the ciphertext. Bob should never ever share his secret key, even with Alice.
And in order to send messages to Alice, Bob only needs Alice's public key. Alice should never ever share her secret key either, even with Bob.

При помощи шифрования с открытым ключом с поддержкой аутентификации, Боб может зашифровать конфиденциальное сообщение для Алисы, используя её открытый ключ.

Используя открытый ключ Боба, Алиса может еще до расшифровки проверить, что зашифрованное сообщение действительно создано Бобом и не подделано.

Алисе нужен только открытый ключ Боба, nonce и зашифрованное сообщение. Боб обязан держать свой закрытый ключ в секрете даже от Алисы.

Чтобы отправлять сообщения Алисе, Бобу нужен только открытый ключ Алисы. Алиса, в свою очередь, должна держать свой закрытый ключ в тайне даже от Боба.

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

Для проверки концепции, я скопировал пример с официального сайта, но случайно ошибся и получил странный результат.

Код теста

#include <string.h>
#include "sodium.h"

#define MESSAGE 		"test"
#define MESSAGE_LEN 	4
#define CIPHERTEXT_LEN (crypto_box_MACBYTES + MESSAGE_LEN)

static bool TestSodium()
{
	unsigned char alice_publickey[crypto_box_PUBLICKEYBYTES];
	unsigned char alice_secretkey[crypto_box_SECRETKEYBYTES];
	crypto_box_keypair(alice_publickey, alice_secretkey);

	unsigned char bob_publickey[crypto_box_PUBLICKEYBYTES];
	unsigned char bob_secretkey[crypto_box_SECRETKEYBYTES];
	crypto_box_keypair(bob_publickey, bob_secretkey);

	unsigned char nonce[crypto_box_NONCEBYTES];
	unsigned char ciphertext[CIPHERTEXT_LEN];
	randombytes_buf(nonce, sizeof nonce);

	// message alice -> bob
	if (crypto_box_easy(ciphertext, (const unsigned char*)MESSAGE, MESSAGE_LEN, nonce, bob_publickey, alice_secretkey) != 0)
	{
		return false;
	}

	unsigned char decrypted[MESSAGE_LEN + 1];
	decrypted[MESSAGE_LEN] = 0;

	// Оригинал
	//if (crypto_box_open_easy(decrypted, ciphertext, CIPHERTEXT_LEN, nonce, alice_publickey, bob_secretkey) != 0)
	// Код с "ошибкой"
	if (crypto_box_open_easy(decrypted, ciphertext, CIPHERTEXT_LEN, nonce, bob_publickey, alice_secretkey) != 0)
	{
		return false;
	}

	if(strcmp((const char*)decrypted, MESSAGE) != 0) return false;

	return true;
}

В тесте для Алисы и Боба сначала случайным образом генерируется пара ключей (crypto_box_keypair), затем опять же случайно заполняется nonce (randombytes_buf). После этого Алиса шифрует свое сообщение для Боба, используя его открытый ключ и формирует MAC при помощи своего закрытого ключа.

// message alice -> bob
if (crypto_box_easy(ciphertext, (const unsigned char*)MESSAGE, MESSAGE_LEN, nonce, bob_publickey, alice_secretkey) != 0)
{
	return false;
}

Однако в процедуре расшифровки я ошибся и передал неверные параметры. Вместо того, чтобы расшифровывать сообщение для Боба его закрытым ключом, я попытался расшифровать сообщение открытым ключом Боба и закрытым ключом Алисы (Copy-paste чтоб его).

// Код с "ошибкой"
if (crypto_box_open_easy(decrypted, ciphertext, CIPHERTEXT_LEN, nonce, bob_publickey, alice_secretkey) != 0)
{
	return false;
}

Каково же было мое удивление, когда сообщение расшифровалось! Это было очень странно и ввело меня в состояние когнитивного диссонанса. Перед глазами была найденная 0-day уязвимость и признание мирового сообщества. Я никак не мог понять, каким образом можно было расшифровать сообщение для Боба без использования его закрытого ключа. А кроме того, ведь была успешно выполнена проверка MAC без использования открытого ключа Алисы!

Первое, что я подумал сделать — это выполнить расшифровку как в оригинальном примере, при этом тоже все прошло без проблем — сообщение было расшифровано и проверено. Таким образом, расшифровать (и проверить!) сообщение можно было любой парой ключей — открытым ключом Боба и закрытым ключом Алисы или наоборот — закрытым ключом Боба и открытым ключом Алисы.

Второй моей мыслью было, что я использую старую версию библиотеки. Обновился до последней версии, но поведение теста не изменилось.

Скажу честно, что у меня было мало времени и желания копаться в исходниках libsodium. Ответ нашелся на Stackoverflow. Оказывается, libsodium понимает под «Public-key authenticated encryption» несколько не то, как это представлялось мне.

После подробного рассмотрения, алгоритм шифрования оказался таким:

  1. При помощи алгоритма ECDH формируется общий ключ для симметричного шифра.
  2. Выполняется шифрование сообщения симметричным шифром XSalsa20 с использованием общего ключа, полученного на первом шаге.
  3. Генерируется имитовставка MAC (Poly1305) с использованием того же общего ключа .

Отсюда вытекают следующие выводы и свойства этого алгоритма:

  • Одинаковое сообщение, сформированное Бобом или Алисой (каждый формирует сообщение своим закрытым ключом и открытым ключом собеседника), порождает одинаковое зашифрованное сообщение.
  • Из предыдущего вывода следует то, что нельзя точно сказать кто кому писал сообщение — Боб Алисе или Алиса Бобу.
  • Если закрытый ключ Алисы будет скомпрометирован, то это позволит расшифровать все ранее отправленные сообщения Бобу (а ведь именно это один из плюсов асимметричной криптографии).
  • Если закрытый ключ Алисы будет скомпрометирован, то атакующий сможет подделывать сообщения от Боба для Алисы (даже не зная закрытого ключа Боба).

Я понимаю, что эти свойства алгоритма являются нормальными и дело в том, что я пытался применить неподходящий алгоритм. Однако, я считаю, что документация на официальном сайте вводит пользователей в заблуждение относительно особенностей работы и ситуаций, подходящих для его применения.

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

Никак не могу отделаться от мысли: а что если бы кто-то воспользовался этим способом при реализации действительно критичных систем, надеясь на надежность проверенной библиотеки, ведь я понял свою ошибку только благодаря тому, что допустил другую и случайно наткнулся на «странное» поведение функции.

Надеюсь, что был кому-то полезен. Всем удачи.

Автор: zunzibar

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js