Транспортные протоколы I2P изначально разрабатывались около 15 лет назад. Тогда основной целью было скрыть передаваемые данные, а не сам факт использования самого протокола. О защите от DPI (глубокая инспекция пакетов) и цензуры протоколов всерьез никто не задумывался. Времена меняются, и хотя исходные транспортные протоколы по-прежнему обеспечивают высокий уровень безопасности, возник спрос на новый транспортный протокол. NTCP2 разработан для противодействия современным угрозам цензуры, в первую очередь анализу длины пакетов средствами DPI. Кроме того, новый протокол использует самые современные достижения криптографии. NTCP2 основан на Noise Protocol Framework, при этом SHA256 используется в качестве хеш‑функции, а x25519 — для обмена ключами Диффи-Хеллмана (DH) на эллиптических кривых.
Полная спецификация протокола NTCP2 доступна здесь.
Новая криптография
NTCP2 требует добавления следующих криптографических алгоритмов в реализацию I2P:
- x25519
- HMAC-SHA256
- Chacha20
- Poly1305
- AEAD
- SipHash
По сравнению с нашим исходным протоколом NTCP, NTCP2 использует x25519 вместо ElGamal для функции DH (Диффи—Хеллмана), AEAD/Chaha20/Poly1305 вместо AES-256-CBC/Adler32 и использует SipHash для сокрытия информации о длине пакета. Функция деривации ключей, используемая в NTCP2, более сложна и теперь использует множество вызовов HMAC-SHA256.
Примечание к реализации i2pd (C++): Все перечисленные выше алгоритмы, за исключением SipHash, реализованы в OpenSSL 1.1.0. SipHash будет добавлен в предстоящем выпуске OpenSSL 1.1.1. Для совместимости с OpenSSL 1.0.2, который используется в большинстве современных систем, основной разработчик i2pd Jeff Becker внёс вклад, предоставив самостоятельные реализации недостающих криптографических алгоритмов.
Изменения RouterInfo
NTCP2 требует наличия третьего ключа (x25519) в дополнение к существующим двум (ключам шифрования и подписи). Он называется статическим ключом и должен быть добавлен в любой из адресов RouterInfo в качестве параметра “s”. Он обязателен как для инициатора NTCP2 (Alice), так и для ответчика (Bob). Если более одного адреса поддерживает NTCP2, например IPv4 и IPv6, параметр “s” должен быть одинаковым для всех. Адрес Alice может содержать только параметр “s” без указания “host” и “port”. Также требуется параметр “v”, который в настоящее время всегда установлен в значение “2”.
Адрес NTCP2 может быть объявлен как отдельный адрес NTCP2 или как адрес NTCP старого образца с дополнительными параметрами; в этом случае он будет принимать как соединения NTCP, так и NTCP2. Реализация I2P на Java использует второй подход, а i2pd (реализация на C++) — первый.
Если узел принимает соединения NTCP2, он должен публиковать свой RouterInfo с параметром “i”, который используется в качестве вектора инициализации (IV) для открытого ключа шифрования при установке этим узлом новых соединений.
Установление соединения
Чтобы установить соединение, обе стороны должны сгенерировать пары эфемерных ключей x25519. На основе этих ключей и “статических” ключей они выводят набор ключей для передачи данных. Обе стороны должны проверить, что другая сторона действительно обладает закрытым ключом, соответствующим этому статическому ключу, и что статический ключ совпадает с указанным в RouterInfo.
Для установления соединения отправляются три сообщения:
Alice Bob
SessionRequest ------------------->
<------------------- SessionCreated
SessionConfirmed ----------------->
Общий ключ x25519, называемый «input key material» (входной ключевой материал), вычисляется для каждого сообщения, после чего ключ шифрования сообщения генерируется функцией MixKey. Значение ck (chaining key) хранится во время обмена сообщениями. Это значение используется в качестве финального входа при генерации ключей для передачи данных.
Функция MixKey выглядит примерно так в реализации I2P на C++:
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 состоит из открытого ключа Алисы x25519 (32 байта), блока данных, зашифрованного с использованием AEAD/Chacha20/Poly1305 (16 байт), хэша (16 байт) и некоторых случайных данных в конце (padding — дополнение). Длина padding определяется в зашифрованном блоке данных. Зашифрованный блок также содержит длину второй части сообщения SessionConfirmed. Блок данных шифруется и подписывается ключом, выведенным из эфемерного ключа Алисы и статического ключа Боба. Начальное значение ck для функции MixKey установлено равным SHA256 (Noise_XKaesobfse+hs2+hs3_25519_ChaChaPoly_SHA256).
Поскольку 32 байта публичного ключа x25519 могут быть обнаружены DPI (глубокая инспекция пакетов), они шифруются алгоритмом AES-256-CBC с использованием хэша адреса Боба в качестве ключа и параметра “i” из RouterInfo в качестве вектора инициализации (IV).
Сообщение SessionCreated имеет ту же структуру, что и SessionRequest, за исключением того, что ключ вычисляется на основе эфемерных ключей обеих сторон. Вектор инициализации (IV), сгенерированный после шифрования/расшифрования открытого ключа из сообщения SessionRequest, используется как IV для шифрования/расшифрования эфемерного открытого ключа.
Сообщение SessionConfirmed состоит из двух частей: публичного статического ключа и RouterInfo Алисы. Отличие от предыдущих сообщений в том, что эфемерный публичный ключ зашифрован с помощью AEAD/Chaha20/Poly1305 с использованием того же ключа, что и SessionCreated. Это приводит к увеличению первой части сообщения с 32 до 48 байт. Вторая часть также зашифрована с помощью AEAD/Chaha20/Poly1305, но с использованием нового ключа, вычисленного из эфемерного ключа Боба и статического ключа Алисы. Часть RouterInfo также может быть дополнена случайным заполнением (padding), но это не требуется, поскольку RouterInfo обычно имеет переменную длину.
Генерация ключей передачи данных
Если все проверки хэшей и ключей прошли успешно, после последней операции MixKey на обеих сторонах должно быть одинаковое значение ck. Это значение используется для генерации двух наборов ключей <k, sipk, sipiv> — по одному для каждой стороны соединения. “k” — это ключ AEAD/Chaha20/Poly1305, “sipk” — ключ SipHash, “sipiv” — начальное значение для IV (инициализирующий вектор) SipHash, которое изменяется после каждого использования.
Код, используемый для генерации ключей, в реализации I2P на C++ выглядит так:
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);
}
Примечание по реализации i2pd (C++): первые 16 байт массива “sipkeys” — это ключ SipHash, последние 8 байт — вектор инициализации (IV). SipHash требует два 8-байтных ключа, но i2pd обрабатывает их как единый 16-байтный ключ.
Передача данных
Данные передаются в кадрах, каждый кадр состоит из 3 частей:
- 2 bytes of frame length obfuscated with SipHash
- data encrypted with Chacha20
- 16 bytes of Poly1305 hash value
Максимальный размер данных, передаваемых в одном кадре, составляет 65519 байт.
Длина сообщения маскируется путём применения операции XOR с первыми двумя байтами текущего инициализационного вектора (IV) SipHash.
Зашифрованная часть данных содержит блоки данных. Перед каждым блоком помещается заголовок длиной 3 байта, который определяет тип блока и его длину. Обычно передаются блоки типа I2NP, то есть сообщения I2NP с изменённым заголовком. Один кадр NTCP2 может передавать несколько блоков I2NP.
Другой важный тип блока данных — случайный блок данных. Рекомендуется добавлять случайный блок данных в каждый кадр NTCP2. Можно добавить только один случайный блок данных, и он должен быть последним блоком.
Это другие блоки данных, используемые в текущей реализации NTCP2:
- RouterInfo — usually contains Bob’s RouterInfo after the connection has been established, but it can also contain RouterInfo of a random node for the purpose of speeding up floodfills (there is a flags field for that case).
- Termination — is used when a host explicitly terminates a connection and specifies a reason for that.
- DateTime — a current time in seconds.
Краткое содержание
Предоставьте ТОЛЬКО перевод, ничего больше:
Новый транспортный протокол I2P NTCP2 обеспечивает эффективную устойчивость к цензуре на основе DPI (Deep Packet Inspection — глубокая проверка пакетов). Он также снижает нагрузку на процессор благодаря использованию более быстрой, современной криптографии. Это делает I2P более пригодным для работы на маломощных устройствах, таких как смартфоны и домашние маршрутизаторы. Обе основные реализации I2P полностью поддерживают NTCP2 и предоставляют возможность его использования, начиная с версии 0.9.36 (Java) и 2.20 (i2pd, C++).