43. Аппаратная криптография: CRC, RNG, HASH, CRYP
Четыре блока, которые отвечают на четыре разных вопроса: не испортились ли данные (CRC), откуда взять число, которое никто не угадает (RNG), те ли это данные, что были (HASH), и как их прочитать только тому, кому положено (CRYP). Первые два есть почти у всех STM32, вторые два — у части семейств, и там, где их нет, ту же работу делает библиотека.
Раздел опирается на пять камней: F103 и F407 — привычные, F437 — как F407 с блоками HASH и CRYP, L4A6 — с компактным AES, G474 — с CRC, у которого можно задать полином.
Как это называется в документации
Оригинал Перевод cyclic redundancy check (CRC) циклический избыточный код, «контрольная сумма» generator polynomial образующий многочлен, полином initial value / final XOR начальное значение / завершающее исключающее ИЛИ reflected input / output отражённый (обратный порядок бит) вход / выход true random number generator (TRNG) истинный генератор случайных чисел seed error / clock error ошибка начального значения / тактирования hash, digest хеш, свёртка collision коллизия: два сообщения с одним хешем pre-image resistance стойкость к восстановлению прообраза HMAC хеш с ключом для подтверждения подлинности block cipher блочный шифр ECB / CBC / CTR / GCM режимы работы блочного шифра initialization vector (IV), nonce вектор инициализации, одноразовое число authentication tag метка подлинности key derivation подготовка (раскрытие) ключа
43.1 CRC: не сумма, а остаток от деления
Слово «контрольная сумма» прижилось, но CRC — не сумма. Это остаток от деления сообщения на заданное число, причём деление ведётся над битами по правилам, где сложение — это исключающее ИЛИ. Отсюда главное свойство: каждый бит сообщения влияет на результат, и один перевёрнутый бит меняет CRC.
Делитель записывают многочленом: x⁸ + x² + x + 1. Степени — это позиции
единиц в числе; старшая степень подразумевается и опускается, поэтому
многочлен восьмой степени записывают восемью битами: 0x07. Для
шестнадцатой — 0x8005 означает x¹⁶ + x¹⁵ + x² + 1.
| Название | Полином | Где встречается |
|---|---|---|
| CRC-8 | 0x07 |
датчики с однопроводным интерфейсом, короткие кадры |
| CRC-16/MODBUS | 0x8005, отражённый, начальное 0xFFFF |
Modbus RTU (раздел 16) |
| CRC-16/XMODEM | 0x1021, начальное 0x0000 |
XMODEM, SD-карты (раздел 41а) |
| CRC-16/CCITT-FALSE | 0x1021, начальное 0xFFFF |
многие протоколы |
| CRC-32 | 0x04C11DB7, отражённый, начальное и завершающее 0xFFFFFFFF |
Ethernet, ZIP, PNG |
| CRC-32/MPEG-2 | 0x04C11DB7, без отражений и завершающего XOR |
аппаратный блок STM32 |
⚠️ Полином — только один из шести параметров. Ширина, полином, начальное значение, завершающее XOR, отражение входа и отражение выхода. Два CRC с одинаковым полиномом
0x04C11DB7дают для строки1234567890xCBF43926и0x0376E6E7— потому что остальные пять параметров различаются. «CRC не сошёлся с компьютером» почти всегда означает именно это, а не ошибку в данных.
Вкладка «CRC» в калькуляторах приложения считает все пять вариантов из таблицы для одних и тех же байтов — сразу видно, какой из них совпал с другой стороной, и что даёт аппаратный блок после дополнения до целых слов.
Проверка на приёме: к сообщению приписывают его CRC и считают CRC заново.
У вариантов без завершающего XOR получается ноль — если CRC приписан
в нужном порядке байтов: у отражённых (MODBUS) младшим байтом вперёд,
у прямых (MPEG-2) старшим. У вариантов с завершающим XOR выходит не ноль,
а постоянное число: у CRC-32 — 0x2144DF1C. Проще и нагляднее другое:
посчитать CRC по данным без приписанного и сравнить с принятым.
Аппаратный блок
У F1 и F4 блок фиксирован: полином 0x04C11DB7, начальное значение
0xFFFFFFFF, никаких отражений, данные — словами по 32 бита.
RCC->AHBENR |= RCC_AHBENR_CRCEN;
CRC->CR = CRC_CR_RESET; /* к начальному значению */
for (i = 0; i < words; i++) {
CRC->DR = buf[i]; /* по слову */
}
result = CRC->DR;
Два следствия. Первое: результат совпадает с CRC-32/MPEG-2, а не с тем,
что считает архиватор, — сравнивать с компьютером надо тем же вариантом.
Второе: блок берёт слово со старшего бита, а слово в памяти Cortex‑M
лежит младшим байтом вперёд (раздел 3а). Байты '1', '2', '3', '4'
приходят в блок как 0x34333231, и первым он считает '4': для строки
12345678 выходит 0xFEFC54F9 — CRC-32/MPEG-2 строки 43218765. Чтобы
получить CRC по потоку байтов, как на компьютере (0x49E3C2FB), каждое
слово переворачивают: CRC->DR = __REV(buf[i]);. Длина буфера при этом —
кратна четырём байтам.
У G4 и L4 блок программируемый: POL задаёт полином, INIT — начальное
значение, POLYSIZE — ширину 7/8/16/32, REV_IN и REV_OUT — отражения,
а данные можно подавать и байтами: сколько байтов уходит в блок, решает
ширина записи в DR — 32-битная запись отправляет четыре, 8-битная один.
Так на нём считают и CRC-16/MODBUS:
CRC->POL = 0x8005u;
CRC->INIT = 0xFFFFu;
CRC->CR = (1u << CRC_CR_POLYSIZE_Pos) /* 16 бит */
| (1u << CRC_CR_REV_IN_Pos) /* отражение по байтам */
| CRC_CR_REV_OUT
| CRC_CR_RESET;
for (i = 0; i < n; i++)
*(volatile uint8_t *)&CRC->DR = data[i]; /* запись байта, а не слова */
crc16 = (uint16_t)CRC->DR; /* результат — младшие 16 бит */
Обычная запись CRC->DR = data[i]; — 32-битная: вместе с байтом в блок
уйдут три нулевых, и для строки 123456789 вместо 0x4B37 выйдет 0xB1C1.
Вместе с DMA блок считает CRC буфера почти без участия ядра — это штатный способ проверять прошивку при старте (раздел 48).
43.2 RNG: откуда берётся случайность
Псевдослучайная последовательность из формулы (rand, LFSR) хороша для
заставки и никуда не годится для ключа: узнав начальное значение,
её повторит любой. Криптографии нужен источник, который нельзя
воспроизвести, — физический шум.
У STM32 это несколько кольцевых генераторов, чей аналоговый шум складывается и снимается сдвиговым регистром. Блок тактируется от той же ветки 48 МГц, что и USB (раздел 20), и выдаёт одно 32-разрядное слово примерно за 40 тактов этого сигнала — около микросекунды, до 1,2 млн слов в секунду.
RCC->AHB2ENR |= RCC_AHB2ENR_RNGEN;
RNG->CR |= RNG_CR_RNGEN;
if (RNG->SR & (RNG_SR_CECS | RNG_SR_SECS)) {
/* ошибка: число использовать нельзя, перезапустить блок */
}
while (!(RNG->SR & RNG_SR_DRDY)) { }
value = RNG->DR;
⚠️ Два флага ошибок читают до данных, а не после.
CECS— тактовая блока ниже допустимой: обычно не настроена ветка 48 МГц.SECS— сбой источника шума: блок увидел в нём больше 64 одинаковых бит подряд или больше 32 чередований 0 и 1 подряд, и всё, что он выдал до перезапуска, случайным считать нельзя. Восстановление: сброситьSEIS, выключить и снова включитьRNGEN, накопленные слова выбросить. Код, который ждёт толькоDRDY, получит числа, выглядящие случайными, — и это худший из отказов, потому что он незаметен.
Совпадение двух слов подряд блок сам не ловит. Если изделию нужно соответствие требованиям сертификации, это делает программа: первое слово после включения не используют, а каждое следующее сравнивают с предыдущим и совпадение считают отказом. Второе правило — не выбрасывать «некрасивые» значения: отбрасывание нулей или повторяющихся байт делает распределение неслучайным.
Где брать случайность там, где блока нет (F103): шум младших разрядов АЦП с висящего входа, дрожание фазы между двумя независимыми генераторами (HSI против LSI), время до нажатия кнопки. Каждый источник по отдельности слаб; их смешивают хешем — и это уже следующий блок.
Когда криптостойкость не нужна — разброс пауз при повторе (раздел 186), мигание «вразнобой», тестовые данные, — хватит формулы. Но не любой.
Линейный конгруэнтный генератор x = x × 1664525 + 1013904223 по модулю
2³² выглядит случайным только в старших битах. Его младший бит строго
чередуется — в опыте 010101010101, — второй бит повторяется через 4 шага,
третий через 8, четвёртый через 16. Взять x % 2 для «орла или решки» —
значит получить чередование; брать нужно старшие биты, x >> 16.
xorshift32 — три сдвига и три «исключающих или»:
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
Он обходит все ненулевые 32-битные значения по разу: в опыте от единицы
до возврата в неё прошло 4 294 967 295 шагов, то есть 2³² − 1. Ноль —
ловушка: из нуля генератор не выходит, поэтому начальное значение нулём
быть не должно. На Cortex-M4 шаг занимает три команды EOR — сдвиг
встроен в саму команду.
43.3 HASH: отпечаток данных
Хеш-функция превращает сообщение любой длины в строку фиксированной длины так, что обратно не вернуться, а найти два сообщения с одинаковым хешем практически невозможно. Изменение одного бита на входе меняет примерно половину бит на выходе — это называют лавинным эффектом.
| Алгоритм | Длина результата | Состояние |
|---|---|---|
| MD5 | 16 байт | сломан: коллизии находятся за секунды; только контроль целостности |
| SHA-1 | 20 байт | сломан для подписей с 2017 года |
| SHA-256 | 32 байта | рабочий стандарт |
| SHA-512 | 64 байта | быстрее на 64-разрядных, на Cortex‑M — медленнее |
Три вещи, для которых хеш нужен в приборе:
Целостность прошивки. Загрузчик считает SHA-256 образа и сравнивает с записанным — это точнее CRC: CRC ловит случайные искажения, а подобрать другой образ с тем же SHA-256 практически невозможно. Но от подмены хеш сам по себе не спасает: кто подменил образ, запишет рядом и его хеш — как и CRC (раздел 48.4). Защищает только эталон, которому можно верить: подпись (раздел 48а), HMAC с секретным ключом или хеш в области, закрытой от записи.
Хранение секрета без секрета. Пароль доступа хранят не сам, а его хеш: проверить можно, восстановить — нет. С оговоркой: короткий пароль подберут по словарю хешей, поэтому к нему добавляют случайную «соль» и хешируют много раз.
Подтверждение подлинности. HMAC — хеш, в который подмешан ключ: сообщение с верным HMAC мог составить только тот, кто ключ знает. Это защита от подделки команд прибору, дешевле шифрования.
HMAC доказывает, кто составил сообщение, но не когда: перехваченную
команду можно отправить ещё раз, и метка у неё верная. Поэтому
в подписанную часть кладут счётчик команд — прибор принимает только номер
больше последнего — или одноразовое число, выданное самим прибором.
А метку сравнивают за постоянное время, все байты до конца: memcmp вправе
выйти на первом несовпадении, и по времени ответа метку можно подобрать
байт за байтом:
uint8_t diff = 0;
for (i = 0; i < 32; i++)
diff |= tag[i] ^ expect[i]; /* все 32 байта, без выхода по дороге */
ok = (diff == 0);
Аппаратный блок у F437/L4A6 считает SHA-1, SHA-256 и MD5, поддерживает
HMAC и принимает данные через DMA. Данные подаются словами; регистр
DATATYPE говорит, как переставить байты: строка abc в памяти лежит
как 61 62 63, словом это 0x00636261, и без правильного DATATYPE
хеш выйдет от другой последовательности байт. Это второй по частоте
источник вопроса «почему хеш не совпадает с компьютером». Третий — длина:
последнее слово заполнено не целиком, и число значащих бит в нём пишут
в поле NBLW регистра HASH_STR (для abc — 24). Без этого к данным
добавится лишний нулевой байт.
43.4 CRYP: шифрование
Блочный шифр берёт блок фиксированной длины и ключ и выдаёт блок той же длины. AES — стандарт: блок 16 байт, ключ 16, 24 или 32 байта (128/192/256 бит). DES и тройной DES остались только для совместимости.
Ключевой вопрос — не «какой шифр», а «в каком режиме»:
| Режим | Что делает | Брать? |
|---|---|---|
| ECB | каждый блок отдельно | нет: одинаковые блоки дают одинаковый шифр, картинка просвечивает |
| CBC | блок сцеплен с предыдущим через IV | для файлов; требует дополнения до 16 байт |
| CTR | шифр превращён в поток: длина любая | для потоков данных |
| GCM | CTR плюс метка подлинности | да, по умолчанию |
GCM даёт то, чего нет у остальных: подменённый байт шифротекста не расшифруется «в мусор», а откажет проверкой метки в 16 байт. Прибор, принимающий команды, обязан знать, что команда не подделана, — шифрование без подлинности этого не даёт.
⚠️ В CTR и GCM одноразовое число не повторяют никогда. Два сообщения с одним ключом и одним IV дают XOR своих открытых текстов, а в GCM ещё и позволяют подделать метку. IV берут либо из счётчика, который хранится во Flash и переживает перезагрузку, либо 96 бит из RNG. Счётчик в ОЗУ, начинающийся после сброса с нуля, — готовая уязвимость. Ключ меняют раньше, чем счётчик может переполниться.
Сообщение в 60 байт в CBC займёт 64 (четыре блока с дополнением), в CTR и GCM — ровно 60, плюс метка в GCM.
Аппаратный блок ускоряет операцию — на F437 это CRYP с DES/TDES/AES
и FIFO на восемь слов, на L4A6 — компактный AES только с AES-128/256
и режимами ECB/CBC/CTR/GCM, у обоих вход через DMA. Тонкость L4:
расшифровка в ECB/CBC требует подготовленного ключа — отдельный режим
MODE перед расшифровкой; в CTR и GCM этого нет, потому что там шифр
работает только в прямую сторону, — ещё один довод за них.
⚠️ Блок — не библиотека. Регистры делают одно преобразование блока. Режимы сцепления, дополнение, метка GCM, обмен ключами, подписи — код поверх. Писать его самому — верный способ получить уязвимость: берут готовую библиотеку, а блок она использует сама, если он есть.
Библиотека производителя покрывает все семейства, включая те, где аппаратных блоков нет вовсе: AES во всех режимах, SHA-1…SHA-512 с HMAC, программный генератор случайных чисел, RSA, эллиптические кривые и подписи ECDSA. Поставляется собранной под Keil, IAR и GCC; подключение — путь к заголовкам и имя библиотеки в компоновщике (раздел 4).
⚠️ Секрет в исходниках — не секрет. Ключ шифрования в массиве
const uint8_t key[16]читается из прошивки за минуту. Ключи хранят в защищённой области с включённой защитой чтения (раздел 11) или получают из обмена при первом подключении; в исходниках — только в тестовых примерах, и с этим знанием.
43.5 Что чем защищать
| Задача | Средство | Блок |
|---|---|---|
| кадр по UART/CAN не исказился при передаче | CRC-16 | программный или аппаратный CRC |
| прошивка во флеш-памяти цела | CRC-32 при старте | CRC + DMA |
| прошивка не подменена | SHA-256 + подпись | HASH или библиотека |
| команда прибору — от своего сервера | HMAC-SHA-256 | HASH или библиотека |
| данные не прочитает посторонний | AES-GCM | CRYP / AES или библиотека |
| ключ сеанса, одноразовое число | RNG | RNG |
| пароль доступа хранится безопасно | соль + многократный SHA-256 | HASH или библиотека |
Правило выбора: CRC — от помех, хеш — от незаметной порчи, подпись или HMAC — от подмены и подделки, шифр — от чтения. Каждый следующий не заменяет предыдущего, а решает другую задачу.
43.6 Грабли
CRC не совпадает с компьютером. Разные параметры при одном полиноме;
у аппаратного блока F1/F4 — вариант MPEG-2, слова по 32 бита, без
отражений, и байты слова он берёт со старшего: без __REV CRC выйдет
от переставленных байтов (43.1).
Хеш не совпадает с компьютером. DATATYPE не переключён на байты;
блок хеширует переставленные байты слов.
RNG выдаёт одинаковые числа. Совпадений блок сам не ловит: SECS
говорит о сбое источника шума, а не о повторе слова. Проверять SECS
и CECS до чтения и ждать DRDY; при SECS — сбросить SEIS
и перезапустить блок. Где нужен непрерывный тест, слово с предыдущим
сравнивает программа.
RNG не готов никогда. CECS: нет тактовой 48 МГц — не настроена ветка
PLL, как для USB.
Расшифровка даёт мусор. Не тот ключ, не тот IV, не тот режим; в ECB/CBC на L4 — не сделана подготовка ключа перед расшифровкой.
GCM отказывает при верном ключе. Подменён или искажён хотя бы байт шифротекста, заголовка или метки — это не ошибка, а работа режима.
Чужая команда прошла проверку GCM. IV повторился с тем же ключом — например, счётчик в ОЗУ после сброса начался с нуля: по двум сообщениям с одним IV метку подделывают без ключа.
ECB «работает», но шифр повторяется. Одинаковые блоки — одинаковый шифротекст; менять режим на GCM.
Ключ виден в прошивке. Массив в исходниках. Защита чтения и хранение вне кода.
Библиотека не компонуется. Взят вариант не для того ядра или без аппаратного блока при его наличии; в архиве — разные сборки.
43.7 Итоги
- CRC — остаток от деления, не сумма; параметров шесть, полином — один из них; аппаратный блок F1/F4 считает вариант MPEG-2 словами.
- Блок G4/L4 программируемый: полином, ширина, отражения — на нём считают и CRC-16/MODBUS.
- RNG выдаёт слово за ~40 тактов 48 МГц; флаги
SECSиCECSчитают до данных, иначе получают неслучайные «случайные» числа. - Хеш: SHA-256 — рабочий стандарт, MD5 и SHA-1 — только для целостности; на аппаратном блоке следить за порядком байтов.
- Шифр: AES в режиме GCM — шифрование с проверкой подлинности; ECB не использовать; IV в CTR и GCM с одним ключом не повторяется никогда.
- Аппаратный блок — ускоритель для библиотеки, а не её замена.
- Ключи не хранят в исходниках.
Дальше: раздел 43а «Откуда TLS берёт случайность: аппаратный RNG, свой источник энтропии и проверка качества» — как прошивка засевает генератор mbedTLS: драйвер RNG F407 с обработкой ошибок, свой источник на F103 и тесты источника на ходу.