Ныне используемый протокол ElGamal/AES обладает рядом существенных недостатков и является одной из причин относительно медленной работы сети I2P. Рассматриваемый в статье протокол призван повысить скорость работы и надежности сети и открывает новые возможности, в том числе передачу потокового аудио и видео. Основан на протоколе Noise и алгоритмах мессенджера Signal. Подробное описание здесь. Особо следует отметить, что новый протокол может использоваться с существующими адресами I2P совместно с ElGamal/AES. Статья посвящена реализации в i2pd
Новая криптография и Elligator
В основном используется та же криптография что и в NTCP2: x25519 заменяет ElGamal, а AEAD/Chacha20/Poly1305 заменяет AES. Помимо этого добавляется HKDF-SHA256, создающая ключи длиной 32 или 64 байт и доступная в OpenSSL версии 1.1.1, для более ранних версий используется своя реализация посредством HMAC-SHA256.
В отличие от временного публичного ключа ElGamal, представляющего собой случайные 256 байт, публичный ключ x25519 является точкой на эллиптической кривой, и злоумышленник может понять, являются ли 32 байта ключом x25519 и сделать определенные выводы. С целью недопущения этого публичный ключ подвергается преобразованию под названием Elligator, сложному самому по себе и заслуживающего отдельной статьи. Подробно как и почему оно работает описано здесь. Практическая реализация на основе OpenSSL выглядит не столь сложной, однако:
Сквозное шифрование I2P
I2P адреса обмениваются между собой I2NP сообщениями типа 11 — Garlic(«чеснок»), содержимое которого представляет собой полностью зашифрованные данные, шифруемые адресом отправителя и расшифровываемые адресом получателя. Более подробно принципы работы описаны здесь. Применительно к ElGamal/AES следует отметить следующие основные моменты:
Установка соединения
При установке соединения происходит обмен двумя сообщениями и участвуют 4 пары ключей x25519: две пары ключей шифрования адресов и две пары временных ключей. Публичный ключ шифрования сервера(Боб) берется из LeaseSet-а его адреса, публичные временные ключи передаются вместе с сообщениями, скрытые с помощью Elligator'а, публичный ключ шифрования клиента(Алиса) передается отдельным зашифрованным блоком.
После успешного согласования создается первый tagset с ck в качестве ключа
Первый tagset
Алиса отправляет сообщения NS до тех пор, пока не получит NSR от Боба, первый полученный NSR определяет текущий сеанс, если сеанс уже существует, то обрабатывается только содержимое пакета. С каждым NS создается отдельный временный tagset, предназначенный исключительно для получения и расшифровки NSR.
Боб создает новый сеанс при получении первого NS, и продолжает отправлять NSR с ключами и тэгами пока не получит первое сообщение установленного сеанса.
Структура сообщения Garlic
Незашифрованное сообщение Garlic в новом протоколе имеет другую структуру, чем в ElGamal/AES. Вместо «чесночин», по сути своей представляющие I2NP сообщения с инструкциями для их доставки, в новом протоколе используются блоки разных типов, аналогичные NTCP2. Каждый блок состоит из заголовка с типом и длиной и содержимого. В настоящий момент используются следующие типы блоков:
Наборы тэгов(tagsets) и создание новых ключей
Упоминавшийся ранее tagset представляет собой генератор троек (номер, тэг, ключ) для каждого нового пакета. Номера идут по порядку начиная с 0. Номер передается как двухбайтное число, поэтому в одном tagset-е может быть сгенерировано не более 65535 тэгов, после чего требуется новый tagset. Практически же используется гораздо меньшее значение, в частности в i2pd новый tagset создается после отправки 4K пакетов.
Tagset начинается с операции DH_INITIALIZE на основе общего ключа и результата DH_INITIALIZE предыдущего tagset-а.
DH_INITIALIZE
Затем вычисляется SESSTAG_CONSTANT, используемая для вычисления тэгов
SESSTAG_CONSTANT и тэги
Собственно вычисление тэга представляет собой простой HKDF от предыдущего
При необходимости создания нового tagset-а отправитель создает новый ключ x25519 и добавляет в следующий пакет блок Next Key с публичным ключом, получатель тоже создает новый ключ и отсылает Next Key в ответ. После успешного согласования стороны начинают использовать новый tagset, иначе продолжают отправлять Next Key, используя предыдущий tagset.
Новый протокол уже реализован и используется в последних релизах I2P: 0.9.46 джавы и 2.32.0 i2pd. Для включения его, в настройки тоннелей следует добавить параметры i2cp.leaseSetType=3 и
i2cp.leaseSetEncType=0,4 для взаимодействия с адресами как со старым так и с новым шифрованием или i2cp.leaseSetType=3 и i2cp.leaseSetEncType=4 только для нового шифрования. Также возможна работа вместе с шифрованными LeaseSet-ми с параметром i2cp.leaseSetType=5.
Автор @orignal, разраб i2pd
Новая криптография и Elligator
В основном используется та же криптография что и в NTCP2: x25519 заменяет ElGamal, а AEAD/Chacha20/Poly1305 заменяет AES. Помимо этого добавляется HKDF-SHA256, создающая ключи длиной 32 или 64 байт и доступная в OpenSSL версии 1.1.1, для более ранних версий используется своя реализация посредством HMAC-SHA256.
В отличие от временного публичного ключа ElGamal, представляющего собой случайные 256 байт, публичный ключ x25519 является точкой на эллиптической кривой, и злоумышленник может понять, являются ли 32 байта ключом x25519 и сделать определенные выводы. С целью недопущения этого публичный ключ подвергается преобразованию под названием Elligator, сложному самому по себе и заслуживающего отдельной статьи. Подробно как и почему оно работает описано здесь. Практическая реализация на основе OpenSSL выглядит не столь сложной, однако:
- Для преобразования пригодна только половина ключей. Пригодность определяется вычислением символа Лежандра
- Пригодный ключ может быть преобразован в случайную последовательность 254 бит и восстановлен из нее обратным преобразованием
- Произвольные 254 бита могут быть преобразованы обратно в какой либо ключ x25519
- В I2P старшие 2 бита заполняются случайными значениями до полных 32 байт
Сквозное шифрование I2P
I2P адреса обмениваются между собой I2NP сообщениями типа 11 — Garlic(«чеснок»), содержимое которого представляет собой полностью зашифрованные данные, шифруемые адресом отправителя и расшифровываемые адресом получателя. Более подробно принципы работы описаны здесь. Применительно к ElGamal/AES следует отметить следующие основные моменты:
- Первое сообщение шифруется публичным ключом ElGamal получателя из его LeaseSet-a. Вместе с ним передается ключ AES и набор 32-байтных тэгов для последующих сообщений. Шифрование и особенно расшифровка ElGamal очень медленные
- Каждый тэг используется один раз. Получатель сначала сравнивает первые 32 байта сообщения с известными ему тэгами, и, в случае нахождения, использует соответствующий ключ AES для расшифровки остатка сообщения. Таким образом, используя тэг, отправитель должен быть уверен в том, что тэг известен получателю, периодически отправлять новые тэги и дожидаться подтверждения получения. Поэтому довольно часто возникает ситуация, что подтвержденных тэгов больше нет и приходится снова использовать ElGamal
- Получатель не знает откуда пришло сообщение
- Длина сообщения, зашифрованного AES, всегда кратна 16 байтам и дает в остатке 2 для зашифрованного ElGamal
Код:
i2p::crypto::HKDF (m_CurrentSymmKeyCK, nullptr, 0, "SymmetricRatchet", m_CurrentSymmKeyCK);
При установке соединения происходит обмен двумя сообщениями и участвуют 4 пары ключей x25519: две пары ключей шифрования адресов и две пары временных ключей. Публичный ключ шифрования сервера(Боб) берется из LeaseSet-а его адреса, публичные временные ключи передаются вместе с сообщениями, скрытые с помощью Elligator'а, публичный ключ шифрования клиента(Алиса) передается отдельным зашифрованным блоком.
В процессе соединения происходит вычисление ключа ck (chaining key) по протоколу Noise на основе вычисленного общего ключа x25519(shared secret), в качестве операции MixKey используется HKDFNewSession(NS) -----------> Боб
Алиса < — NewSessionReply(NSR)
Код:
i2p::crypto::HKDF (m_CK, sharedSecret, 32, "", m_CK);
Первый tagset
Код:
uint8_t keydata[64];
i2p::crypto::HKDF (m_CK, nullptr, 0, "", keydata); // keydata = HKDF(chainKey, ZEROLEN, "", 64)
// k_ab = keydata[0:31], k_ba = keydata[32:63]
auto receiveTagset = std::make_shared<RatchetTagSet>(shared_from_this ());
receiveTagset->DHInitialize (m_CK, keydata); // tagset_ab = DH_INITIALIZE(chainKey, k_ab)
receiveTagset->NextSessionTagRatchet ();
m_SendTagset = std::make_shared<RatchetTagSet>(shared_from_this ());
m_SendTagset->DHInitialize (m_CK, keydata + 32); // tagset_ba = DH_INITIALIZE(chainKey, k_ba)
m_SendTagset->NextSessionTagRatchet ();
Боб создает новый сеанс при получении первого NS, и продолжает отправлять NSR с ключами и тэгами пока не получит первое сообщение установленного сеанса.
Структура сообщения Garlic
Незашифрованное сообщение Garlic в новом протоколе имеет другую структуру, чем в ElGamal/AES. Вместо «чесночин», по сути своей представляющие I2NP сообщения с инструкциями для их доставки, в новом протоколе используются блоки разных типов, аналогичные NTCP2. Каждый блок состоит из заголовка с типом и длиной и содержимого. В настоящий момент используются следующие типы блоков:
- Garlic Clove — содержит I2NP сообщение с инструкциями для доставки. Как правило данные или LeaseSet. Инструкции для доставки в i2pd в настоящий момент не используются и при по получении игнорируются, однако заполняются при отправке для совместимости с джавовскими маршрутизаторами. Заголовок I2NP изменен с целью уменьшения объема передаваемых данных
- Next Key — используется для согласования ключей нового tagset-а
- ACK Request — запрос подтверждения получения сообщения. Обычно запрашивается вместе с отправкой нового LeaseSet-а
- ACK — ответ на ACK request
- Padding — блок случайной длины 0-16 байт. Всегда последний
Наборы тэгов(tagsets) и создание новых ключей
Упоминавшийся ранее tagset представляет собой генератор троек (номер, тэг, ключ) для каждого нового пакета. Номера идут по порядку начиная с 0. Номер передается как двухбайтное число, поэтому в одном tagset-е может быть сгенерировано не более 65535 тэгов, после чего требуется новый tagset. Практически же используется гораздо меньшее значение, в частности в i2pd новый tagset создается после отправки 4K пакетов.
Tagset начинается с операции DH_INITIALIZE на основе общего ключа и результата DH_INITIALIZE предыдущего tagset-а.
DH_INITIALIZE
Код:
// DH_INITIALIZE(rootKey, k)
uint8_t keydata[64];
i2p::crypto::HKDF (rootKey, k, 32, "KDFDHRatchetStep", keydata); // keydata = HKDF(rootKey, k, "KDFDHRatchetStep", 64)
memcpy (m_NextRootKey, keydata, 32); // nextRootKey = keydata[0:31]
i2p::crypto::HKDF (keydata + 32, nullptr, 0, "TagAndKeyGenKeys", m_KeyData.buf);
// [sessTag_ck, symmKey_ck] = HKDF(keydata[32:63], ZEROLEN, "TagAndKeyGenKeys", 64)
memcpy (m_SymmKeyCK, m_KeyData.buf + 32, 32);
SESSTAG_CONSTANT и тэги
Код:
i2p::crypto::HKDF (m_KeyData.GetSessTagCK (), nullptr, 0, "STInitialization", m_KeyData.buf); // [sessTag_ck, sesstag_constant] = HKDF(sessTag_ck, ZEROLEN, "STInitialization", 64)
memcpy (m_SessTagConstant, m_KeyData.GetSessTagConstant (), 32);
Собственно вычисление тэга представляет собой простой HKDF от предыдущего
[sessTag_ck, tag] = HKDF(sessTag_ck, SESSTAG_CONSTANT, «SessionTagKeyGen», 64)При необходимости создания нового tagset-а отправитель создает новый ключ x25519 и добавляет в следующий пакет блок Next Key с публичным ключом, получатель тоже создает новый ключ и отсылает Next Key в ответ. После успешного согласования стороны начинают использовать новый tagset, иначе продолжают отправлять Next Key, используя предыдущий tagset.
Новый протокол уже реализован и используется в последних релизах I2P: 0.9.46 джавы и 2.32.0 i2pd. Для включения его, в настройки тоннелей следует добавить параметры i2cp.leaseSetType=3 и
i2cp.leaseSetEncType=0,4 для взаимодействия с адресами как со старым так и с новым шифрованием или i2cp.leaseSetType=3 и i2cp.leaseSetEncType=4 только для нового шифрования. Также возможна работа вместе с шифрованными LeaseSet-ми с параметром i2cp.leaseSetType=5.
Автор @orignal, разраб i2pd