Ныне используемый протокол ElGamal/AES обладает рядом существенных недостатков и является одной из причин относительно медленной работы сети I2P. Рассматриваемый в статье протокол призван повысить скорость работы и надежности сети и открывает новые возможности, в том числе передачу потокового аудио и видео. Основан на протоколе Noise и алгоритмах мессенджера Signal. Подробное описание здесь. Особо следует отметить, что новый протокол может использоваться с существующими адресами I2P совместно с ElGamal/AES. Статья посвящена реализации в i2pd
В основном используется та же криптография что и в NTCP2: x25519 заменяет ElGamal, а AEAD/Chacha20/Poly1305 заменяет AES. Помимо этого добавляется HKDF-SHA256, создающая ключи длиной 32 или 64 байт и доступная в OpenSSL версии 1.1.1, для более ранних версий используется своя реализация посредством HMAC-SHA256.
В отличие от временного публичного ключа ElGamal, представляющего собой случайные 256 байт, публичный ключ x25519 является точкой на эллиптической кривой, и злоумышленник может понять, являются ли 32 байта ключом x25519 и сделать определенные выводы. С целью недопущения этого публичный ключ подвергается преобразованию под названием Elligator, сложному самому по себе и заслуживающего отдельной статьи. Подробно как и почему оно работает описано здесь.
Практическая реализация на основе OpenSSL выглядит не столь сложной, однако:
Та же самая проблема существует и с NTCP2, там публичный ключ x25519 шифруется AES с адресом маршрутизатора в качестве ключа, но здесь другая модель угрозы, потому что злоумышленник находится внутри I2P и такой способ сокрытия ключа не подходит.
I2P адреса обмениваются между собой I2NP сообщениями типа 11 — Garlic(«чеснок»), содержимое которого представляет собой полностью зашифрованные данные, шифруемые адресом отправителя и расшифровываемые адресом получателя. Более подробно принципы работы описаны здесь. Применительно к ElGamal/AES следует отметить следующие основные моменты:
В новом протоколе вводится понятие сеанса(session) между ключами шифрования адресов, ключ шифрования однозначно определяет адрес, но у одного адреса может быть несколько ключей, например при multihoming или при рестарте. Между парой ключей может быть только один сеанс, сеансы двунаправленные, при получении пакета сеанс определяет отправителя. Все пакеты, кроме первого, начинаются с 8-байтного тэга, связывающего пакет с tagset-ом и сеансом. Тэги не передаются, а вычисляются одновременно на обеих сторонах сеанса.
Пакет шифруется AEAD/Chacha20/Poly1305, ключ шифрования уникален для каждого пакета и вычисляется на обеих сторонах на основе предыдущего с помощью HKDF.
При установке соединения происходит обмен двумя сообщениями и участвуют 4 пары ключей x25519: две пары ключей шифрования адресов и две пары временных ключей. Публичный ключ шифрования сервера(Боб) берется из LeaseSet-а его адреса, публичные временные ключи передаются вместе с сообщениями, скрытые с помощью Elligator'а, публичный ключ шифрования клиента(Алиса) передается отдельным зашифрованным блоком.
NewSession(NS) -----------> Боб
Алиса < — NewSessionReply(NSR)
В процессе соединения происходит вычисление ключа ck (chaining key) по протоколу Noise на основе вычисленного общего ключа x25519(shared secret), в качестве операции MixKey используется HKDF
После успешного согласования создается первый tagset с ck в качестве ключа
Алиса отправляет сообщения NS до тех пор, пока не получит NSR от Боба, первый полученный NSR определяет текущий сеанс, если сеанс уже существует, то обрабатывается только содержимое пакета. С каждым NS создается отдельный временный tagset, предназначенный исключительно для получения и расшифровки NSR.
Боб создает новый сеанс при получении первого NS, и продолжает отправлять NSR с ключами и тэгами пока не получит первое сообщение установленного сеанса.
Незашифрованное сообщение Garlic в новом протоколе имеет другую структуру, чем в ElGamal/AES. Вместо «чесночин», по сути своей представляющие I2NP сообщения с инструкциями для их доставки, в новом протоколе используются блоки разных типов, аналогичные NTCP2. Каждый блок состоит из заголовка с типом и длиной и содержимого. В настоящий момент используются следующие типы блоков:
Упоминавшийся ранее tagset представляет собой генератор троек (номер, тэг, ключ) для каждого нового пакета. Номера идут по порядку начиная с 0. Номер передается как двухбайтное число, поэтому в одном tagset-е может быть сгенерировано не более 65535 тэгов, после чего требуется новый tagset. Практически же используется гораздо меньшее значение, в частности в i2pd новый tagset создается после отправки 4K пакетов.
Tagset начинается с операции DH_INITIALIZE на основе общего ключа и результата DH_INITIALIZE предыдущего tagset-а.
Затем вычисляется SESSTAG_CONSTANT, используемая для вычисления тэгов
При необходимости создания нового 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.
Новая криптография и 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 байт
Та же самая проблема существует и с NTCP2, там публичный ключ x25519 шифруется AES с адресом маршрутизатора в качестве ключа, но здесь другая модель угрозы, потому что злоумышленник находится внутри I2P и такой способ сокрытия ключа не подходит.
Сквозное шифрование I2P
I2P адреса обмениваются между собой I2NP сообщениями типа 11 — Garlic(«чеснок»), содержимое которого представляет собой полностью зашифрованные данные, шифруемые адресом отправителя и расшифровываемые адресом получателя. Более подробно принципы работы описаны здесь. Применительно к ElGamal/AES следует отметить следующие основные моменты:
- Первое сообщение шифруется публичным ключом ElGamal получателя из его LeaseSet-a. Вместе с ним передается ключ AES и набор 32-байтных тэгов для последующих сообщений. Шифрование и особенно расшифровка ElGamal очень медленные
- Каждый тэг используется один раз. Получатель сначала сравнивает первые 32 байта сообщения с известными ему тэгами, и, в случае нахождения, использует соответствующий ключ AES для расшифровки остатка сообщения. Таким образом, используя тэг, отправитель должен быть уверен в том, что тэг известен получателю, периодически отправлять новые тэги и дожидаться подтверждения получения. Поэтому довольно часто возникает ситуация, что подтвержденных тэгов больше нет и приходится снова использовать ElGamal
- Получатель не знает откуда пришло сообщение
- Длина сообщения, зашифрованного AES, всегда кратна 16 байтам и дает в остатке 2 для зашифрованного ElGamal
В новом протоколе вводится понятие сеанса(session) между ключами шифрования адресов, ключ шифрования однозначно определяет адрес, но у одного адреса может быть несколько ключей, например при multihoming или при рестарте. Между парой ключей может быть только один сеанс, сеансы двунаправленные, при получении пакета сеанс определяет отправителя. Все пакеты, кроме первого, начинаются с 8-байтного тэга, связывающего пакет с tagset-ом и сеансом. Тэги не передаются, а вычисляются одновременно на обеих сторонах сеанса.
Пакет шифруется AEAD/Chacha20/Poly1305, ключ шифрования уникален для каждого пакета и вычисляется на обеих сторонах на основе предыдущего с помощью HKDF.
i2p::crypto::HKDF (m_CurrentSymmKeyCK, nullptr, 0, "SymmetricRatchet", m_CurrentSymmKeyCK);
Установка соединения
При установке соединения происходит обмен двумя сообщениями и участвуют 4 пары ключей x25519: две пары ключей шифрования адресов и две пары временных ключей. Публичный ключ шифрования сервера(Боб) берется из LeaseSet-а его адреса, публичные временные ключи передаются вместе с сообщениями, скрытые с помощью Elligator'а, публичный ключ шифрования клиента(Алиса) передается отдельным зашифрованным блоком.
NewSession(NS) -----------> Боб
Алиса < — NewSessionReply(NSR)
В процессе соединения происходит вычисление ключа ck (chaining key) по протоколу Noise на основе вычисленного общего ключа x25519(shared secret), в качестве операции MixKey используется HKDF
i2p::crypto::HKDF (m_CK, sharedSecret, 32, "", m_CK);
После успешного согласования создается первый tagset с 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 от Боба, первый полученный NSR определяет текущий сеанс, если сеанс уже существует, то обрабатывается только содержимое пакета. С каждым NS создается отдельный временный tagset, предназначенный исключительно для получения и расшифровки NSR.
Боб создает новый сеанс при получении первого 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, используемая для вычисления тэгов
SESSTAG_CONSTANT и тэги
Собственно вычисление тэга представляет собой простой HKDF от предыдущего
[sessTag_ck, tag] = HKDF(sessTag_ck, SESSTAG_CONSTANT, «SessionTagKeyGen», 64)
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.
Ulum
orignal Автор
Транзитные тоннели ничего не знают о данном протоколе: это протокол более высокого уровня и его сообщения передаются в зашифрованном виде внутри I2NP сообщений Tunnel и TunnelGateway.