Как стать автором
Обновить

Потроха IPsec, меримся с TLS 1.3, ГОСТ и Go

Время на прочтение 37 мин
Количество просмотров 18K
Приветствую! Очень хочется рассказать про устройство современного стэка IPsec протоколов ESPv3 и IKEv2. IPsec, как мне кажется, незаслуженно обходится многими стороной и детального разбора его работы, его протоколов и возможностей я не видел на русском языке. Кроме того, сделаю странное — сравню IPsec ESPv3 и IKEv2 (оба 2005-го года) с современным, модным, state-of-art TLS 1.3 2018-го года.


Почему я вообще так увлечён темой IPsec — возможно самого сложного стэка протоколов для защиты сетей? Ведь сложность это главный враг надёжности и безопасности! Во-первых, чем больше узнаёшь про его протоколы, особенно IKEv2, тем больше понимаешь как много возможностей в него закладывалось и впечатляешься его продуманностью, в отличии от распространённого подхода разработчиков «костыль костылём погоняет» и решением серьёзных проблем «пока гром не грянет». Во-вторых, IPsec протоколы хорошо продуманы с криптографической точки зрения и, даже старые ESP/IKEv1, фактически являются единственными промышленными массово используемыми протоколами в которых не было сколь либо серьёзных уязвимостей. Тот же SSL (1995-ый год) стал достойно продуманным только с версии 1.3. А нелюбовь к IPsec у многих связана с монструозной сложностью IKEv1, которой больше нет в v2.

В идеале, если бы разработчики операционных систем не тормозили в своё время с реализацией и внедрением IPsec и IPv6 (для доступности компьютеров, чтобы никакого NAT), то никаких SSL/TLS не должно было появится в принципе. Мир оказался не идеален, но сейчас IPsec из коробки (хотя бы SA/SP + ESP часть стэка) есть в любой хоть сколько-то распространённой ОС (лично мне известна только DragonFly BSD, выпилившая IPsec из-за нехватки разработчиков для его поддержки), а IPv6 в некоторых развитых странах сразу доступен преобладающему большинству людей.

IPsec это стэк протоколов, вызовов API, framework для того чтобы приложения и/или администратор могли сообщать какая им нужна безопасность при связи и она прозрачно бы обеспечивалась на сетевом уровне (IP security). Речь может идти как про IP пакеты только одного сокета (например TCP соединения), так и про трафик между целыми сетями.

Под безопасностью трафика подразумевается: обеспечение конфиденциальности данных, их аутентичности/целостности и защиты от атак перепроигрывания (replay attack). Как и практически все протоколы, IPsec имеет транспортную часть, обеспечивающую защиту IP пакетов, и часть рукопожатия, связанную с согласованием ключей, параметров, конфигурации и аутентификацией сторон.

TLS 1.3: обеспечивает только per-socket защиту данных TCP соединения. DTLS может обеспечить защиту датаграмм (DTLS 1.3 стандарта ещё нет), но далеко не каждая библиотека это поддерживает.

Транспортные протоколы


Для IPsec транспорта используются IP протоколы:

  • AH (authentication headers). Про AH я дальше говорить не буду, так как он не обеспечивает конфиденциальности данных и, насколько слышал, его сделали исключительно чтобы как-то «мириться» с законами некоторых стран в 1990-х об ограничениях использования шифрования. Шифрование настолько легковесно относительно всего остального, что не имеет смысла им жертвовать. Но почти везде, где упоминается ESP, также подразумевается и AH.
  • ESP (encapsulating security payloads). ESP со временем немного эволюционировал и сейчас используется его ESPv3 версия, которая часто обратно совместима и не отличается от прошлой версии.

Безопасность IP-трафика обеспечивается только транспортным уровнем. А так как речь может идти о многих миллионах пакетов в секунду, то де-факто ESP реализуется на ядерном уровне операционной системы, в её сетевом стэке, как минимум, чтобы не делать дорогущее переключение контекста между ядром и userspace (как это штатно происходит с TLS, SSH, OpenVPN, и прочими).

Подчёркиваю, что AH и ESP это протоколы IP-уровня, сетевого, а не транспортного. Почему не UDP? Контрольная сумма избыточна и сжигает CPU, а криптография и так обеспечит целостность. Но, если ваш NAT ничего не знает про ESP (а он не знает), то работать это всё за ним не будет. Позже придумали костыли в виде NAT-T (NAT traversal), когда IPsec трафик оборачивается в UDP пакет на 4500 порту и сможет проходить через NAT, но это лишний overhead и необходимость править IPsec стэк в ядре, ведь именно оно уже должно понимать эти особые UDP пакеты и доставать из них ESP для его штатной обработки.

SP, SA, SPI и наше первое IPsec шифрование


Как ядро узнает что надо делать с IP пакетом: шифровать ли его на каком-то ключе, дешифровать ли пришедший ESP или пропускать не трогая? Для этого в ядре есть политики безопасности (Security Policies (SP)). Это правила как в firewall-е. Кроме них, в ядре присутствуют Security Associations (SA): контексты для выполнения криптографических операций (ключи, счётчики, replay окна, и т.д.). В общем случае, ни SP не являются IPsec-специфичным, ни SA — они могут использоваться и для других задач/протоколов (например для OSPF).

Настройка SP/SA может производится как через специальный API (PF_KEYv2), так и руками через какую-нибудь setkey утилиту. Например, если мы хотим сообщить ядру, что все IP пакеты идущие с fc::123 адреса на fc::321 надо обезопашивать через ESP, то это легко сделать вызовом из командной строки:

$ echo "spdadd fc00::123 fc00::321 any -P out ipsec esp/transport//require;" | setkey -c

До этой команды мы видели ping-и:

IP6 fc00::123 > fc00::321: ICMP6, echo request, seq 0, length 16
IP6 fc00::321 > fc00::123: ICMP6, echo reply, seq 0, length 16
IP6 fc00::123 > fc00::321: ICMP6, echo request, seq 1, length 16
IP6 fc00::321 > fc00::123: ICMP6, echo reply, seq 1, length 16

После не увидим, так как ядро пока ещё не знает «чем» надо шифровать. Нужно добавить SA и это тоже можно сделать вручную, задав для простоты AES-GCM-16 алгоритм AEAD шифрования и случайный 160-бит ключ:

echo "add fc00::123 fc00::321 esp 0xdeadbabe -E aes-gcm-16 0x0c09d1d90f804b0b4cef80e255e29c0894db1928 ;" | setkey -c

Если на удалённом хосте мы выполним те же команды (только не забыв указать -P in), то увидим:

IP6 fc00::123 > fc00::321: ESP(spi=0xdeadbabe,seq=0x1), length 52
IP6 fc00::321 > fc00::123: ICMP6, echo reply, seq 0, length 16
IP6 fc00::123 > fc00::321: ESP(spi=0xdeadbabe,seq=0x2), length 52
IP6 fc00::321 > fc00::123: ICMP6, echo reply, seq 1, length 16

Request зашифрован ESP, а reply нет. Потому что ESP работает по умолчанию в «одну сторону» и для двусторонней связи нужно зеркально добавить ещё SP/SA для противоположного направления.

0xdeadbabe в данном примере это Security Parameters Index (SPI) — уникальный идентификатор ESP «туннеля» между двумя IP адресами, по которому ядро может найти соответствующий SA контекст и взять из него ключ дешифрования. А esp/transport//require это требование использовать ESP в транспортном режиме (об этом ниже).

Потроха ESP


Схематично ESP пакет устроен так:

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---
|               Security Parameters Index (SPI)                 | ^
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | A
|                      Sequence Number                          | | u
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | t
~                       IV (variable)                           ~ | h
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | e -----
|                    Payload Data  (variable)                   | | n   ^ E
~                                                               ~ | t   | n
|                                                               | | i   | c
+               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | c   | r
|               |         TFC Padding * (optional, variable)    | | a   | y
+-+-+-+-+-+-+-+-+         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | t   | p
|                         |        Padding (0-255 bytes)        | | e   | t
+-+-+-+-+-+-+-+-+-+-+-+-+-+     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | d   | e
|                               |  Pad Length   | Next Header   | v     v d
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---------
~         Integrity Check Value-ICV   (variable)                ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

  • SPI — 32-бит уникальный идентификатор сессии/туннеля/соединения ESP между IP адресами. Как правило, по {SrcIP, DstIP, SPI} находится SA и криптографический контекст.
  • SeqNum — 32-бит последовательный номер пакета. Увеличивается с каждым отправляемым пакетом. Нужен для понимания не является ли пакет повтором, для защиты от replay attack.
  • payload — полезная нагрузка ESP, варьируемый размер.
  • TFC padding — опциональные данные (Traffic Flow Confidentiality), используемые исключительно для того, чтобы дополнить размер пакета до какого-то заданного размера, скрывая настоящий размер передаваемых данных. Длина TFC нигде в явном виде не задана, поэтому его можно использовать только тогда, когда payload может понять свои границы, свой размер. Например, если payload это IP пакет, содержащий длину. Зачастую поддержка TFC не требует какого-либо изменения в ядре, которое просто отбросит лишнее.
  • Padding — ESP требует чтобы payload был выровнен по 32-бит границе, для удобства работы. Кроме того, некоторые режимы шифрования (например CBC) требуют кратность открытого текста размеру шифроблока. Это поле используется для дополнения размера под эти требования. Может быть нулевой длины.
  • Pad Length — 8-бит длина Padding поля.
  • Next Header — 8-бит идентификатор IP протокола находящегося в payload. Есть особый идентификатор «no next header», обозначающий ESP-пакет пустышку — это можно использовать для скрытия фактов отсутствия полезного трафика, для генерирования трафика константной скорости. Вместе с TFC это позволяет полностью скрыть факты наличия полезных пакетов и их размеров — мало кто может предложить такой уровень безопасности метаданных.
  • ICV — Integrity Check Value, содержащий аутентификационные данные (MAC).

