---
tags: [security, network]
verified: 2026-09-13
---

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

## Что обещает AEAD

AEAD — authenticated encryption with associated data — это один примитив вместо двух: он одновременно скрывает содержимое сообщения и гарантирует, что сообщение не подменили. Интерфейс описан в [RFC 5116](https://www.rfc-editor.org/rfc/rfc5116.html) и с тех пор не менялся: на вход идут ключ, 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](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf), приложение A, называет пластичностью (malleability): переворот бита в шифртексте переворачивает ровно тот же бит в открытом тексте после расшифровки. Противнику не нужно знать ключ — ему достаточно догадываться о содержимом, чтобы менять это содержимое прицельно.

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

```bash
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»](https://eprint.iacr.org/2000/025) (2000) и Кравчик в [«The order of encryption and authentication»](https://eprint.iacr.org/2001/045) (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](https://www.isg.rhul.ac.uk/tls/Lucky13.html) (2013), где различие во времени проверки MAC — считанные микросекунды — восстанавливало открытый текст по таймингу. Лечение Lucky13 в реализациях состояло в том, чтобы сделать расшифровку MAC-then-Encrypt константной по времени, и это оказалось заметно сложнее, чем сменить порядок.

Вердикт здесь не «используйте Encrypt-then-MAC», а более сильный: не собирайте композицию руками. AEAD вытеснил ручную склейку не потому, что быстрее, — в нём попросту нет места, где можно ошибиться порядком, забыть включить в MAC заголовок или сравнить теги через `memcmp`. Порядок зафиксирован внутри примитива, а интерфейс из [RFC 5116](https://www.rfc-editor.org/rfc/rfc5116.html) не даёт вызывающему коду доступа к неаутентифицированному открытому тексту: у операции расшифровки, §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 и длин обеих частей; вне охвата остаются порядок сообщений и выбор ключа](./assets/crypto-aead-interface.svg)

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

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

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

Тег считается не от конкатенации «AAD ‖ шифртекст», а от структуры, куда явно дописаны длины обеих частей. В [RFC 8439](https://www.rfc-editor.org/rfc/rfc8439.html), §2.8, вход Poly1305 собирается так: AAD, выравнивание нулями до кратности 16 байт, шифртекст, снова выравнивание, затем длина AAD и длина шифртекста — каждая 64-битным little-endian числом. GCM в [SP 800-38D](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf) делает то же самое блоком `[len(A)]₆₄ ‖ [len(C)]₆₄`.

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

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

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

Усечение тега. [RFC 8439](https://www.rfc-editor.org/rfc/rfc8439.html), §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](https://www.rfc-editor.org/rfc/rfc8439.html), §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](https://www.rfc-editor.org/rfc/rfc9846.html), §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](https://www.rfc-editor.org/rfc/rfc5116.html), §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](./assets/crypto-aead-nonce-reuse.svg)

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

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

Атака, известная как forbidden attack, описана Антуаном Жу в [комментарии к стандартизации GCM](https://csrc.nist.gov/CSRC/media/Projects/Block-Cipher-Techniques/documents/BCM/Comments/800-38-Series-Drafts/GCM/Joux_comments.pdf) (2006). Механизм: два сообщения под одним nonce дают два многочлена, у которых слагаемое от nonce одинаково; сложив их, противник получает многочлен с полностью известными коэффициентами, корнем которого является `H`. Разложение многочлена над GF(2¹²⁸) — дешёвая операция, кандидатов немного, а второй повтор nonce отсекает лишних.

Практические последствия довели до конца Бёк, Цаунер, Девлин, Соморовский и Йованович в [Nonce-Disrespecting Adversaries](https://eprint.iacr.org/2016/475.pdf) (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](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf): «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»](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/) (ревизия 13, сентябрь 2026) записывает это как `CA ≤ (s + q + 1)² / 2¹²⁹`.

ChaCha20 — псевдослучайная функция на 512-битном состоянии. Функция не обязана быть биективной, и у ChaCha20 она таковой и не является: раундовая часть обратима, но результат складывается с исходным состоянием ([RFC 8439](https://www.rfc-editor.org/rfc/rfc8439.html), §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](https://www.rfc-editor.org/rfc/rfc9846.html), §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](https://www.rfc-editor.org/rfc/rfc9001.html), §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](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03), §2: «prefixed with 4 NUL bytes»), так что менять сам ChaCha20 не приходится. HChaCha20 — это ровно одна перестановка ChaCha20 без финального сложения (там же, §2.2), то есть один дополнительный вызов блочной функции на сообщение — добавка фиксированная и от длины сообщения не зависящая. Оттого и заметность её разная: в пакете на полторы тысячи байт гамма занимает два десятка блоков, и лишний блок в них теряется, а на сообщении в один блок он удваивает работу блочной функции. Это арифметика по числу вызовов; замеров производительности здесь не делалось.

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

### Misuse-resistant режимы

AES-GCM-SIV ([RFC 8452](https://www.rfc-editor.org/rfc/rfc8452.html), апрель 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](https://datatracker.ietf.org/doc/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](https://www.rfc-editor.org/rfc/rfc9771.html) «Properties of AEAD Algorithms» (IRTF, информационный, май 2025) вводит два уровня — key commitment (обязательство по ключу, нотация CMT-1) и full commitment (обязательство по всем входам, CMT-4), явно отмечая, что второе влечёт первое, но не наоборот. Обязательного к применению алгоритма с этим свойством ни в TLS, ни в SSH не появилось. Из кандидатов ближе всех к публикации [AEGIS](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aegis-aead/) (ревизия 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](https://www.openssh.com/txt/release-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 сбрасывает счётчик на границе и запрещает посторонние сообщения во время обмена ключами](./assets/crypto-aead-terrapin.svg)

Тот же вопрос в файловом формате звучит как «что мешает переставить, повторить или отрезать чанки». Ответ должен быть столь же конкретным. В 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. Не алгоритм |

---

## Источники

- [RFC 5116](https://www.rfc-editor.org/rfc/rfc5116.html) — интерфейс AEAD, требования к генерации nonce, чекпойнтинг состояния
- [RFC 8439](https://www.rfc-editor.org/rfc/rfc8439.html) — ChaCha20 и Poly1305, конструкция AEAD с длинами, запрет усечения тега, сравнение в постоянном времени
- [RFC 9846](https://www.rfc-editor.org/rfc/rfc9846.html) — TLS 1.3 (июль 2026, отменяет RFC 8446): per-record nonce (§5.3), лимиты на использование ключа (§5.5), `KeyUpdate`
- [RFC 9001](https://www.rfc-editor.org/rfc/rfc9001.html) — QUIC: раздельные лимиты на шифрование и на неудачные расшифровки (§6.6)
- [RFC 8452](https://www.rfc-editor.org/rfc/rfc8452.html) — AES-GCM-SIV: nonce misuse resistance, два прохода, границы по объёму
- [RFC 9771](https://www.rfc-editor.org/rfc/rfc9771.html) — свойства AEAD: определения key commitment (CMT-1) и full commitment (CMT-4)
- [draft-irtf-cfrg-aead-limits](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/) — вывод лимитов: `CA ≤ 0` для ChaCha20-Poly1305, birthday-граница для AES-GCM; формулы цитируются по [тексту ревизии 13](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-aead-limits-13), ссылка выше всегда ведёт на последнюю
- [draft-irtf-cfrg-xchacha](https://datatracker.ietf.org/doc/draft-irtf-cfrg-xchacha/) — XChaCha20 и HChaCha20: [текст ревизии 03](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03) и статус документа (истёк, Dead IRTF Document)
- [draft-irtf-cfrg-aegis-aead](https://datatracker.ietf.org/doc/draft-irtf-cfrg-aegis-aead/) — AEGIS: заявленное обязательство по ключу и его границы
- [NIST SP 800-38D](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf) — GCM: требование уникальности IV, лимит 2³² при случайном IV, допустимые длины тега, пластичность CTR (приложение A)
- [Второй предварительный запрос комментариев к SP 800-38D Rev. 1](https://csrc.nist.gov/pubs/sp/800/38/d/r1/2prd) — объявленное удаление тегов короче 96 бит (июнь 2026)
- [A. Joux, Authentication failures in NIST version of GCM](https://csrc.nist.gov/CSRC/media/Projects/Block-Cipher-Techniques/documents/BCM/Comments/800-38-Series-Drafts/GCM/Joux_comments.pdf) — forbidden attack
- [Nonce-Disrespecting Adversaries](https://eprint.iacr.org/2016/475.pdf) — практические подделки в TLS, результаты сканирования сети (WOOT 2016)
- [M. Bellare, C. Namprempre, Authenticated Encryption](https://eprint.iacr.org/2000/025) — сравнение трёх схем композиции
- [H. Krawczyk, The order of encryption and authentication](https://eprint.iacr.org/2001/045) — почему генерически стоек только Encrypt-then-MAC
- [Lucky Thirteen](https://www.isg.rhul.ac.uk/tls/Lucky13.html) — цена MAC-then-Encrypt в TLS
- [Online Authenticated-Encryption (STREAM)](https://eprint.iacr.org/2015/189) — конструкция для чанкованных форматов
- [Terrapin Attack](https://terrapin-attack.com/) и [OpenSSH 9.6](https://www.openssh.com/txt/release-9.6) — CVE-2023-48795 и strict KEX
- [Спецификация формата age](https://github.com/C2SP/C2SP/blob/main/age.md) — чанки, счётчик в nonce, признак финального чанка
- [WireGuard: протокол и криптография](https://www.wireguard.com/protocol/) — счётчик как nonce, XChaCha20-Poly1305 для cookie-ответов
- [wireguard-linux: `messages.h`](https://git.zx2c4.com/wireguard-linux/tree/drivers/net/wireguard/messages.h) — значения `REKEY_AFTER_MESSAGES` и `REJECT_AFTER_MESSAGES` в реализации в ядре
- [libsodium: XChaCha20-Poly1305](https://doc.libsodium.org/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction) — реализация расширенного nonce
- [restic: references](https://restic.readthedocs.io/en/stable/100_references.html) — пример ручной композиции Encrypt-then-MAC в формате бэкапа

---

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

- [Обратный прокси и TLS: nginx vs Caddy](./reverse-proxy-tls.md) — где терминируется TLS, шифры которого разбираются здесь
- [SSH углублённо](./ssh-advanced.md) — постквантовый обмен ключами в OpenSSH: как стороны добывают ключ, который здесь считается уже готовым
- [ChaCha20 и Poly1305: устройство связки и почему она такая](./crypto-chacha20-poly1305.md) — как устроены обе половины разбираемой здесь связки: откуда берётся одноразовый ключ Poly1305 и почему конструкция OpenSSH оказалась чувствительна к Terrapin
- [Обмен ключами: X25519, KEM, вывод ключей и привязка к транскрипту](./crypto-key-exchange.md) — откуда берётся ключ, который здесь считается уже готовым, и почему привязка к транскрипту обязательна
- [OpenVPN против WireGuard](./openvpn-vs-wireguard.md) — почему DCO принимает только AEAD-шифры и откуда разрыв между AES-GCM и ChaCha20 на разных процессорах
- [Самохостинг за NAT: VPS как точка входа](./selfhosting-behind-nat.md) — 16 байт тега Poly1305 в расчёте MTU туннеля
- [Резервное копирование: стратегия, инструменты, проверка восстановления](./backup-strategy.md) — ключ репозитория и шифрование по паролю, где обязательство по ключу перестаёт быть теорией
- [AES-GCM против ChaCha20-Poly1305: как выбирать](./crypto-aes-vs-chacha20.md) — во что превращаются цена повтора nonce и лимиты на объём, когда из них делают выбор между двумя схемами
- [KDF для паролей: почему медленно и с памятью](./crypto-password-kdf.md) — откуда берётся ключ, когда его выводят из пароля, и почему именно там обязательство по ключу перестаёт быть необязательным
- [Криптографические примитивы — план цикла статей](./crypto-knowledge-base-plan.md) — состав цикла, порядок статей и сквозные решения
