Реализация нового транспортного протокола NTCP2 сети I2P

Транспортные протоколы I2P были разработаны почти 15 лет назад, когда основной задачей было сокрытие содержимого трафика, а не факт использования того или иного протокола. dpi и блокировку трафика в то время никто не принимал в расчет. Однако времена меняются и хотя существующие протоколы I2P по прежнему защищены довольно хорошо, возникла необходимость в новом транспортном протоколе, отвечающему на существующие и будущие угрозы, и, в первую очередь, dpi, анализирующий длину пакетов. Помимо этого, новый протокол использует самые современные достижения криптографии. Полное описание протокола здесь. За основу взят Noise, в котором в качестве хэш-функции используется SHA256, а в качестве DH (в терминологии Noise) — x25519.

image

Новая криптография


Для NTCP2 в дополнение к уже существующим в I2P необходимо реализовать следующие криптографические алгоритмы:

  • x25519
  • HMAC-SHA256
  • Chacha20
  • Poly1305
  • AEAD
  • Siphash


Все они, за исключением Siphash, реализованы в openssl 1.1.0. В свою очередь Siphash появится в openssl 1.1.1, релиз которого состоится в ближайшее время. Для совместимости с openssl 1.0.2, входящей в большинство ныне используемых ОС, в i2pd были добавлены собственные реализации, написанные одним из разработчиков i2pd Jeff Becker-ом, известным в I2P как psi.
По сравнению с NTCP x25519 заменяет DH, AEAD/Chaha20/Poly1305 заменяет AES-256-CBC/Adler32, а Siphash используется для шифрования длины передаваемых сообщений. Процедура вычисления общего ключа стала более сложной: с множеством вызовов HMAC-SHA256.

Изменения в RouterInfo


Для работы по протоколу NTCP2 в дополнение к двум уже существующим ключам (шифрования и подписи) вводится третий ключ x22519, называемый статическим ключем, который обязательно должен присутствовать в каком нибудь адресе RouterInfo как параметр «s» и для клиентов и для серверов. Если более одного адреса поддерживают NTCP2, например ipv4 и ipv6, то «s» обязан быть везде одинаковым. Для клиентов адрес может содержать только «s» и не содержать параметры «host» и «port». Также обязательным параметром NTCP2 является «v», в настоящий момент всегда равный »2».
Адрес NTCP2 может задаваться как адрес типа «NTCP» с дополнительными параметрами — в этом случае соединение может быть установлено как по NTCP так и по NTCP2, или же как адрес типа «NTCP2», поддерживающий только NTCP2 соединения. В джавовском I2P применяется первый способ, в i2pd — второй.
Если узел принимает входящие NTCP2 соединения, то он должен опубликовать параметр «i» со значением IV для шифрования публичного ключа при установке соединения.

Установка соединения


В процессе установки соединения стороны генерируют пары временных ключей x25519, и на основе их и статических ключей вычисляют наборы ключей для передачи данных. Также происходит проверка подлинности статических ключей и соответствие содержимому RouterInfo.
Стороны обмениваются тремя сообщениями:

SessionRequest ------------------->
< — SessionCreated
SessionConfirmed ----------------->

для каждого из которых вычисляется общий ключ x25519. называемый «input key material», и затем генерируется ключ шифрования сообщения с помощью операции MixKey, при этом значением ck (chaining key) сохраняется между сообщениями и является результатом, на основе которого вычисляются ключи для передачи данных. Реализация MixKey выглядит примерно так:

Код MixKey
 void NTCP2Establisher::MixKey (const uint8_t * inputKeyMaterial, uint8_t * derived)
{
        // temp_key = HMAC-SHA256(ck, input_key_material)
        uint8_t tempKey[32]; unsigned int len;
        HMAC(EVP_sha256(), m_CK, 32, inputKeyMaterial, 32, tempKey, &len);  
        // ck = HMAC-SHA256(temp_key, byte(0x01)) 
        static uint8_t one[1] =  { 1 };
        HMAC(EVP_sha256(), tempKey, 32, one, 1, m_CK, &len);        
        // derived = HMAC-SHA256(temp_key, ck || byte(0x02))
        m_CK[32] = 2;
        HMAC(EVP_sha256(), tempKey, 32, m_CK, 33, derived, &len);   
}



SessionRequest состоит из 32-х байтного публичного ключа x25519 клиента, и зашифрованного AEAD/Chacha20/Poly1305 16 байтного блока данных + 16 байт хэша, а также набор случайных данных (padding), длина которого передается в зашифрованном блоке. Также там передается длина второй половины сообщения SessionConfirmed. Блок шифруется и подписывается ключем на основе временного ключа клиента и статического ключа сервера. Начальный ck для MixKey устанавливается в SHA256 («Noise_XKaesobfse+hs2+hs3_25519_ChaChaPoly_SHA256»)
Поскольку 32 байта публичного ключа x25519 могут быть распознаны dpi, то они шифруются с помощью AES-256-CBC, где ключем служит хэш адреса сервера, а IV берется из параметра «i» адреса в RouterInfo.

SessionCreated по структуре аналогично SessionRequest, за исключением того, что ключ вычисляется на основе временных ключей обеих сторон, а для IV для шифрования/расшифровки публичного ключа является IV после расшифровки/шифрования публичного ключа из SessionRequest.

