Библиотека — AEAD: аутентифицированное шифрование, nonce и откуда берутся лимиты [X]
Шифр
crypto-aead
Разделы
network security
Сверено
13 сентября — в этот день утверждения статьи сверялись с первоисточниками, а не правился текст
Объём
41 247 знаков · 25 разделов · чтения около 40 минут

AEAD: аутентифицированное шифрование, nonce и откуда берутся лимиты


Что обещает AEAD

AEAD — authenticated encryption with associated data — это один примитив вместо двух: он одновременно скрывает содержимое сообщения и гарантирует, что сообщение не подменили. Интерфейс описан в RFC 5116 и с тех пор не менялся: на вход идут ключ, nonce, открытый текст и «связанные данные» (AAD), на выход — шифртекст и тег аутентификации. Расшифровка либо возвращает открытый текст, либо возвращает ошибку; третьего состояния нет, и это главное свойство интерфейса.

Практическую сторону этого примитива видно везде, где в базе речь заходит про шифрованный канал. TLS 1.3 не умеет ничего, кроме AEAD-шифров. WireGuard построен ровно на одном AEAD и тратит на тег 16 из 80 байт своих накладных расходов. Data channel offload в OpenVPN принимает только AEAD-шифры. SSH после Terrapin научился сбрасывать счётчик пакетов — и это тоже про AEAD, точнее про то, чего AEAD не обещает. Эта статья разбирает механизм, общий для всех перечисленных случаев, чтобы соседние заметки могли на него ссылаться, а не пересказывать.

Обещание AEAD формулируется точно, и в нём важна каждая оговорка: при условии, что пара (ключ, nonce) не повторяется, и при условии, что объём зашифрованного под одним ключом не превысил определённого порога, противник не узнает ничего о содержимом, кроме длины, и не сможет предъявить принимающей стороне ни одного сообщения, которого отправитель не отправлял. Дальше по тексту разбирается, откуда берётся каждое из двух условий, что происходит при их нарушении и что делать, если обеспечить их не удаётся. Это первая статья цикла crypto-*; устройство конкретной связки ChaCha20 с Poly1305 и выбор между ней и AES-GCM — темы следующих двух, и вводимые здесь понятия они уже не переопределяют.


Почему шифрование без аутентификации не работает

Поточный шифр (и блочный в режиме счётчика, что то же самое) вырабатывает гамму — псевдослучайную последовательность, которая складывается с открытым текстом по XOR. Отсюда следует свойство, которое NIST в SP 800-38D, приложение A, называет пластичностью (malleability): переворот бита в шифртексте переворачивает ровно тот же бит в открытом тексте после расшифровки. Противнику не нужно знать ключ — ему достаточно догадываться о содержимом, чтобы менять это содержимое прицельно.

Это не теоретическая придирка, а рабочий механизм подмены, и проверяется он за минуту:

key=$(printf '%032x' 1); iv=$(printf '%032x' 0)
printf 'transfer 10 EUR' > plain.txt
openssl enc -aes-128-ctr -K "$key" -iv "$iv" -in plain.txt -out cipher.bin

# байт 9 открытого текста — единица; правим её на девятку прямо в шифртексте
python3 - <<'PY'
d = bytearray(open('cipher.bin', 'rb').read())
d[9] ^= ord('1') ^ ord('9')
open('cipher.bin', 'wb').write(d)
PY

openssl enc -d -aes-128-ctr -K "$key" -iv "$iv" -in cipher.bin   # transfer 90 EUR

Шифртекст изменён вслепую, ключ не понадобился, а получатель увидит корректный текст с другой суммой. Режим CBC ведёт себя иначе в деталях (правка блока портит его целиком и переворачивает биты в следующем), но вывод тот же: конфиденциальность без целостности не защищает ни от чего, кроме пассивного чтения.

Три способа склеить шифрование и MAC

Исторический ответ — добавить к шифру код аутентификации сообщения. Вариантов композиции ровно три, и они не равнозначны; систематически их разобрали Белларе и Намбемпре в «Authenticated Encryption: Relations among notions» (2000) и Кравчик в «The order of encryption and authentication» (2001).

Композиция Порядок Что аутентифицировано Где ломается
Encrypt-and-MAC шифруем текст, MAC считаем от открытого текста, отправляем оба открытый текст MAC детерминирован и вычислен от секрета — он сам утекает как признак равенства сообщений; проверка требует предварительной расшифровки
MAC-then-Encrypt MAC от открытого текста, затем шифруем текст вместе с MAC открытый текст получатель обязан расшифровать (и снять паддинг) до того, как узнает, подлинны ли данные — это и есть padding oracle
Encrypt-then-MAC шифруем, затем MAC от шифртекста шифртекст генерически стойка при стойких шифре и MAC

Кравчик доказал, что генерически безопасен только Encrypt-then-MAC: MAC-then-Encrypt, применявшийся в SSL и TLS до версии 1.2 включительно, стоек лишь при дополнительных предположениях о шифре, и на практике этих предположений не оказалось. Цену заплатили дважды: атака Воденэ на паддинг CBC (2002) и Lucky Thirteen (2013), где различие во времени проверки MAC — считанные микросекунды — восстанавливало открытый текст по таймингу. Лечение Lucky13 в реализациях состояло в том, чтобы сделать расшифровку MAC-then-Encrypt константной по времени, и это оказалось заметно сложнее, чем сменить порядок.

Вердикт здесь не «используйте Encrypt-then-MAC», а более сильный: не собирайте композицию руками. AEAD вытеснил ручную склейку не потому, что быстрее, — в нём попросту нет места, где можно ошибиться порядком, забыть включить в MAC заголовок или сравнить теги через memcmp. Порядок зафиксирован внутри примитива, а интерфейс из RFC 5116 не даёт вызывающему коду доступа к неаутентифицированному открытому тексту: у операции расшифровки, §2.2, «only a single output, either a plaintext value P or a special symbol FAIL» — состояния «вот открытый текст, но проверка не прошла» в интерфейсе попросту нет. Там, где ручная композиция всё же встречается — например, restic шифрует блобы AES-256-CTR и аутентифицирует их Poly1305-AES, — она сделана в правильном порядке, и это ровно тот случай, когда автор формата знал, что делает.


Анатомия интерфейса

У операции шифрования четыре входа и два выхода, и их роли различаются сильнее, чем кажется по списку аргументов.

Конвейер AEAD: открытый текст шифруется и попадает в шифртекст, AAD не шифруется и идёт открытым, тег считается от AAD, шифртекста, nonce и длин обеих частей; вне охвата остаются порядок сообщений и выбор ключа
рассмотреть

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

Что не покрыто ничем: порядок сообщений, их количество, факт пропажи сообщения и то, каким именно ключом сообщение расшифровывается. Первые три — предмет раздела про границы обещания, последнее — предмет раздела про key commitment.

Зачем в тег входят длины

Тег считается не от конкатенации «AAD ‖ шифртекст», а от структуры, куда явно дописаны длины обеих частей. В RFC 8439, §2.8, вход Poly1305 собирается так: AAD, выравнивание нулями до кратности 16 байт, шифртекст, снова выравнивание, затем длина AAD и длина шифртекста — каждая 64-битным little-endian числом. GCM в SP 800-38D делает то же самое блоком [len(A)]₆₄ ‖ [len(C)]₆₄.

Без этих двух чисел граница между AAD и шифртекстом становится подвижной. Противник, умеющий предъявить получателю другую разбивку той же байтовой последовательности, переносит байты из аутентифицируемой-но-открытой части в зашифрованную и обратно, не трогая тег. Длины делают разбивку частью аутентифицируемого сообщения. Это же объясняет, почему выравнивание нулями безопасно: сами нули в тег не вносят неоднозначности, поскольку длины уже зафиксированы.

Тег усекать нельзя, сравнивать надо в постоянном времени

Оба требования выглядят как советы реализатору, но записаны как нормативные требования спецификаций, и причина в том, что нарушение видно только снаружи — с той стороны, где сидит противник.

Усечение тега. RFC 8439, §4, формулирует прямо: «any protocol using this algorithm MUST include the complete tag … Tag truncation MUST NOT be done». У GCM история сложнее: NIST допускает теги 128, 120, 112, 104 и 96 бит, а 64 и 32 бита — только для отдельных приложений и с жёсткими ограничениями из приложения C на длину сообщений и время жизни ключа. Направление движения при этом не обсуждается, а объявлено: во втором предварительном запросе комментариев к SP 800-38D Rev. 1 (опубликован 1 июня 2026, комментарии принимались до 31 июля 2026; собственно черновика на дату сверки ещё нет) NIST пишет — «The revision to GCM will remove support for authentication tags whose lengths are less than 96 bits», — а комментариев просит лишь о том, какие из оставшихся длин (96, 104, 112, 120, 128) ещё нужны приложениям. Арифметика простая: усечение до t бит даёт противнику шанс 2^-t на попытку подделки, и вопрос лишь в том, сколько попыток протокол ему позволит — см. раздел про лимиты, где число попыток становится отдельным параметром.

Сравнение тегов. Наивное побайтовое сравнение выходит из цикла на первом расхождении, и время ответа выдаёт длину совпавшего префикса. RFC 8439, §4, разбирает это в терминах онлайновых протоколов: неудачная проверка тега не «расходует» nonce («nonces and AAD contents are not "used up" until a valid message is received»), поэтому противник может слать одно и то же сообщение с разными тегами сколько угодно, а утечка по времени, как сказано там же, значительно сокращает нужное число попыток — «the number of messages can be reduced significantly». Насколько именно, RFC не пишет, но арифметика простая: побайтовое сравнение превращает перебор 2¹²⁸ вариантов в 16 независимых переборов по 256. Отсюда «implementation MUST use a constant-time comparison function rather than … the C language's memcmp()». Это требование протокола ровно потому, что перебор доступен противнику снаружи: локальная «оптимизация» сравнения меняет стойкость всего канала.


Nonce: уникальность, а не непредсказуемость

Nonce не обязан быть секретным и не обязан быть непредсказуемым. Он обязан быть новым. Это единственное требование, и вся его нетривиальность в том, что обеспечивает его не криптография, а бухгалтерия вызывающей стороны.

Счётчик или случайность

Счётчик даёт гарантию уникальности бесплатно, пока состояние счётчика принадлежит ровно одному писателю и переживает все перезапуски. Случайный выбор гарантии не даёт вовсе — он даёт вероятность, и вероятность считается по парадоксу дней рождения: для nonce длиной n бит коллизия становится вероятной при примерно 2^(n/2) сообщениях, а точнее вероятность коллизии после q сообщений ≈ q²/2^(n+1).

Для стандартного 96-битного nonce это значит: 50% шанс повтора набирается на ~2⁴⁸ сообщений, а безопасный порог гораздо ниже. Отсюда прямо следует требование NIST в SP 800-38D, §8.3, и область его действия шире разбираемого случая: под него попадает и детерминированная конструкция, если длина IV не 96 бит, и конструкция на генераторе случайных чисел при любой длине IV — не покрыт только детерминированный 96-битный IV. Для всех прочих «the total number of invocations of the authenticated encryption function shall not exceed 2³²» на ключ — это ровно то число, при котором вероятность повтора укладывается в заявленный в §8 потолок 2⁻³². Четыре миллиарда сообщений — много для веб-сервера и мало для чего-нибудь, что шифрует записи в потоке.

Для 192-битного nonce (XChaCha20) тот же расчёт даёт ~2⁹⁶ сообщений до вероятной коллизии, то есть порог уходит за границу достижимого. Именно в этом ценность расширенного nonce: он не делает случайность лучше, он делает её достаточной.

Вердикт: счётчик, когда состояние принадлежит одному писателю и надёжно сохраняется; случайный nonce — только при 192 битах либо в misuse-resistant режиме. Случайный 96-битный nonce — законный вариант лишь с явным учётом лимита в 2³² сообщений на ключ, и если такой учёт в системе никто не ведёт, вариант незаконный.

Кто владеет счётчиком

Владелец счётчика — это то, что стоит определить до выбора алгоритма, и на практике вариантов три.

Протокол с рукопожатием владеет счётчиком сам и обычно не даёт реализации к нему доступа. TLS 1.3 (RFC 9846, §5.3) строит per-record nonce как 64-битный номер записи, дополненный слева нулями и сложенный по XOR со статическим client_write_iv/server_write_iv; номер обнуляется при каждой смене ключа, и первая запись под новым ключом обязана иметь номер 0. Ошибиться тут нечем — реализация вообще не выбирает nonce. Это прямая реакция на TLS 1.2, где 64-битную явную часть nonce выбирала реализация, и спецификация не объясняла как; чем это кончилось, разбирает следующий раздел.

WireGuard владеет счётчиком явно: в транспортном пакете nonce — это 32 нулевых бита и 64-битный little-endian счётчик, который «cannot be wound backward». Перевыработка ключей начинается после REKEY_AFTER_MESSAGES = 2⁶⁰ сообщений, а пакеты со счётчиком выше REJECT_AFTER_MESSAGES (в реализации в ядре Linux — U64_MAX − COUNTER_WINDOW_SIZE − 1) отбрасываются. Показательно, что в том же протоколе для cookie-ответов используется XChaCha20-Poly1305 со случайным 24-байтным nonce: там отвечающая сторона умышленно не хранит состояния, счётчика у неё нет, и расширенный nonce покупает ровно эту возможность.

Файловый формат владеет счётчиком в пределах файла. Формат age режет полезную нагрузку на чанки по 64 КиБ и шифрует каждый ChaCha20-Poly1305 с 12-байтным nonce, где первые 11 байт — big-endian счётчик чанков с нуля, а последний байт равен 0x01 для финального чанка и 0x00 для всех прочих. Ключ полезной нагрузки при этом свой на файл: HKDF-SHA-256(ikm = file key, salt = nonce, info = "payload"), где nonce — 16 случайных байт в заголовке. Счётчик начинается с нуля в каждом файле, потому что ключ в каждом файле новый.

Где эта бухгалтерия ломается

Три сценария, и все три — не про криптографию.

Параллельные отправители под одним ключом. Два процесса, две ноды за балансировщиком, два потока с общим ключом — каждый ведёт свой счётчик с нуля, и все nonce совпадают. RFC 5116, §3.1, предписывает единственное разумное решение: выделить в nonce поле, различающее устройства, — либо, если координация невозможна, «changing a key in a situation in which nonce coordination is difficult». Смена ключа дешевле координации почти всегда.

Восстановление состояния. Снепшот виртуальной машины, откат тома, потеря питания, форк процесса с унаследованным состоянием генератора — все они возвращают счётчик к уже использованному значению. RFC 5116 требует чекпойнтить nonce в энергонезависимую память, причём с запасом вперёд: сохранить значение 100 и после этого использовать 1…99, чтобы после сбоя гарантированно продолжить с неиспользованного места. NIST в §9.1 формулирует то же как требование: «When power is restored, neither the preceding IV nor any other previous IV shall immediately be repeated for the key». Практический вывод для виртуализации: снепшот работающей VM, шифрующей поток с длинноживущим ключом, — операция, после которой ключ надо считать испорченным, а не просто откатить машину.

Случайный nonce без учёта объёма. Это тот же сбой, но растянутый: никто ничего не ломал, просто сообщений стало больше, чем предполагал расчёт.


Цена повтора nonce различается на порядок

«Повторять nonce нельзя» верно для любого AEAD, но последствия у разных схем несопоставимые, и это довод при выборе, а не деталь реализации.

Что общее на весь ключ и что своё у каждого nonce: в AES-GCM ключ аутентификации H один на весь ключ, поэтому повтор nonce отдаёт H и ломает подлинность всего соединения; в ChaCha20-Poly1305 одноразовый ключ Poly1305 выводится из пары ключ-nonce, и повтор ломает только этот nonce
рассмотреть

AES-GCM: восстановление ключа аутентификации

GCM аутентифицирует через GHASH — многочлен над GF(2¹²⁸), коэффициенты которого суть блоки AAD и шифртекста, а точка вычисления — подключ H = E_K(0¹²⁸). Ключевое здесь то, что H зависит только от ключа: он один и тот же для всех сообщений, сколько бы их ни было.

Атака, известная как forbidden attack, описана Антуаном Жу в комментарии к стандартизации GCM (2006). Механизм: два сообщения под одним nonce дают два многочлена, у которых слагаемое от nonce одинаково; сложив их, противник получает многочлен с полностью известными коэффициентами, корнем которого является H. Разложение многочлена над GF(2¹²⁸) — дешёвая операция, кандидатов немного, а второй повтор nonce отсекает лишних.

Практические последствия довели до конца Бёк, Цаунер, Девлин, Соморовский и Йованович в Nonce-Disrespecting Adversaries (WOOT 2016). Их вывод: «a single repeated nonce is usually enough to fully recover the connection's authentication key». Дальше, зная H и имея любую подлинную запись, противник снимает с её тега вычислимый теперь GHASH, получает маску E_K(J₀) для этого nonce и выписывает корректный тег к произвольному шифртексту. Каким при этом окажется открытый текст, он тоже выбирает, но в границах, которые называет NIST в приложении A к SP 800-38D: «knowing the original plaintext that corresponds to the given ciphertext, the adversary can construct the substitute ciphertext in such a way as to correspond to any desired plaintext of the same length» — нужен известный открытый текст наблюдавшейся записи, и подмена ограничена её длиной. Радиус поражения — соединение целиком, и это формулировка самих авторов: в абстракте повтор nonce «fully breaks the authenticity of the connections», в §3.2 — «authenticity is lost and an attacker is able to manipulate TLS-protected content». Сканирование сети нашло 184 HTTPS-сервера, действительно повторявших nonce, — по описанию авторов, среди них крупные корпорации, финансовые организации и компания — эмитент кредитных карт, — и больше 70 000 серверов, выбиравших 64-битную явную часть nonce случайно, то есть попадавших под риск при достаточном объёме одного соединения.

ChaCha20-Poly1305: ущерб ограничен одним nonce

Poly1305 — одноразовый аутентификатор в духе Вегмана–Картера: пара (r, s) обязана быть уникальной и непредсказуемой на каждый вызов. В связке из RFC 8439 эта пара берётся из первого блока гаммы ChaCha20 для пары (ключ, nonce) — то есть выводится заново для каждого nonce.

Отсюда разница. RFC 8439, §4, описывает последствие повтора так: совпадают и гамма, и одноразовый ключ Poly1305, поэтому раскрывается XOR открытых текстов (конфиденциальность) и перестаёт работать одноразовость MAC (подлинность). Но восстановленная пара (r, s) относится только к этому nonce: для любого другого nonce она выводится из другого блока гаммы и с точки зрения противника независима. Мастер-ключ при этом не компрометируется ни в одной из схем — но в GCM компрометируется подключ, общий на весь ключ, а здесь — материал, общий только внутри одного nonce.

Вердикт: если конструкция системы не позволяет доказать уникальность nonce — а она не позволяет всюду, где есть параллельные писатели, снепшоты или случайные 96-битные nonce, — AES-GCM в ней стоит дороже, чем ChaCha20-Poly1305, при равных прочих. Это не отменяет запрета на повтор: ChaCha20-Poly1305 при повторе тоже сломана, просто локально.


Откуда берутся лимиты на объём под одним ключом

Второе условие обещания AEAD — ограничение на объём. Его часто подают списком чисел, но числа вторичны: они следуют из того, чем именно является нижележащий примитив, и это структурное различие переживёт любую ревизию таблиц.

Перестановка против функции

AES — псевдослучайная перестановка на 128-битном блоке. Перестановка биективна, поэтому выходы на различных входах всегда различны. Гамма AES-CTR — это выходы E_K на последовательности различных счётчиков, то есть блоки гаммы попарно различны по построению. Настоящая же случайная последовательность блоки повторяет: по парадоксу дней рождения коллизия среди q 128-битных блоков появляется с вероятностью около q²/2¹²⁹. Отсутствие повторов — и есть отличие от случайности, и различитель набирает заметное преимущество к 2⁶⁴ блокам. Черновик CFRG «Usage Limits on AEAD Algorithms» (ревизия 13, сентябрь 2026) записывает это как CA ≤ (s + q + 1)² / 2¹²⁹.

ChaCha20 — псевдослучайная функция на 512-битном состоянии. Функция не обязана быть биективной, и у ChaCha20 она таковой и не является: раундовая часть обратима, но результат складывается с исходным состоянием (RFC 8439, §2.3: «At the end of 20 rounds … we add the original input words to the output words»), и эта обратная связь убивает обратимость. Коллизии в выходах допустимы и нормальны, различать по их отсутствию нечего, а если считать по размеру блока — порог был бы на 2²⁵⁶. Тот же черновик CFRG в §6.3.1 пишет для AEAD_CHACHA20_POLY1305 буквально CA ≤ 0: «there is no limit beyond the PRF security of the underlying ChaCha20 block function».

Ограничение на подделки остаётся у обеих схем, потому что это другая величина: не сколько зашифровано, а сколько раз противнику позволили не угадать тег. Для ChaCha20-Poly1305 черновик даёт IA ≤ (v · (L' + 1)) / 2¹⁰³.

Как это выглядит в протоколе

Прямое следствие видно в TLS 1.3. RFC 9846, §5.5 (июль 2026; этот документ отменяет RFC 8446, и формулировка в нём стала строже — было SHOULD do a key update, стало MUST either close the connection or do a key update):

For AES-GCM, up to 2^24.5 full-size records (about 24 million) may be encrypted under a given set of keys while keeping a safety margin of approximately 2^-57 for Authenticated Encryption (AE) security. For ChaCha20/Poly1305, the record sequence number would wrap before the safety limit is reached.

То есть для одной из двух схем лимит достижим на длинной загрузке, а для другой раньше кончится 64-битный номер записи. Ради первой в TLS 1.3 и существует KeyUpdate — сообщение, после которого обе стороны выводят новый traffic key и обнуляют номер записи. Тот же RFC добавляет два уточнения, каждое из которых логично: перевыработать ключ для early data нельзя, поэтому лимит там нарушать запрещено вовсе, а принимающей стороне лимиты навязывать не рекомендуется — «future analyses may result in updated values», и жёсткая проверка на приёме превратила бы уточнение оценок в несовместимость.

QUIC (RFC 9001, §6.6) считает два лимита раздельно, и причина различия структурная, а не численная. TLS работает поверх TCP и закрывает соединение при первой же неудачной проверке тега — число попыток подделки там равно единице по построению. QUIC неаутентифицируемые пакеты молча отбрасывает, поэтому попытки накапливаются, и их приходится считать отдельно: для AES-GCM это 2²³ зашифрованных пакетов и 2⁵² неудачных расшифровок, для ChaCha20-Poly1305 лимит на шифрование больше числа возможных пакетов и игнорируется, а лимит на подделки — 2³⁶.

Почему в файловом формате лимит выглядит иначе

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

Отсюда типовое решение, видное в age: ключ полезной нагрузки выводится заново для каждого файла — из случайного file key, который спецификация требует брать как 16 байт вывода CSPRNG и прямо запрещает переиспользовать между файлами, и из случайного nonce в заголовке (формула приведена выше, в разделе про владельца счётчика), а внутри файла счётчик чанков начинается с нуля. Долгоживущий ключ получателя при этом данные не шифрует вовсе: он только оборачивает file key в заголовке. Поэтому «сколько записей под одним ключом» через файлы не накапливается — общего ключа шифрования у двух файлов нет по построению, — и остаётся единственный лимит: сколько чанков помещается в один файл (ограничено шириной счётчика — 11 байт по 64 КиБ, то есть недостижимо). Вывод ключа заменяет перевыработку.

Второе отличие тоньше и чаще пропускается. Лимит на подделки — величина про онлайновый оракул: она считает, сколько раз противник может предъявить системе поддельный шифртекст и увидеть результат. Файл на диске оракула не даёт, и v фактически равно единице. Но как только над тем же форматом появляется сервис, принимающий чужие блобы и отвечающий «не расшифровалось», оракул возвращается, и лимит на подделки снова надо считать — по формулам для протокола, а не для файла.

Протокол (TLS, QUIC, WireGuard) Файловый формат (age, репозиторий бэкапа)
Что ограничивает объём число записей под traffic key ширина счётчика чанков внутри файла
Как снимается ограничение перевыработка ключа в канале (KeyUpdate, новое рукопожатие) свой случайный ключ на каждый файл, из него — ключ полезной нагрузки
Лимит на подделки считается: TLS закрывает соединение, QUIC накапливает попытки по умолчанию неприменим — нет оракула; появляется вместе с сервисом расшифровки
Кто ведёт nonce протокол, номер записи или счётчик пакетов формат, счётчик чанков от нуля в каждом файле

Ответы на неизбежность ошибок

Требование «не повторять nonce» выполнимо, но опирается на дисциплину состояния, а состояние теряется. Существуют три разных по цене ответа на этот факт.

Расширенный nonce

XChaCha20-Poly1305 берёт 24-байтный nonce, отдаёт первые 16 байт вместе с ключом в HChaCha20 и получает 256-битный подключ, которым дальше работает обычный ChaCha20-Poly1305 с оставшимися 8 байтами nonce — дописав к ним слева четыре нулевых байта до штатных двенадцати (draft-irtf-cfrg-xchacha-03, §2: «prefixed with 4 NUL bytes»), так что менять сам ChaCha20 не приходится. HChaCha20 — это ровно одна перестановка ChaCha20 без финального сложения (там же, §2.2), то есть один дополнительный вызов блочной функции на сообщение — добавка фиксированная и от длины сообщения не зависящая. Оттого и заметность её разная: в пакете на полторы тысячи байт гамма занимает два десятка блоков, и лишний блок в них теряется, а на сообщении в один блок он удваивает работу блочной функции. Это арифметика по числу вызовов; замеров производительности здесь не делалось.

Цена предсказуемая, выигрыш прямой: 192-битный nonce делает случайный выбор безопасным без учёта количества сообщений. Оговорка, которую надо назвать вслух: draft-irtf-cfrg-xchacha остановился на ревизии 03, истёк в июле 2020 и помечен в datatracker как Dead IRTF Document — на дату сверки конструкция так и не стала RFC. При этом она реализована в libsodium, используется в age и в cookie-ответах WireGuard; то есть де-факто это стандарт без документа, и ссылаться в спецификации своего формата придётся на истёкший черновик либо на реализацию.

Misuse-resistant режимы

AES-GCM-SIV (RFC 8452, апрель 2019) меняет обещание: при повторе nonce он раскрывает только факт равенства открытых текстов — «the minimum amount of information that a deterministic algorithm can leak in this situation». Достигается это тем, что тег вычисляется до шифрования и служит начальным значением счётчика, а ключи шифрования и хеширования выводятся из пары (ключ, nonce).

Цена названа в самом RFC: «Encryption is, perforce, slower than AES-GCM, because two passes are required». Два прохода означают не столько потерю скорости (расшифровка — в пределах 5% от AES-GCM, шифрование — около двух третей), сколько потерю потоковости: чтобы посчитать тег, нужен весь открытый текст целиком. Плюс §9 требует не отдавать наружу непроверенный открытый текст, а расшифровка SIV его сначала получает — то есть весь открытый текст должен поместиться в память. Для сетевого потока это неприемлемо, для шифрования записей и конвертов — нормально.

Отдельная строка того же жанра — draft-gueron-cfrg-dndkgcm (DNDK-GCM, ревизия 04, март 2026, индивидуальный черновик без статуса рабочего документа CFRG): выводить ключ AES-GCM из 24-байтного случайного nonce, то есть применить к GCM тот же приём, что XChaCha применяет к ChaCha20, и получить обязательство по ключу — но не заодно: в черновике это опция («optionally to provide for key commitment»), заданная отдельным параметром, и определены варианты как с обязательством, так и без него. Смотреть на него стоит как на направление работ, а не как на то, что можно включить сегодня.

Key commitment

AEAD не обещает, что шифртекст расшифровывается только под одним ключом. Это не дефект реализации, а граница определения: стойкость AEAD формулируется относительно противника, который ключа не знает, а здесь ключи выбирает как раз он. Для GCM и ChaCha20-Poly1305 предъявить один шифртекст, корректно расшифровывающийся под двумя разными ключами в два разных осмысленных текста, — решаемая задача.

Практические последствия возникают там, где ключ выбирается противником или имеет низкую энтропию: шифрование по паролю (ускоряет перебор, превращая его в подобие двоичного поиска), конвертное шифрование с несколькими получателями, message franking, PAKE.

Статус темы на дату сверки: терминология зафиксирована, алгоритмы — нет. RFC 9771 «Properties of AEAD Algorithms» (IRTF, информационный, май 2025) вводит два уровня — key commitment (обязательство по ключу, нотация CMT-1) и full commitment (обязательство по всем входам, CMT-4), явно отмечая, что второе влечёт первое, но не наоборот. Обязательного к применению алгоритма с этим свойством ни в TLS, ни в SSH не появилось. Из кандидатов ближе всех к публикации AEGIS (ревизия 18, в очереди RFC-редактора на дату сверки): он заявляет key commitment в receiver-binding game и full commitment в ограниченной постановке, когда AAD противнику не подконтролен, — а для снятия ограничения предлагает подавать в AAD хеш реальных связанных данных. Ascon RFC 9771 приводит как пример полностью обязующей схемы.

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

Ответ Что покупает Чем платит Когда брать
XChaCha20-Poly1305 безопасный случайный nonce один вызов блочной функции; конструкция не стандартизована по умолчанию, когда счётчик вести негде
AES-GCM-SIV повтор nonce не катастрофичен два прохода, нет потоковости, весь текст в памяти записи и конверты, где уникальность не доказывается
Обязательство по ключу шифртекст привязан к ключу своими руками через AAD; готовых умолчаний нет пароли, конверты, несколько получателей

Границы обещания: сообщение, а не последовательность

AEAD защищает сообщение. О том, что было до и после этого сообщения, он не знает ничего: ни что это сообщение третье, ни что первого не было вовсе, ни что предыдущие пять пришли в другом порядке. Каждый корректный тег говорит только «этот текст с этим AAD зашифрован под этим ключом и этим nonce». Целостность последовательности — работа протокола, и делается она одним приёмом: то, что должно быть зафиксировано, вносится в аутентифицируемый вход — в AAD или в сам nonce.

Показательный случай — Terrapin (CVE-2023-48795). В SSH номер пакета входит в вычисление тега, но сам номер ни к чему не привязан: он просто считается с нуля. Противник посередине добавляет пакеты в незашифрованную часть рукопожатия и удаляет столько же сразу после включения шифрования — номера сходятся, теги проверяются, обе стороны ничего не замечают. Наиболее серьёзное применение по описанию авторов и релиз-ноутам OpenSSH 9.6 (18 декабря 2023) — удаление SSH2_MSG_EXT_INFO и отключение части защит, согласуемых этим сообщением. Уязвимы были chacha20-poly1305@openssh.com и CBC в связке с ETM-режимами MAC.

Существенно здесь то, что ни один AEAD-алгоритм в этой истории не подвёл: и ChaCha20-Poly1305, и Encrypt-then-MAC отработали ровно по спецификации. Лечится это на уровне протокола — strict KEX сбрасывает счётчики пакетов после обмена ключами и запрещает посторонние сообщения во время рукопожатия, то есть привязывает нумерацию к транскрипту. Подмена алгоритма на «более стойкий» не дала бы ничего.

Ось SSH-соединения с границей включения шифрования: сквозной счётчик пакетов границу пересекает и не сбрасывается, поэтому вставка пакетов противником до границы и удаление такого же количества сразу после неё оставляют номера у сторон согласованными и теги проверяемыми; strict KEX сбрасывает счётчик на границе и запрещает посторонние сообщения во время обмена ключами
рассмотреть

Тот же вопрос в файловом формате звучит как «что мешает переставить, повторить или отрезать чанки». Ответ должен быть столь же конкретным. В age номер чанка входит в nonce, поэтому перестановка не пройдёт проверку тега; признак последнего чанка — отдельный байт в том же nonce, поэтому обрезка файла обнаруживается; и спецификация дополнительно требует: «Streaming decryption MUST signal an error if the end of file is reached without successfully decrypting a final chunk». Формат, у которого последний чанк ничем не отличается от прочих, допускает молчаливое усечение — и это ошибка проектирования формата, а не выбора шифра.

Практическая проверка, применимая к любому формату и протоколу: выписать, что именно попадает в тег, и посмотреть, чего в этом списке нет. Версия формата, идентификатор получателя, номер чанка, признак конца, роль стороны — всё, что не попало в AAD или в nonce, противник может менять.


Сводка решений

Вопрос Ответ
Собирать шифрование и MAC руками? Нет. AEAD; ручная композиция только Encrypt-then-MAC и только с очень веской причиной
Счётчик или случайный nonce? Счётчик, если состояние принадлежит одному писателю и переживает перезапуск; иначе 192-битный nonce
Случайный 96-битный nonce? Только с явным учётом лимита 2³² сообщений на ключ
Что делать при риске повтора nonce? XChaCha20-Poly1305 для потока, AES-GCM-SIV для записей целиком в памяти
Усекать тег? Нет
Кто следит за лимитами объёма? Протокол — перевыработкой ключа; файловый формат — выводом ключа на файл
Кто защищает порядок сообщений? Протокол или формат, через AAD и nonce. Не алгоритм

Источники


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

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

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