Вся часть пакета от payload до Next Header зашифрована. Всё, кроме MAC, аутентифицировано. Длина ICV, наличие IV (initialization vector, вектора инициализации) зависит от используемых режимов/алгоритмов шифрования и аутентификации.

TLS 1.3: опциональный padding данных до заданного размера появился только в версии 1.3. В остальном, шифрование и аутентификация полностью схожи. TLS 1.3 обязывает использовать только AEAD алгоритмы, что правильно и хорошо. ESP поддерживает AEAD, но есть выбор и более архаичных решений. Полей типа SPI или SeqNum нет, так как TCP гарантирует очерёдность и доставку, кроме того, на практике и не передаётся в явном виде никакого вектора инициализации — TLS record layer пакет поэтому немного короче. DTLS уже содержит SeqNum, а также данные касающиеся фрагментации сообщения.

32-бит номер пакета на практике может оказаться слишком коротким. Это всего лишь 4+ млрд. IP пакетов, что на 10+ Mpps скоростях может пролететь за считанные минуты. Что будет когда счётчик переполнится? Он обнулится. Но, это будет означать, что значение SPI+SeqNum у нас начнёт повторяться и ранее перехваченные ESP пакеты можно будет использовать для replay атаки. Для решения этой проблемы был придуман ESN (Extended Sequence Number). Это 64-бит счётчик, но только «нижние» 32-бита которого передаются в SeqNum поле, а верхние 32-бита хранятся в памяти. Аутентифицируется полное значение ESN — поэтому стороны обязаны согласовать факт применения ESN заранее.

Шифрование ESP


Как же конкретно происходит шифрование/аутентификация ESP пакета, например, при использовании AES-GCM-16? Для работы с ESP, в нём используется 64-бит вектор инициализации, находящийся в начале payload. Кроме того, используется 32-бит соль, являющаяся частью ключевого материала. В примере для setkey я предоставил не 128-бит ключ, а 128+32-бит. Могут быть ситуации, когда ключ используется повторно, а IV заполняется плохим генератором псевдослучайных чисел (PRNG), значения которого могут повториться. Соль призвана обезопасить от этого опаснейшего случая, приводящего потенциально к дешифрованию перехваченных пакетов. Само шифрование/аутентификация ESP в AES-128-GCM-16-ESP режиме происходит так:

AES-GCM(
    key             = 128-bit key,
    plaintext       = 64-bit IV || payload || TFC || pad || padLen || NH,
    nonce           = 32-bit salt || IV,
    associated-data = SPI || {ESN или SeqNum},
) -> encrypted-payload || 128-bit ICV

ESP = SPI || SeqNum || IV || encrypted-payload || ICV

Для российских ГОСТовых алгоритмов (шифры Магма или Кузнечик) входные данные аналогичны. Оба шифра используются в режиме MGM (я бы сказал, улучшенной версией GCM), а также применяется регулярная ESPTREE ротация ключевого материала, используя HMAC-Стрибог-256. Это уменьшает нагрузку на ключ. Главным образом, в контексте IPsec, это нужно не столько для увеличения времени его использования, сколько для уменьшения поверхности атаки по побочным каналам. Например из-за key meshing (схожей технологии постоянной ротации ключа), ГОСТ 28147-89 блочный шифр с 64-бит размером блока оказался неуязвим к SWEET32 атаке.

С точки зрения безопасности, к ESP с AEAD алгоритмами нареканий нет. Но для AEAD алгоритмов IV является просто 64-бит счётчиком, явно передаваемым с каждым пакетом, тратящим место в пакете. SeqNum слишком короток, а ESN полностью не передаётся, хотя полностью подошёл бы в качестве IV. Для не AEAD алгоритмов IV может быть уже необходим и нести непредсказуемое значение, но, ни в коем случае, не счётчик. Это legacy, отъедающее драгоценное место в пакете и вес тут на надёжность не влияет.



Если бы IV для AEAD-ов мог иметь значения от 128-бит, то можно было бы использовать алгоритмы типа XSalsa20/XChaCha20 с 192-бит nonce-ом, 128-бит которого псевдослучайно генерировать при запуске, а оставшиеся 64-бит использовать для счётчика. Это могло бы быть спасением для систем которые потеряли своё состояние счётчиков, но хотят продолжать использовать уже имеющиеся ключи.

TLS 1.3: в качестве nonce используется XOR между счётчиком сообщений и вектором инициализации, выработанным вместе с ключом. Так как, ни счётчик, ни IV не передаются в явном виде, то TLS 1.3 немного компактнее. Если в ESP используются не AEAD алгоритмы, то они могут потребовать генерирования непредсказуемого IV, что может быть ощутимо ресурсоёмко для CPU.

Туннельный и транспортный режимы


Что попадает в payload пакета? Это зависит от того, в каком режиме работает ESP: транспортном или туннельном. Транспортный режим заменяет payload передаваемого IP пакета на ESP с этим payload-ом. То есть, было:

---------------------------------------
| orig IP hdr |[ext hdrs]| TCP | Data |
---------------------------------------

стало:

---------------------------------------------------------
| orig |hop-by-hop,dest*,|   |dest|   |    | ESP   | ESP|
|IP hdr|routing,fragment.|ESP|opt*|TCP|Data|Trailer| ICV|
---------------------------------------------------------
                             |<--- encryption ---->|
                         |<---- authenticity ----->|

В туннельном режиме весь IP пакет полностью оборачивается в ESP и формируется новый IP пакет, как правило, с новыми заголовками и адресами SrcIP/DstIP. Этот режим используется для туннелирования пакетов между сетями.

----------------------------------------------------------
| new* |new ext|   | orig*|orig ext|   |    | ESP   | ESP|
|IP hdr| hdrs* |ESP|IP hdr| hdrs * |TCP|Data|Trailer| ICV|
----------------------------------------------------------
                   |<--------- encryption --------->|
               |<---------- authenticity ---------->|

Например, через setkey я могу указать, что все пакеты между 2001:ac::/64 и 2001:dc::/64 сетями должны проходить в зашифрованном виде через два endpoint-а туннеля с адресами 2001::123, 2001::321.

spdadd 2001:ac::/64 2001:dc::/64 any -P out ipsec esp/tunnel/2001::123-2001::321/require ;
spdadd 2001:dc::/64 2001:ac::/64 any -P in  ipsec esp/tunnel/2001::321-2001::123/require ;

Транспортный режим часто называют host-to-host подключением. Если для туннелирования используется какой-нибудь GRE или IPv*-over-IPv* протокол, уже работающий между двумя endpoint-ами, то смысла, в данном случае, применять режим туннелирования на уровне IPsec уже нет. Однако, транспортный режим не аутентифицирует IP-заголовок. Как правило это не важно и не критично, но если хочется убедиться что никакие расширенные заголовки IPv6 или flow label пакетов не изменены, то тогда стоит применять туннельный режим, пускай даже между двумя хостами, ценой overhead-а.

ISAKMP


Что будет если я перезагружу компьютеры, у них из памяти пропадёт SA со всеми значениями счётчика, а я снова руками загружу прежние SP/SA команды? Во-первых, пакеты, у которых совпадёт IV, можно будет дешифровать, так как это равносильно двойному использованию шифроблокнота. Во-вторых, раз SPI/salt/ESN/SeqNum совпадают, то все ранее перехваченные пакеты будут валидно аутентифицированы и можно сделать их replay. Повторное использование таких setkey-установленных SA катастрофично для безопасности. В-третьих, особенно если не используется ESN (например, в FreeBSD на момент написания, ещё не поддерживается), при долгой работе SA можно не заметить что счётчик «израсходован».

