Библиотека — Обмен ключами: X25519, KEM, вывод ключей и привязка к транскрипту [X]
Шифр
crypto-key-exchange
Разделы
network security
Сверено
13 сентября — в этот день утверждения статьи сверялись с первоисточниками, а не правился текст
Объём
46 801 знак · 17 разделов · чтения около 45 минут

Обмен ключами: X25519, KEM, вывод ключей и привязка к транскрипту


Откуда берётся ключ

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 заканчивается общим секретом с тем, кто был на другом конце. Кто это был, схема не сообщает, и противник посередине отрабатывает без всяких ухищрений: он проводит два независимых обмена, с каждой стороной свой, и дальше расшифровывает и перешифровывает. Обе стороны видят корректный обмен, оба секрета настоящие. Ошибки нет ни у кого — просто вопрос «с кем» не задавался.

Ответить на него можно тремя способами, и различаются они тем, что у сторон есть заранее.

Путь от эфемерных ключей к рабочим: слева и справа стороны, между ними канал; открытые доли пересекают канал, закрытые ключи и общий секрет не пересекают его никогда; всё пересёкшее накапливается в транскрипте, который подписывается или входит в вычисление тега; внизу общий секрет проходит extract и расходится через expand на ключи с разными метками
рассмотреть

Транскрипт и три способа его закрыть

Общее у всех трёх ответов одно: стороны считают транскрипт — хеш всего, что прошло по каналу с самого первого байта, — и привязывают к нему доказательство подлинности. Смысл именно в этом: подписывается не эфемерный ключ и не секрет, а весь разговор целиком, включая согласование алгоритмов. Иначе противник правит то, что не покрыто, а покрытое оставляет в покое.

Подпись транскрипта долгоживущим ключом — путь 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 роли расходятся, одна сторона генерирует пару и выполняет Decaps, вторая по чужому открытому ключу выполняет Encaps и получает шифртекст вместе с секретом, который никогда не покидает сторон
рассмотреть

В Диффи-Хеллмане роли сторон одинаковы: каждая генерирует пару и применяет свой закрытый ключ к чужому открытому. 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

Связанные заметки

Из библиотеки института уносят только знания. Читателей здесь не считают: посещение не регистрируется, отметок в издании не остаётся, счётчиков нет ни у одного издания фонда.

Издание пополняется и пересверяется. Текст доступен на условиях CC BY-SA 4.0 — унесённое можно передавать дальше на тех же условиях.