libsodium: Public-key authenticated encryption або як я розшифрував повідомлення без закритого ключа

Ця стаття є прикладом того, як непорозуміння роботи криптографічних примітивів і зайва самовпевненість можуть призвести до критичних помилок при реалізації криптографічного захисту. Сподіваюся моя помилка стане для когось корисним прикладом.

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

Реалізація криптографічних примітивів вручну справа невдячна і загрожує помилками, тому було вирішено використовувати будь-яку бібліотеку з відкритим вихідним кодом для реалізації зазначеної вище задумки. Так як у проекті вже використовувалися бібліотеки 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 формується general ключ для симетричного шифру.
  2. Виконується шифрування повідомлення симетричним шифром XSalsa20 з використанням відкритого ключа, отриманого на першому кроці.
  3. Генерується имитовставка MAC (Poly1305 з використанням того ж загального ключа .
Звідси випливають наступні висновки і властивості алгоритму:

  • Однакове повідомлення, сформоване Бобом або Алісою (кожен формує повідомлення своїм закритим ключем і відкритим ключем співрозмовника), породжує однакову зашифроване повідомлення.
  • З попереднього висновку випливає те, що не можна точно сказати, хто кому писав повідомлення — Боб Алісі або Аліса Бобу.
  • Якщо закритий ключ Аліси буде скомпрометований, то це дозволить розшифрувати всі раніше надіслані повідомлення Бобу (а адже саме це один з плюсів асиметричної криптографії).
  • Якщо закритий ключ Аліси буде скомпрометований, то атакуючий зможе підробляти повідомлення від Боба для Аліси (навіть не знаючи закритого ключа Боба).
Я розумію, що ці властивості алгоритму є нормальними і справа в тому, що я намагався застосувати невідповідний алгоритм. Однак, я вважаю, що документація на офіційному сайті вводить користувачів в оману щодо особливостей роботи і ситуацій, придатних для його застосування.

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

Ніяк не можу позбутися думки: а що якби хтось скористався цим способом при реалізації дійсно критичних систем, сподіваючись на надійність перевіреної бібліотеки, адже я зрозумів свою помилку тільки завдяки тому, що допустив іншу і випадково натрапив на дивну поведінку функції.

Сподіваюся, що комусь корисний. Всім удачі.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.