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

Главы 74–76 дали вам все алгоритмические примитивы — но в реальном мире взломанные системы в большинстве своём пали не от взлома алгоритмов, а от ошибок системного слоя: ключ попал в журнал, функция сравнения утекает таймингом, неверный источник случайности, самодельная комбинация протокола. Эта глава — «инструкция по применению» трёх предыдущих и сводка дисциплины тома 8 перед переходом от примитивов к сертификатам и TLS. Четыре темы: дисциплина постоянного времени (почему сравнение MAC обязано не выходить досрочно), жизненный цикл ключей (генерация — использование — хранение — уничтожение, свои правила и API на каждом отрезке), дисциплина источников случайности (граница между безопасственной и обычной случайностью), дисциплина комбинирования (не изобретать протоколы, единственность отказа, привязка контекста). Образец-финал crypto/session собирает примитивы трёх глав в одну полную временную сессию — это скелетная миниатюра TLS 1.3; рукопожатие главы 82 стоит на ней, лишь добавляя сертификаты и машину состояний.

77.1引入

Взгляните на абстракции трёх реальных аварий. Первая: сервис проверяет сеансовые токены через memcmp — атакующий меряет время отклика и по байтам подбирает законный токен: первый байт верен — отклик медленнее на десятки наносекунд; миллион запросов полностью «ощупывает» токен. Вторая: разработчик очищает ключ memset-ом перед освобождением памяти — компилятор решает, что эту память «больше не читают», и удаляет обнуление целиком; ключ открытым текстом остаётся в куче, и последующее выделение его считывает. Третья: два модуля порознь генерируют «случайные» challenge-значения предсказуемым обычным источником — атакующий предсказывает вывод, и аутентификация существует только на бумаге.

Общее у трёх аварий: алгоритмы все верны, вызовы все неверны. Безопасность криптографических API существует только в контексте «правильного употребления» — три неприметные функции xrtConstTimeEqual, xrtSecureZero и xrtSecureRandom и есть выходы, приготовленные XRT для этих трёх классов аварий. Глава разъясняет их вместе с большей «картиной комбинирования».

77.2概念

Трёхслойная модель провала: на каком слое происходит ошибочное употребление

Представьте систему безопасности тремя слоями; атакующий всегда выбирает мягчайший:

1Слой алгоритмовматематическая прочность AES/SHA/кривых - примитивы тома уже гарантируют, стать слабым звеном труднее всего
2Слой протоколовпорядок полей, машина состояний, привязка контекста, пути отказа - аварийная зона самодельных протоколов
3Системный слойхранение ключей, источник случайности, способ сравнения, содержимое журналов - главный фронт главы

Практический самотест: написав строку криптокода, спросите «какой слой эта строка ломает». memcmp над шифртекстом ломает системный слой; «проверка не прошла, но продолжаем обработку» ломает слой протокола; выбор MD5 ломает слой алгоритмов. Из трёх слоёв алгоритмический ошибается менее всего (достаточно верного API), поэтому внимание инженера распределяется наоборот — больше всего на системный, затем протокольный, меньше всего на алгоритмический. Список ловушек главы целиком родом из системного слоя.

Дисциплина постоянного времени: сравнение — тоже поверхность атаки

memcmp(Left, Right, N) выходит досрочно на первом различающемся байте — эта «быстрота» и есть уязвимость: время сравнения пропорционально длине общего префикса, и атакующий дистанционным хронометрированием восстанавливает секрет побайтно (токен, MAC, ключ). xrtConstTimeEqual(Left, Right, N) накапливает различие поразрядно и выносит вердикт в конце — время исполнения зависит только от N. Две точные границы: она не скрывает само N (длина публична); она и не полная защита от боковых каналов (энергопотребление и кеш — другие измерения) — но на самой реалистической поверхности «дистанционного тайминга» она необходима. Объекты применения: сравнение MAC/тегов, токенов, общих секретов, отпечатков шифртекста — всякое сравнение, где одна сторона — секрет. Для данных, публичных с обеих сторон (контрольные суммы файлов, ключи кеша), хватает memcmp: постоянное время не бесплатно по производительности.