SessionConfirmed состоит из двух частей: статический публичный ключ клиента и RouterInfo клиента. В отличие от предыдущих сообщений, публичный ключ шифруется AEAD/Chaha20/Poly1305 с тем же ключем, что и SessionCreated. Поэтому длина первой части не 32, а 48 байт. Вторая часть шифруется тоже AEAD/Chaha20/Poly1305, но с новым ключем, вычисляем на основе временного ключа сервера и статического ключа клиента. Также к RouterInfo может быть добавлен блок случайных данных, но, как правило, в этом нет необходимости, потому что длина RouterInfo разная.

Генерация ключей для передачи данных


Если все проверки хэшей и ключей в процессе установки соединения прошли успешно, то после последнего MixKey на обеих сторонах должен быть одинаковый ck, из которого будут сгенерированы 2 набора троек ключей в каждую сторону свой, где k — ключ AEAD/Chaha20/Poly1305, sipk — ключ для Siphash, sipiv — начальное значение IV для Siphash, которое изменяется после каждого его применения.

Код, реализуюший генерацию ключей
void NTCP2Session::KeyDerivationFunctionDataPhase ()
{
        uint8_t tempKey[32]; unsigned int len;
        // temp_key = HMAC-SHA256(ck, zerolen)
        HMAC(EVP_sha256(), m_Establisher->GetCK (), 32, nullptr, 0, tempKey, &len); 
        static uint8_t one[1] =  { 1 };
        // k_ab = HMAC-SHA256(temp_key, byte(0x01)).
        HMAC(EVP_sha256(), tempKey, 32, one, 1, m_Kab, &len); 
        m_Kab[32] = 2;
        // k_ba = HMAC-SHA256(temp_key, k_ab || byte(0x02))
        HMAC(EVP_sha256(), tempKey, 32, m_Kab, 33, m_Kba, &len);  
        static uint8_t ask[4] = { 'a', 's', 'k', 1 }, master[32];
        // ask_master = HMAC-SHA256(temp_key, "ask" || byte(0x01))
        HMAC(EVP_sha256(), tempKey, 32, ask, 4, master, &len); 
        uint8_t h[39];
        memcpy (h, m_Establisher->GetH (), 32);
        memcpy (h + 32, "siphash", 7);
        // temp_key = HMAC-SHA256(ask_master, h || "siphash")
        HMAC(EVP_sha256(), master, 32, h, 39, tempKey, &len); 
        // sip_master = HMAC-SHA256(temp_key, byte(0x01))  
        HMAC(EVP_sha256(), tempKey, 32, one, 1, master, &len); 
        // temp_key = HMAC-SHA256(sip_master, zerolen)
        HMAC(EVP_sha256(), master, 32, nullptr, 0, tempKey, &len); 
       // sipkeys_ab = HMAC-SHA256(temp_key, byte(0x01)).
        HMAC(EVP_sha256(), tempKey, 32, one, 1, m_Sipkeysab, &len); 
        m_Sipkeysab[32] = 2;
         // sipkeys_ba = HMAC-SHA256(temp_key, sipkeys_ab || byte(0x02)) 
        HMAC(EVP_sha256(), tempKey, 32, m_Sipkeysab, 33, m_Sipkeysba, &len);
}



Первые 16 байт массива sipkeys представляют собой собой ключ Siphash, вторые 8 байт — IV.
На самом деле для Siphash требуется два ключа по 8 байт, но в i2pd они рассматриваются как 1 ключ длиной 16 байт.

Передача данных


Данные передаются фреймами, каждый фрейм состоит из 3-х частей:

  1. 2 байта длины фрейма, зашифрованной Siphash
  2. данные, зашифрованные Chacha20
  3. 16 байт хэша Poly1305


Максимальная длина передаваемых данных в одном фрейме — 65519 байт.
Длина сообщения шифруется применением операции XOR с первым двумя байтами текущего IV Siphash.
Данные состоят из блоков, каждому блоку предшествует 3-х байтный заголовок с типом блока и длиной. В основном передаются блоки типа I2NP, содержащие сообщения I2NP с измененным заголовком. В одном фрейме может быть передано несколько I2NP блоков.
Другим важным типом блока является блок случайных данных, который рекомендуется добавлять к каждому фрейму. Он может быть только один и последним.
Кроме них, в текущей реализации NTCP2 встречаются еще 3 типа блока:
RouterInfo — обычно содержит RouterInfo сервера сразу после установки соединения, но может быть передано и RouterInfo произвольного узла в любой момент с целью ускорения работы floodfill-ов, для чего в сообщении предусмотрено поле флагов.
Termination — отсылается узлом при разрыве соединения по его инициативе с указанием причины.
DateTime — текущее время в секундах.

Таким образом, новый транспортный протокол позволяет не только эффективно противостоять dpi, но существенно снижает нагрузку на процессор за счет более современной и быстрой и криптографии, что особенно важно при работе на слабых устройствах типа смартфонов и маршрутизаторах. В настоящий момент поддержка NTCP2 полностью реализована как в официальном I2P, так и в i2pd и появится официально в следующих релизах 0.9.36 и 2.20 соответственно. Для включения ntcp2 в i2pd следует указать конфигурационный параметр ntcp2.enabled=true, и ntcp2.published=true и ntcp2.port=<порт> для входящих соединений.

© Habrahabr.ru