Всё это значит, что нам нужно регулярно менять ESP ключи. А ещё договариваться об алгоритме шифрования, наличии ESN, TFC, транспортном/туннельном режиме, значениях SPI. Де-факто для этого используется ISAKMP протокол (Internet Security Association and Key Management Protocol). Хотя, с лёгкостью можно прикрутить какой-нибудь IM с OTR/PGP/OMEMO аутентифицированным шифрованием и просто отсылать shell скриптом setkey команды на сервер, в которых ключи генерируются читая /dev/urandom. Ядру не имеет значения как что было согласовано. Как в OpenVPN: аутентификация X.509 сертификатами и согласование ключей происходит вообще по TLS, а сам транспортный VPN протокол уже свой.

В «чистом» виде ISAKMP не применяется, так как в нём нет никакой криптографии. Для аутентификации собеседников и выработки ключевого материала применяется сторонний протокол, внутри себя инкапсулирующий ISAKMP. Мне известны:

  • KINKKerberized Internet Negotiation of Keys, где для аутентификации и согласования используется третья доверенная Kerberos KDC сторона. Кроме описания из Wikipedia я больше про KINK ничего не знаю и не видел в живую.
  • IKE(v1) — Internet Key Exchange. Возможно до сих пор самый популярный протокол, хоть и создан аж в 1998-ом году.
  • IKEv2 — вторая версия IKE, 2005-го года, про которую я и буду рассказывать.

IKE протоколы очень расширяемы за счёт большого количества разных типов payload-ов. IKEv1 имеет большое количество опций для конфигурации одного только туннеля для своей работы. Не один десяток RFC описывающий в целом всю связку ISAKMP и IKEv1 с распространёнными payload-ами. Пугающая сложность. Плюс возможность легко напортачить в не fool-proof конфигурации и известный, отчасти заслуженно верный, миф что гарантированно IKEv1 будет работать только если, чуть ли не полностью, скопировать конфигурационный файл.

Благо, появился IKEv2: одна удобная RFC (для большинства features), существенно упрощённый протокол согласования параметров и, соответственно, его конфигурации. Как правило, в нём меньше round-trip-ов на весь процесс рукопожатия и согласования ключей чем в IKEv1. Поэтому рассматриваться будет только он, так как смысла в IKEv1 уже нет (но и гнаться заменять уже запущенные и работающие instance тоже вряд ли стоит, раз работают). IKEv2, в отличии от IKEv1, для шифрования собственных сообщений использует абсолютно аналогичные алгоритмы и подходы как и ESP. Также в нём появилась EAP-аутентификация и возможность каждой стороны аутентифицироваться разными методами (например клиент использует PSK, а сервер X.509 сертификаты).

IKE демон


      +-------------+
      |Демон  ключей|
      +-------------+
       |           |
       |           |
       |           |      Приложения/userspace
=====[PF_KEY]====[PF_INET]====================
       |           |                   Ядро ОС
+-----------+   +-------------+
|База данных|   |TCP/IP,      |
|  SA и SP  |---|включая IPsec|
+-----------+   +-------------+
                     |
                 +-----------+
                 | Сетевой   |
                 | интерфейс |
                 +-----------+

Эта часть стэка IPsec уже работает, как правило, в userspace. Во-первых, это не сильно нагруженные демоны: между собой они могут вообще хоть раз в сутки связываться, а начальный handshake занимает считанные round-trip-ы по UDP. Во-вторых, количество возможностей ISAKMP/IKE такое, что и кода в сотни раз больше чем в полной реализации SA/SP/ESP. Реализаций ISAKMP демонов масса: strongSwan (IKEv1/v2) (а также Openswan, Libreswan), isakmpd (IKEv1), OpenIKED (IKEv2), racoon (IKEv1), racoon2 (IKEv1/v2, KINK) и другие.

Заметка: правильнее бы писать и говорить «деймоны» (daemons), как это я встречал в переводах художественной литературы. Но в технических русскоязычных кругах уже прижились «демоны».

TLS 1.3: в общем случае, весь стэк TLS является библиотечными функциями работающими в каждом отдельном приложении и его же памяти хранящими ключевой материал. Вся криптография при этом выполняется с переключением в userspace, что огромный overhead. Однако, как минимум, в FreeBSD и Linux уже есть и ядерные offload реализации TLS, когда, аналогично IPsec, транспортная часть обрабатывается полностью в ядре, а рукопожатие происходит в userspace.

IKEv2 работает поверх UDP, по умолчанию на 500-ом порту (isakmp сервис). Демоны создают безопасный канал, аутентифицируют друг друга, согласуют/создают/удаляют ESP SA/SP, обновляют ключи, делают heartbeat (Dead Peer Detection (DPD)) и многое другое. Всё общение демонов между собой происходит в виде обмена парой request/response сообщений. На любой запрос должен быть ответ. Раз это UDP, то что делать при пропаже пакета? Учитывать это в своём state, перепосылать запросы после timeout на которые не получены ответы, перепосылать ответы на повторные запросы, игнорировать повторные ответы. Пакеты могут приходить в хаотичном порядке, могут непредсказуемо пропадать — многое учтено в IKEv2 стандарте и описано как себя надо вести при различных race condition-ах.

TLS 1.3: TCP-природа TLS берёт на себя все заботы о порядке и доставке сообщений. Но TCP занимает ощутимые ресурсы в ядре ОС и огромное количество TCP сессий может стать проблемой (в отличии от UDP). Но в DTLS все схожие проблемы аналогично возникнут как и в IKE, плюс добавится геморрой с обработкой фрагментированных сообщений. Смена IP адресов endpoint-ов для UDP не является проблемой. IKE соединения, как правило, очень долгоживущие (IKE state небольшой и хранится только в памяти userspace демона) и поэтому реже требуют делать рукопожатие, тогда как в TLS после потери TCP соединения придётся проделывать (хотя есть и ускоренные методы продолжения сессий, если state не был потерян, например при перезапуске программы). Так как IKE демон один на всю систему (как правило), то если какое-то приложение захочет безопасной связи с тем, с кем уже имеется IKE соединение, то он его или сразу же может использовать или демон, одним round-trip-ом, создаст дополнительный ESP SA для приложения.

Потроха IKE


Первым обменом (request-response) демонов будет IKE_SA_INIT, создающий IKE SA для дальнейшего безопасного общения. Замечу, что ESP SA «хранится» в ядре, а IKE SA в userspace демоне. Затем идёт IKE_AUTH обмен, где происходит аутентификация сторон. В этом же обмене происходит и создание дочернего SA (Child SA), использующийся для ESP SA. В общем случае, достаточно этих двух обменов чтобы аутентифицировать стороны и согласовать ESP SA параметры с ключами и дальше уже гонять шифрованный ESP трафик между компьютерами. Между демонами при этом на долгое время остаётся работающий IKE SA. Дальше, в любое время, они могут произвести CREATE_CHILD_SA обмен, для создания ещё дочерних SA, а также INFORMATIONAL обмен (самые разные цели).

Заголовок всех IKEv2 сообщений имеет следующую структуру:

                     1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       IKE SA Initiator's SPI                  |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       IKE SA Responder's SPI                  |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Next Payload |    Version    | Exchange Type |     Flags     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Message ID                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            Length                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

  • SPIi — 64-бит IKE SA Initiator SPI. Случайно сгенерированный инициатором IKE сессии идентификатор.
  • SPIr — 64-бит IKE SA Responder SPI. Аналогично, но только SPI со стороны ответчика. В самом первом сообщении от инициатора это поле заполнено нулевыми байтами.
  • NP — 8-бит Next payload. Идентификатор полезной нагрузки идущей после заголовка.
  • Version — 8-бит версия IKE протокола.
  • ExchType — 8-бит тип IKE обмена: IKE_SA_INIT, IKE_AUTH, CREATE_CHILD_SA или INFORMATIONAL.
  • Flags — 8-бит разных флагов. Единственным интересным флагом является указание является ли сообщение исходящим от инициатора или же это ответ.
  • MsgID — 32-бит порядковый номер сообщения. Используется для обнаружения дубляжа пакетов, их пропажи, replay-атак. Инкрементируется при каждом новом обмене — пара request/response будет иметь один и тот же MsgID. Повторяемые сообщения от инициатора обязаны нести тот же номер, чтобы ответчик мог понять что это идёт повтор.
  • Len — 32-бит длина всего сообщения (заголовок + полезная нагрузка).

SPIi+SPIr занимают 128-бит. Зачем так много, когда в ESP всего 32-бита отводится? Во-первых, так как они не согласуются, а, псевдослучайно генерируются, то 64-бит для одной стороны будет достаточно чтобы не было коллизий. Во-вторых, ESP также привязан и к IP адресам, а IKE сессия в общем случае нет — стороны спокойно могут менять свои IP адреса (мобильный клиент) и продолжать общение.

