Ориентация главы

Предыдущая глава закрыла «содержимое не изменено» и «откуда ключи»; эта закрывает «содержимое невидимо» — и упаковывает три свойства безопасности за раз. 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Шифрование (Seal/Encrypt)открытый текст -> шифротекст (равной длины) + 16-байтовый тег аутентичности
3Передачашифротекст + тег (+ публичные AAD и nonce) вместе доходят до другой стороны
4Расшифровка (Open/Decrypt)сначала проверяется тег — несовпадение, полный отказ; открытый текст никогда не выдаётся

Три пункта. (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 привязывает заголовок версии:

examples/crypto/aes_gcm/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/crypto/aes_gcm/main.c -lws2_32 -liphlpapihello from aes-gcm

Что только что произошло. (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: одна бесстойная пара входов делает аутентифицированное шифрование/расшифровку на месте:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/crypto/chacha20_poly1305/main.c -lws2_32 -liphlpapiauthenticated message

Что только что произошло. (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-кода.

Типичные ошибки

Ловушка 1: повторное использование nonce:Ловушка 1: повторное использование nonce

Симптомы: аудит безопасности сразу проваливает; хуже — никто вам не сообщит: повтор 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));
}
Ловушка 2: использование выходного буфера после неудачи Open:Ловушка 2: использование выходного буфера после неудачи Open

Симптомы: в журналах мусорный «открытый текст», нижний парсер получает никогда не писавшиеся данные; или скрытнее — ветка ошибки забыла 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);
Ловушка 3: самокомпозиция «AES-шифрование + HMAC»:Ловушка 3: самокомпозиция «AES-шифрование + HMAC»

Симптомы: композиция выглядит работающей, но ревью отказывает; или её действительно атакуют — 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-GCMInit привязывает ключ и длину тега (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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки