Откуда берётся ключ
AEAD разбирает, что делают с ключом, и нигде не говорит, откуда он взялся: в TLS, WireGuard и age он там уже есть. Эта статья закрывает предыдущий шаг — как две стороны получают одинаковый секрет по каналу, который противник читает целиком, и что превращает этот секрет в набор ключей с разными ролями. Понятия ключа, nonce, AAD и тега здесь не переопределяются; всё, что про их использование, осталось в соседней заметке.
Задача формулируется неприятно прямо. Канал прослушивается полностью, всё отправленное противник видит; в конце обмена у обеих сторон должно оказаться одно и то же значение, которого у него нет. Никакой предварительно розданной тайны при этом может не быть вовсе. Решение существует с 1976 года и опирается на асимметрию между вычислением и его обращением: возвести в степень дёшево, вернуть показатель — дорого.
Дальше всё интересное растёт из двух фактов. Первый: общий секрет получается с кем-то, а не с кем нужно, — обмен ключами не аутентифицирует никого, и вся конструкция рукопожатий существует ради того, чтобы это исправить. Второй: полученное значение не годится в качестве ключа — оно неравномерно и не размечено по ролям, и между ним и рабочим ключом обязан стоять вывод ключей. Разделы ниже идут в этом порядке: сначала как добывается секрет, потом почему его мало, потом как из него получаются ключи.
Общий секрет по открытому каналу
Одна конструкция, две арифметики
Диффи-Хеллман — это утверждение про коммутативность. В группе с генератором g каждая сторона выбирает случайный секрет, публикует g^x, возводит полученное от партнёра значение в свой секрет и получает g^xy. Сторон две, операций две, порядок не важен — вот и вся схема. Различаются только группы, и вместе с группой различается то, что в ней трудно.
В конечнополевом варианте (FFC, finite field cryptography) группа — подгруппа мультипликативной группы вычетов по большому простому модулю. Трудная задача — дискретный логарифм в этом поле, и решается она решетом числового поля, алгоритмом субэкспоненциальной сложности. Существеннее самой сложности устройство алгоритма: у решета есть тяжёлая первая фаза, зависящая только от модуля, и дешёвая вторая, зависящая от конкретного значения. Авторы Logjam на этом и построили атаку — «the first step in the number field sieve… is dependent only on this prime». Поскольку в реальности все используют одни и те же стандартные простые числа, разовое вложение в предвычисление окупается на всём интернете сразу: по их оценке взлом единственного самого популярного 1024-битного простого дал бы пассивное чтение трафика 18% из миллиона популярнейших HTTPS-доменов, а второго — 66% VPN-серверов и 26% SSH-серверов.
На эллиптической кривой группа — точки кривой над конечным полем, а трудная задача — дискретный логарифм в этой группе, для которого субэкспоненциального алгоритма не известно: лучшее, что есть, — методы вида ρ-Полларда со сложностью порядка корня из размера группы. Отсюда разница в размерах, и она не косметическая. RFC 9142, таблица 4, оценивает 2048-битную MODP-группу (group14) в 112 бит стойкости, 3072-битную — в 128. RFC 7748, §7, оценивает curve25519 в «slightly under the standard 128-bit level».
| Открытое значение | Стойкость | Что трудно | |
|---|---|---|---|
MODP group14 (2048 бит) |
256 байт | ~112 бит | дискретный логарифм в поле, решето числового поля |
MODP group15 (3072 бита) |
384 байта | ~128 бит | то же |
| X25519 (curve25519) | 32 байта | чуть меньше 128 бит | дискретный логарифм на кривой, ρ-Поллард |
Двенадцатикратная разница в размере при равной стойкости — это и есть причина, по которой эллиптический вариант вытеснил конечнополевой везде, где был выбор.
Вердикт: конечнополевой DH сегодня — легаси, и держится он не на технических достоинствах, а на требованиях совместимости. Показательно, что живее всего он именно там, где это записано в стандарте: RFC 9142, §3.2.2, делает diffie-hellman-group14-sha256 обязательным к реализации в SSH («the diffie-hellman-group14-sha256 method MUST be implemented»), тогда как curve25519-sha256 там всего лишь SHOULD. В TLS 1.3 FFDHE-группы из RFC 7919 остались определены, но из первого сообщения ушли: OpenSSL 3.5 (8 апреля 2025) отправляет в ClientHello ключевые доли только для X25519MLKEM768 и X25519. Разница между «долями» и «списком поддерживаемых групп» здесь существенна: в умолчании TLS_DEFAULT_GROUP_LIST (ssl/t1_lib.c ветки openssl-3.5) ffdhe2048 и ffdhe3072 остались — просто без звёздочки, которой помечены группы, отправляемые долями. Клиент их по-прежнему объявляет, и сервер волен выбрать FFDHE, заплатив лишним раундом через HelloRetryRequest. Новую систему на конечнополевом DH строить не за чем; поддерживать его приходится ровно столько, сколько живут собеседники, которые ничего другого не умеют.
X25519: чего в нём нет
X25519, описанный в RFC 7748, ценен не скоростью, а вычеркнутыми строчками. Каждое его конструктивное решение убирает из реализации место, где принято ошибаться.
Кривая взята в форме Монтгомери, и работа идёт лестницей Монтгомери — алгоритмом, который на каждой итерации выполняет одни и те же операции независимо от бита секрета. RFC формулирует следствие в §5.1: «the same sequence of field operations is performed for all values of the secret key, thus eliminating a common source of side-channel leakage». Ветвлений по секрету нет не потому, что программист был аккуратен, а потому, что их негде разместить.
Лестница работает только с u-координатой, и наружу тоже выдаётся только она. Это убирает целый слой формата: нет точек с двумя координатами, нет сжатия и разжатия, нет разбора кодировки. Главное — нет отдельного шага «проверить, что присланная точка лежит на нашей кривой». Именно этот шаг регулярно забывали в реализациях коротких кривых Вейерштрасса, и цена известна: Ягер, Швенк и Соморовский в «Practical Invalid Curve Attacks on TLS-ECDH» (ESORICS 2015) восстанавливали статический закрытый ключ сервера, подсовывая точки с кривых, отличающихся от настоящей, — проверка отсутствовала в Java JSSE и Bouncy Castle.
Формат входа при этом описан так, что отвергать вообще нечего. RFC 7748, §5, требует маскировать старший бит последнего байта и добавляет: «Implementations MUST accept non-canonical values and process them as if they had been reduced modulo the field prime». Бернштейн в описании Curve25519 отвечает на вопрос о валидации открытых ключей одним словом — «Don't», и объясняет: «The Curve25519 function was carefully designed to allow all 32-byte strings as Diffie-Hellman public keys». Держится это на порядках: у кривой 8 × простое, у её квадратичного твиста 4 × простое, поэтому точка, случайно оказавшаяся на твисте, не даёт противнику ничего.
Проверить обе половины утверждения можно за минуту (вывод получен на pyca/cryptography 41.0.7 поверх OpenSSL):
python3 - <<'PY'
import os
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
a = X25519PrivateKey.generate()
# произвольные 32 байта — валидный открытый ключ, обмен проходит
print(a.exchange(X25519PublicKey.from_public_bytes(os.urandom(32))).hex()[:16])
# точка порядка 8 — обмен даёт нулевой секрет, и библиотека отказывается его отдать
low = (325606250916557431795983626356110631294008115727848805560023387167927233504)
try:
a.exchange(X25519PublicKey.from_public_bytes(low.to_bytes(32, 'little')))
except ValueError as e:
print('отказ:', e)
PY
Наконец, клампинг: закрытый ключ — это 32 случайных байта, у которых принудительно обнуляются три младших бита и старший, а второй по старшинству выставляется в единицу (RFC 7748, §5). Рационала RFC не даёт — только битовые операции; за ним приходится идти в статью о Curve25519, где обе меры перечислены среди проектных решений: «multiply the secret key by a small power of 2 to account for cofactors in the curve group and the twist group» и «use a fixed position for the leading 1 in the secret key». Первое делает скаляр кратным восьми и гасит кофактор вместе с подгруппами малого порядка — §3 сводит это к «secret keys n by definition have n mod 8 = 0 and thus n mod b = 0», где b пробегает возможные малые порядки. Второе задаёт лестнице постоянные 255 удвоений (§5). Первая мера избавляет от проверок протокол, вторая — реализацию лестницы.
Альтернатива X25519 — короткие кривые Вейерштрасса NIST, P-256 и P-384, и выбор между ними не вполне технический. RFC 10042, §5, называет обе стороны разом: X25519 «is generally considered easier to implement securely without side channels than its NIST counterparts (with P256 and P384)», но «NIST curves are sometimes preferred for regulatory compliance». Третьего аргумента в этом сравнении нет: стойкость сопоставима, разница в скорости не решает, а всё перечисленное выше — отсутствие ветвлений, отсутствие валидации точки, отсутствие формата, в котором можно ошибиться, — на P-256 приходится обеспечивать руками. Вердикт: X25519 везде, где нет внешнего требования; NIST-кривые только там, где их требует сертификация. Выбор тянется и дальше, в постквантовый гибрид: RFC 10024 определяет SecP256r1MLKEM768 и SecP384r1MLKEM1024 рядом с X25519MLKEM768, и порядок слагаемых в них обратный не случайно — FIPS требует, чтобы первый секрет давала сертифицированная реализация одобренной схемы, поэтому «for SecP256r1MLKEM768 and SecP384r1MLKEM1024, the ECDHE implementation must be certified, whereas the ML-KEM implementation does not require certification. In contrast, for X25519MLKEM768, the ML-KEM implementation must be certified». В SSH то же различие выражено отдельными методами — mlkem768nistp256-sha256 и mlkem1024nistp384-sha384 (RFC 10042, §2.3).
Что конструкция не гарантирует, разобрано ниже, в разделе про поломки: contributory behaviour и уникальность открытого ключа как идентификатора остаются на протоколе, и здесь RFC 7748 и Бернштейн расходятся в акцентах.
Обмен ключами ничего не говорит о собеседнике
Чистый DH заканчивается общим секретом с тем, кто был на другом конце. Кто это был, схема не сообщает, и противник посередине отрабатывает без всяких ухищрений: он проводит два независимых обмена, с каждой стороной свой, и дальше расшифровывает и перешифровывает. Обе стороны видят корректный обмен, оба секрета настоящие. Ошибки нет ни у кого — просто вопрос «с кем» не задавался.
Ответить на него можно тремя способами, и различаются они тем, что у сторон есть заранее.
Транскрипт и три способа его закрыть
Общее у всех трёх ответов одно: стороны считают транскрипт — хеш всего, что прошло по каналу с самого первого байта, — и привязывают к нему доказательство подлинности. Смысл именно в этом: подписывается не эфемерный ключ и не секрет, а весь разговор целиком, включая согласование алгоритмов. Иначе противник правит то, что не покрыто, а покрытое оставляет в покое.
Подпись транскрипта долгоживущим ключом — путь TLS. Сообщение CertificateVerify (RFC 9846, §4.5.2) содержит подпись над Transcript-Hash(Handshake Context, Certificate), и подписывается не сам хеш, а конкатенация из 64 байт 0x20, контекстной строки "TLS 1.3, server CertificateVerify", нулевого байта-разделителя и содержимого. Обе добавки — заплатки по следам прошлых провалов, и RFC называет их прямо: паддинг стирает управляемый противником 32-байтный префикс, из-за которого в прежних версиях можно было выпросить подпись сообщения с выбранным началом, а контекстная строка «is used to provide separation between signatures made in different contexts, helping against potential cross-protocol attacks». Сертификат при этом ручается за имя, а подпись — за то, что владелец имени участвовал именно в этом разговоре.
Доказательство знания заранее известного открытого ключа — путь SSH и WireGuard. В SSH обмен завершается вычислением exchange hash H = hash(V_C ‖ V_S ‖ I_C ‖ I_S ‖ K_S ‖ e ‖ f ‖ K) (RFC 4253, §8) — в него входят строки версий, оба сообщения SSH_MSG_KEXINIT с перечнями алгоритмов, ключ хоста и обе публичные доли; сервер подписывает H ключом хоста. Клиент обязан сверить ключ хоста с тем, что знал раньше, и RFC 4253 честно проговаривает альтернативу: «C is also allowed to accept the key without verification; however, doing so will render the protocol insecure against active attacks». Это и есть TOFU из known_hosts — не свойство криптографии, а осознанная уступка.
WireGuard обходится вовсе без подписи. Его рукопожатие — паттерн Noise IKpsk2, где роль транскрипта играет переменная hash, начинающаяся со статического открытого ключа отвечающего и вбирающая каждое поле каждого сообщения. Эта переменная подаётся как AAD в каждый AEAD рукопожатия, так что проверка тега и есть проверка транскрипта. Аутентификацию даёт отдельная операция DH(initiator.static_private, responder.static_public), подмешанная в цепочку: расшифровать следующее поле сумеет только тот, у кого есть закрытый ключ, парный известному открытому. Инициатор обязан знать открытый ключ отвечающего до начала — без него не собрать даже стартовое значение hash, — и поэтому MITM в WireGuard невозможен по построению, ценой того, что незнакомцу подключиться нельзя в принципе.
| Что есть у сторон заранее | Чем закрывается транскрипт | Кто так делает | Цена |
|---|---|---|---|
| Доверенный корень PKI | подпись долгоживущим ключом, сертификат за имя | TLS | нужна инфраструктура сертификатов и их отзыв |
| Открытый ключ партнёра | DH со статическим ключом, транскрипт в AAD или в подписи | WireGuard, SSH | ключи надо раздать заранее |
| Низкоэнтропийный общий секрет | PAKE или подмешивание PSK в вывод ключей | PAKE-протоколы, PSK в WireGuard | защита от перебора нужна отдельно |
Вердикт: выбор задаётся не вкусом, а тем, знает ли клиент собеседника заранее. Публичный сервис, к которому приходят незнакомцы, обязан идти через PKI; закрытая сеть с фиксированным списком участников должна брать модель WireGuard, потому что она убирает и PKI, и возможность MITM разом; пароль — всегда худший из трёх и требует PAKE, а не самодельной схемы.
Что происходит, когда привязка к транскрипту неполна, подробно разобрано в статье про AEAD на примере Terrapin (CVE-2023-48795): счётчик пакетов в SSH пересекал границу включения шифрования, ни к чему не привязанный, и это позволяло вырезать сообщение из рукопожатия. Повторять разбор здесь незачем, но вывод стоит держать в голове при чтении дальнейшего: транскрипт должен покрывать всё, включая границы фаз и согласование алгоритмов.
Пароль как общий секрет
Пароль в качестве входа в обмен ключами — отдельный жанр, потому что энтропии в нём мало и любая схема, позволяющая проверить догадку офлайн, проиграна заранее. Задача PAKE — сделать так, чтобы каждая попытка угадать пароль стоила противнику одного активного прогона протокола. У CFRG два результата: сбалансированный CPace (обе стороны знают пароль) — draft-irtf-cfrg-cpace, ревизия 21, на дату сверки в очереди RFC-редактора и ещё не RFC, — и асимметричный OPAQUE, где сервер хранит не пароль, а проверочные данные: он опубликован как RFC 9807 (IRTF, информационный, июль 2025).
Вердикт: самостоятельно собирать аутентификацию по паролю поверх DH не нужно — либо готовый PAKE, либо пароль вообще не участвует в обмене ключами, а проверяется внутри уже аутентифицированного канала, как это делает SSH с password-методом поверх проверенного ключа хоста.
KEM: другой интерфейс к той же задаче
В Диффи-Хеллмане роли сторон одинаковы: каждая генерирует пару и применяет свой закрытый ключ к чужому открытому. KEM — механизм инкапсуляции ключа — разводит роли. Одна сторона выполняет KeyGen и публикует ключ инкапсуляции; вторая выполняет Encaps от этого ключа и получает две вещи сразу: шифртекст, который отправляется обратно, и общий секрет, который никуда не отправляется. Первая применяет Decaps к шифртексту и получает тот же секрет.
Разведённые роли соблазняют прочитать это как «доставку секрета владельцу открытого ключа», и для KEM вообще так и есть, а для ML-KEM — нет. NIST SP 800-227 (сентябрь 2025), §2.2, разделяет эти случаи явно: обобщённая конструкция KEM из схемы шифрования (KEMFROMPKE там же) — чистый транспорт, отправитель выбирает ключ сам и шифрует его чужим открытым ключом; в ML-KEM иначе — «the shared secret key in ML-KEM is a function of both the randomness provided by Bob and the (randomly generated) encapsulation key of Alice. Therefore, ML-KEM could be viewed as a key-agreement scheme». То есть в ML-KEM вклад в секрет вносят обе стороны — вопрос, к которому статья ещё вернётся ниже, в разделе про поломки, уже про X25519. Различие интерфейсов при этом не так велико, как кажется, и читается в обе стороны: §5.1.1 того же документа показывает обратное преобразование — если в DH стороны ходят по очереди, открытая доля первой оказывается ключом инкапсуляции, а доля второй шифртекстом, и ECDH читается как KEM.
Стандартизация постквантовой схемы ушла к KEM не из вкусовых соображений. Решётчатые конструкции шумные — вся их стойкость держится на добавленной погрешности, — и прямого аналога «обе стороны возводят в степень» в них нет: симметричная схема даёт сторонам лишь приближённо равные значения, которые приходится доводить до совпадения отдельной процедурой. Процедура эта существует и называется примирением (reconciliation): её предложил Пайкерт в «Lattice Cryptography for the Internet» (2014) — «a simple, low-bandwidth reconciliation technique that allows two parties who "approximately agree" on a secret value to reach exact agreement», — на ней построен NewHope, и в таком виде постквантовый обмен ключами успел поездить в Chrome под именем CECPQ1 в 2016 году. FIPS 203 (финальная редакция, август 2024) идёт другим путём: сначала из задачи MLWE строится схема шифрования с открытым ключом K-PKE, затем она преобразованием Фудзисаки—Окамото превращается в KEM, и §3.2 отмечает, что именно преобразование даёт стойкость в существенно более сильной модели — «the resulting KEM provides security in a significantly more general attack model than the PKE scheme… ML-KEM is believed to satisfy so-called IND-CCA2 security». Сама K-PKE при этом объявлена непригодной к самостоятельному использованию. Почему выбор пал на KEM, а не на схему с примирением, FIPS 203 не объясняет — слова «Диффи-Хеллман» в стандарте нет вовсе, — так что связка «шум мешает симметричной схеме» выше реконструирована, а не взята из источника.
Трудная задача здесь — Module Learning With Errors: восстановить секрет из набора «зашумлённых» линейных уравнений над модулем над кольцом многочленов. Никакой связи с дискретным логарифмом у неё нет, и в этом весь смысл: машина, ломающая одно, не ломает другое.
Цена — размеры. Секрет остаётся 32-байтным, а вот всё, что летит по проводу, вырастает разительно.
| ML-KEM-768 (FIPS 203, табл. 3) | Байт | X25519 |
|---|---|---|
| Ключ инкапсуляции (уходит от инициатора) | 1184 | 32 |
| Шифртекст (уходит в ответ) | 1088 | 32 |
| Ключ декапсуляции (не уходит никуда) | 2400 | 32 |
| Общий секрет | 32 | 32 |
Дальше эта арифметика бьёт в неожиданное место. Гибридная доля X25519MLKEM768 занимает 1216 байт, и ClientHello с ней перестаёт помещаться в один сегмент на пути с типичной MTU 1500. Cloudflare, выкатывая постквантовый обмен к origin-серверам, описывает последствие так: «it used to be so exceedingly rare to see a split ClientHello that there is plenty of software and hardware out there that falsely assumes it never happens», и называет протокольную оссификацию главной трудностью перехода; в их сканировании меньше 0.34% origin-серверов не устанавливали соединение, когда постквантовая доля приходила в первом ClientHello. Вычислительно же решётчатая схема дешевле, чем принято думать, — с оговоркой, что мерили не ML-KEM. Замеры опубликованы в сентябре 2023 года, до финальной редакции FIPS 203, и в таблице у Cloudflare стоят Kyber768 (31 тыс. операций в секунду у клиента, 70 тыс. у сервера) и черновой гибрид X25519Kyber768Draft00 (11 тыс. и 14 тыс.) против X25519 (17 тыс. и 17 тыс.): Kyber768 обгоняет X25519, а гибрид медленнее обоих ровно потому, что делает обе работы. Прямых замеров ML-KEM-768 в том же стенде нет. Платят за постквантовый обмен байтами, а не тактами.
Ещё одно практическое отличие от X25519 — вход надо проверять. FIPS 203, §7.2, задаёт проверку ключа инкапсуляции из двух шагов: длина обязана равняться 384k + 32, и перекодировка коэффициентов туда-обратно обязана дать исходные байты. RFC 10024 переносит требование в TLS: «the server MUST perform the encapsulation key check described in Section 7.2 of NIST-FIPS-203 on the client's encapsulation key and abort with an illegal_parameter alert if it fails». Привычка «ничего не проверяем, все строки валидны», выработанная на X25519, на ML-KEM не переносится.
Гибрид отвечает на конкретную угрозу
Гибридный обмен — это не «два алгоритма на всякий случай». Это ответ на одну названную угрозу: записанный сегодня трафик расшифруют потом. RFC 9954 «Hybrid key exchange in TLS 1.3» (IETF, информационный, июль 2026) формулирует её в §1.2: «If a cryptographic assumption is broken due to the advent of a quantum computer or some other cryptanalytic breakthrough, confidentiality of information can be broken retroactively by any adversary who has passively recorded handshakes and encrypted communications». Определение гибрида оттуда же, §1.1: две и более схем на разных предположениях, и итоговый ключ стоек, «as long as at least one of the component key exchange algorithms remains unbroken». Классическая половина страхует от изъяна в свежей решётчатой схеме, постквантовая — от будущей машины; убрать любую из них означает довериться одному предположению целиком.
Механика сборки проще, чем кажется, и различается между протоколами. В TLS (RFC 10024, Proposed Standard, август 2026, кодовая точка 0x11EC) клиентская доля — это ключ инкапсуляции ML-KEM, за которым идёт доля X25519; серверная — шифртекст ML-KEM и доля X25519; общий секрет получается простой конкатенацией двух 32-байтных секретов в 64 байта, без всякого хеширования. Хеширования там нет потому, что дальше секрет попадает на вход HKDF-Extract в key schedule, который и делает всю работу; RFC 9954, §6, отмечает, что конструкция из §3.3 соответствует dual-PRF-комбинатору и сохраняет стойкость при условии, что хеш-функция ведёт себя как dual-PRF, — и оговаривает границу применимости: «under the assumption that shared secrets are fixed length once the combination is fixed». Общее правило сформулировано в NIST SP 800-227, §4.6.2, и слово в слово про этот случай: «Any protocol using multi-algorithm KEMs with a concatenation key combiner should ensure that the final shared secret key from the key combiner is passed through a KDF before use».
В SSH (RFC 10042, IETF, информационный, август 2026) решение другое: K = HASH(K_PQ ‖ K_CL). Документ сам объясняет, чем это отличается от TLS, в §2.4 — «in [RFC9954], the classical and post-quantum exchanged secrets are concatenated and used in the key schedule, whereas in this document they are concatenated and hashed before being used in SSH's key derivation methodology». Причина — устройство вывода ключей в SSH, к которому мы вернёмся ниже: отдельного шага извлечения там нет, и его приходится изображать хешем.
Несимметричность перехода — то, что объясняет наблюдаемые умолчания. Ключ и подпись стареют по-разному. Записанное рукопожатие можно расшифровать через пятнадцать лет, а подпись подделывают в момент проверки: подделанная задним числом подпись к разговору, который давно закончился, никому не нужна. RFC 9958 «Post-Quantum Cryptography for Engineers» (июнь 2026) формулирует именно так: для аутентификации «signatures have a very short lifetime between signing and verifying (such as during a TLS handshake)», и оговаривает исключение — подпись прошивки или дистрибутива живёт десятилетиями и срочна наравне с шифрованием. Добавьте сюда, что миграция подписей тянет за собой сертификаты, удостоверяющие центры и HSM, а миграция обмена ключами не тянет ничего, кроме двух реализаций, — и картина умолчаний становится единственно возможной.
Она и наблюдается. Постквантовый обмен ключами включён по умолчанию: mlkem768x25519-sha256 в OpenSSH с версии 10.0, X25519MLKEM768 в OpenSSL с 3.5. Постквантовые подписи — нет: гибридный тип mldsa44-ed25519 появился в OpenSSH 10.4 и остаётся выключенным (подробности — в заметке про SSH, там же про предупреждение WarnWeakCrypto о непостквантовом обмене). Доля трафика следует за умолчаниями: Cloudflare сообщает о росте клиентской поддержки постквантового шифрования «from under 3% at the start of 2024, to over 60% in February 2026».
Отдельно стоит заметить, что гибридом занимаются не только асимметричные схемы. Необязательный preshared key в WireGuard подмешивается в цепочку ключей ровно с этой целью — авторы называют её «an additional layer of symmetric-key crypto… for, say, post-quantum resistance». Симметричный секрет квантовому компьютеру не по зубам, и 32 байта, розданные заранее, дают тот же эффект, что ML-KEM, — при условии, что раздать их есть как.
Сроки же, по которым всё это планируется, на дату сверки формально не утверждены: NIST IR 8547 «Transition to Post-Quantum Cryptography Standards» остаётся первоначальным публичным черновиком (ноябрь 2024, комментарии собраны в январе 2025); финальной редакции не опубликовано. Читать его таблицы 3 и 4 надо целиком, обе строки. Первая, которую обычно и цитируют: классика на 112 битах стойкости — «Deprecated after 2030, Disallowed after 2035». Вторая касается предмета этой статьи напрямую: «≥ 128 bits of security strength — Disallowed after 2035», без промежуточной стадии, то есть после 2035 года запрещаются и X25519, и P-256. Ссылаться на эти даты как на решённое нельзя.
Из одного секрета — много ключей
extract и expand — разные операции
Общий секрет использовать как ключ напрямую нельзя по двум независимым причинам, и их полезно не смешивать.
Первая — он неравномерен. Результат DH — это элемент группы, а не случайная битовая строка: у него есть структура, часть значений недостижима, распределение не плоское. RFC 5869, §3.3, говорит об этом без обиняков: «if the IKM is a Diffie-Hellman value… then the extract part SHOULD NOT be skipped. Doing so would result in using the Diffie-Hellman value g^{xy} itself (which is NOT a uniformly random or pseudorandom string) as the key PRK for HMAC». Именно эту работу делает HKDF-Extract: собирает энтропию, размазанную по неравномерному входу, в одну равномерную строку длиной с выход хеша.
Вторая — у секрета нет ролей. Ключей нужно много: свой в каждую сторону, отдельные для рукопожатия и для данных, отдельные для экспорта и возобновления. Это работа HKDF-Expand, который из одного псевдослучайного ключа разворачивает произвольное число выходов, различаемых меткой.
Разделение операций не декоративно: у них разные предположения и разные входы, и RFC 9846, §F.1.1, фиксирует порядок их применения как правило — «one SHOULD NOT use an output of HKDF-Extract as an input to another application of HKDF-Extract without an HKDF-Expand in between», тогда как многократный Expand от одних входов законен, «as long as these are differentiated via the key and/or the labels».
Пропускать extract можно ровно тогда, когда вход уже равномерен. Канонический пример — секрет ML-KEM: FIPS 203, §3.3, разрешает использовать его без всякой обработки, «can be used directly as a shared secret key for symmetric cryptography», потому что он выходит из хеш-функции, а не из группы. NIST SP 800-227, §4.3, возводит это в свойство всего класса: «KEMs by definition output a key K that is ready to use» — в отличие от схем вроде Диффи-Хеллмана, где вывод ключа приходится делать отдельным шагом. Практического значения это, впрочем, почти не имеет: в гибриде рядом с ним стоит неравномерный секрет X25519, и extract всё равно нужен.
Соль и метка: разделение доменов
У extract есть второй вход — соль, у expand — info. Оба необязательны в определении и оба важны.
Соль не секретна и может повторяться; её задача — развести применения хеш-функции между собой. RFC 5869, §3.1: она обеспечивает «independence between different uses of the hash function» и поддерживает извлечение, не зависящее от источника. Типовой приём — взять солью то, что и так известно обеим сторонам и уникально для сеанса: открытые доли, нонсы, текущее состояние ключевого расписания.
info отвечает за разделение доменов. RFC 5869, §3.2, объясняет, что его задача — привязать вывод к контексту, и что он «may prevent the derivation of the same keying material for different contexts (when the same input key material is used in such different contexts)». Без этого один и тот же секрет в двух ролях даёт один и тот же ключ, и материал начинает протекать между контекстами — а из этого растут и путаница между направлениями передачи, и приём подписи, сделанной для другой цели.
Три решения одной задачи
Один и тот же механизм, развёрнутый тремя способами. Смотреть здесь стоит не на формулы, а на то, чем управляется вывод.
TLS 1.3 (RFC 9846, §7.1) строит ключевое расписание конвейером: каждый новый секрет добавляется вызовом HKDF-Extract, где солью служит текущее состояние, а IKM — новый секрет. Из PSK получается Early Secret, из асимметричного секрета — Handshake Secret, дальше Main Secret; на каждом уровне Derive-Secret отводит именованные ветки. Ключевое в Derive-Secret — то, что контекстом ему служит хеш транскрипта:
Derive-Secret(Secret, Label, Messages) =
HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length)
То есть каждый рабочий ключ TLS привязан не только к секрету, но и ко всему разговору, который к нему привёл. Метки при этом — строки вида "c hs traffic", "s ap traffic", "key", "iv", и все они получают префикс "tls13 " внутри структуры HkdfLabel, что разводит домены между версиями и расширениями протокола.
WireGuard разворачивает то же самое в цепочку. Его KDF — три вызова HMAC-BLAKE2s подряд: temp = HMAC(chaining_key, input), затем chaining_key = HMAC(temp, 0x1) и при необходимости key = HMAC(temp, chaining_key ‖ 0x2). Это ровно HKDF: первый вызов — extract, где солью служит текущий chaining key, остальные — expand с однобайтовым счётчиком. Разница с TLS не в схеме, а в том, сколько раз она применяется: каждый DH, каждая эфемерная доля и preshared key по очереди вмешиваются в цепочку, и после последнего шага из неё вынимаются два транспортных ключа — свой на каждое направление. Транскрипт при этом ведётся отдельной переменной и уходит в AAD, а не в KDF.
age пользуется HKDF напрямую и показывает третий приём — соль как привязка к участникам. В блоке получателя ключ обёртки выводится с salt = ephemeral share ‖ recipient и info = "age-encryption.org/v1/X25519"; ключ полезной нагрузки выводится отдельно, с меткой "payload" (эта вторая формула уже разобрана в статье про AEAD, повторять её здесь незачем). Спецификация дополнительно требует свежий эфемерный секрет «for each stanza and each file» и прерывание, если общий секрет оказался нулевым.
| Что подаётся в extract | Чем солится | Чем разделяются роли | |
|---|---|---|---|
| TLS 1.3 | PSK, затем асимметричный секрет | текущее состояние расписания | метка "tls13 " + имя, контекст — хеш транскрипта |
| WireGuard | каждый DH и PSK по очереди | текущий chaining key | позиция в цепочке и счётчик 0x1/0x2 |
| age | общий секрет X25519 | эфемерная доля ‖ ключ получателя | info с URI протокола, отдельная метка на полезную нагрузку |
| SSH (RFC 4253) | extract отсутствует | — | буква "A"…"F" в хеше |
Строка про SSH здесь выбивается, и намеренно. Вывод ключей там — прямой хеш: HASH(K ‖ H ‖ "A" ‖ session_id) и так далее до "F" (RFC 4253, §7.2), где единственная буква разводит шесть ролей. Схема предшествует HKDF и несёт его недостатки: сам RFC рядом предупреждает, что «this process will lose entropy if the amount of entropy in K is larger than the internal state size of HASH». Оценка, данная этому решению в 2026 году, содержится в приложении A к RFC 10042: «it probably would be a good approach for SSH to move from basic hashing everywhere to use proper KDFs with extract/expand, but that should be a separate effort».
Вердикт для собственных протоколов и форматов: HKDF с непустыми солью и info, метка — строка, однозначно называющая протокол, версию, роль и направление. Самодельный вывод ключей хешированием конкатенации работает ровно до первой добавленной роли.
Где ломается
Три класса отказов, и ни один не про стойкость алгоритма.
Эфемерный ключ, который не эфемерен. «Эфемерный» — свойство дисциплины, а не имени переменной: пара генерируется на одно рукопожатие и уничтожается после. Сервер, кеширующий пару ради экономии, ломает прямую секретность и открывает дорогу атакам, которым нужно много обращений с одним и тем же ключом. Raccoon — именно этот случай: тайминговая атака на вывод ключей в TLS 1.2 требует, чтобы «the server uses ephemeral cipher suites (TLS-DHE) and reuses ephemeral keys for multiple connections». Ответ спецификации оказался запретом, и он свежий: RFC 9846 перечисляет «forbid the reuse of KeyShare values between connections» среди технических изменений относительно RFC 8446 и формулирует требование как «Clients and Servers MUST NOT reuse a key share for multiple connections» — с оговоркой, что принимающая сторона обязана переиспользование партнёром терпеть, поскольку прежняя редакция его разрешала. Если в коде эфемерный ключ живёт в поле объекта дольше одного рукопожатия, это ошибка независимо от алгоритма.
Отсутствие разделения доменов. Один ключ в двух ролях и подпись, принятая не в том контексте, для которого делалась, — самая дешёвая из возможных ошибок и самая частая. Против неё работают контекстная строка в CertificateVerify, префикс "tls13 " в HkdfLabel, URI протокола в info у age. Проверка своего протокола на этот дефект простая: выписать каждый выводимый ключ и каждую подпись вместе с полным набором входов и посмотреть, есть ли две записи, отличающиеся только намерением автора.
Сюда же относится менее очевидный случай — кодировка. RFC 10042, §5, объясняет, почему гибридные секреты в SSH кодируются байтовыми массивами фиксированной длины, а не целыми: переменная длина меняет число обрабатываемых блоков хеша и «could leak the most significant bit of the derived secret and/or allow detection of when the most significant bytes are zero», что уже приводило к Lucky Thirteen и Raccoon. Разделение доменов — это и про то, что входы KDF должны разбираться однозначно.
Инварианты X25519, которые не инварианты протокола. RFC 7748, §7, говорит прямо: «Protocol designers using Diffie-Hellman over the curves defined in this document must not assume "contributory behaviour"» — точка малого порядка на входе обнуляет вклад второй стороны, и обнаружить это можно только проверкой результата на ноль, которую §6.1 объявляет необязательной («Both MAY check… whether K is the all-zero value and abort if so»). Бернштейн считает проверку лишней для собственно Диффи-Хеллмана и нужной лишь «unusual non-Diffie-Hellman elliptic-curve protocols». Противоречия тут нет, есть разница в области действия: пока общий секрет служит только тем, чтобы разговаривать с партнёром, навязанное нулевое значение вредит одному противнику; как только протокол трактует знание секрета как доказательство чего-либо, проверка обязательна. Практика разошлась в пользу проверки — Go в crypto/ecdh возвращает ошибку на нулевой результат, спецификация age требует прервать работу, pyca/cryptography бросает исключение (проверено выше).
Второе предупреждение из того же §7 пропускают чаще: «for each public key, there are several publicly computable public keys that are equivalent to it, i.e., they produce the same shared secrets», и потому «using a public key as an identifier and knowledge of a shared secret as proof of ownership (without including the public keys in the key derivation) might lead to subtle vulnerabilities». Лечится это одной строкой — включить обе публичные доли в вывод ключей, что и делают age своей солью ephemeral share ‖ recipient, WireGuard хешем рукопожатия и SSH exchange hash'ем. Протокол, выводящий ключ из одного только общего секрета, этой защиты лишён.
Сводка решений
| Вопрос | Ответ |
|---|---|
| Конечнополевой DH в новой системе? | Нет. Только ради совместимости с тем, что не умеет иначе |
| Какая кривая по умолчанию? | X25519: нет ветвлений по секрету, нет валидации точки, нет формата для ошибок. NIST-кривые — только там, где их требует сертификация |
| Проверять входящие открытые ключи? | X25519 — нет; ML-KEM — да, проверка ключа инкапсуляции обязательна |
| Кто аутентифицирует стороны? | Не обмен ключами. PKI, заранее известный открытый ключ или PAKE |
| Что подписывать? | Транскрипт целиком, включая согласование алгоритмов, с контекстной строкой |
| Гибрид с ML-KEM? | Да для обмена ключами, уже сегодня и по умолчанию. Для подписей — нет, кроме долгоживущих |
| Секрет прямо в ключ? | Нет. extract, затем expand с меткой на каждую роль; пропускать extract можно только для равномерного входа |
Соль и info оставить пустыми? |
Нет. Соль — публичные доли или состояние расписания, info — протокол, версия, роль, направление |
| Кешировать эфемерную пару? | Нет; в TLS 1.3 это прямо запрещено |
Источники
- RFC 7748 — X25519 и X448: лестница Монтгомери и постоянное время (§5.1), клампинг и приём неканонических значений (§5), проверка на нулевой результат (§6.1), contributory behaviour и эквивалентные открытые ключи (§7)
- RFC 5869 — HKDF: роль соли (§3.1), роль
infoи разделение доменов (§3.2), почему extract нельзя пропускать для DH-значения (§3.3) - RFC 9846 — TLS 1.3 (июль 2026, отменяет RFC 8446): ключевое расписание (§7.1), CertificateVerify и контекстные строки (§4.5.2), запрет переиспользования key share (§1.2, §4.3.8), порядок extract и expand (§F.1.1)
- RFC 9954 — гибридный обмен ключами в TLS 1.3 (июль 2026): определение гибрида (§1.1), угроза «записать сейчас, расшифровать потом» (§1.2), конкатенация секретов (§3.3) и её обоснование через dual-PRF с оговоркой о фиксированной длине (§6)
- RFC 10024 — X25519MLKEM768 и родственные группы для TLS 1.3 (август 2026): состав долей, конкатенация секретов, обязательная проверка ключа инкапсуляции,
SecP256r1MLKEM768иSecP384r1MLKEM1024и порядок секретов в них под требования сертификации - RFC 10042 — гибридные методы обмена ключами для SSH (август 2026):
K = HASH(K_PQ ‖ K_CL)и отличие от TLS (§2.4), методы на NIST-кривых (§2.3), X25519 против NIST-кривых, кодировка секретов и побочные каналы (§5), разобранные альтернативы комбинирования (приложение A) - RFC 9958 — постквантовая криптография для инженеров (июнь 2026): асимметрия между срочностью перехода для шифрования и для подписей
- RFC 4253 — транспортный уровень SSH: вывод ключей по буквам
"A"…"F"(§7.2), exchange hash и подпись над ним (§8) - RFC 9142 — методы обмена ключами в SSH: стойкость MODP-групп (табл. 4),
diffie-hellman-group14-sha256как обязательный к реализации (§3.2.2) - RFC 9807 — OPAQUE, асимметричный PAKE (IRTF, июль 2025)
- draft-irtf-cfrg-cpace — CPace, сбалансированный PAKE; статус документа (ревизия 21, ещё не RFC)
- FIPS 203 — ML-KEM: MLWE и преобразование Фудзисаки—Окамото (§3.2), размеры ключей и шифртекстов (табл. 3), использование секрета без дополнительного вывода (§3.3), проверка ключа инкапсуляции (§7.2)
- NIST SP 800-227 — рекомендации по KEM (финал, сентябрь 2025): определение KEM, key agreement против key transport и статус ML-KEM (§2.2), готовность секрета к использованию (§4.3), одобренные комбинаторы ключей и требование KDF после конкатенации (§4.6.2), построение KEM из Диффи-Хеллмана (§5.1.1)
- NIST IR 8547 ipd — предполагаемые сроки вывода классических алгоритмов, таблицы 3 и 4; на дату сверки первоначальный публичный черновик
- Logjam / weakdh.org — предвычисление решета числового поля по простому числу, доля затронутых серверов
- Raccoon Attack — что даёт противнику переиспользование эфемерного ключа
- T. Jager, J. Schwenk, J. Somorovsky, Practical Invalid Curve Attacks on TLS-ECDH — цена пропущенной проверки принадлежности точки кривой (ESORICS 2015)
- D. J. Bernstein, Curve25519 — «How do I validate Curve25519 public keys? Don't», порядки кривой и твиста, оговорка про contributory behaviour
- D. J. Bernstein, Curve25519: new Diffie-Hellman speed records — проектные решения, из которых следует клампинг (§1), малые порядки и
n mod 8 = 0(§3), постоянные 255 удвоений в лестнице (§5) - C. Peikert, Lattice Cryptography for the Internet — примирение (reconciliation) как альтернативный путь к решётчатому обмену ключами
- WireGuard: Protocol & Cryptography — рукопожатие
Noise_IKpsk2, цепочка на BLAKE2s, транскрипт в AAD, preshared key как симметричный гибрид - Спецификация формата age — блок получателя X25519: соль из эфемерной доли и ключа получателя,
infoс URI, требование свежего эфемерного секрета - OpenSSH — release notes — 10.0 (
mlkem768x25519-sha256по умолчанию), 10.4 (гибридные подписи), 10.5 (11 августа 2026, последняя на дату сверки) - OpenSSL 3.5 release notes — поддержка ML-KEM и смена умолчаний по группам и ключевым долям (8 апреля 2025)
- Cloudflare: post-quantum to origins — разрезанный ClientHello, оссификация, доля не отвечающих origin-серверов, замеры производительности
- Cloudflare: Radar, origin PQ и key transparency — динамика доли клиентов с постквантовым обменом ключами (февраль 2026)
- Go,
crypto/ecdh— ошибка при нулевом результате X25519
Связанные заметки
- AEAD: аутентифицированное шифрование, nonce и лимиты — что происходит с ключом после того, как он выведен, и разбор Terrapin как примера неполной привязки к транскрипту
- ChaCha20 и Poly1305: устройство связки и почему она такая — что делает с выведенным здесь ключом самый распространённый AEAD: ARX, многочлен Poly1305 и конструкция RFC 8439
- SSH углублённо — умолчания OpenSSH по обмену ключами и подписям,
known_hostsкак реализация модели «ключ партнёра известен заранее» - Обратный прокси и TLS: nginx vs Caddy — где рукопожатие TLS заканчивается на практике и чей сертификат участвует в подписи транскрипта
- OpenVPN против WireGuard — два способа аутентифицировать туннель: PKI управляющего канала против статических ключей Noise
- Самохостинг за NAT: VPS как точка входа — рукопожатие WireGuard как наблюдаемый признак живого туннеля
- AES-GCM против ChaCha20-Poly1305: как выбирать — та же развилка «свой выбор против сертификации», но в симметричной части: список одобренных FIPS функций против RFC
- KDF для паролей: почему медленно и с памятью — зеркальная задача: HKDF выводит ключи из уже случайного секрета и обязан быть быстрым, парольный KDF — из человеческого пароля и обязан быть медленным
- Криптографические примитивы — план цикла статей — состав цикла, порядок статей и сквозные решения
Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.
Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.