TLS 1.3: смена IP адреса приведёт к разрыву соединения. Нужно будет делать rehandshake, даже с iPSK, экономя на ресурсах для асимметричной криптографии, это 1.5 round-trip плюс round-trip-ы для установки TCP соединения. Создание дочерних ESP SA на новых IP адресах, в уже установленном IKE соединении (не имеющим привязки к адресам), займёт всего один round-trip (+round-trip на удаление старых, но это уже произойдёт в фоне нового работающего ESP SA).

После IKE заголовка идёт одна или более полезных нагрузок (payload-ов). Каждый payload имеет заголовок общего формата, а дальше специфичное для его типа содержимое. Содержимое выровнено по 32-бит границе. Общий для всех заголовок:

                     1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Next Payload  |C|  RESERVED   |         Payload Length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

  • Next Payload — 8-бит указывающие какой тип полезной нагрузки пойдёт после текущей. Все payload-ы связаны в цепочку и каждый предыдущий указывает кто пойдёт после него. Нулевое значение означает что это последний payload. Указатель типа первого payload находится в IKE заголовке. Единственное исключение это Encrypted Payload, который обязан быть последним payload-ом и его NP равен не нулю (хоть это и последний payload в контексте IKE сообщения), а типу первого payload-а внутри шифротекста.
  • C — флаг «критичности» payload. Если IKEv2 демон не знает и не умеет обрабатывать payload и он помечен как критичный, то IKE сессию придётся прекратить. Не критичный payload можно проигнорировать. В IKE есть много vendor-specific расширений, не представляющих интереса для стороннего ПО.
  • Len — 16-бит полная длина payload (заголовок + содержимое).

Таким образом, IKE сообщения состоят из IKE заголовка и связанных между собой в цепочку payload-ов. Не критичные и неизвестные payload-ы демоны могут проигнорировать. Содержимое payload-а типа Nonce (после заголовка) это просто случайный набор данных, не фиксированного размера. Но есть и гораздо более сложные структуры. В IKE стандартах приняты короткие обозначения типов payload-ов (например N* для nonce сообщений, где "*" это или «i» (initiator) или «r» (responder)).

SIGMA


С криптографической точки зрения, IKEv1/IKEv2 относятся к STS, ISO/IEC IS 9798-3 и SIGMA (SIGn-and-MAc) классу протоколов аутентифицированного обмена ключами. Это очень хорошо изученные и математически верифицированные (SIGMA) решения. В своей «P2P F2F E2EE IM за один вечер» статье я уже описывал принцип работы и реализацию SIGMA-I протокола. IKEv2 полностью аналогичен. Когда мы обсуждаем безопасность протокола рукопожатия, то что ожидаем?

  • конфиденциальность передаваемых сообщений;
  • аутентичность и целостность передаваемых сообщений — их изменение должно быть обнаружено;
  • защиту от атак перепроигрывания (replay attack) — факт пропажи или повтора сообщений должен быть обнаружен;
  • двустороннюю идентификацию и аутентификацию собеседников;
    наличие perfect forward secrecy свойства (PFS) — компрометация нашего долгоживущего PSK или ключа подписи не должна приводить к возможности чтения предыдущих сообщений (а IKE сообщения могут содержать ключевой материал ESP SA). Запись перехваченного трафика становится бесполезной;
    действительность/валидность сообщений (транспортных и рукопожатия) только в пределах одной IKE сессии. Вставка корректно подписанных/аутентифицированных сообщений из другой сессии (даже с этим же собеседником) не должна быть возможной;
    пассивный наблюдатель не должен видеть ни идентификаторов сторон, ни передаваемых долгоживущих публичных ключей, ни хэшей от них. Некая анонимность от пассивного наблюдателя.

После такого IKE_SA_INIT обмена у демонов есть адреса друг друга, SPIi+SPIr значение IKE сессии, согласованные SA алгоритмы (в случае IKE-а это алгоритмы согласования ключей (DH), шифрования/аутентификации сообщений (ENCR), выработки ключей (PRF)), публичный ключ (DH) противоположной стороны. Этого достаточно чтобы сохранить в памяти state и выполнить согласование ключей (Диффи-Хельман, ГОСТ Р 34.10-VKO, curve25519 и тому подобные), выработав симметричный ключ для шифрования полезной нагрузки последующих IKE сообщений.

TLS 1.3: формат сообщений рукопожатия сильно отличается, есть много legacy, но принципиально ничего выделяющегося. Вместо nonce используется поле random. Вместо payload-ов многочисленные extensions. Вместо сложной SA proposal структуры, используются идентификаторы шифронаборов (ciphersuites), что компактнее и проще. На мой взгляд, гибкость SA proposals избыточна, но в IKEv2 это всё равно не проблема и в конфигурационном файле прописываются похожие на ciphersuite значения. Только с TLS 1.3 версии становится обязательным DH обмен.

IKE ключевой материал


После IKE_SA_INIT вырабатывается SKEYSEED:
SKEYSEED = PRF(Ni[:8] || Nr[:8], DH-KEY)

PRF алгоритм выбирается в IKE SA. Например для ГОСТ IKEv2 это функция HMAC-Стрибог-512. Ключом PRF является 64-бит кусок от каждого из nonce-ов.

Выглядит несерьёзно, ведь nonce-ы передаются в открытую, а значит и ключ для данного PRF известен любому кто перехватил трафик. Но PRF тут используется исключительно для выработки ключа из, уже неизвестного злоумышленнику DH-KEY, результата вычисления DH. Результатом DH функции может быть огромное и неравномерно содержащее энтропию значение, может быть точка на эллиптической кривой — всё это нельзя использовать в качестве короткого высокоэнтропийного симметричного ключа. Поэтому нужно сделать «выжимку» (extract) энтропии из DH-KEY (это как-раз SKEYSEED), а дальше «расширить» (expand) её до нужного количества ключей:

PRF+(SKEYSEED, Ni || Nr || SPIi || SPIr) ->
    SK_d || SK_ai || SK_ar || SK_ei || SK_er || SK_pi || SK_pr

PRF+(K,S) = T1 || T2 || T3 || T4 || ...
T1 = PRF(K,       S || 0x01)
T2 = PRF(K, T1 || S || 0x02)
T3 = PRF(K, T2 || S || 0x03)
T4 = PRF(K, T3 || S || 0x04)

Всё это классическая операция выработки ключей с extract/expand стадиями, аналогичная HKDF функции. Но если HKDF предполагает использование хэш-функций, то данная PRF/PRF+ конструкция может использоваться и просто с симметричными шифрами — в случае с распространённым AES-GCM + AES-XCBC-PRF у нас вообще нигде не будет использоваться хэш-функция, а малое количество используемых примитивов всегда хорошо.

Вырабатываются следующие ключи:

  • SK_d ключ для выработки ключей дочерних ESP SA.
  • SK_a[ir] ключи для аутентификации IKE сообщений. Не вырабатывается/не используется если согласован AEAD алгоритм (AES-GCM, Кузнечик/Магма-MGM, ChaCha20-Poly1305, и т.д.).
  • SK_e[ir] ключи шифрования IKE сообщений.
  • SK_p[ir] ключи используемые при вычислении аутентификаторов AUTH.

TLS 1.3: имеет значительно более сложное ключевое расписание (key scheduling). Выжимка энтропии производится сразу из целых сообщений рукопожатия, а не отдельных полей. Генерируемая расширенная последовательность не просто режется на ряд ключей (+соль для них, когда требуется), но и сопровождается HMAC преобразованиями с метками (label) для каждого контекста применения этих ключей или генерируемых IV. Использование текстовых label/application/context для любого рода вырабатываемых значений это хорошая современная практика, которую проще всегда делать, чем раздумывать нужна ли она. Хэширование вообще всего что попадается — тоже очень хорошая практика, «хуже не будет». Однако, это всё не означает что безопасность IKEv2 хуже или что можно легко придумать хотя бы отдалённо теоретическую ситуацию когда отсутствие label может быть на руку злоумышленнику. В IKEv2 подход минималистичности, а у TLS 1.3 «лучше перебдеть» (ибо сколько косяков или сложностей было понаделано в предыдущих версиях протокола!). IKEv2 всё равно использует проверенные подходы и примитивы, аутентифицирует всё что надо, выжимает/учитывает всю передаваемую энтропию, для каждой стороны и задачи применяет отличающиеся ключи.

IKE_AUTH


Дальше производится IKE_AUTH обмен, аутентифицирующий обе стороны и согласующий ESP SA:

    SK{IDi, [CERT, ...], [CERTREQ], [IDr], AUTH, SAi2, TSi, TSr} -->
<-- SK{IDr, [CERT, ...],                   AUTH, SAr2, TSi, TSr}

  • IKE сообщения содержат зашифрованный (SK) payload, внутри которого находятся все остальные.
  • Инициатор предоставляет свой идентификатор (IDi), аутентификатор (AUTH), SA предложение по ESP (SAi2) и пару initiator/responder, так называемых, селекторов трафика (traffic selectors (TS*)). Он также может опционально послать и ожидаемый идентификатор ответчика, что можно считать неким аналогом SNI из TLS.
  • В ответ он получает идентификатор ответной стороны, согласованное предложение по SA ESP, подтверждённые селекторы трафика и аутентификатор.
  • После чего, обе стороны считают друг друга аутентифицированными, имеют договорённость об ESP SA, трафике который должен относится к этому ESP и могут в ядро уже отдать команду на создание SA и, возможно, SP (есть демоны вообще не занимающиеся SP).

Теперь более подробно об этих payload-ах:

  • ID — идентификатор стороны. Содержит тип идентификации и специфичные для неё данные. Идентифицироваться стороны могут уймой способов: IPv4/IPv6 адресу, FQDN (fully qualified domain name, просто строка, самый популярный способ), RFC822 email адресу, ASN.1 DER Distinguished Name (самый распространённый способ при использовании X.509 сертификатов) или General Name, а также vendor-specific.
  • AUTH — тип аутентификации и специфичный для неё аутентификатор. На практике это либо значение PRF функции (используемой как функция MAC-а), ключом которой является заранее распределённый ключ (pre-shared key (PSK)), либо цифровая подпись. Аутентифицируются следующие данные (TBS*):

    TBSi = Msg0 || Nr || PRF(SK_pi, IDi)
    TBSr = Msg1 || Ni || PRF(SK_pr, IDr)
    

    Инициатор явно аутентифицирует полностью своё первое сообщение (Msg0), nonce противоположной стороны (Nr), и свой идентификатор (IDi), «связывая» вместе контекст использования всего этого вместе. Кроме того, он подтверждает факт выработки одинакового SK_pi (соответственно и всего ключевого материала). Ответчик аутентифицирует всё «зеркально».

    Аутентификация это самая ответственная часть протоколов рукопожатия и ошибки/проблемы в ней сводят к нулю безопасность вообще всего. В доморощенных протоколах рукопожатия недостатки чаще всего с аутентификацией. Крайне важно связать сообщения обмена вместе (связать Ni и Nr), которые уже образуют некую сессию рукопожатия. А также крайне важно связать свой идентификатор с сессией, явно сказав что да, именно я участвую в этой сессии.

    Многие протоколы не делают явного подтверждения корректности согласования симметричных ключей, которые будут использованы для транспорта. Если ключ не был корректно согласован (значения сторон расходятся), то первое же транспортное сообщение не будет аутентифицировано и сессию можно завершить. Во многих случаях это не является проблемой, ведь всё равно будет рано или поздно понятно что что-то не так. Многие не делают явного подтверждения знания ключа чтобы сэкономить round-trip-ы. SIGMA-протоколы делают явное подтверждение, как и IKEv2, чтобы не устанавливать в ядро ESP SA, которые ещё не известно согласован ли корректно. Некоторые протоколы посылают хэш от ключа, что плохо, так как хэш это тоже знание о ключе. SIGMA используют MAC c выработанным симметричным ключом (достаточно подтвердить корректность выработки любого из SK_*). IKEv2 использует PRF, что равносильно. Также замечу, что PRF(ID*) в открытом виде нигде не передаётся и не присутствует, поэтому его не получится использовать для оценки успешности brute-force атак (как хэш от ключа) на выработанный ключ.

    При использовании PSK, аутентификатор вычисляется так:

    AUTHi = PRF(PRF(PSK, "Key Pad for IKEv2"), TBSi)
    AUTHr = PRF(PRF(PSK, "Key Pad for IKEv2"), TBSr)
    

    Зачем нужен PRF(PSK) и почему не просто использовать PSK в качестве ключа? Потому что PSK может быть отличной длины от той что нужно выбранной PRF функции. PSK может быть вообще парольной фразой, не подходящей для прямого использования в функциях шифрования/аутентификации. PRF() это «выжимка» энтропии. Значение PRF(PSK) можно хранить в БД PSK вместо оригинального PSK значения, чтобы не иметь его в открытом виде (но для паролей всё равно лучше бы применять Argon2, Balloon и подобные средства усиления пароля).
  • SA*2 — уже знакомая нам структура SA предложений, но уже для ESP протокола.
  • TS* — селекторы трафика описывающие какой трафик будет идти со стороны инициатора и какой ожидается со стороны ответчика. Селектор трафика представляет из себя структуру содержащую: IPv4/IPv6 выбор, идентификатор IP протокола (опционально), начальный/конечный порт (опционально), начальный/конечный адрес. Например селекторы:

    TSi = ((proto=17, port=100, fc::123 - fc::123),
           (proto=17, port=200, fc::123 - fc::123))
    TSr = ((proto=17, port=300, :: - ffff:..:ffff),
           (proto=17, port=400, :: - ffff:..:ffff))
    

    означают, что инициатор будет посылать UDP пакеты (идентификатор протокола = 17), с 100-го или 200-го порта fc::123 адреса, и отсылать на любой адрес с целевыми UDP портами 300 или 400. Если идентификатор протокола нулевой, значит любой IP пакет попадает под выбор. Порты, соответственно, можно указать только для некоторых IP протоколов, где есть это понятие (как исключение, тип ICMP сообщения тоже можно указать в виде порта). Нулевое значение порта означает, что любые разрешены.

    В данном примере возможны все комбинации между этими четырьмя UDP портами. Если же хочется указать жёстко, что возможна передача, например, только с 100 на 300-ый, то для этого необходимо создать отдельный ESP SA со своими селекторами трафика.

    Ответная сторона присылает свой подтверждённый выбор селекторов, который или совпадает или может иметь более узкие диапазоны выбора.

Все эти payload-ы зашифрованы на выработанном IKE SA ключе после первого обмена сообщениями. Шифрование необходимо для скрытия в открытую передаваемых идентификаторов сторон, их сертификаты и прочую приватную информацию. Однако активный злоумышленник может вклиниться в первый IKE_SA_INIT обмен и увидеть эту информацию, хотя продолжить сессию уже не в состоянии.

TLS 1.3:

  • Клиент может посылать application данные после второго пакета рукопожатия (после ServerHello ||… || Finished, которые, как правило, идут скопом), а сервер после третьего (Client Finished). IKEv2 позволяет обмениваться данными по ESP SA только после полных двух полных round-trip-ов, но в нём отсутствует TCP/SCTP handshake.
  • Однако, ожидаемый идентификатор сервера (IDr со стороны иинициатора), которым можно считать SNI, передаётся в ClientHello в открытом виде. В IKEv2 идентификаторы зашифрованы. Сейчас пытаются разрабатывать и внедрять ESNI, однако это сложная реализация, привлекающая использование DNS, мало распространённая и уже блокируемая DPI.
  • IKEv2 позволяет независимо от того, кто «клиент»/«сервер» (все одноранговы), проводить аутентификацию каждой из сторон как по PSK, так и по ЭЦП, или используя EAP. В TLS 1.3 есть чёткое разделение ролей и X.509 сертификат сервера требуется в большинстве случаев. В ГОСТ TLS 1.3 стандарте поддерживаются только X.509 сертификаты. В RFC TLS 1.3 упоминаются и «голые» публичные ключи. В IKEv2 поддерживаются самые разнообразные форматы публичных ключей и/или сертификатов.
  • В TLS 1.3 есть спорные и очень опасные, с точки зрения безопасности, режимы работы когда application данные посылаются сразу же уже вместе с первым ClientHello ещё совершенно не аутентифицированной стороне (EarlyData), а также возможность отсылать application данные со стороны сервера до приёма Client Finished сообщения. Например ГОСТ TLS 1.3 даже не рассматривает EarlyData как разрешённую возможность использования.
  • TLS сессии штатно можно продолжать (session resumption), аутентифицируясь только по iPSK ключу, выработанному в предыдущей сессии, что позволяет вообще не использовать асимметричную криптографию для продолжения. IKEv2 штатно такой возможности не предоставляет, но есть RFC 5723 описывающий похожую возможность. Но для IKE соединений это не так актуально, так как они долгоживущи, не занимают ресурсов сетевого стэка (нет TCP/SCTP/whatever соединения) не привязаны явно к IP адресам.
  • Алгоритмы шифрования для рукопожатия и транспортного протокола в TLS одинаковы. В IKEv2 IKE SA и любые дочерние ESP SA могут использовать какие угодно различные алгоритмы. С одной стороны это приятно, что самую критическую часть (рукопожатие) можно использовать с high-grade криптографией, а транспортную с более быстрой и простой. Но в современных реалиях это, как правило, будет экономия на копейках, не стоящая раздумий. Можно смело выбрать какой-нибудь ChaCha20-Poly1305, AES-256-GCM-16, Кузнечик-MGM и не беспокоиться о безопасности симметричных алгоритмов. Но не стоит забывать что в IKE SA могут согласовываться самые разношёрстные алгоритмы ESP типа ГОСТ-овых и NIST-овых.

Шифрование SK payload-а AEAD шифрами не хитрое и полностью схоже с ESP, например для AES-GCM (в котором, аналогично AES-GCM-ESP, соль является частью ключевого материала):

AES-GCM(
    key             = SK_*e,
    plaintext       = 64-bit IV || payloads || pad || 8-bit padLen,
    nonce           = 32-bit salt || IV,
    associated-data = IKEHdr || unencrypted payloads
) -> ciphertext

Аутентификация с ЭЦП


А если какая либо сторона захочет аутентифицировать подписью и X.509 сертификатами? Для этого, уже в IKE_SA_INIT может посылаться CERTREQ payload, запрашивающий противоположную сторону предоставить сертификат в виде CERT payload-ов. CERT и CERTREQ содержат идентификатор формата сертификатов и специфичное для формата содержимое. Как правило, сертификаты могут быть представлены в виде ASN.1 DER или в виде SHA1 хэша сертификата + URL откуда его можно скачать. Так как размер UDP ограничен MTU, а размеры сертификатов могут быть гораздо бОльшими, то hash+URL вариант тут спасителен (хотя его можно считать и костылём).

В одном только IKEv2 RFC перечислены, кроме DER закодированных X.509 сертификатов и SHA1+URL: PKCS #7 wrapped X.509 certificate, PGP certificate, DNS signed key, SPKI certificate, X.509 Attribute certificate, «сырые» публичные ключи. Если хочется использовать IPsec аналогично самому частому use-case TLS: аутентифицированный по X.509 сертификату сервер и анонимный клиент, то в IKEv2 нет возможности не производить аутентификацию одной из сторон. Но RFC 5386 описывает Better-Than-Nothing-Security подход, при котором «клиент» может использовать голый публичный ключ и сервер сможет считать его за анонимного.

Кроме того, штатно поддерживается EAP аутентификация, добавляющая round-trip-ы в IKE_AUTH обмен. EAP может как сказать аутентифицирована сторона или нет, так ещё и выработать ключ, который IKEv2 будет учтён и использован. Покажу только схему как может работать EAP:

                 SAi1, KEi, Ni  -->
                                <--  SAr1, KEr, Nr
SK{IDi, [IDr], SAi2, TSi, TSr}  -->
                                <--  SK{IDr, AUTH, EAP}
                       SK{EAP}  -->
                                <--  SK{EAP(success)}
                      SK{AUTH}  -->
                                <--  SK{AUTH, SAr2, TSi, TSr}

TLS 1.3: в нём подпись (или MAC в Finished сообщении) ставится над хэшом от всех увиденных сообщений участвовавших в рукопожатии. Тоже простой и надёжный хороший подход. Разнообразия методов аутентификации нет. А ведь хотелось бы видеть какой-нибудь сильный протокол аутентифицированного согласования ключей по паролю (PAKE), типа российского SESPAKE или OPAQUE.

Ключевой материал ESP SA и его обновление


Итак, мы проверили аутентификацию, подтвердили корректность согласования ключей, согласовали ESP SA и селекторы трафика. Остаётся выработать симметричные ключи для ESP и нужные SA/SP можно установить в ядро:
PRF+(SK_d, Ni || Nr) -> KEYMAT0 || KEYMAT1

Для двусторонней связи нужно по два ESP SA, поэтому IKEv2 вырабатывает сразу два ключевых материала, которые уже передаются напрямую в ядро в соответствующий SA. Длина материала зависит от используемого алгоритма ESP (так например AES-GCM-ESP требует, кроме ключа, ещё и 32-бит соль). В качестве SPI используется SPI значение указанное каждой стороной в ESP SA предложениях.

Что делать если нам надо согласовать несколько ESP SA/SP, например, потому что в единичной паре TSi/TSr не все пожелания можно указать? Для этого используются CREATE_CHILD_SA обмены, идущие в любое время после IKE_AUTH. Создание дочернего SA происходит следующим обменом:

    SK{SA, Ni, [KEi], TSi, TSr} -->
<-- SK{SA, Nr, [KEr], TSi, TSr}

Делается SA предложение, отправляются nonce-ы, селекторы трафика. Всё как и прежде. Ключевой материал вырабатывается уже с использованием этих новых nonce-ов. Опционально можно задействовать key exchange payload-ы, добавляющие энтропию и вынуждающие стороны использовать ещё больше асимметричной криптографии. Понадобится это может для постоянного соблюдения PFS свойства (в OTR протоколе вообще с каждым сообщением отправляются эфемерные DH ключи). Ключевой материал при этом будет выработан так:

PRF+(SK_d, DH-KEY || Ni || Nr) -> KEYMAT0 || KEYMAT1

А если мы хотим обновить ключ IKE SA соединения? Делаем следующий CREATE_CHILD_SA обмен:

    SK{SA, Ni, KEi} -->
<-- SK{SA, Nr, KEr}

где SA будет содержать уже IKE SA предложения и будет выработан новый SKEYSEED:

PRF(SK_d_old, DH-KEY || Ni || Nr) -> SKEYSEED

Обновление ключа ESP SA производится либо через создание нового ESP SA (с другим SPI) и удалением старого, либо посылая особое оповещение (о нём ниже). Переключение трафика на использование новых ESP SA произойдёт прозрачно и без потерь. Какое-то непродолжительное время стороны будут иметь по два активных ESP SA, что позволит обрабатывать трафик ещё находящийся в пути на каналах связи.

Удаление ESP SA делается отсылкой DELETE payload-а в последующих INFORMATIONAL обменах, в котором перечисляются SPI требующие удаления. Так как все ESP SA существуют попарно (для двусторонней связи), то каждая сторона посылает значения SPI только ESP SA отвечающих за исходящий трафик. В ответ получаются SPI значения ESP SA для входящего трафика.

    SK{D(SPIi)} -->
<-- SK{D(SPIr)}

Удаление IKE SA делается также посредством DELETE, но уже с IKE SPI и приёмом пустого аутентифицированного ответа:

    SK{D} -->
<-- SK{}

TLS 1.3: есть механизм ротации ключей через KeyUpdate сообщения, но возможности внесения дополнительной энтропии или выполнения DH при этом нет. TLS явно не предназначен для очень долгоживущих соединений. В лучшем случае можно будет только разорвать сессию и продолжить/создать новую с iPSK-ECDHE рукопожатием.

IKEv1 имеет как отдельную процедуру обновления ключа IKE, так и отдельную для повторной аутентификации. В IKEv2 повторной аутентификации нет. Для этого просто с нуля создаётся новый IKE SA, старый удаляется через DELETE.

TLS 1.3: имеет post-handshake возможность аутентификации клиента, когда в любой момент после рукопожатия (Finished сообщений от обеих сторон) сервер может прислать запрос на аутентификацию клиента по X.509 сертификату. Например клиент, бродя по сайту, перешёл на страницу личного кабинета. В IKEv2 такой возможности нет — аутентификация проводится только в момент рукопожатия.

NOTIFY


А как же согласовываются туннельный/транспортный режимы, TFC? Для этого в запрос добавляются payload-ы «оповещений» NOTIFY (N). Видов оповещений в одном только IKEv2 RFC не один десяток. Оповещения используются для сигнализации об ошибках, о проблемах согласования SA предложений, селекторов трафика и т.д…

Чтобы сигнализировать о желании использовать транспортный режим в согласуемом ESP SA, добавляется N(USE_TRANSPORT_MODE) оповещение как инициатором, так и ответчиком, для подтверждения согласования режима. N(ESP_TFC_PADDING_NOT_SUPPORTED) оповещение сигнализирует что TFC не поддерживается. А N(HTTP_CERT_LOOKUP_SUPPORTED) сигнализирует что поддерживается скачивание сертификата по URL.

Возможность обновления ключа ESP SA, без создания новых ESP SA, производится аналогично процедуре создания дочернего ESP SA, но инициатором добавляется N(REKEY_SA) оповещение, содержащее SPI текущего ESP SA:
    SK{N(REKEY_SA), SA, Ni, [KEi], TSi, TSr} -->
<-- SK{             SA, Nr, [KEr], TSi, TSr}



DPD


INFORMATIONAL обмен с пустым SK используется для dead peer detection (DPD), в качестве heartbeat-а между демонами. Если IKE демон долго недоступен, то, скорее всего, он и потерял своё состояние и поэтому за ESP SA на противоположной стороне никто не следит или они уже и не активны. Поэтому, когда ясно что удалённая сторона недоступна, имеет смысл удалить все относящиеся к ней ESP/IKE SA. Пустой SK означает что в нём нет payload-а, но у него есть аутентифицированные данные (как минимум, IKE заголовки с счётчиками), поэтому аутентификация такого пакета это доверенный признак жизни.

    SK{} -->
<-- SK{}

А что будет если одна сторона быстро перезапустилась, потеряв состояние, и начала устанавливать IKE соединение с нуля? Противоположная сторона могла даже и не заметить недоступность другой и она будет считать, что та решила произвести или процедуру повторной аутентификации или создания новых дочерних SA в другом IKE соединении. Ничего катастрофичного то не будет, но старые ESP SA ещё приличное время могут жить. Инициатор может поместить N(INITIAL_CONTACT) оповещение в свой IKE_AUTH обмен, сигнализируя что для него это единственное известное IKE соединение с этой стороной. Увидев такое аутентифицированное оповещение, можно с чистой совестью удалить все старые IKE/ESP SA.

DoS и плохой KE


Уже в самом начале IKE_SA_INIT посылается KEi payload с эфемерным публичным ключом DH. Но ведь инициатор ещё не обменивался IKE SA и откуда ему знать какой алгоритм поддерживает принимающая сторона? Он может только сделать предположение или запомнить в долговременной памяти что использовалось прежде при связи с этим адресом. Если ответчик не поддерживает алгоритм, то пошлёт N(INVALID_KEY_PAYLOAD) оповещение, в котором будет указан идентификатор предпочитаемого алгоритма DH. Инициатор вынужден будет повторить свой запрос, но с новым KEi.

TLS 1.3: может послать сразу несколько эфемерных публичных ключей на разных алгоритмах, авось какой-нибудь да подойдёт. Но это ресурсы и трафик. Он может вообще не посылать публичный ключ и сервер ему ответит HelloRetryRequest со своими предпочтениями — плюсом будет то, что дорогая асимметричная криптография вообще не задействуется, пока точно не станут известны предпочитаемые алгоритмы сервера, но ценой лишнего round-trip. Если клиент изначально предоставил не подходящий алгоритм публичного ключа, то, как и в IKEv2, получит HelloRetryRequest с алгоритмами на выбор.

А что будет если посылать один и тот же начальный пакет от инициатора? Возможно каждый раз генерировать там новый SPIi. Ответчик будет, как минимум, честно производить DH вычисления и отвечать IKE_AUTH. DH это очень ресурсоёмкая операция, сжигающая CPU и источник энтропии — поэтому ответчика можно будет вывести из строя.

В IKEv2 (но не IKEv1) имеется защита от этого, в виде ответа N(COOKIE) оповещением, содержащим строку-cookie, после которого инициатор должен повторить свой запрос, но добавив к нему этот N(COOKIE) payload:

           SAi1, KEi, Ni -->
                         <-- N(COOKIE)
SAi1, KEi, Ni, N(COOKIE) -->
                         <-- SAr1, KEr, Nr, [CERTREQ]

Запрос должен иметь идентичные SPI/Ni что и первый. Его достаточно просто дополнить payload-ом. Ответчик может сохранить state о связи запроса и отосланной ему cookie и только после их совпадения, после выполнения этой работы по добавлению cookie к запросу инициатором, ответчик может штатно продолжить IKE_AUTH обмен.

Но можно состояние сохранить прямо внутри cookie, делая её «самоаутентифицирующейся». Она может нести факт того, что ответчик видел запрос от инициатора (видел его Ni и SPIi, как минимум):
Cookie = MAC(some-secret, Ni || SPIi || timestamp)

Таким образом, любителю DoS придётся хранить state и перерабатывать свои повторные сообщения, что делает атаку значительно дороже. Cookie-защиту имеет смысл включать только когда есть подозрения на DoS атаку, чтобы не заставлять всех выполнять лишний round-trip.

TLS 1.3: имеет аналогичную опциональную защиту. Сервер может ответить HelloRetryRequest сообщением, содержащим Cookie расширение, которое клиент должен вставить в свой повторный ClientHello2.

CP


IKEv2 позволяет согласовать конфигурацию IP-сетей/адресов. Configuration payload (CP) позволяет сделать запрос на получение конфигурации (CFG_REQUEST/CFG_REPLY типы пакетов) и установку конфигурации противоположной стороне (CFG_SET/CFG_ACK типы). Запрос конфигурации содержит атрибуты которые сторона хочет узнать/установить. Атрибутами могут быть: «внутренний» адрес, адрес DNS, DHCP, знание о подсети, либо другие типы, описанные в смежных RFC. Например инициатор в IKE_AUTH обмене может сделать запрос на выдачу ему внутрисетевого адреса (подключаясь к сети компании) и DNS сервера:

    SK{IDi, [IDr], AUTH, CP(CFG_REQUEST), SAi2, TSi, TSr} -->
<-- SK{IDr,        AUTH, CP(CFG_REPLY),   SAr2, TSi, TSr}

CP(CFG_REQUEST) =
  INTERNAL_IP6_ADDRESS()
  INTERNAL_IP6_DNS()
TSi = (proto=0, port=0-65535, :: - ffff:...:ffff)
TSr = (proto=0, port=0-65535, :: - ffff:...:ffff)

CP(CFG_REPLY) =
  INTERNAL_IP6_ADDRESS(2001:db8::5/64)
  INTERNAL_IP6_DNS(2001:db8::1)
  INTERNAL_IP6_SUBNET(2001:db8:abcd::/64)
TSi = (proto=0, port=0-65535, 2001:db8::5 - 2001:db8::5)
TSr = (proto=0, port=0-65535, 2001:db8::0 - 2001:db8::ffff:ffff:ffff:ffff)

  • Инициатору выделяется 2001:db8::5 адрес.
  • По установленному ESP SA туннелю он может ходить в 2001:db8::/64 сеть.
  • При этом в ней есть 2001:db8::1 DNS сервер.
  • А также известно что на стороне ответчика есть и 2001:db8:abcd::/64 сеть, и если в неё хочется попасть, то надо создать отдельный ESP SA, или возможно настроить маршрутизацию через хост в 2001:db8:: сети.

Причём тут Go?


Для проверки современных отечественных реализаций IPsec стэка с ГОСТ алгоритмами решили написать совершенно независимую (от Linux, FreeBSD, strongSwan и прочих стэков) реализацию. А для скорости и простоты разработки на Go языке, с уже существующей реализацией ГОСТ алгоритмов GoGOST библиотекой. Прежде уже был опыт интеграции ГОСТ в TLS 1.3 реализацию crypto/tls и crypto/x509 Go библиотек.

Проект gostipsec это свободное ПО, состоящее из двух демонов: ESPER (ESPv3) и IKER (IKEv2):

          ┌──────┐          ┌────┐          ┌─────┐          ┌────┐
          │remote│          │iker│          │esper│          │ipfw│
          └──┬───┘          └─┬──┘          └──┬──┘          └─┬──┘
             │                │                │               │
╔══════╤═════╪════════════════╪════════════╗   │               │
║ UDP  │     │                │            ║   │               │
╟──────┘     │    IKEv2...    │            ║   │               │
║            │ <───────────────            ║   │               │
║            │                │            ║   │               │
║            │    IKEv2...    │            ║   │               │
║            │ ───────────────>            ║   │               │
╚════════════╪════════════════╪════════════╝   │               │
             │                │                │               │
             │                │                │               │
             │    ╔═══════════╪══╤═════════════╪════════════╗  │
             │    ║ UNIX-SOCKET  │             │            ║  │
             │    ╟─────────────setkey-commands│            ║  │
             │    ║           │ ───────────────>            ║  │
             │    ╚═══════════╪════════════════╪════════════╝  │
             │                │                │               │
             │                │                │               │
             │                │   ╔════════════╪═══╤═══════════╪════════════╗
             │                │   ║ DIVERT-SOCKET  │           │            ║
             │                │   ╟──────────────encrypted ESP │            ║
             │                │   ║            │ <──────────────            ║
             │                │   ║            │               │            ║
             │                │   ║            │ decrypted ESP │            ║
             │                │   ║            │ ──────────────>            ║
             │                │   ║            │               │            ║
             │                │   ║            │ unencrypted IP│            ║
             │                │   ║            │ <──────────────            ║
             │                │   ║            │               │            ║
             │                │   ║            │  encrypted IP │            ║
             │                │   ║            │ ──────────────>            ║
             │                │   ╚════════════╪═══════════════╪════════════╝
             │                │                │               │

