Ориентация главы
Том начинается с древнейших и самых употребимых примитивов. Хеш-функции сжимают произвольные данные в дайджест фиксированной длины — контрольные суммы файлов, адресация по содержимому, отпечатки множеств держатся на них; HMAC даёт хешу ключ, поднимая «целостность» до «аутентифицированной целостности»; KDF (функции деривации ключей) превращают парольные фразы или согласованные секреты в настоящие ключи. Глава покрывает семейство дайджестов от MD5 до SHA-512/256, потоковую трёхшаговость и одноразовую формы вызова, предвычисленное состояние HMAC и пару ясно разделённых функций деривации PBKDF2 и HKDF. Это кирпичи всего дальнейшего: ключи AEAD приходят из KDF, дайджест рукопожатия TLS есть хеш, подпись сертификата ставится на дайджест. После главы вы должны для любого сценария отвечать «какой дайджест, нужен ли ключ, деривировать или использовать напрямую».
74.1引入
Три реальных сценария. Вам нужен контрольный код большого файла для сравнения скачивающими — файл в несколько ГиБ, целиком в память не влезает. Ваш сервис принимает Webhook-обратные вызовы и должен проверять «этот запрос действительно от партнёра с настроенным ключом» — голый хеш не годится: любой может хешировать любое содержимое; вы проверяете «только держатель ключа может вычислить». Ваши пользователи входят по парольной фразе, и вам нужен симметричный ключ шифрования из неё — энтропия пароля мала (человек не запомнит 256 случайных бит), и прямое использование пароля как ключа падает под словарную атаку мгновенно.
Три сценария — три класса примитивов: дайджесты (без ключа, публично вычислимы, для отпечатков), MAC (с ключом, вычислимы только держателем, для аутентификации) и KDF (которые деривируют годный ключевой материал из малоэнтропийных паролей или высокоэнтропийных согласованных секретов). XRT реализует трёх в тонко обрезаемом семействе возможностей — SHA-256 и HMAC-SHA-256 суть два отдельных макроса, и инструменту контрольных сумм не нужно вкомпилировать код MAC.
74.2概念
Панорама семейства дайджестов
XRT несёт семь алгоритмов дайджеста, метаданные длин унифицированы xrtCryptoHashSize:
| Алгоритм | Длина дайджеста | Позиционирование |
|---|---|---|
| MD5 | 16 байт | Только интероперация с историческими протоколами (HTTP Digest и др.); стойкость к коллизиям уже сломана |
| SHA-1 | 20 байт | WebSocket и исторические протоколы; в новых дизайнах подписей использовать нельзя |
| SHA-224 | 28 байт | Разделяет реализацию сжатия с SHA-256 |
| SHA-256 | 32 байта | Современное умолчание — рабочая лошадка целостности, HMAC и дайджестов подписей |
| SHA-384 | 48 байт | Разделяет ядро сжатия с SHA-512 |
| SHA-512 | 64 байта | Длинный дайджест для 64-битных платформ |
| SHA-512/256 | 32 байта | 256-битный вариант на ядре SHA-512, стойкий к расширению длины |
Первая интуиция выбора: новые дизайны стартуют с SHA-256, всегда; MD5/SHA-1 — только для разговора со старыми протоколами и только в полях, требуемых протоколом, никогда для решений безопасности. xrtCryptoHashSize(XCRYPTO_HASH_SHA256) возвращает 32 — заметьте, это запрос метаданных, независимый от того, вкомпилирован ли алгоритм в программу; расчёты ёмкостей не зависят от конфигурации сборки.
Потоковая трёхшаговость и одноразовость
Данные лежат в памяти далеко не всегда целиком — файлы в гигабайты и прибытие по кускам требуют кормить понемногу, считать понемногу. Всё семейство дайджестов и HMAC делит одну машину состояний:
Init строит состояние; Update можно звать сколько угодно раз и с любой длиной — внутри накопление по блокам, побитово идентично одноразовой подаче; Final выписывает дайджест фиксированной длины. Одноразовый вход xrtSha256(数据, 长度, 输出) — «три шага в один», семантически эквивалентен. Структуры состояний (xsha256 и др.) — объекты-значения на стеке вызывающего: без кучи, без разделяемого состояния, любое число конкурентных экземпляров.
HMAC: дать хешу ключ
Сигнатура xrtHmacSha256(密钥, 密钥长, 数据, 数据长, 输出) отличается от голого хеша только парой ключевых параметров, но семантика — скачок качества: только держатель ключа может вычислить тот же MAC. HmacSha256Init предвычисляет два состояния дайджеста inner/outer — ключ обрабатывается один раз, и никакой последующий Update ключа больше не касается. Это стандартная практика «подписи API, проверки Webhook, сессионных токенов»: отправитель прикладывает HMAC(密钥, 内容) к сообщению, получатель пересчитывает и сравнивает — через xrtConstTimeEqual, не memcmp (глава 77 объясняет почему).
Семейство HMAC обрезается независимо по дайджесту-носителю: версия SHA-256 зависит только от SHA-256; SHA-384/512 делят реализацию. Длина ключа любая (сверхдлинные предварительно хешируются); рекомендация — 32 случайных байта.
Двойня KDF: PBKDF2 и HKDF
Две функции деривации решают противоположные задачи с поразительно симметричными сигнатурами — запомните разделение и не перепутаете:
- PBKDF2 (пароль → ключ): вход — малоэнтропийная парольная фраза, и фактор работы (число итераций) поднимает цену атакующего — каждая догадка пароля тоже стоит тех же ста тысяч итераций.
xrtPbkdf2Sha256(口令, salt, 迭代数, 输出). Salt обязан быть случайным на пользователя и храниться при шифротексте — против радужных таблиц и против одинаковых паролей, деривирующих одинаковые ключи. Деривация без единого выделения кучи, безопасно очищает все промежуточные состояния перед возвратом. - HKDF (высокая энтропия → ключ): вход — уже высокоэнтропийный материал (общий секрет DH, мастер-ключ), замедление не нужно — он решает задачу формы и назначения: превращает произвольную по длине энтропию в произвольные по длине ключи и параметром
infoсвязывает назначение.xrtHkdfSha256(salt, IKM, info, 输出). Из одного IKMinfoсо значениями"session"и"cookie"деривируют несвязанные ключи — изоляция одного источника по назначениям, стандартная практика; всё ключевое расписание TLS 1.3 движется HKDF.
HKDF раскрыты снизу вверх в три слоя: Extract (salt+IKM → PRK фиксированной длины), Expand (PRK+info → OKM произвольной длины) и комбинированная функция (PRK на стеке, одним вызовом) — большинству сценариев хватает комбинированной; разбирать на слои нужно только потоковым рукопожатиям (как TLS).
Граница с хеш-библиотекой главы 12
В XRT есть и некриптографическая хеш-библиотека (глава 12, некриптографические хеши семейства SipHash) — она куда быстрее, но без гарантий безопасности, только для хеш-таблиц, отпечатков дедупликации и ключей кэша. Граница одной фразой: где вход контролируется атакующим и результат участвует в решении безопасности — всегда криптодайджест этой главы; чисто скоростные сценарии — глава 12.
74.3示例
Первая полная программа: потоковая трёхшаговость SHA-256
Следующая программа из examples/crypto/sha256/main.c считает дайджест по двум блокам входа "hello " + "world" — SHA-256 от "hello world" есть самый употребимый вектор самопроверки:
#include <stdio.h> #include <xrt.h> */ /* 分块计算 SHA-256,并从不消耗状态的快照取得摘要。 */ int main(void) { xsha256 State; uint8 arrDigest[XRT_SHA256_SIZE]; xrtSha256Init(&State); if ( !xrtSha256Update(&State, "hello ", 6) || !xrtSha256Update(&State, "world", 5) || !xrtSha256Final(&State, arrDigest) ) { return 1; } for ( size_t i = 0; i < sizeof(arrDigest); i++ ) { printf("%02x", (unsigned int)arrDigest[i]); } printf("\n"); return 0; }
Что только что произошло. (1) Состояние xsha256 на стеке, выходной буфер объявлен константой XRT_SHA256_SIZE — длины всегда из макросов, не из рукописных чисел. (2) Два вызова Update дают результат, побитово идентичный одноразовому xrtSha256("hello world", 11, ...) — это и есть выкуп «прибытие по кускам не требует собирать целый буфер»: в файловых потоках главы 45 и приёмных колбэках главы 67 дайджесты копятся ровно так. (3) Вывод — сырые байты; пример печатает их циклом %02x в 64 hex-символа — API дайджестов производит только байты; текстуализация — дело вызывающего (подходит HEX-кодек главы 28).
Вторая полная программа: HMAC — вычисляет только держатель ключа
Вторая программа из examples/crypto/hmac_sha256/main.c, стандартный MAC ключа "secret" по сообщению "message":
#include <stdio.h> #include <xrt.h> */ /* 用一次性入口计算请求正文的 HMAC-SHA256。 */ int main(void) { uint8 arrMac[XRT_SHA256_SIZE]; if ( !xrtHmacSha256("secret", 6, "message", 7, arrMac) ) { return 1; } for ( size_t i = 0; i < sizeof(arrMac); i++ ) { printf("%02x", (unsigned int)arrMac[i]); } printf("\n"); return 0; }
Что только что произошло. Прочтите рядом с первой программой — различий только два: Init принимает параметр ключа, а длина вывода по-прежнему 32 — но семантика из другого мира. Любой может вычислить SHA-256 от "message", поэтому голый хеш не доказывает «от кого сообщение»; только сторона, знающая "secret", может вычислить этот MAC. В реальных системах ключ приходит из конфигурации или согласования и использует полные 32 случайных байта; проверяющая сторона сравнивает через xrtConstTimeEqual. Заметьте: у HMAC тоже есть потоковая трёхшаговость (HmacSha256Init/Update/Final); после того как Init предвычислил состояние ключа, в многосообщенческих сценариях на каждое сообщение нужен один Update — examples/crypto/hash_tour демонстрирует потоковую и одноразовую самосогласованность всех вариантов.
Третья полная программа: PBKDF2 — безопасное производство ключа из пароля
Третья программа из examples/crypto/pbkdf2_sha256/main.c, деривация 32-байтового ключа из парольной фразы за сто тысяч итераций:
#include <stdio.h> #include <xrt.h> */ /* 使用显式 salt 和工作因子派生 32 字节密码密钥。 */ int main(void) { uint8 arrKey[32]; static const uint8 Salt[16] = { 0x8Du, 0xCBu, 0x89u, 0x2Eu, 0xD0u, 0x31u, 0x42u, 0x49u, 0xA1u, 0x7Eu, 0x1Fu, 0x5Bu, 0x12u, 0x6Du, 0xE9u, 0x75u }; if ( !xrtPbkdf2Sha256( "correct horse battery staple", 28, Salt, sizeof(Salt), 100000, arrKey, sizeof(arrKey) ) ) { return 1; } for ( size_t i = 0; i < sizeof(arrKey); i++ ) { printf("%02x", (unsigned int)arrKey[i]); } printf("\n"); xrtSecureZero(arrKey, sizeof(arrKey)); return 0; }
Что только что произошло. (1) Порядок параметров — (пароль, salt, итерации, вывод); 100000 итераций есть фактор работы: легитимный пользователь платит сто тысяч итераций за каждый вход (миллисекунды — приемлемо), и атакующий платит ту же цену за каждую догадку, поднимая стоимость словарной атаки в сто тысяч раз. (2) Salt — явная 16-байтовая константа — фиксирована для воспроизводимости; продакшн обязан генерировать случайно на пользователя и хранить при шифротексте (ловушка 3). (3) Финал xrtSecureZero(arrKey, ...) стирает выведенный ключ со стека — ключевой материал очищается сразу по использовании, фиксированный жест всего тома. Могут ли пароль и salt перекрываться? Нет — вывод не должен перекрываться со входами; контракт проверяет все параметры до первой записи.
Четвёртая полная программа: HKDF — изоляция ключей одного источника по назначению
Четвёртая программа из examples/crypto/hkdf_sha256/main.c, один вызов превращает три куска материала в сессионный ключ:
#include <stdio.h> #include <xrt.h> */ /* 一行派生 32 字节会话密钥。 */ int main(void) { uint8 arrKey[32]; if ( !xrtHkdfSha256( "salt", 4, "input key material", 18, "session", 7, arrKey, sizeof(arrKey) ) ) { return 1; } for ( size_t i = 0; i < sizeof(arrKey); i++ ) { printf("%02x", (unsigned int)arrKey[i]); } printf("\n"); return 0; }
Что только что произошло. Три параметра при деле каждый: salt — публичное случайное значение (сменили salt — совсем новый ключ), IKM — сырая энтропия (здесь демонстрационная строка; в реальных сценариях — общий секрет DH, глава 76), info — привязка назначения. Замените info на "cookie" и прогоните снова — вывод совершенно другой: это и есть «одна энтропия деривирует много ключей, ключи не влияют друг на друга», изоляция назначения. Сессионный пример главы 77 поставит HKDF в полную цепь: согласование x25519 → деривация HKDF → шифрование AEAD.
74.4契约
- Семейство дайджестов: семь алгоритмов (MD5/SHA-1/SHA-224/256/384/512/512_256);
xrtCryptoHashSizeдаёт метаданные длин (независимо от вкомпиляции); потоковая трёхшаговость и одноразовость побитово идентичны; состояния — стековые объекты-значения, без кучи, произвольная конкурентность экземпляров. - Граница MD5/SHA-1: только интероперация с историческими протоколами (HTTP Digest, WebSocket); запрещены в новых дизайнах подписей, сертификатов, доверия содержимому и хранения паролей.
- HMAC: Init предвычисляет состояние ключа; Update ключа больше не касается; SHA-256 обрезается независимо, SHA-384/512 делят реализацию; длина ключа любая, рекомендация 32 случайных байта.
- PBKDF2: только для малоэнтропийных паролей; итерации и длина вывода обязаны быть больше нуля; вывод не должен перекрываться с паролем/salt; ноль выделений кучи, состояния HMAC и промежуточные значения очищаются перед возвратом; максимум вывода
UINT32_MAX × HashLen(32-битный счётчик блоков). - HKDF: только для высокоэнтропийного материала; три уровня входов (Extract/Expand/комбинированный); info связывает назначение, давая изоляцию одного источника; комбинированная функция держит PRK на стеке.
- Ядро инструментов:
xrtConstTimeEqualсравнение за постоянное время (без раннего выхода по первому различию);xrtSecureZerovolatile-очистка (против удаления мёртвой записи); источник случайности — отдельная возможностьRANDOM_SECURE(глава 12), crypto_core случайности не несёт. - Ошибки: неудачи объявляются возвращаемым значением с деталями в слоте ошибки потока; ошибки параметров/диапазонов/пределов блоков возвращаются до первой записи вывода — неудача не загрязняет выходной буфер.
Типичные ошибки
Симптомы: аудит не проходит, зависимости отказывают в интеграции; в худшем раскладе атакующий строит два содержимого с одним дайджестом (коллизии MD5 давно операция секундного масштаба).
Причина: стойкость MD5 и SHA-1 к коллизиям сломана. XRT вкомпилирует их, потому что протоколы HTTP Digest и WebSocket всё ещё требуют — необходимость интероперации не равно пригодность для безопасности.
uint8 Digest[16]; xrtMd5(Content, iSize, Digest); /* "проверка целостности" нового дизайна */ store(Digest); /* атакующий может построить коллизионное содержимое */
uint8 Digest[XRT_SHA256_SIZE]; xrtSha256(Content, iSize, Digest); /* новые дизайны по умолчанию с SHA-256 */ store(Digest); и никогда как вход решения безопасности */
Симптомы: поддельные запросы проходят беспрепятственно — «я хешировал и совпало» не останавливает атакующего, потому что атакующий вычислит тот же хеш.
Причина: хеш без ключа, вычислим любым. Проверка целостности (против ошибок передачи) и аутентифицированная целостность (против злой подделки) — две вещи, между которыми ключ.
/* запрос несёт ?sig=sha256(body); сервер пересчитывает и сравнивает */ uint8 Digest[XRT_SHA256_SIZE]; xrtSha256(Body, iSize, Digest); if ( memcmp(Digest, SentSig, sizeof(Digest)) == 0 ) { accept(); /* атакующий правит body, пересчитывает sig — и подделывает */ }
/* HMAC: легитимный MAC производит только держатель ключа */ uint8 Mac[XRT_SHA256_SIZE]; xrtHmacSha256(ApiKey, sizeof(ApiKey), Body, iSize, Mac); if ( xrtConstTimeEqual(Mac, SentMac, sizeof(Mac)) ) { accept(); }
Симптомы: у двух пользователей одинаковый пароль → одинаковые деривированные ключи → утечка ключа одного вскрывает данные другого; предвычисленные радужные таблицы работают по всей базе.
Причина: миссия salt — «сделать каждый вход деривации неповторимым». Глобальный salt равен отсутствию salt — атакующий предвычислит словарь паролей под этот salt.
static const uint8 Salt[16] = { /* жёстко зашито в исходнике */ }; xrtPbkdf2Sha256(Password, iLen, Salt, sizeof(Salt), 100000, Key, sizeof(Key));
uint8 Salt[16]; if ( !xrtSecureRandom(Salt, sizeof(Salt)) ) { /* случайно на пользователя */ return false; } xrtPbkdf2Sha256(Password, iLen, Salt, sizeof(Salt), 100000, Key, sizeof(Key)); save_with_ciphertext(Salt); /* salt не секрет; хранится при шифротексте */
Практические задания
Чтением файлов главы 45 + потоковой трёхшаговостью этой главы реализуйте печать SHA-256-дайджеста (64 hex-символа) файла по любому пути. Критерий приёмки: чтение кусками по 1 КиБ и целиком даёт одинаковый результат; вывод совпадает с системным инструментом (sha256sum / certutil -hashfile ... SHA256).
Реализуйте bool verify_webhook(cstr sBody, const uint8* pKey, size_t iKeySize, const uint8* pSentMac): пересчёт HMAC-SHA-256 и сравнение через xrtConstTimeEqual. Затем нарочно замените сравнение на memcmp и подумайте в оптике тайминговой атаки, которую разберёт глава 77: почему здесь обязательно постоянное время? Запишите в комментарии кода.
Реализуйте derive_keys(IKM, iSize) → {会话密钥, cookie 密钥, 备份密钥}: одним HKDF-SHA-256, один IKM, три разных info — три 32-байтовых ключа. Затем проверьте изоляцию: смена одного байта IKM меняет все три ключа целиком; фиксированный IKM при повторной деривации даёт побитово идентичный результат. В конце все три ключа — xrtSecureZero. Критерий приёмки: ноль выделений кучи на всём пути (PBKDF2/HKDF не выделяют, и ваша обёртка не должна).
Итоги
| Тема | Краткая справка |
|---|---|
| Выбор алгоритма | новые дизайны с SHA-256; MD5/SHA-1 только для интероперации; длинные дайджесты на 64-битных — SHA-512 |
| Метаданные длин | xrtCryptoHashSize(XCRYPTO_HASH_SHA256) и др.: 16/20/28/32/48/64, независимо от вкомпиляции |
| Потоковая трёхшаговость | Init → Update×N → Final; побитово идентично одноразовой; стековые состояния, произвольная конкурентность |
| HMAC | Init предвычисляет состояние ключа; вычисляет только держатель; сравнение через xrtConstTimeEqual |
| PBKDF2 | малоэнтропийный пароль + фактор работы (итерации); salt случайно на пользователя, при шифротексте; промежуточное очищается перед возвратом |
| HKDF | высокоэнтропийный материал; три уровня Extract/Expand/комбинированный; info связывает назначение, изолируя источник; движок TLS 1.3 |
| Ядро инструментов | xrtConstTimeEqual сравнение за постоянное время; xrtSecureZero volatile-очистка |
| Граница с главой 12 | решения безопасности — эта глава; хеш-таблицы/отпечатки/ключи кэша — некриптохеши главы 12 |
| Контракт ошибок | ошибки параметров возвращаются до первой записи вывода; вывод не перекрывается со входом (PBKDF2) |
| Обрезка | SHA/HMAC/PBKDF2/HKDF — независимые макросы; сборка только с дайджестом не несёт MAC |
📖 Справочники API в этой главе:crypto (140 записей)hash (11 записей)random (41 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки