Протокол и криптография
Categories:
Криптографические примитивы
В WireGuard используются следующие протоколы и примитивы:
- ChaCha20 для симметричного шифрования, аутентифицированного с помощью Poly1305, с использованием AEAD-конструкции из RFC7539
- Curve25519 для ECDH (обмена ключами по эллиптическим кривым)
- BLAKE2s для хеширования и ключевого хеширования, описанный в RFC7693
- SipHash24 для ключей хеш-таблиц
- HKDF для производной ключей, как описано в RFC5869
Эти примитивы выбраны за их высокую производительность и криптографическую стойкость. Например, ChaCha20-Poly1305 обеспечивает скорость шифрования, сравнимую с AES, но без аппаратного ускорения, что делает его идеальным для встраиваемых устройств. Curve25519 считается одним из самых безопасных и быстрых алгоритмов для обмена ключами.
Протокол без установки соединения
Любой безопасный протокол требует сохранения некоторого состояния, поэтому существует начальное очень простое рукопожатие, которое устанавливает симметричные ключи для передачи данных. Это рукопожатие происходит каждые несколько минут для обеспечения вращения ключей и совершенной прямой секретности (Perfect Forward Secrecy — PFS). PFS означает, что даже если долговременный приватный ключ будет скомпрометирован, это не позволит расшифровать ранее перехваченные сессии.
Рукопожатие выполняется на основе времени, а не содержимого предыдущих пакетов, поскольку спроектировано для корректной обработки потери пакетов. Это важно, потому что в сетях с высокой загрузкой или нестабильным соединением пакеты могут теряться, и протокол должен продолжать работать без сбоев.
Используется умный механизм пульса (pulse mechanism), чтобы гарантировать, что последние ключи и рукопожатия актуальны, с повторным согласованием при необходимости. Этот механизм автоматически обнаруживает, когда рукопожатия устарели, и инициирует новые. Используется отдельная очередь пакетов на хост, чтобы минимизировать потерю пакетов во время рукопожатий, обеспечивая стабильную производительность для всех клиентов.
Другими словами, вы поднимаете устройство, и всё остальное обрабатывается автоматически. Вам не нужно беспокоиться о переподключении, отключении или переинициализации. WireGuard сам управляет всем жизненным циклом соединения.
Таймеры протокола
В протоколе используются следующие таймеры:
REKEY_TIMEOUT — повторная попытка рукопожатия выполняется через
REKEY_TIMEOUT + jitterмс, если ответ не был получен, гдеjitter— случайное значение от 0 до 333 мс. Случайная задержка предотвращает синхронизацию попыток от множества клиентов.KEEPALIVE — если пакет был получен от данного пира, но мы не отправили ему пакет в течение
KEEPALIVEмс, мы отправляем пустой пакет. Это поддерживает NAT-маппинг активным.Если мы отправили пакет данному пиру, но не получили пакет от него в течение
KEEPALIVE + REKEY_TIMEOUTмс, мы инициируем новое рукопожатие.Все эфемерные приватные ключи и симметричные сессионные ключи обнуляются после
REJECT_AFTER_TIME * 3мс, если новые ключи не были обменены. Это гарантирует, что старые ключи не будут случайно сохранены в памяти.После отправки пакета, если количество отправленных пакетов с использованием этого ключа превышает
REKEY_AFTER_MESSAGES, мы инициируем новое рукопожатие. Это ограничивает количество данных, зашифрованных одним ключом.После отправки пакета, если отправитель был первоначальным инициатором рукопожатия и текущий сессионный ключ старше
REKEY_AFTER_TIMEмс, мы инициируем новое рукопожатие. Если отправитель был первоначальным ответчиком, мы не инициируем новое рукопожатие послеREKEY_AFTER_TIMEмс, как это делает инициатор.После получения пакета, если получатель был первоначальным инициатором рукопожатия и текущий сессионный ключ старше
REKEY_AFTER_TIME - KEEPALIVE_TIMEOUT - REKEY_TIMEOUTмс, мы инициируем новое рукопожатие.Рукопожатия инициируются только один раз в
REKEY_TIMEOUTмс, с применением строгого ограничения скорости.Пакеты отбрасываются, если счётчик сессии больше
REJECT_AFTER_MESSAGESили если его ключ старшеREJECT_AFTER_TIMEмс.После
REKEY_ATTEMPT_TIMEмс попыток инициировать новое рукопожатие, попытки прекращаются, и очищаются все существующие пакеты, поставленные в очередь для отправки. Если пакет явно поставлен в очередь для отправки, этот таймер сбрасывается.
В будущем планируется настроить REKEY_TIMEOUT для использования экспоненциальной задержки (exponential back-off), что повысит эффективность при больших нагрузках.
Ключевое подтверждение
После завершения рукопожатия, с сообщением от инициатора к ответчику и затем обратно от ответчика к инициатору, инициатор может отправлять зашифрованные пакеты сессии, но ответчик не может. Ответчик обязан дождаться получения зашифрованного пакета сессии от инициатора для подтверждения ключа. Это требование обеспечивает подтверждение того, что инициатор действительно получил ключи и может использовать их.
До получения первого пакета с новым ключом ответчик должен либо поставить пакеты в очередь для отправки позже, либо использовать предыдущую сессию, если она существует и действительна. Поэтому после получения ответа от ответчика, если у инициатора нет немедленно готовых к отправке пакетов данных, он должен отправить пустой пакет для подтверждения ключа.
Пример: Представьте, что клиент устанавливает VPN-соединение с сервером. Клиент отправляет рукопожатие, сервер отвечает, и после этого клиент отправляет первый зашифрованный пакет данных. Только после получения этого пакета сервер начинает использовать новый ключ для ответов. Если клиенту нечего отправлять сразу, он отправляет пустой пакет, чтобы сервер знал, что ключ подтверждён.
Обмен ключами и пакеты данных
WireGuard использует рукопожатие Noise_IK из Noise Protocol Framework, основанное на работах CurveCP, NaCL, KEA+, SIGMA, FHMQV и HOMQV. Все эти протоколы внесли вклад в создание надёжного и безопасного механизма обмена ключами. Все пакеты передаются через UDP, что обеспечивает низкую задержку и высокую производительность.
Свойства обмена ключами
Обмен ключами обладает следующими свойствами:
Избегает компрометации ключей с подменой личности — даже если злоумышленник получит доступ к долговременному ключу одной стороны, он не сможет выдать себя за другую сторону.
Избегает атак повторного воспроизведения — каждый пакет содержит уникальные данные, предотвращающие его повторное использование.
Обеспечивает совершенную прямую секретность (PFS) — компрометация долговременных ключей не раскрывает содержимое прошлых сессий.
Достигает “AKE Security” — обеспечивает безопасность аутентифицированного обмена ключами в соответствии с современными криптографическими стандартами.
Обеспечивает скрытие идентичности — информация о сторонах, участвующих в обмене, защищена от пассивного наблюдения.
Предварительный общий ключ (PSK)
Если требуется дополнительный уровень симметричной криптографии (например, для устойчивости к квантовым атакам), WireGuard поддерживает опциональный предварительный общий ключ (Pre-Shared Key), который смешивается с криптографией с открытым ключом. Это обеспечивает дополнительный уровень защиты: даже если кто-то сможет взломать асимметричную криптографию с помощью квантового компьютера, PSK останется барьером.
Когда режим PSK не используется, значение предварительного общего ключа считается строкой из 32 нулевых байт. Таким образом, протокол всегда работает одинаково, независимо от использования PSK.
Используемые функции
В протоколе используются следующие криптографические функции:
DH(private key, public key)— умножение точек Curve25519, возвращает 32 байта общего секрета. Это основной механизм обмена ключами.DH_GENERATE()— генерация случайного приватного ключа Curve25519 (32 байта). Каждый раз генерируется новый ключ для обеспечения PFS.RAND(len)— возвращаетlenслучайных байт. Используется для генерации nonce и других случайных значений.DH_PUBKEY(private key)— вычисление публичного ключа Curve25519 из приватного ключа (32 байта).AEAD(key, counter, plain text, auth text)— ChaCha20Poly1305 AEAD, как указано в RFC7539, с nonce, состоящим из 32 бит нулей, за которыми следует 64-битное значениеcounterв little-endian. Обеспечивает одновременно шифрование и аутентификацию.XAEAD(key, nonce, plain text, auth text)— XChaCha20Poly1305 AEAD со случайным 24-байтовым nonce. Используется для cookie-пакетов.AEAD_LEN(plain len)—plain len + 16, где 16 байт — размер тега аутентификации.HMAC(key, input)— HMAC-Blake2s(key, input, 32), возвращает 32 байта. Используется для создания ключей.MAC(key, input)— Keyed-Blake2s(key, input, 16), возвращает 16 байт. Используется для создания MAC (Message Authentication Code).HASH(input)— Blake2s(input, 32), возвращает 32 байта. Используется для хеширования.TAI64N()— TAI64N-метка текущего времени (12 байт). Используется для защиты от атак повторного воспроизведения.CONSTRUCTION— UTF-8 значениеNoise_IKpsk2_25519_ChaChaPoly_BLAKE2s(37 байт). Идентифицирует используемую конструкцию Noise.IDENTIFIER— UTF-8 значениеWireGuard v1 zx2c4 Jason@zx2c4.com(34 байта). Идентифицирует реализацию WireGuard.LABEL_MAC1— UTF-8 значениеmac1----(8 байт). Используется для маркировки MAC1.LABEL_COOKIE— UTF-8 значениеcookie--(8 байт). Используется для маркировки cookie.
Структура пакетов
Первое сообщение: Инициатор → Ответчик
Инициатор отправляет это сообщение:
msg = handshake_initiation {
u8 message_type // Всегда 1
u8 reserved_zero[3] // Зарезервировано для будущего использования
u32 sender_index // Индекс отправителя
u8 unencrypted_ephemeral[32] // Эфемерный публичный ключ (не зашифрован)
u8 encrypted_static[AEAD_LEN(32)] // Зашифрованный статический публичный ключ
u8 encrypted_timestamp[AEAD_LEN(12)] // Зашифрованная метка времени
u8 mac1[16] // Первый MAC
u8 mac2[16] // Второй MAC (cookie)
}
Пример: Клиент (инициатор) отправляет это сообщение серверу (ответчику), чтобы начать установку VPN-соединения. Поле unencrypted_ephemeral содержит эфемерный публичный ключ, который не шифруется, чтобы сервер мог его использовать для вычисления общего секрета. Поля encrypted_static и encrypted_timestamp зашифрованы и содержат долговременный публичный ключ клиента и текущее время для защиты от повторных атак.
Второе сообщение: Ответчик → Инициатор
Ответчик отправляет это сообщение после обработки первого сообщения:
msg = handshake_response {
u8 message_type // Всегда 2
u8 reserved_zero[3] // Зарезервировано
u32 sender_index // Индекс отправителя
u32 receiver_index // Индекс получателя (из первого сообщения)
u8 unencrypted_ephemeral[32] // Эфемерный публичный ключ
u8 encrypted_nothing[AEAD_LEN(0)] // Зашифрованное пустое сообщение
u8 mac1[16] // Первый MAC
u8 mac2[16] // Второй MAC (cookie)
}
Пример: Сервер (ответчик) отвечает клиенту своим эфемерным публичным ключом. Поле receiver_index содержит индекс, полученный от клиента, что позволяет клиенту сопоставить ответ с правильной сессией. Зашифрованное пустое сообщение (encrypted_nothing) служит для подтверждения того, что сервер правильно вычислил ключи.
Пакеты данных
После завершения рукопожатия инициатор и ответчик обмениваются пакетами данных:
msg = packet_data {
u8 message_type // Всегда 4
u8 reserved_zero[3] // Зарезервировано
u32 receiver_index // Индекс получателя
u64 counter // Счётчик пакетов (nonce)
u8 encrypted_encapsulated_packet[] // Зашифрованный инкапсулированный пакет
}
Пример: После установления ключей клиент отправляет серверу зашифрованный IP-пакет. Поле counter увеличивается с каждым пакетом, предотвращая повторное использование nonce. Сами данные (encrypted_encapsulated_packet) содержат зашифрованный IP-пакет, который сервер расшифровывает и передаёт в сеть.
Производные ключей данных
После обмена двумя сообщениями ключи вычисляются инициатором и ответчиком для отправки и получения данных:
temp1 = HMAC(initiator.chaining_key, [empty])
temp2 = HMAC(temp1, 0x1)
temp3 = HMAC(temp1, temp2 || 0x2)
initiator.sending_key = temp2
initiator.receiving_key = temp3
initiator.sending_key_counter = 0
initiator.receiving_key_counter = 0
temp1 = HMAC(responder.chaining_key, [empty])
temp2 = HMAC(temp1, 0x1)
temp3 = HMAC(temp1, temp2 || 0x2)
responder.receiving_key = temp2
responder.sending_key = temp3
responder.receiving_key_counter = 0
responder.sending_key_counter = 0
Затем все предыдущие цепочки ключей, эфемерные ключи и хеши обнуляются. Это гарантирует, что даже если злоумышленник получит доступ к памяти, он не сможет восстановить предыдущие ключи.
Объяснение: Процесс использует HKDF (HMAC-based Key Derivation Function) для создания независимых ключей для каждого направления. temp1 — это начальный ключевой материал, из которого через серию HMAC-операций создаются sending_key (ключ для отправки) и receiving_key (ключ для приёма). Счётчики инициализируются нулём и увеличиваются с каждым пакетом.
Защита от DoS-атак
WireGuard требует аутентификации в первом сообщении рукопожатия, что не требует выделения состояния на сервере для потенциально неаутентифицированных сообщений. Это критически важно для защиты от DoS-атак: злоумышленник не может заставить сервер выделять ресурсы для каждого поддельного пакета.
Сервер вообще не отвечает неавторизованному клиенту; он молчит и невидим. Это делает WireGuard устойчивым к сканированию портов и DoS-атакам.
Защита от атак повторного воспроизведения
Однако это создаёт проблему: аутентификация в первом пакете всегда уязвима для атак повторного воспроизведения. Злоумышленник может воспроизвести начальные сообщения рукопожатия, чтобы заставить сервер перегенерировать свой эфемерный ключ, тем самым разорвав соединение легитимного клиента (хотя это не влияет на безопасность сообщений).
Для защиты используется TAI64N-метка времени в первом сообщении. Сервер отслеживает наибольшую полученную метку времени для каждого клиента и отбрасывает пакеты с метками, меньшими или равными предыдущей.
Пример: Клиент отправляет рукопожатие с меткой времени 2024-01-01 12:00:00.000. Сервер запоминает эту метку. Если злоумышленник попытается воспроизвести это же рукопожатие через час, сервер отбросит его, так как метка времени меньше или равна уже полученной.
Если сервер перезапускается и теряет это состояние, это не проблема: более ранний пакет может быть воспроизведён, но он не может нарушить текущие сессии, так как сервер только что перезапустился. После переподключения клиенты будут использовать более новые метки времени, делая предыдущие недействительными.
Cookie-пакеты
Вычисление функции DH() требует значительных вычислительных ресурсов. Для защиты от атак на исчерпание CPU сервер может не обрабатывать сообщения рукопожатия, а вместо этого отвечать cookie-пакетом, если он находится под нагрузкой.
Чтобы сервер оставался молчаливым, если не получает действительный пакет, все сообщения должны содержать MAC, который комбинирует публичный ключ получателя и опционально PSK в качестве ключа MAC. Когда сервер находится под нагрузкой, он принимает только пакеты, которые дополнительно имеют второй MAC предыдущих байтов сообщения, использующий cookie в качестве ключа MAC.
Cookie истекают через две минуты и представляют собой MAC IP-адреса отправителя с использованием меняющегося (каждые две минуты) секрета сервера в качестве ключа MAC. Это позволяет доказать владение IP-адресом, который затем может быть правильно ограничен по скорости.
Сервер, вычислив эти MAC и сравнив их с полученными в сообщении, должен отклонять сообщения с недействительным msg.mac1 и при нагрузке — сообщения с недействительным msg.mac2.
Cookie-пакет
Как упоминалось выше, когда получено сообщение с действительным msg.mac1, но msg.mac2 равен нулю или недействителен, и сервер находится под нагрузкой, сервер может отправить cookie-пакет:
msg = packet_cookie_reply {
u8 message_type // Всегда 3
u8 reserved_zero[3] // Зарезервировано
u32 receiver_index // Индекс получателя
u8 nonce[24] // Случайный nonce для XChaCha20
u8 encrypted_cookie[AEAD_LEN(16)] // Зашифрованный cookie
}
Пример: Злоумышленник пытается инициировать множество рукопожатий с сервера под нагрузкой. Сервер не выполняет дорогостоящие вычисления DH для каждого, а вместо этого отправляет cookie-пакет. Клиент должен расшифровать cookie и отправить его обратно в следующем рукопожатии, доказывая, что он может получить пакеты по указанному IP-адресу.
Защита от повторного использования nonce
Nonce никогда не повторяются. Используется 64-битный счётчик, который не может быть уменьшен. Это означает, что каждый пакет имеет уникальный nonce, что критически важно для безопасности ChaCha20-Poly1305.
UDP иногда доставляет сообщения в неправильном порядке. Для этого используется скользящее окно, в котором отслеживается наибольший полученный счётчик и окно примерно из 2000 предыдущих значений, проверяемых после верификации тега аутентификации. Это позволяет принимать пакеты, пришедшие не по порядку, но не позволяет повторно использовать уже принятые nonce.
Пример: Клиент отправляет пакеты с счётчиками 1, 2, 3, 4, 5. Пакет 4 приходит первым, затем пакет 2, затем пакет 5. Скользящее окно позволяет принять все эти пакеты, так как их счётчики находятся в допустимом диапазоне. Если злоумышленник попытается повторно отправить пакет с счётчиком 2, он будет отброшен, так как этот счётчик уже был принят.
DiffServ
При работе с IP-пакетами WireGuard учитывает следующие аспекты DiffServ:
DSCP рукопожатий: 0x88 (AF41) — эти пакеты имеют наивысший приоритет и наименьшую вероятность отбрасывания, так как они необходимы для управления туннелем. Это гарантирует, что даже при перегрузке сети рукопожатия будут доставлены.
DSCP данных: 0 — значение DSCP из внутреннего пакета никогда не копируется во внешний пакет. Это предотвращает утечку информации о типе трафика, проходящего через зашифрованный туннель.
ECN (Explicit Congestion Notification): Бит ECN копируется между внутренним и внешним пакетами в соответствии с логикой, описанной в RFC6040. Это позволяет сохранять информацию о перегрузке сети внутри туннеля.
Пример: Если внутри туннеля передаётся VoIP-трафик с высоким приоритетом, внешние пакеты WireGuard не будут показывать этот приоритет. Внешний наблюдатель увидит только обычные UDP-пакеты без информации о их содержимом.
Схема протокола
sequenceDiagram
participant I as Инициатор
participant R as Ответчик
Note over I,R: 1. Инициация рукопожатия
I->>R: message_type=1, ephemeral, encrypted_static, timestamp
Note over R: Проверка MAC, дешифрование<br/>Генерация ephemeral ключа
Note over I,R: 2. Ответ
R->>I: message_type=2, ephemeral, encrypted_nothing
Note over I: Проверка, дешифрование<br/>Производная ключей
Note over I,R: 3. Передача данных
I->>R: message_type=4, encrypted_data
Note over R: Дешифрование данными ключами