На данный момент, ESPER работает только с DIVERT сокетами (ничего настолько же простого под Linux я не нашёл), поэтому поддерживается только на FreeBSD (возможно OpenBSD, не проверял) ОС. ESPER, как и IKER, в качестве интерфейса между ESP<->IKE связкой, использует не PF_KEYv2, который бы потребовал C-bindings, а текстовый setkey-like интерфейс, уже упоминаемый в начале статьи. IKER поэтому можно использовать и для согласования ключей ядерной ESP реализации, вызывая настоящую setkey команду. Выглядят эти команды для ESPER так:

add fc00::ac fc00::dc esp 0x12345678 -u 123 -E aes-gcm-16 0xd3537e657fde5599a2804fbb52d1aaed94b65d3e ;
add fc00::dc fc00::ac esp 0x12345679 -u 234 -E aes-gcm-16 0x9a2dae68e475eacb39d41f23c3cbef890e9f6276 tfc:1320 ;

spdadd fc00::ac/128 fc00::dc/128 all -P in ipsec esp/transport//unique:123 ;
spdadd fc00::dc/128 fc00::ac/128 all -P out ipsec esp/transport//unique:234 ;

ESPER поддерживает: AES-128/256-GCM-16, Магма/Кузнечик-MGM, ESN, TFC, транспортный/туннельный режимы, IPv6/IPv4 (поддержка последнего, куда более сложного, не так тщательно тестировалась, да и кому нужен IPv4 для новых проектов?), защиту от атак перепроигрывания. IKER же позволяет согласовать: AES-128/256-GCM-16 + AES-XCBC + curve25519, Магма/Кузнечик-MGM + HMAC-Стрибог-512 + ГОСТ Р 34.10-2012-VKO-256/512, ESN/TFC/транспортный/туннельный-режимы, аутентифицировать по PSK и X.509 цифровым подписям (ECDSA, ГОСТ Р 34.10-2012). Конфигурируется единственным Hjson файлом:

{
    IKEAlgos: [
        gost128-vko512
        aes256gcm16-aesxcbc-curve25519
        aes128gcm16-aesxcbc-curve25519
    ]
    ESPAlgos: [
        gost128-esn
        gost64-esn
        aes256gcm16-esn
        aes256gcm16-noesn
        aes128gcm16-esn
        aes128gcm16-noesn
    ]
    SigHashes: [
        streebog512
        streebog256
        sha512
        sha256
    ]
    DPDTimeout: 300
    Peers: [
        {
            Autostart: true
            OurIP: fc00::dc
            TheirIP: fc00::ac
            OurId: our.company.net
            TheirId: CN=example.com
            OurTSS: [
                fc00::dc/128[tcp]
                fc00::dc/128[udp/53]
            ]
            TheirTSS: [
                fc00::ac/128
            ]
            Mode: transport
            # Won't be used, because of X.509 signature authentication
            PSK: DEADBABE
            TheirCertHash: a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447
            OurCert: our.company.net.cer.pem
            OurPrvKey: our.company.net.key.pem
            TFC: 1200
        }
    ]
}

В этом примере мы задали единственного нам известного участника:

  • К которому демон будет автоматически подключаться
  • Каждые пять минут делать dead peer detection
  • Согласовать в приоритете ГОСТ алгоритмы с ESN, с fallback-ом до зарубежных, а также TFC до 1200 байт.
  • Согласовывать селектор трафика TCP и DNS запросов от fc00::dc адреса до fc00::ac в транспортном режиме.
  • Аутентифицировать по X.509 подписям, где от противоположной стороны ожидается сертификат с CN=example.com subject-ом и жёстко заданным SHA256 хэшом от SubjectPublicKeyInfo его сертификата. OurId при этом будет вычислен на основе предоставленного OurCert сертификата.
  • Если бы OurCert/OurPrvKey не были указаны, то тогда с нашей стороны использовалась PSK аутентификация с FQDN указанным в OurId.

IKER пока не поддерживает полного набора всех фич IKEv2 (CREATE_CHILD_SA, rekeying), не блюдёт пропажу пакетов и плевать хотел на DON'T PANIC принцип. Поэтому пока не может рассматриваться как кандидат для «промышленного» использования.

Tarball gostipsec уже содержит все зависимости, собранную .info документацию и цели для redo сборочной системы, хотя сборка исполняемых файлов легко делается и штатным go build вызовом.

Hjson?


Holywar-ная тема, но всё же выскажу своё фи:

  • INI не позволяет задавать такие размашистые структуры, да и нет никакого стандарта на .ini файлы.
  • capabilities database формат, termcap-like, имеющий популярность в BSD системах, имеет уже разные типы данных (булевы, строки, числа), при этом реализация парсера простая даже на C. Изначально IKER использовал этот формат.
  • XML — не для людей.
  • JSON — не прост для парсинга, но из коробки всякие Python и Go имеют библиотеки для работы с ним. Часто используется для конфигов поэтому, ибо и относительно дружелюбен к человеку и позволяет сложные структуры описывать. Но сколько ненависти из-за невозможности вставить комментарий и необходимость обращать внимание на запятые в конце списков!
  • YAML — объём спецификации, количество возможностей, разнообразие вариантов записи одного и того же выше чем у кого бы то ни было среди всех названных форматов. С одной стороны, он удобнее многих для человека. С другой, я видел не мало YAML файлов, которые просто не в состоянии, будучи человеком, интерпретировать. Он сложен для людей и лишь небольшое его подмножество будет понятно и удобно для чтения. Размер библиотек для его парсинга огромен и зачастую больше чем размер основной программы. Гарантий совместимости между его реализациями де-факто нет никаких. А ещё оригинальный YAML имеет проблемы с Норвегией (и не только с ней) из-за типизации (хотя и решённые в StrictYAML диалекте).
  • TOML — по началу мне этот формат очень понравился: достаточно функциональный, достаточно минималистичный, достаточно простой для человека, достаточно простой для обработки компьютером. Но, видя как:

    [[foo.bar]]
    baz = 123
    
    [[foo.bar]]
    abc = 123
    


    превращается в:

    {
      "foo": {
        "bar": [
          {"baz": 123 },
          {"abc": 123 }
        ]
      }
    }
    

    и когда есть возможность «дополнять» словари/массивы в любом месте конфигурационного файла, симпатия у меня прошла. А увидев, после перевода на TOML, во что превращаются конфиги NNCP с многочисленными словарями и массивами, я зарёкся его использовать в своих проектах. Нет, это не плохой формат, но с сложными структурами он уже не так легко читается людьми.
  • Hjson — лично меня JSON не удовлетворяет только комментариями и запятыми (если отталкиваться от того, что в стандартной библиотеке языка программирования он поставляется из коробки), что и решает Hjson. github.com/hjson/hjson-go реализация преобразует Hjson в JSON, который уже парсится обычной родной библиотекой. Он просто убирает синтаксический сахар. Библиотека вполне компактна и проста, полностью удовлетворяя нужды для человеческих конфигурационных файлов. Плюс приятно, что любой JSON является валидным Hjson.

Заключение


В общем случае, если реализовывать подмножество возможностей аналогичных TLS 1.3 (аутентификация только по PSK и X.509 сертификатам, никакого серьёзного rekeying), то ESPv3 с IKEv2 и IPv6 (с ним куда проще работать!) будет, с точки зрения программиста, незначительно сложнее в реализации. RFC даже не обязывает поддерживать CREATE_CHILD_SA обмены. Безопасность будет на превосходном уровне, без спорных и опасных возможных режимов работы TLS 1.3. Производительность IPsec решения будет, как правило, выше, за счёт транспорта на ядерном уровне и долгоживущих IKE сессий.

Видно что в IPsec всё заточено под защиту колоссального количества трафика между целыми сетями, но BTNS (better than nothing security) рабочая группа IETF написала несколько RFC демонстрирующих что IPsec без проблем можно использовать и для per-socket соединений, где одна из сторон (клиент) анонимна, тем самым полностью ставя под сомнение целесообразность использования TLS. Connection latching, в данном случае, позволил бы любому сетевому приложению, сделав тривиальный системный вызов типа setsockopt, указать что ему нужен ESP до FQDN=банк.com адреса, представившись X.509 сертификатом (или оставаясь анонимным), а дальше прозрачно, быстро и безопасно работая с этим банк.com, без костылей в виде userspace транспортных per-application библиотек.

Сергей Матвеев, шифропанк, Python/Go/C-разработчик, главный специалист ФГУП НТЦ Атлас.
Теги:
Хабы:
+24
Комментарии 12
Комментарии Комментарии 12

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн