Привет, %username%!
Не всё в этом мире крутится вокруг браузеров и бывают ситуации, когда TLS избыточен или вообще неприменим. Далеко не всегда есть необходимость в сертификатах, очень часто хватает обычных публичных ключей, взять тот же SSH.
А еще есть IoT, где впихивать TLS целиком это вообще задача не для слабонервных. И бэкенд, который, я почти уверен, у всех после балансера общается друг с другом по обычному HTTP. И P2P и еще и еще и еще…
Не так давно в сети появилась спецификация Noise Protocol Framework. Это по сути конструктор протоколов безопасной передачи данных, который простым языком описывает стадию хэндшейка и то, что происходит после неё. Автор — Trevor Perrin, ведущий разработчик мессенджера Signal, а сам Noise используется в WhatsApp. Так что, был отличный повод рассмотреть этот протокольный фреймворк поближе.
Он так понравился нам своей простотой и лаконичностью, что мы решили на его основе запилить аж целый новый протокол сетевого уровня, который не уступает TLS в безопасности, а в чём-то даже превосходит. Мы презентовали его на DEF CON 25, где он был очень тепло принят. Пора поговорить о нём и у нас.
Сначала немного о непосредственно ядре NoiseSocket, а именно
Noise Protocol Framework
По сути, любой из описанных Noise Framework протоколов представляет собой последовательность из передаваемых публичных ключей и производимых над ними операций Diffie-Hellman.
Главная идея Noise Protocol Framework в том, что абсолютно все действия во время хэндшейка влияют на состояние протокола, а следовательно и на итоговые сессионные ключи. Все DH, все дополнительные данные, которые передаются или учитываются во время рукопожатия, подмешиваются к общему состоянию с помощью хеширования и формируют в итоге общие симметричные ключи.
Всё это происходит внутри нехитрой системы состояний, которая состоит из трёх частей.
Есть, кстати видео, на английском, где David Wong довольно доступно рассказывает, как там чего работает.
HandshakeState отвечает за процессинг токенов и сообщений.
SymmetricState формирует симметричные ключи из результатов DH и обновляет их с каждым новым DH. Таким образом сразу же после самого первого DH данные, которые идут после него (статические ключи, полезная нагрузка), уже шифруются каким-то симметричным ключом.
Еще SymmetricStatе хэширует в себя дополнительные данные, такие как сами ключи, опциональный пролог, имена протоколов и т.д. Всё это позволяет протоколу быть целостным и защищенным от вмешательства извне на всех этапах передачи данных.
CipherState это просто инициализированный каким то ключом симметричный AEAD шифр + nonce (счетчик), который инкрементится с каждым вызовом функции шифрования.
Протоколы в Noise описаны специальным языком, который состоит из паттернов, сообщений и токенов
Рассмотрим для примера один из протоколов, Noise_XX, который позволяет установить защищенное соединение, обменявшись в процессе статическими ключами сервера и клиента:
Noise_XX(s, rs):
-> e
<- e, ee, s, es
-> s, se
Noise_XX — это паттерн. Он описывает последовательность сообщений и их содержимое.
(s, rs) обозначает, что клиент и сервер инициализируются своими статическими (s) ключевыми парами. Это те, что генерируются единожды. r означает remote.
Как мы видим тут есть три строчки со стрелками. Одна строка — одно сообщение. Стрелка означает кто кому шлёт. Если направо, то клиент серверу, иначе наоборот.
Каждая строка состоит из токенов. Это одно или двухбуквенные выражения, разделенные запятыми. Однобуквенные токены бывают лишь e и s и означают эфемерный и статический публичные ключи соответственно. Эфемерный генерируется один раз на соединение, статический многоразовый.
Вообще в Noise все протоколы начинаются с передачи эфемерного ключа. Таким образом достигается Perfect Forward Secrecy. Примерно то же самое придумали в TLS 1.3 когда отменили все не-эфемерные ciphersuites.
Двухбуквенные токены означают Diffie-Hellman между одним из ключей клиента и сервера. Они бывают, как несложно догадаться, четырех видов:
ee, es, se, ss. В зависимости от того, между какими ключами делается DH, он выполняет различные функции. ee, к примеру, нужен чтобы рендомизировать итоговый ключ для транспортной сессии, а DH с участием статических ключей отвечают за взаимную аутентификацию.
Как видно, в паттерне XX статический ключ клиента передаётся серверу, ровно, как и наоборот. Поэтому тут и используется три сообщения. Есть паттерны, которые предполагают наличие у клиента статического ключа сервера (например, когда он первый раз сделал XX) и сокращение количества сообщений до двух. К тому же, появляется возможность передавать зашифрованные данные сразу в первом сообщении, что зовётся 0-RTT и сокращает время ответа от сервера.
К каждому handshake сообщению можно дописывать полезную нагрузку. Это могут быть настройки протокола верхнего уровня, те же сертификаты, просто цифровые подписи, в общем, всё что угодно в пределах 64к байт. Все Noise пакеты ограничены этим размером. Так упрощается парсинг, длина всегда помещается в 2 байта, проще работать с памятью.
В результате хэндшейка у нас на руках остаётся по сути лишь два симметричных ключа, которые являются результатом всех DH, произошедших ранее. Один для отправки сообщений, второй для их приёма. Всё, после этого мы можем отправлять шифрованные пакеты, инкрементируя nonce каждый раз после отправки.
Помимо паттерна Noise Protocol характеризуется алгоритмами, которые использует в каждом конкретном случае. В спецификации указаны алгоритмы для DH, AEAD и хеш. Большe Noise ничего не нужно.
DH: Curve25519, Curve448,
AEAD: AES-GCM, ChachaPoly1305,
Hash: Blake2, SHA2
Все примитивы очень быстрые, никаких RSA и прочего тормозного старья. Хотя, конечно, если хочется, можно сделать самому, никто не запрещает.
NoiseSocket
Увидели мы всю эту красоту и поняли, что это идеальный кандидат на роль транспортного протокола следующего поколения. Ведь тут сразу из коробки есть все нужные security фичи, производительность, возможность прикручивать свои механизмы аутентификации. А прогнозируемый размер кода должен позволить создавать нормальные защищенные соединения из любых, даже самых мелких девайсов. И начали думать.
Первый PoC я написал на Go где-то в районе нового 2017 года. Почти ничего не добавлял к оригинальному Noise, только длину пакетов. Показал это ребятам, написал в Noise Mailing List и к концу июня мы наконец пришли к общему знаменателю, который можно было реализовывать на большем количестве платформ.
Итак, что же мы в итоге добавили к Noise? После долгих разговоров и десятков вариантов остались по сути только три вещи:
- Negotiation data
- Padding
- Processing rules
Negotiation data
Это просто набор байт, в который вы можете положить всё что угодно. Он нужен для того, чтобы согласовать алгоритмы и паттерны между клиентом и сервером. В нашей версии он выглядит следующим образом:
Всего 6 байт, но этого достаточно чтобы сервер понял как обрабатывать любые Noise сообщения, которые к нему попадают.
Padding
Очень рад, что он попал в итоговую спеку, иначе всем бы пришлось придумывать его самим. Это выравнивание пакетов, которое позволяет скрыть их истинный размер и предотвратить угадывание содержимого даже без расшифровки. Реализуется как дополнительные 2 байта вначале зашифрованных данных, которые указывают на настоящий размер пакета. Всё, что после — мусор, который нужно выкинуть.
Processing rules
Это несколько несложных правил того, как сервер отвечает клиенту, если он принимает его сообщение или наоборот, не понимает его и хочет переключиться на другой протокол.
Why?
В Virgil есть свой PKI и нам очень не хватало возможности устанавливать защищенные соединения сразу с использованием публичных ключей, а не с помощью сертификатов и потом сверху всё еще раз валидировать. А теперь мы сделали NGINX модуль и можем весь бекенд обслуживать через NoiseSocket, добавив к нему цифровые подписи статических ключей.
Вы наверное думаете что нужно всё менять для того, чтобы перейти на NoiseSocket? А вот нет.
Если вы пишете на Go и у вас уже есть HTTP сервисы, то нужно лишь подменить метод DialTLS для клиентов и Listen для серверов, всё остальное будет думать, что работает по TLS. Это всё благодаря Go библиотеке, которую мы тоже реализовали.
Конечно, предстоит еще много работы по коду и спецификации, но, черт возьми, у нас есть альтернатива TLS!
Теперь не нужно выдумывать что-то своё когда вы хотите построить защищенный линк между двумя узлами, имея на руках лишь публичные ключи. Tor, i2p, bitcoin, где нода часто идентифицируется именно публичным ключом, могут использовать NoiseSocket сразу безо всяких добавок.
SSH, VPN, всевозможные туннели могут добавить цифровые подписи статических ключей и получить полноценный защищенный линк с минимальным оверхедом без необходимости тащить к себе openssl, можно обойтись Libsodium или вообще Nacl.
Приблизительный размер скомпилированных криптопримитивов конечно зависит от архитектуры, но вселяет надежду, что мы влезем в 30, а то и 20 килобайт с минимальной реализацией NoiseSocket. В девайсах, где приватный ключ зашит в железе, это решение просто не будет иметь особых альтернатив.
Заключение
У меня были большие надежды на TLS 1.3, так как там и handshake roundtrips уменьшены с 8-9 до трех, как в Noise, и добавили 25519. Но
Во-первых, решили не добавлять возможность работать без сертификатов, просто по ключам, хотя такое предложение было.
Во-вторых, сертификаты ed25519 непонятно когда появятся, а в Noise я могу использовать подписи 25519 уже сегодня.
К тому же, не так давно один из паттернов Noise, IK (который 0-RTT) получил формальное доказательство корректности от авторов WireGuard VPN, что лишь усилило нашу уверенность в правильности выбора.
Я предлагаю ознакомиться со спецификацией NoiseSocket и уверен, в каких-то проектах он подойдет вам больше, чем TLS. А мы тем временем ждем ваших комментариев.
Ссылки
Спецификация Noise Socket
Github
Спецификация Noise Protocol Framework