Власна реалізація https з використанням crypto++ для початкового завантаження I2P

Кожен новий вузол I2P при першому запуску повинен звідкись отримати початковий список вузлів. Для цього існують спеціальні сервера (reseed), адреси яких жорстко прописані в коді. Раніше завантаження здійснювалася по http, проте з недавніх пір reseed-и стали переходити на https. Для успішної роботи «пурпурного» I2P також потрібно внести відповідні зміни. Використовувана там криптографічна бібліотека crypto++ не підтримує ssl. Замість використання додаткової бібліотеки типу openssl, фактично дублюючої криптографію, був обраний розглянутий нижче варіант.
Початкова завантаження це єдине місце в I2P, де використовується https.
З іншого боку, стаття буде цікава тим кому цікаво зрозуміти, як працює ssl і спробувати самому.



Винаходимо велосипед

Нашою метою є отримання файлу i2pseeds.su3 розміром порядку 100K з одного з reseed вузлів I2P. Даний файл підписаний окремим сертифікатом, незалежним від сертифіката сайту, тому перевірку сертифіката можна виключити. Відносно невелика довжина отриманих даних дозволяє нам реалізовувати механізми стиснення і відновлення розірваних з'єднань.
Буде використовуватися виключно TLS 1.2 і набір шифрів TLS_RSA_WITH_AES_256_CBC_SHA256. Інакше кажучи, для шифрування даних використовується AES256 в режимі CBC, і RSA — для узгодження ключа.
Даний вибір обумовлений тим, що AES256-CBC є найбільш використовуваним шифруванням в I2P, а RSA для спрощення реалізації протоколу шляхом скорочення числа повідомлень, необхідних для узгодження ключа. Крім RSA і AES також потрібні такі криптографічні функції з crypto++:

  • HMAC для обчислення контрольних сум зашифрованих повідомлень і випадкових функцій. Слід звернути увагу, що використовується стандартна реалізація HMAC, а не з I2P
  • SHA256 хеш для використання разом з HMAC для обчислення контрольної суми всіх повідомлень, які брали участь у встановленні з'єднання
  • Функції для роботи з описами мовою ASN.1 в кодуванні DER. Потрібно для вилучення публічного ключа сертифіката X. 509


Використовується реалізація RSA на основі PKCS v1.5. Довжина ключа може бути будь-якою і визначається сертифікатом.


Передача повідомлень по SSL

Абсолютно всі передані повідомлення починаються з 5-байтного заголовка, перший байт якого містить тип повідомлення, наступні 2 байта — номер версії протоколу (0x03, 0x03 для TSL 1.2) і потім довжина решти(вмісту) повідомлення — 2 байта в Big Endian, тим самим визначаючи межі повідомлень.
Таким чином, при отриманні нових даних слід спочатку слід прочитати 5 байтів заголовка, а потім скільки байтів міститься в поле довжини.
Зустрічаються повідомлення 4-х типів:
  1. 0x17 — дані. Вміст являє собою зашифровані HTTP повідомлення, а нашому випадку за допомогою AES256, ключ якого обчислюється в процесі встановлення з'єднання. Розмір даних повинен бути кратний 16 байт
  2. 0x16 — встановлення з'єднання. Декількох типів, що визначаються відповідним полем усередині вмісту. Незашифровані, за винятком повідомлення типу 'finished', отсылаемого останнім.
  3. 0x15 — попередження. Повідомлення про те, що «щось пішло не так». Закриваємо з'єднання. Містить коди того, що ж пішло не так, можна використовувати для налагодження.
  4. 0x14 — зміна шифру. Надсилається відразу ж після узгодження ключа. Вміст являє 1 байт, завжди містить 0x01. Фактично є частиною процесу встановлення з'єднання.


У нашій реалізації зашифровані дані виглядають наступним чином:
16 байт IV для CBC, в TSL 1.2 для кожного повідомлення власний IV;
дані довжиною аж то 64K — довжина заголовків;
32 байт MAC, обчислюваний для 13-байтного заголовка і даних, заголовок складається з 8 байтного порядкового номера, починаючи з нуля, типу повідомлення (0x17 або 0x16), версії і довжини даних. Все в BigEndian. Ключ для HMAC також обчислюється в процесі встановлення з'єднання;
заповнювач, з тим розрахунком, щоб довжина зашифрованих даних був кратна 16 байт, останній байт містить число байтів заповнювача без урахування його самого. Якщо довжина повідомлення виявляється кратній 16 байт, то буде додано ще 16 байтів заради цього останнього байта з довжиною.

Установка з'єднання

В процесі установки ми повинні вирішити два завдання:
  1. Погодити та обчислити ключі для шифрування і HMAC
  2. Відіслати правильну послідовність повідомлень, щоб інша сторони не закрила з'єднання, а перейшла в режим обміну даними


У нашому випадку послідовність повідомлень виглядає наступним чином:
ClientHello--> (0x01)
<--ServerHello (0x02)
<--Certificate (0x0B)
<--ServerHelloDone (0x0E)
-->ClientKeyExchange (0x10)
-->ChangeCipherSpec
-->Finished (0x14)
<--ChangeCipherSpec
<--Finished (0x14)
де "-->" означає відправку ообщения, а "<--" — отримання.
Всі повідомлення, за винятком ChangeChiperSpec є повідомленням типу 0x16 — встановлення з'єднання. Вміст повідомлення цього типу починається з власного 4-байтного заголовка, перший байт якого це типу повідомлення установки з'єднання, як зазначено вище, і 3 байти довжини залишився повідомлення, старший байт якої в нашому випадку завжди нуль.
Розглянемо ці повідомлення докладно.

ClientHello
Перше повідомлення, яке відправляється нами сервера після успішного підключення. Оскільки ми використовуємо один конкретний набір шифрів, то в нашому випадку воно буде постійним. Ось таким:
static uint8_t clientHello[] = 
{
0x16, // handshake
0x03, 0x03, // version (TLS 1.2)
0x00, 0x2F, // length of handshake
// handshake
0x01, // handshake type (client hello)
0x00, 0x00, 0x2B, // length of handshake payload 
// client hello
0x03, 0x03, // highest version supported (TLS 1.2)
0x45, 0xFA, 0x01, 0x19, 0x74, 0x55, 0x18, 0x36, 
0x42, 0x05, 0xC1, 0xDD, 0x4A, 0x21, 0x80, 0x80, 
0xEC, 0x37, 0x11, 0x93, 0x16, 0xF4, 0x66, 0x00, 
0x12, 0x67, 0xAB, 0xBA, 0xFF, 0x29, 0x13, 0x9E, // 32 random bytes
0x00, // session id length
0x00, 0x02, // chiper suites length
0x00, 0x3D, // RSA_WITH_AES_256_CBC_SHA256
0x01, // compression methods length
0x00, // no compression
0x00, 0x00 // extensions length
}; 


Це повідомлення говорить серверу про те, що ми підтримуємо TLS 1.2, це нове з'єднання (довжина ідентифікатора сеансу дорівнює нулю) і підтримуємо єдиний набір шифрів — RSA з AES256. Також ми передаємо набір з 32-х «випадкових» байтів для генерації ключів. Якщо ці байти дійсно випадкові, то їх слід десь запам'ятати, тому що вони знадобляться надалі.

ServerHello
«Брат-близнюк» ClientHello, за винятком того, що тип повідомлення 0x02 замість 0x01, і непорожній ідентифікатор сеансу. З цього повідомлення нам потрібно лише 32 випадкових байт.

Certificate
Може містити декілька сертифікатів, спочатку йде довжина всієї групи сертифікатів, потім перед кожним сертифікатом своя довжина. Нас цікавить тільки перший сертифікат і прочитати довжину слід 2 рази. Сам сертифікат являє собою X. 509 в кодуванні DER. З нього нам потрібен публічний ключ RSA.

ServerHelloDone
Не містить нічого корисного, однак враховується при обчисленні хеша для Finished.

ClientKeyExchange
До цього моменту у нас достатньо інформації для генерації і узгодження ключів, що відбуваються в 3 етапи: генерація випадкового секретного ключа, обчислення майстер-ключа, розширення майстер ключа для отримання ключів шифрування і контрольної суми.
Випадковий секретний ключ являє собою 48 байт, перші 2 з яких являють собою номер версії (0x03, 0x03), і залишилися 46 генеруються випадковим чином. Далі ці 48 байт шифруються публічним ключем RSA, і разом з довжиною зашифрованого блоку відправляються сервера. Слід зазначити, що довжина зашифрованого блоку дорівнює довжині ключа, а не 48 байт. Наприклад для сертифікатів з 2048-бітним ключем ця довжина буде 256, а довжина переданих даних — 258.

ChangeCipherSpec
Відправляється відразу ж після ClientKeyExchange. Завжди однакове:
static uint8_t changeCipherSpecs[]=
{
0x14, // change cipher specs
0x03, 0x03, // version (TLS 1.2)
0x00, 0x01, // length
0x01 // type
};

Це повідомлення типу 0x14 і обчислення хеша для Finished не бере участь.

Псевдослучайная функція (PRF)
Для подальшого обчислення ключів нам потрібно псевдослучайная функція, що приймає на вході 4 параметри: секретний ключ, тільки що відісланий сервера, мітку у вигляді тексту, блок первинних даних і бажану довжину результату.
У TLS 1.2 вона визначена наступним чином:
PRF(secret, label, seed) = P_SHA256(secret, label + seed);
P_SHA256(secret, seed) = HMAC_SHA256(secret, A(1) + seed) +
HMAC_SHA256(secret, A(2) + seed) +
HMAC_SH256(secret, A(3) + seed) +…
де A визначається за індукції
A(0) = seed,
A(i) = HMAC_SHA256 (secret, A(i -1)).
Тобто на кожному кроці ми робимо перерахунок контрольної суми з попереднього кроку, а потім обчислюємо котрольную суму від об'єднання результату з текстовим рядком і початковими даними, повторюючи це доти, поки не вийде потрібна довжина.

Тепер майстер ключ обчислюється за формулою
PRF(secret, «master secret», clientRandom + serverRadom, 48);
де clientRandom це 32 випадкових байта з ClientHello, а serverRandom — з ServerHello.
Далі його слід розширити до 128-байтного блоку, що містить 4 32-байтних ключа в наступній послідовності: ключ MAC для відправки, ключ MAC для отримання, ключ шифрування для відправки, ключ дешифрування для отримання.
Ключ MAC для отримання нами не використовується.
Розширення ключа здійснюється за формулою
PRF(masterSecret, «key expansion», serverRandom + clientRadom, 128)
clientRadom і serverRadom тут міняються місцями.

Finished
До цього моменту у нас є все необхідне, щоб почати обмін даними, але, на жаль, ми повинні послати повідомлення Finished, містять правильні дані, в противному випадку сервер розірве з'єднання.
Якщо всі попередні повідомлення були досить тривіальними, то Finshed є більш складним. По перше воно типу 0x16, але його вміст повністю зашифровано, при цьому при обчисленні контрольної суми також фігурує 0x16, а не 0x17 як для інших зашифрованих повідомлень.
Саме повідомлення містить перші 12 байт від
PRT(masterSecret, «client finished», hash, 12)
де hash це SHA256 від такій послідовності повідомлень:
ClientHello, ServerHello, Certfіcate, ServerHelloDone, ClientKeyExchange. Всі повідомлення враховуються без 5 байтного заголовка.
Якщо повідомлення сформовано коректно, то сервер відповість ChangeCipherSpec і Finished, в іншому випадку повідомленням про помилку.
Після цього ми сервер готовий до обміну даними і ми посилаємо наш запит HTTP отримуємо відповідь.

Висновки

Розглянутий у статті підхід дозволяє ефективно працювати з https для додатків, які потребують його повної реалізації. Замість сторонніх реалізацій ssl, тягнуть власну криптографію, можна використовувати вже наявну в проекті, як це показано на прикладі crypto++, що зменшує число залежностей, покращує підтримку і переносимість.
Реалізовано і використовується практично в i2pd — C++ реалізації I2P

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

0 коментарів

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