Четыре отрезка жизненного цикла ключа

Генерацияисточник законен (случайность/KDF/обмен), никогда не захардкожен →Использование
Использованиеминимальное пребывание (стек/спецструктура), никогда в журналы и дампы →Хранение
Хранениепокинул область видимости - немедленно очистить →Уничтожение
УничтожениеxrtSecureZero или специализированный Clear, покрывая все копии →Конец
  • Генерация: симметричные ключи и приватные — только из xrtSecureRandom (следующий раздел) или деривации KDF; фиксированные ключи тестовых векторов обязаны быть помечены «только для воспроизведения», как в образцах.
  • Использование: ключ живёт только в стековом буфере или специализированной структуре состояния (расписание ключа xaesgcm, развёрнутое состояние xed25519key); не копируется в другие буферы — каждая лишняя копия расширяет поверхность уничтожения.
  • Хранение: ключ никогда не входит в журналы (Logger главы 38 — не исключение), в сообщения об ошибках, в персистентные структуры файла подкачки (это за пределами возможностей xrtSecureZero — контракт прямо говорит: он покрывает только данный вызовом интервал; регистровые копии компилятора, crash-дампы и подкачка ОС требуют системных мер: mlock, отключение дампов, шифрование диска).
  • Уничтожение: xrtSecureZero обнуляет volatile-записями; обычный memset компилятор может удалить как мёртвое хранилище (вторая авария из начала); объекты с состоянием — специализированными Clear/KeyClear (они знают все чувствительные углы структуры). Чистить и успешный путь, и путь отказа — cleanup-сегмент образцов главы и есть шаблон.

Дисциплина источников случайности: две случайности не смешиваются

XRT делит случайность на два мира: обычная (глава 11, последовательность, посеянная xrtFastRandSeed, — быстрая, сеемая, воспроизводимая) для тестовых данных, распределения нагрузки, тасования; безопасственная (xrtSecureRandom) для всех ключей, токенов, солей, nonce — платформенный CSPRNG (Windows CNG, Linux getrandom), несеемый, непредсказуемый. Пробный камень границы: повлечёт ли предсказание этого значения последствия для безопасности? Да — безопасственная случайность, пусть она и на два порядка медленнее. Обратное правило не менее важно: безопасственная случайность не для воспроизводимых сцен (тестовые векторы, данные базисов) — не ради производительности, а потому что детерминированный вывод попросту не её обещание. Криптомодуль зависит от источника через независимую возможность RANDOM_SECURE (таблица обрезки главы 74) — файловые и сетевые модули, переиспользующие базовую случайность, не утаскивают за собой криптоалгоритмы: эта граница позволяет «серьёзность источника случайности» и «лёгкость модулей» сосуществовать.

Дисциплина комбинирования: не изобретайте протоколы

С примитивами в руках главный соблазн — «слепить свой криптопротокол»; четыре замены и одно железное правило:

  • Сперва ищите готовый протокол: транспортная безопасность — прямо TLS главы 82; токены — HMAC или подпись; шифрование файлов — одиночная запись AEAD. Каждое решение самодельного протокола (порядок полей, машина состояний рукопожатия, семантика отказа) заставляет заново наступать на грабли, по которым прошло всё индустрию.
  • Отказ обязан быть единственным: проверка не прошла — это отказ; без «деградировать и продолжить», без «предупредить, но пропустить». Атаки на даунгрейд (заставить стороны откатиться к слабым параметрам) эксплуатируют именно путь «после отказа продолжаем».
  • Привязка контекста: деривация ключей связывает назначение через info (глава 74), шифрование записей связывает номер и версию через AAD (глава 75), дайджест рукопожатия связывает все его сообщения (глава 82) — каждый слой вписывает в область аутентификации «кто я и который по счёту», и атакующий не может переставить законный шифртекст в другую позицию для повтора.
  • Временные ключи прежде: раз можно сеансовый временный — не пользуйтесь долгосрочным; радиус утечки решает масштаб аварии. Прямая секретность TLS 1.3 — предельная форма этой дисциплины: на каждое соединение новая пара x25519, долгосрочный приватный ключ только подписывает рукопожатие и в шифровании не участвует.

77.3示例

Первая полная программа: вход в два железных правила

Программа ниже из examples/crypto/core/main.c — метаданные дайджеста, сравнение постоянного времени и безопасное обнуление одним махом:

examples/crypto/core/main.c源码 ↗
#include <stdio.h>
#include <xrt.h>





















 */


/* 展示摘要元数据、常量时间比较和敏感数据清理。 */
int main(void)
{
	uint8 Left[XRT_SHA256_SIZE] = { 1u, 2u, 3u };
	uint8 Right[XRT_SHA256_SIZE] = { 1u, 2u, 3u };
	bool bEqual;

	/* 算法元数据不要求把对应摘要实现编入程序。 */
	if ( xrtCryptoHashSize(XCRYPTO_HASH_SHA256) != sizeof(Left) ) {
		return 1;
	}

	/* 密钥、标签和摘要等固定长度数据应使用常量时间比较。 */
	bEqual = xrtConstTimeEqual(Left, Right, sizeof(Left));
	xrtSecureZero(Left, sizeof(Left));
	xrtSecureZero(Right, sizeof(Right));

	printf("equal: %s\n", bEqual ? "yes" : "no");
	return bEqual ? 0 : 1;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/crypto/core/main.c -lws2_32 -liphlpapiequal: yes

Что только что произошло. (1) xrtCryptoHashSize — чистые метаданные: запросить длину SHA-256 не значит втянуть реализацию SHA-256; вычисление ёмкости расцеплено с конфигурацией компиляции. (2) Два буфера дайджеста сравниваются xrtConstTimeEqual — здесь демонстрация «содержимое совпадает», в продакшене это «моё вычисленное MAC против полученного»: даже если подделка атакующего отличается от истины только последним байтом, время сравнения совершенно одинаково — тайминг-атаке не за что зацепиться. (3) Сразу после сравнения оба буфера проходят xrtSecureZero — они изображают ключевой материал; volatile-запись гарантирует, что это обнуление не исчезнет. Вся программа меньше 20 строк — но это три самые частые функции тома.

Вторая полная программа: одна полная временная сессия (скелетная миниатюра TLS 1.3)

Вторая программа из examples/crypto/session/main.c сцепляет примитивы глав 74/75/76 в настоящую протокольную форму:

examples/crypto/session/main.c源码 ↗
#include <stdio.h>
#include <string.h>
#include <xrt.h>























 */


/* 组合 X25519、HKDF-SHA256 和 ChaCha20-Poly1305 建立一次临时会话。 */
int main(void)
{
	static const char Message[] = "authenticated session message";
	static const char Salt[] = "xrt-session-v1";
	static const char Aad[] = "record:0";
	uint8 AlicePrivate[XRT_X25519_PRIVATE_SIZE];
	uint8 AlicePublic[XRT_X25519_PUBLIC_SIZE];
	uint8 BobPrivate[XRT_X25519_PRIVATE_SIZE];
	uint8 BobPublic[XRT_X25519_PUBLIC_SIZE];
	uint8 AliceShared[XRT_X25519_SHARED_SIZE];
	uint8 BobShared[XRT_X25519_SHARED_SIZE];
	uint8 Transcript[XRT_X25519_PUBLIC_SIZE * 2u];
	uint8 SessionKey[XRT_CHACHA20_POLY1305_KEY_SIZE];
	uint8 Nonce[XRT_CHACHA20_POLY1305_NONCE_SIZE] = { 0 };
	uint8 Buffer[96];
	size_t iMessageSize = sizeof(Message) - 1u;
	size_t iSealedSize = iMessageSize + XRT_CHACHA20_POLY1305_OVERHEAD;
	bool bValid = false;

	/* 双方共享秘密必须相同,且对端公钥应进入派生上下文。 */
	if ( !xrtX25519KeyPair(AlicePrivate, AlicePublic) ||
		 !xrtX25519KeyPair(BobPrivate, BobPublic) ||
		 !xrtX25519Shared(AlicePrivate, BobPublic, AliceShared) ||
		 !xrtX25519Shared(BobPrivate, AlicePublic, BobShared) ||
		 !xrtConstTimeEqual(
			AliceShared, BobShared, sizeof(AliceShared)
		) ) {
		goto cleanup;
	}
	memcpy(Transcript, AlicePublic, sizeof(AlicePublic));
	memcpy(
		Transcript + sizeof(AlicePublic), BobPublic, sizeof(BobPublic)
	);

	/* 派生后的会话密钥才进入 AEAD,原始共享秘密不直接使用。 */
	if ( !xrtHkdfSha256(
			Salt,
			sizeof(Salt) - 1u,
			AliceShared,
			sizeof(AliceShared),
			Transcript,
			sizeof(Transcript),
			SessionKey,
			sizeof(SessionKey)
		) ) {
		goto cleanup;
	}

	/* 新临时密钥下的首条记录使用序号零,后续记录必须递增 nonce。 */
	memcpy(Buffer, Message, iMessageSize);
	if ( !xrtChaCha20Poly1305Seal(
			SessionKey,
			Nonce,
			Aad,
			sizeof(Aad) - 1u,
			Buffer,
			iMessageSize,
			Buffer,
			sizeof(Buffer)
		) || !xrtChaCha20Poly1305Open(
			SessionKey,
			Nonce,
			Aad,
			sizeof(Aad) - 1u,
			Buffer,
			iSealedSize,
			Buffer,
			sizeof(Buffer)
		) ) {
		goto cleanup;
	}
	bValid = memcmp(Buffer, Message, iMessageSize) == 0;

cleanup:
	xrtSecureZero(AlicePrivate, sizeof(AlicePrivate));
	xrtSecureZero(BobPrivate, sizeof(BobPrivate));
	xrtSecureZero(AliceShared, sizeof(AliceShared));
	xrtSecureZero(BobShared, sizeof(BobShared));
	xrtSecureZero(SessionKey, sizeof(SessionKey));
	xrtSecureZero(Buffer, sizeof(Buffer));
	printf("session round trip: %s\n", bValid ? "valid" : "invalid");
	return bValid ? 0 : 1;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/crypto/session/main.c -lws2_32 -liphlpapisession round trip: valid

Что только что произошло. Три точки безопасного дизайна, каждая отвечает дисциплине главы. (1) Пары ключей обеих сторон: xrtX25519KeyPair сэмплирует из безопасственного источника — временный приватный ключ каждой стороны никогда её не покидает; общий секрет считается xrtX25519Shared (безопасный вход, стойкий к злой публичной части) и сверяется на согласованность xrtConstTimeEqual. (2) Деривация связывает идентичность: сырой общий секрет напрямую в AEAD не идёт (позитивная демонстрация ловушки 2 главы 76) — salt у HKDF это строка версии протокола, info это Transcript из конкатенации публичных ключей обеих сторон: производный сеансовый ключ одновременно привязан к «идентичности обеих сторон данной negociación»; подмените посредником любой публичный ключ — обе стороны выведут разные ключи, и следующие сообщения взаимно не пройдут проверку (инженерная форма защиты от unknown key-share). (3) Запись связывает номер: AAD это "record:0" — первая запись с номером ноль, далее по нарастанию; повтор старой записи или доставка не по порядку будет отвергнута Open-ом из-за несовпадения AAD. Cleanup-финал проводит пять классов чувствительного материала (оба приватных ключа, оба общих секрета, сеансовый ключ, буфер открытого текста) через xrtSecureZero по одному — это шаблонный код четвёртого отрезка жизненного цикла. Сверьте с главой 81: рукопожатие TLS 1.3 добавляет поверх лишь проверку сертификатов и более полную машину состояний — скелет у вас уже весь.

77.4契约

  • Постоянное время: время xrtConstTimeEqual пропорционально только параметру длины; не скрывает длину, не полная защита от боковых каналов; обязательно для всякого сравнения «одна сторона — секрет».
  • Безопасное обнуление: xrtSecureZero — volatile-записи, не удаляется как мёртвое хранилище; покрывает только данный вызовом интервал — регистровые копии компилятора, журналы, файл подкачки, crash-дампы требуют системных мер; объекты с состоянием — через специализированные Clear/KeyClear.
  • Источник случайности: безопасственное назначение (ключи/соли/nonce/токены) — только xrtSecureRandom; воспроизводимое назначение (тесты/базисы) — только обычная случайность; криптомодуль зависит от безопасственной через независимую возможность RANDOM_SECURE.
  • Ключи не входят в персистентные записи: журналы, сообщения об ошибках, аварийные отчёты, отладочная печать — ноль содержимого ключей; «отладочное удобство» шестнадцатеричного дампа ключа и есть авария утечки.
  • Отказ единственный: проверка не прошла — это отказ; без даунгрейда-продолжения; путь ошибки исполняет ту же очистку чувствительного материала, что и путь успеха.
  • Привязка контекста: info у KDF, AAD у AEAD, Transcript у рукопожатия — каждый связывает назначение/номер/идентичность; шифртекст неотделим от своего контекста.
  • Временные ключи: на каждую сессию новая пара (прямая секретность); долгосрочный ключ только подписывает, не шифрует данные.
  • Приоритет комбинирования: готовый протокол > комбинация стандартных примитивов > самодельный протокол; каждый шаг самоделки требует безопасственного ревью.

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

Ловушка 1: сравнение секретов через memcmp:Ловушка 1: сравнение секретов через memcmp

Симптом: дистанционная тайминг-атака восстанавливает токен/MAC побайтно — время отклика меняется с длиной общего префикса; атакующему хватает нескольких тысяч запросов на байт.

Причина: memcmp выходит досрочно на первом различии. Функциональные тесты совершенно зелёные (результат верен), безопасственный атрибут совершенно отсутствует (утечка тайминга).

错误写法
uint8 Mac[XRT_SHA256_SIZE];
xrtHmacSha256(Key, sizeof(Key), Body, iSize, Mac);
if ( memcmp(Mac, SentMac, sizeof(Mac)) == 0 ) {
	accept();   /* время-канал: при верном префиксе возврат медленнее */
}
正确写法
uint8 Mac[XRT_SHA256_SIZE];
xrtHmacSha256(Key, sizeof(Key), Body, iSize, Mac);
if ( xrtConstTimeEqual(Mac, SentMac, sizeof(Mac)) ) {
	accept();   /* время зависит только от длины, без информации о префиксе */
}
Ловушка 2: очистка ключа через memset:Ловушка 2: очистка ключа через memset

Симптом: аудит памяти находит ключи открытым текстом в освобождённой памяти; дизассемблер показывает, что кода обнуления вовсе нет.

Причина: компилятор видит «после memset нет чтений» и удаляет обнуление как мёртвое хранилище — стандарт C это разрешает: значение после memset в абстрактной машине действительно никто не использует.

错误写法
void handle_key(uint8* pKey, size_t iSize) {
	use(pKey, iSize);
	memset(pKey, 0, iSize);   /* удалено оптимизацией DSE */
}
正确写法
void handle_key(uint8* pKey, size_t iSize) {
	use(pKey, iSize);
	xrtSecureZero(pKey, iSize);   /* volatile-запись, оптимизатор удалить не может */
}
Ловушка 3: отладочный журнал печатает ключи/токены:Ловушка 3: отладочный журнал печатает ключи/токены

Симптом: платформа сбора журналов (со всеми нисходящими: поиск, резервные копии, сторонняя аналитика) получила продакшен-ключи; аварийный ответ с ротацией ключей занимает всю ночь — журнал неотзываем.

Причина: «глянуть, верный ли ключ» из периода разработки попало в уровень журнала по умолчанию, и при выпуске никто не вспомнил его удалить. Ключ, войдя в журнал-систему, завершил неконтролируемое распространение.

错误写法
/* "наспех впечатать" при разборе */
log_debug("key=%02x%02x... nonce=%02x%02x...",
	Key[0], Key[1], Nonce[0], Nonce[1]);
正确写法
/* 只打印密钥的存在性与长度,永不打印内容 */
log_debug("key ready: %zu bytes, nonce ready: %zu bytes",
	iKeySize, iNonceSize);

   而不是密钥本身 */

Практические задания

Базовое: сравнительный опыт тайминга

Постройте две пары 32-байтовых буферов: одна полностью идентична, другая различается только последним байтом. Прогоните сравнение миллион десять раз каждой пары сначала memcmp, затем xrtConstTimeEqual, хронометрируя четыре группы. Критерий приёмки: memcmp при «первых 31 байтах одинаковы» заметно быстрее, чем при «различие в первом же байте»; две группы xrtConstTimeEqual статистически неразличимы.

Продвинутое: аудит ключей вашей кодовой базы

Вернитесь к каналу записей из упражнения главы 75 и построчно пометьте позиции генерации, использования и очистки каждого класса чувствительного материала (ключи/nonce/открытый текст); проверьте: не безопасственное ли значение сгенерировано обычной случайностью? нет ли сравнения секретов через memcmp? покрывает ли cleanup путь успеха? Исправьте всё и зафиксируйте правки. Критерий приёмки: чек-лист аудита покрывает все пункты системного слоя трёхслойной модели провала.

Вызов: сессия-образец на двунаправленность и много записей

Расширьте crypto/session: поддержка произвольного числа записей в обе стороны — 64-битный номер записи, nonce заполняется номером (счётчикный метод), AAD связывает «направление + номер» (у каждого направления свой независимый счётчик); после последнего сообщения обе стороны активно уничтожают сессию (чистка ключей и печать session destroyed). Атакующий опыт: перехватите третью запись и повторите — Open обязан отказать; доставка не по порядку — обязана отказать. Критерии приёмки: по десять записей в каждую сторону проходят круглую поездку; весь чувствительный материал обнулён в сегменте уничтожения (контроль статистикой главы 6).

Итоги

ТемаКратко
Три слоя провалаалгоритмы (труднее всего сломать) < протоколы < система (ломаются чаще всего) — внимание наоборот
Постоянное времяxrtConstTimeEqual зависит только от длины; обязателен для сравнения секретов; длину не скрывает, не все боковые каналы
Безопасное обнулениеxrtSecureZero volatile-записи против DSE; покрывает лишь данный интервал; объекты состояния — Clear/KeyClear
Граница случайностибезопасственные значения (ключи/соли/nonce/токены) → xrtSecureRandom; воспроизводимые → обычная
Жизненный цикл ключагенерация (законный источник) → использование (минимальное пребывание) → хранение (не в журналы/дампы) → уничтожение (SecureZero)
Отказ единственныйпроверка не прошла — отказ; без даунгрейда; путь ошибки чистит так же
Привязка контекстаinfo KDF — назначение / AAD — номер и версия / Transcript — идентичность
Временные ключиновая пара на сессию; прямая секретность; долгосрочный только подписывает
Приоритет комбинированияготовый протокол > стандартная комбинация > самоделка; образец session — шаблон стандартной комбинации
Скелет сессииKeyPair → Shared → HKDF(Transcript) → AEAD(AAD=номер) → SecureZero всё

📖 Справочники API в этой главе:crypto (140 записей)random (41 записей)

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