Ориентация главы
Предыдущая глава закрыла «содержимое не изменено» и «откуда ключи»; эта закрывает «содержимое невидимо» — и упаковывает три свойства безопасности за раз. AEAD (аутентифицированное шифрование с ассоциированными данными) — единственно рекомендуемая форма современного симметричного шифрования: тег аутентичности вычисляется одновременно с шифрованием, а перед расшифровкой сначала проверяется тег — при несовпадении отказ целиком, никогда «возможно подделанный открытый текст». XRT даёт две равноценные реализации: AES-GCM (первый выбор на платформах с аппаратным AES-NI) и ChaCha20-Poly1305 (первый выбор постоянного времени без аппаратного ускорения). Глава разбирает машины состояний и бесстойные формы обоих семейств, привязку назначения через AAD и линию жизни AEAD — управление nonce: один повтор nonce под тем же ключом обрушивает конфиденциальность и целостность одновременно. Записной слой TLS главы 87 — по-пунктное применение примитивов этой главы.
75.1引入
Представьте, что вы проектируете сервис синхронизации конфигурации: клиент кладёт шифрованную конфигурацию в недоверяемое объектное хранилище, сервер забирает и расшифровывает. Первая версия наивна — зашифровать открытый текст AES, положить шифротекст, забрать и расшифровать. Перед запуском безопасность спрашивает одно: атакующий меняет один байт шифротекста — что сделает ваша расшифровка? Ответ: AES-расшифровка «успешно» вернёт мусор — ваша программа держит данные, которые сама не писала, но проверка пройдена. Это классическая атака бит-флиппинга: шифрование не даёт целостности; голые режимы CBC/CTR/ECB — все одинаковы.
Вы прикладываете к шифротексту HMAC — сначала шифрование, потом MAC? Порядок ошибочен и открывает уязвимости класса расширения длины; сначала MAC, потом шифрование? Теоретически безопасно, но в реализации крайне легко ошибиться. Десятилетие шишек привело криптографию к одному выводу: не компонуйте шифрование и аутентификацию сами — берите AEAD напрямую. Шифрование производит «шифротекст + тег аутентичности»; расшифровка сначала проверяет тег, потом выдаёт открытый текст; один примитив — три гарантии (конфиденциальность, целостность, аутентичность), порядок и границы на совести реализации — в этом весь смысл двух семейств этой главы.
75.2概念
Тройка AEAD: один вызов, три гарантии
Быстрейший способ понять AEAD — его формат записи:
Три пункта. (1) Тег — отпечаток всей записи: шифротекст, AAD, nonce и длина участвуют в вычислении — изменение любого бита валит проверку. (2) AAD аутентифицируется, но не шифруется: заголовки версий, порядковые номера записей, идентификаторы контекста — поля, которые «нужно видеть, но не нужно прятать», привязываются в область аутентификации через AAD; атакующий, меняющий номер версии, тоже валит проверку — это и есть «привязка назначения». (3) Отказ атомарен: когда Open возвращает false, выходной буфер неопределён; правильная программа выбрасывает его целиком, а не «пробует применить».
AES-GCM: форма машины состояний
XRT_FEATURE_CRYPTO_AES_GCM даёт состояниее xaesgcm:
xrtAesGcmInit(状态, 密钥, 密钥长, 标签长): при инициализации фиксирует ключ AES-128/192/256 и длину тега (4..16 байт, по умолчанию 16) — расписание ключа разворачивается один раз и на каждое сообщение больше не повторяется; фиксированная на ключ длина тега тоже соответствует контракту протокола.xrtAesGcmEncrypt / Decrypt: форма с отделённым тегом — шифротекст и тег пишутся в два буфера, протокольный слой сам решает их размещение.xrtAesGcmSeal / Open: непрерывная форма — тег дописывается прямо за шифротекстом; длина буфера = открытый текст + длина тега; для сетевых пакетов удобнее всего.xrtAesGcmClear: вытирает расписание ключа из состояния; очищайте по завершении.- Рекомендация nonce — 12 байт (
XRT_AES_GCM_NONCE_DEFAULT_SIZE); реализация автоматически идёт по аппаратному пути AES-NI.
ChaCha20-Poly1305: бесстойный слой удобства
XRT_FEATURE_CRYPTO_CHACHA20_POLY1305 реализует RFC 8439: ключ 32 байта, nonce 12 байт, тег 16 байт — фиксированная тройка (выбора длины тега нет — протокол говорит 16). API имеет обе формы — отделённую (Encrypt/Decrypt) и непрерывную (Seal/Open), но без состояния: каждый вызов принимает ключ напрямую, не поддерживая внешнего расписания ключа. Компромисс: нет управления состоянием, цена — повторная обработка ключа на каждое сообщение; высокочастотные одного-ключа сценарии (вроде записного слоя TLS) внутри используют состояниее вариант; случайное шифрование (конфиги, токены) напрямую берёт слой удобства. ChaCha20 — программный потоковый шифр, постоянного времени, без таблиц — на ARM-устройствах и IoT-целях без аппаратного AES он быстрее программного AES и стойче к побочным каналам. XRT_CHACHA20_POLY1305_OVERHEAD даёт фиксированную надбавку шифротекста над открытым текстом (16 байт тега); в расчётах буферов — макрос, не числа.
Дисциплина nonce: линия жизни AEAD
Под одним ключом nonce не должен повторяться никогда — это не стилистический совет, а математическая граница: один повтор GCM-nonce позволяет восстановить ключ аутентичности, обнажая все записи, которые этот ключ когда-либо защищал (включая прошлые). Два безопасных способа управления:
- Счётчик: nonce каждой записи = случайное стартовое значение + возрастающий порядковый номер; отправитель гарантирует монотонность без повторов — лучшая производительность, требует одноточечного порядка (ключ используется ровно на одном пути отправки).
- Случайность: 96-битный случайный nonce на запись — по границе «дней рождения» вероятность повтора становится значимой примерно после 2^32 записей под одним ключом; годится короткоживущим ключам (одна сессия).
Сессионный пример главы 77 использует комбинацию «AAD привязывает номер записи + счётчик». Ротация самих ключей (выбросить по концу сессии) — последняя страховка: эфемерный ключ со счётчиковым nonce — стандартная поза TLS 1.3.
Отношение к голому потоковому ChaCha20
XRT_FEATURE_CRYPTO_CHACHA20 даёт и голое преобразование ChaCha20 (без аутентификации) — оно существует только как основа композиции Poly1305 и как интероперационная деталь особых протоколов. Дисциплина изоморфна MD5: шифрование прикладного уровня всегда через AEAD; голое шифрование — только там, где протокол явно требует. Аналогично AES: блочный слой CRYPTO_AES нужен GCM и напрямую кодом приложения не трогается.
75.3示例
Первая полная программа: запечатывание и открытие AES-GCM
Следующая программа из examples/crypto/aes_gcm/main.c: шифрование на месте с последующей расшифровкой на фиксированном ключе, AAD привязывает заголовок версии:
#include <stdio.h> #include <string.h> #include <xrt.h> */ /* 展示固定密钥状态上的 AES-GCM 原位封装与打开。 */ int main(void) { uint8 Key[XRT_AES256_KEY_SIZE] = { 0 }; uint8 Nonce[XRT_AES_GCM_NONCE_DEFAULT_SIZE] = { 0 }; uint8 Buffer[64] = "hello from aes-gcm"; const char* pAad = "message-v1"; size_t iPlainSize = strlen((const char*)Buffer); xaesgcm State; if ( !xrtAesGcmInit( &State, Key, sizeof(Key), XRT_AES_GCM_TAG_DEFAULT_SIZE ) ) { return 1; } if ( !xrtAesGcmSeal( &State, Nonce, sizeof(Nonce), pAad, strlen(pAad), Buffer, iPlainSize, Buffer, sizeof(Buffer) ) ) { xrtAesGcmClear(&State); return 1; } if ( !xrtAesGcmOpen( &State, Nonce, sizeof(Nonce), pAad, strlen(pAad), Buffer, iPlainSize + XRT_AES_GCM_TAG_DEFAULT_SIZE, Buffer, sizeof(Buffer) ) ) { xrtAesGcmClear(&State); return 1; } Buffer[iPlainSize] = '\0'; printf("%s\n", Buffer); xrtAesGcmClear(&State); return 0; }
Что только что произошло. (1) Буферы ключа и nonce объявлены через XRT_AES256_KEY_SIZE/XRT_AES_GCM_NONCE_DEFAULT_SIZE — длины всегда из макросов; нулевой ключ — только для воспроизводимости; продакшн-ключи приходят из KDF или источника случайности. (2) После привязки Init ключа и 16-байтового тега Seal шифрует на месте: выходной буфер есть входной (перекрытие разрешено), пишется 19 байт шифротекста + дописанный 16-байтовый тег, всего 35 — ёмкости 64 достаточно. (3) Open принимает 35 байт «шифротекст+тег», проверяет тег и расшифровывает на месте; несовпадение тега вернёт false, содержимое буфера недостоверно. (4) AAD "message-v1" передаётся одинаково на обеих сторонах — он не входит в шифротекст, но входит в тег: смена номера версии валит открытие — «видно, но не трогаемо». (5) Финальный Clear вытирает расписание ключа — в структуре состояния лежит развёрнутый ключ, обращения с ним как с самим открытым ключом.
Вторая полная программа: слой удобства ChaCha20-Poly1305
Вторая программа из examples/crypto/chacha20_poly1305/main.c: одна бесстойная пара входов делает аутентифицированное шифрование/расшифровку на месте:
#include <stdio.h> #include <string.h> #include <xrt.h> */ /* 使用 packed 便捷层完成一次可认证的原位加解密。 */ int main(void) { uint8 Key[XRT_CHACHA20_POLY1305_KEY_SIZE]; uint8 Nonce[XRT_CHACHA20_POLY1305_NONCE_SIZE]; uint8 Message[64] = "authenticated message"; size_t iPlainSize = strlen((cstr)Message); size_t iSealedSize = iPlainSize + XRT_CHACHA20_POLY1305_OVERHEAD; /* 固定材料仅用于复现示例;生产中同一密钥下的 nonce 必须唯一。 */ for ( size_t i = 0; i < sizeof(Key); i++ ) { Key[i] = (uint8)(0x40u + i); } for ( size_t i = 0; i < sizeof(Nonce); i++ ) { Nonce[i] = (uint8)i; } if ( !xrtChaCha20Poly1305Seal( Key, Nonce, "header", 6, Message, iPlainSize, Message, sizeof(Message) ) || !xrtChaCha20Poly1305Open( Key, Nonce, "header", 6, Message, iSealedSize, Message, sizeof(Message) ) ) { xrtSecureZero(Key, sizeof(Key)); return 1; } Message[iPlainSize] = 0; printf("%s\n", (cstr)Message); xrtSecureZero(Key, sizeof(Key)); return 0; }
Что только что произошло. (1) Нет структуры состояния — ключ используется и уходит: Seal напрямую принимает Key/Nonce/AAD/открытый текст и выдаёт шифротекст + 16-байтовый тег (iSealedSize = 明文长 + XRT_CHACHA20_POLY1305_OVERHEAD). (2) Ключ заполнен паттерном 0x40+i, nonce нарастает побайтно — фиксированный материал воспроизводит вывод; комментарий заголовка прямо напоминает «в продакшне nonce под одним ключом обязан быть уникальным». (3) AAD "header" демонстрирует привязку заголовка протокола; оба вызова делят один буфер на месте. (4) Финальный xrtSecureZero(Key, ...) вытирает ключ — слою удобства нечего Clear-ить; сам массив ключа и есть чувствительный материал.
Бонус: подмена означает отказ (проверьте сами один раз)
Оба примера проходят успешный путь «шифрование → расшифровка». Ключевой момент понимания AEAD — увидеть отказ своими глазами: добавьте строку Buffer[0] ^= 1; после Seal в примере aes_gcm (переворот первого бита шифротекста), затем вызовите Open — вернётся false, программа пойдёт по ветке ошибки. Замените подмену на последний байт тега или смените AAD на "message-v2" — снова отказ. Переворот любого бита любого из трёх входов отвергается проверкой — в этом вес слов «аутентифицированное шифрование» и первая выкупленная точка дисциплины тома «неудача обязана быть единственной».
75.4契约
- Состояние AES-GCM:
Initфиксирует ключ (128/192/256) и длину тега (4..16, по умолчанию 16); расписание ключа разворачивается один раз и переиспользуется между сообщениями;Clearвытирает состояние;TagSizeзапрашивает связанное значение. - Две формы вызова:
Encrypt/Decryptотделяют шифротекст и тег (расположение определяет протокол);Seal/Openдержат тег сразу за шифротекстом (удобнее для пакетов); обе делят расписание ключа и стойкость. - ChaCha20-Poly1305: фиксированные параметры RFC 8439 (32 ключ/12 nonce/16 тег); бесстойный слой удобства обрабатывает ключ на каждое сообщение; TLS внутри использует состояниее вариант; программная реализация постоянного времени.
- Nonce: уникальность под одним ключом — математическая граница: счётчик (монотонность, одноточечный порядок) или 96-битная случайность (короткоживущие ключи); nonce может передаваться открыто; секретен ключ, не nonce.
- AAD: аутентифицируется, но не шифруется; заголовки версий/номера/контекст привязываются через AAD; обе стороны обязаны совпадать побайтно.
- Семантика отказа:
Open/Decryptпри несовпадении тега отказывает целиком, выходной буфер неопределён — правильная программа выбрасывает запись целиком; «частичной расшифровки» не существует. - Перекрытие:
Seal/Openподдерживают на-месте (вход и выход — один буфер); выходной и теговый буферы отделённой формы не должны перекрываться со входом (проверяется контрактом). - Ошибки: ошибки параметров/ёмкости возвращаются до первой записи вывода; нелегальные длины ключа (не 128/192/256) отвергаются сразу.
- Обрезка: AES, AES-GCM, ChaCha20, Poly1305 и слой композиции — все отдельные макросы возможностей: IoT-цель с одним ChaCha20-Poly1305 не несёт ни байта AES-кода.
Типичные ошибки
Симптомы: аудит безопасности сразу проваливает; хуже — никто вам не сообщит: повтор GCM-nonce восстанавливает ключ аутентичности, давая атакующему способность подделывать произвольные записи, необнаружимо по шифротексту задним числом.
Причина: доказательство стойкости GCM стоит на «nonce под одним ключом не повторяется». Случайные 12-байтовые nonce с фиксированным долгосрочным ключом на массе записей растят вероятность повтера квадратично — значимо после 2^32 записей.
static const uint8 Nonce[12] = { 0 }; /* фиксированный nonce */ for ( each record ) { xrtAesGcmSeal(&State, Nonce, sizeof(Nonce), NULL, 0, Plain, iSize, Out, sizeof(Out)); /* со второго вызова ключ аутентичности постепенно утекает */ }
uint64 iCounter = 0; uint8 Nonce[XRT_AES_GCM_NONCE_DEFAULT_SIZE]; for ( each record ) { memset(Nonce, 0, sizeof(Nonce)); /* счётчиковый nonce: одноточечная монотонность под одним ключом — повторов нет */ memcpy(Nonce + 4, &iCounter, sizeof(iCounter)); iCounter++; xrtAesGcmSeal(&State, Nonce, sizeof(Nonce), NULL, 0, Plain, iSize, Out, sizeof(Out)); }
Симптомы: в журналах мусорный «открытый текст», нижний парсер получает никогда не писавшиеся данные; или скрытнее — ветка ошибки забыла return и бежит дальше с загрязнённым буфером.
Причина: при несовпадении тега Open отказывает целиком, выход неопределён — возможно частично записан, возможно старое значение. Понимать «при неудаче в буфере что-то есть» как «при неудаче в буфере оригинал» — фатальное заблуждение.
uint8 Plain[64]; if ( !xrtAesGcmOpen(&State, Nonce, 12, pAad, iAad, Cipher, iCipherSize, Plain, sizeof(Plain)) ) { /* забыли return: бежим дальше с неопределённым Plain */ } parse(Plain); /* обработка данных под влиянием атакующего */
if ( !xrtAesGcmOpen(&State, Nonce, 12, pAad, iAad, Cipher, iCipherSize, Plain, sizeof(Plain)) ) { return false; /* вся запись недействительна: это единственно правильное действие */ } parse(Plain);
Симптомы: композиция выглядит работающей, но ревью отказывает; или её действительно атакуют — Encrypt-then-MAC теоретически корректен, но любая ручная ошибка в границах длин, привязке nonce или времени сравнения ломает стойкость напрямую.
Причина: десятилетний урок криптоинженерии — композиция ошибается легко: порядок, охват, сравнение постоянного времени, разделение ключей (один ключ и на шифрование, и на MAC — катастрофа); у каждого пункта есть известные случаи отказа. AEAD интернализирует все эти решения.
/* голое блочное/потоковое шифрование, затем приклеенный вручную HMAC */ encrypt_only(Key1, Nonce, Plain, iSize, Cipher); xrtHmacSha256(Key2, ..., Cipher, iSize, Mac); /* порядок, охват, разделение ключей... каждый шаг — поверхность атаки на ревью */
xrtAesGcmSeal(&State, Nonce, 12, pAad, iAad, Plain, iSize, Out, sizeof(Out)); /* или ChaCha20-Poly1305: один вызов — конфиденциальность + целостность + аутентичность */
Практические задания
Прогнав пример aes_gcm, поставьте три эксперимента: переворот одного байта шифротекста, переворот одного байта тега, замена одного символа AAD — Open обязан отказать во всех трёх. Три пути отказа переделайте в печать rejected с ненулевым возвратом. Критерий приёмки: все три отказа печатают одно и то же (вы не отличите, какая была подмена — это свойство, а не дефект).
Реализуйте seal_config(密钥, 版本号, JSON, 输出) и open_config: AAD привязывает "config-v1" и номер версии (версия вплетается в строку AAD); Seal/Open через слой удобства ChaCha20-Poly1305. Нарочно откройте запись v2 ключом v1 — обязано отказать. Подсказка: AAD строится из строки версии одной общей на обе стороны функцией — без расползшихся ручных копий.
На одном TCP-соединении (достаточно синхронной грани главы 67) реализуйте простой протокол шифрования: рукопожатие фиксированным тестовым ключом, далее каждая запись = 8-байтовый номер (открытым текстом) + запечатанный Seal-ом шифротекст с тегом; nonce заполняется номером (метод счётчика), а номер дополнительно идёт в AAD против переупорядочивания. Приёмник проверяет строгое возрастание номеров и рвёт соединение при нарушении порядка. Критерий приёмки: записи разбираются кадрировщиком главы 72; атаки — повтор старой записи, переупорядочивание, подмена — все три рвут соединение; 2^16 записей без роста памяти (статистика главы 6).
Итоги
| Тема | Краткая справка |
|---|---|
| Три гарантии AEAD | конфиденциальность + целостность + аутентичность одним вызовом; атомарный отказ (подозрительный открытый текст не выдаётся) |
| Машина состояний AES-GCM | Init привязывает ключ и длину тега (4..16, по умолчанию 16) → расписание ключа разворачивается один раз; Clear вытирает состояние |
| Две формы | Encrypt/Decrypt отделяют тег; Seal/Open дописывают тег за шифротекстом (+16 байт) |
| ChaCha20-Poly1305 | фиксированные RFC 8439 32/12/16; бесстойный слой удобства; постоянное время, первый выбор без аппаратного AES |
| Выбор | есть AES-NI → AES-GCM; ARM/IoT → ChaCha20-Poly1305; оба равноценны и взаимозаменяемы |
| Дисциплина nonce | уникальность под одним ключом — математическая граница; счётчик (одноточечная монотонность) или 96-битная случайность (короткоживущие ключи) |
| AAD | аутентифицируется, но не шифруется; привязка версия/номер/контекст; побайтовое совпадение сторон |
| Семантика отказа | неудача Open оставляет выход неопределённым; выбросить всю запись — единственно правильное действие |
| На-месте | Seal/Open поддерживают один буфер входа-выхода; теговый буфер отделённой формы отдельный |
| Обрезка | AES/AES-GCM/ChaCha20/Poly1305/слой композиции — все отдельные макросы; голые преобразования только для интероперации протоколов |
📖 Справочники API в этой главе:crypto (140 записей)random (41 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки