43а. Откуда TLS берёт случайность: аппаратный RNG, свой источник энтропии и проверка качества
В 43.2 разобран блок RNG сам по себе: шум кольцевых генераторов, флаги
SECS и CECS, первое слово не брать. Но ключ TLS (107.7, 37.4),
одноразовое число AES (48г) или ключ подписи (48а) делает не блок, а
криптобиблиотека, и случайность она берёт у генератора CTR_DRBG. На компьютере его засевает
операционная система. На микроконтроллере без ОС сеять нечем, пока
программа сама не скажет библиотеке, откуда брать шум.
Здесь: как mbedTLS собирает энтропию и что ей нужно от нашей функции; драйвер RNG F407, который не выдаёт числа при отказе блока; у F103 своего RNG нет — свой сборщик из шума АЦП и дрожания генератора LSI; как оценить, сколько случайности в одном отсчёте, и два простых теста, которые ловят поломку источника на ходу; почему хеш поверх плохого источника не спасает и что бывает, когда два прибора засеяны одинаково.
Опоры: RM0090 Rev 19 (глава 24 — RNG: 24.3.1 порядок работы и
непрерывный тест, 24.3.2 ошибки, 24.4 регистры), RM0008 Rev 17 (калибровка
LSI через TIM5 и бит TIM5CH4_IREMAP; датчик температуры — 11.10; такт АЦП
не выше 14 МГц, калибровка), заголовки CMSIS stm32f407xx.h и
stm32f10x.h из пакетов Keil, исходники mbedTLS 3.6.4
(github.com/Mbed-TLS/mbedtls, тег v3.6.4; лицензия Apache-2.0 или
GPL-2.0-or-later): entropy.h, entropy.c, entropy_poll.h,
ctr_drbg.c, mbedtls_config.h. Тесты здоровья и оценка энтропии в
разделе — свои, простые; со стандартами на источники энтропии они не
сверялись. Источники шума F103 и их числа — модели на компьютере, а не
замер платы.
Как это называется в документации
Оригинал Перевод entropy source источник энтропии, физического шума min-entropy минимальная энтропия: по самому вероятному значению conditioning кондиционирование: сжатие сырых отсчётов хешем health test тест здоровья источника на ходу repetition count test тест повторов подряд adaptive proportion test тест доли одного значения в окне DRBG, CTR_DRBG детерминированный генератор, на AES в режиме счётчика seed, reseed, nonce зерно, пересев, одноразовая добавка personalization string строка персонализации NV seed зерно в энергонезависимой памяти MBEDTLS_ENTROPY_HARDWARE_ALTсвой источник через mbedtls_hardware_poll()continuous random number generator test непрерывный тест: соседние слова не равны
43а.1 Цепочка от шума до ключа
Генератор CTR_DRBG — формула: AES в режиме счётчика. Из одного и того же зерна он выдаёт одну и ту же последовательность, поэтому вся стойкость ключей — в зерне. Зерно делает модуль энтропии: он опрашивает источники, складывает их байты в хеш-аккумулятор и, когда каждый источник дал свой порог, выдаёт хеш.
Что в mbedTLS 3.6.4 устроено так (сверено с исходниками и прогоном):
MBEDTLS_ENTROPY_HARDWARE_ALTв конфигурации — и модуль энтропии сам добавляет источникmbedtls_hardware_poll()с порогомMBEDTLS_ENTROPY_MIN_HARDWARE= 32 байта и пометкой «сильный». Функцию пишет программа; первым аргументом она должна приниматьNULL.MBEDTLS_NO_PLATFORM_ENTROPY— иначе библиотека ищет/dev/urandomили CryptoAPI и на голом кристалле не соберётся.- За один вызов модуль просит до
MBEDTLS_ENTROPY_MAX_GATHER= 128 байт; отдать можно меньше, записав число в*olen. - Байт источника модуль считает полным байтом энтропии. Значит,
mbedtls_hardware_pollдолжна отдавать уже сжатые, полноценные байты, а не сырые отсчёты АЦП. - Когда включён только SHA-256, аккумулятор — SHA-256, блок 32 байта, и CTR_DRBG при севе берёт 32 байта энтропии плюс 16 на одноразовую добавку — два обращения к модулю.
- Ненулевой код из источника обрывает сбор: сев возвращает
MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED= −0x0034. Источник, который «успешно» отдаёт ноль байт, модуль спрашивает доENTROPY_MAX_LOOP: в опыте 257 раз, затем −0x003C.
Отсюда правило всего раздела: при любом сомнении в источнике функция возвращает ошибку, а программа, получив ошибку сева, не делает ключей вовсе. Это лучше, чем ключ из предсказуемых байт.
43а.2 RNG F407: драйвер, который не выдаёт плохие числа
Блок RNG — три регистра: RNG_CR (бит RNGEN), RNG_SR (DRDY,
CECS, SECS, CEIS, SEIS), RNG_DR. Адреса и биты — свои в
regs43a.h, сверены с stm32f407xx.h: блок по 0x50060800, тактирование
— бит 6 RCC_AHB2ENR. По RM0090 слово готово не позже чем через 40 тактов
RNG_CLK; тактовая — ветка 48 МГц от PLL (PLLQ). Если RNG_CLK ниже
HCLK/16, встаёт CECS.
static void rng_restart(void)
{
RNG_SR = ~RNG_SR_SEIS; /* SEIS сбрасывается записью нуля, остальные биты только читаются */
RNG_CR &= ~RNG_CR_RNGEN;
RNG_CR |= RNG_CR_RNGEN;
have_prev = 0;
rng_stat.restarts++;
}
int rng_init(void)
{
RCC_AHB2ENR |= RCC_AHB2ENR_RNGEN;
(void)RCC_AHB2ENR; /* дождаться включения тактирования */
RNG_CR |= RNG_CR_RNGEN;
have_prev = 0;
return RNG_OK;
}
Порядок проверок — по RM0090: сначала ошибки, потом DRDY. SEIS при
сброшенном SECS значит «сбой был и прошёл», но слово в DR всё равно
брать нельзя — это тоже повод для перезапуска. CEIS без CECS — такт
был ниже нормы и восстановился; прежние слова RM0090 разрешает
использовать. Без ограничения опросов драйвер на неработающем блоке висел
бы вечно: RNG_POLL_MAX = 1000.
static int rng_wait(uint32_t *w)
{
for (uint32_t n = 0; n < RNG_POLL_MAX; n++) {
uint32_t sr = RNG_SR;
if (sr & (RNG_SR_SECS | RNG_SR_SEIS))
return RNG_ERR_SEED;
if (sr & RNG_SR_CECS) {
RNG_SR = ~RNG_SR_CEIS;
rng_stat.clock_err++;
return RNG_ERR_CLOCK;
}
if (sr & RNG_SR_CEIS) /* тактовая была ниже нормы, но восстановилась: */
RNG_SR = ~RNG_SR_CEIS; /* прежние слова годны (RM0090, 24.3.2) */
if (sr & RNG_SR_DRDY) {
*w = RNG_DR;
return RNG_OK;
}
}
rng_stat.timeouts++;
return RNG_ERR_TIMEOUT;
}
Первое слово после включения — только для сравнения (непрерывный тест RM0090, 24.3.1), каждое следующее сравнивается с предыдущим. После сбоя источника — до трёх перезапусков:
int rng_get(uint32_t *out)
{
unsigned tries = 0;
for (;;) {
uint32_t w;
int r = rng_wait(&w);
if (r == RNG_ERR_SEED) {
rng_stat.seed_err++;
if (++tries > RNG_SEED_RETRY)
return RNG_ERR_SEED;
rng_restart();
continue;
}
if (r != RNG_OK)
return r;
if (!have_prev) { /* первое слово после включения не выдаём */
prev = w;
have_prev = 1;
continue;
}
if (w == prev) { /* непрерывный тест: два одинаковых слова подряд */
rng_stat.repeats++;
return RNG_ERR_REPEAT;
}
prev = w;
rng_stat.words++;
*out = w;
return RNG_OK;
}
}
Драйвер прогнан на компьютере против модели блока (sim43a.c): модель
выставляет DRDY через 14 обращений к регистрам, по-настоящему отрабатывает
сброс SEIS/CEIS записью нуля и выключение RNGEN. Шесть вызовов
rng_get подряд:
| Сценарий модели | Коды | Что сделал драйвер |
|---|---|---|
| исправный блок | 0 ×6 | 6 слов; модель выдала 7 — первое отброшено |
| один сбой источника на 3-м слове | 0 ×6 | 1 перезапуск; модель выдала 9 слов |
| источник сбоит всегда | −3 ×6 | на каждый вызов 4 ошибки и 3 перезапуска |
тактовая ниже нормы (CECS) |
−2 ×6 | ни одного слова |
блок не отвечает (SR = 0) |
−1 ×6 | по 1000 опросов и выход |
| слово повторилось | 0, 0, 0, −4, 0, 0 | повтор пойман на 4-м вызове |
был сбой такта, прошёл (CEIS) |
0 ×6 | CEIS сброшен, 6 слов |
После повтора драйвер следующим вызовом снова отдаёт слова: что делать с отказом, решает вызывающий. mbedTLS решает просто — прекращает сев.
43а.3 Подключение к mbedTLS
Конфигурация — только то, что нужно для зерна: модуль энтропии, CTR_DRBG, AES, SHA-256. Под опыт с ключами на компьютере добавляется ECP P-256, под прибор без источника — зерно во флеше (43а.6):
/* cfg43a.h — раздел 43а: mbedTLS 3.6 только для сбора энтропии и CTR_DRBG.
* CFG43A_NV — вариант без аппаратного источника: зерно во флеше (NV seed).
* CFG43A_KEYS — ещё ECP P-256, чтобы показать ключи (только опыты на компьютере). */
#define MBEDTLS_ENTROPY_C
#define MBEDTLS_CTR_DRBG_C
#define MBEDTLS_AES_C
#define MBEDTLS_AES_ROM_TABLES
#define MBEDTLS_AES_FEWER_TABLES
#define MBEDTLS_SHA256_C
#define MBEDTLS_SHA256_SMALLER
#define MBEDTLS_MD_C
#define MBEDTLS_NO_PLATFORM_ENTROPY /* нет /dev/urandom и CryptoAPI */
#define MBEDTLS_ENTROPY_MAX_SOURCES 4 /* вместо 20: каждый слот — место в ОЗУ */
#if defined(CFG43A_NV)
#define MBEDTLS_PLATFORM_C
#define MBEDTLS_ENTROPY_NV_SEED
#define MBEDTLS_PLATFORM_NV_SEED_READ_MACRO nv43a_read
#define MBEDTLS_PLATFORM_NV_SEED_WRITE_MACRO nv43a_write
#include <stddef.h>
int nv43a_read(unsigned char *buf, size_t len);
int nv43a_write(unsigned char *buf, size_t len);
#else
#define MBEDTLS_ENTROPY_HARDWARE_ALT /* своя mbedtls_hardware_poll() */
#endif
#if defined(CFG43A_KEYS)
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_ECP_C
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#endif
Сам источник для F407 — тонкая обёртка над драйвером. За один вызов он отдаёт 32 байта — ровно порог; просить блок о 128 нет смысла:
int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t *olen)
{
(void)data;
*olen = 0;
if (len > 32)
len = 32; /* порог источника — 32 байта, больше за раз не нужно */
if (rng_fill(output, len) != RNG_OK)
return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
*olen = len;
return 0;
}
Сев на той же модели блока: mbedtls_ctr_drbg_seed, затем 16 байт из
mbedtls_ctr_drbg_random. Вызовы mbedtls_hardware_poll посчитаны
перехватом компоновщика (--wrap):
| Сценарий | mbedtls_ctr_drbg_seed |
Вызовов источника | Байт | Слов RNG |
|---|---|---|---|---|
| исправный блок | 0 | 2 (просит по 128) | 64 | 16 |
| один сбой источника | 0 | 2 | 64 | 16 |
| был сбой такта, прошёл | 0 | 2 | 64 | 16 |
| источник сбоит всегда | −0x0034 | 1 | 0 | 0 |
| тактовая ниже нормы | −0x0034 | 1 | 0 | 0 |
| блок не отвечает | −0x0034 | 1 | 0 | 0 |
| слово повторилось | −0x0034 | 1 | 0 | 3 |
64 байта на сев — 32 + 16 байт, каждый раз с отдельным сбором.
Код возврата сева проверять обязательно. После неудачного сева
mbedtls_ctr_drbg_random в опыте вернула 0 и выдала байты — во всех
четырёх отказах одни и те же: 530f8afbc74536b9…. Генератор не знает, что
его не засеяли, и выдаёт последовательность, которую повторит любой. (У
исправного блока и «сбоя такта» выход тоже совпал, 4afe8942f8435c6d…, —
модель блока выдаёт одни и те же слова; настоящий блок — нет.)
В QEMU (7.2, плата netduinoplus2 — STM32F405 с тем же блоком по тому же
адресу) RNG нет: он заведён как «нереализованное устройство». Прошивка
fw43a.c пишет 0x00000004 в RNG_CR, RNG_SR читается нулём,
rng_get возвращает −1 (тайм-аут), сев — 0xFFFFFFCC, то есть −0x0034;
регистр SR прочитан 2001 раз — 1000 опросов в rng_get, 1000 в севе и
один для печати. Поэтому настоящие числа RNG в разделе — только из модели;
на плате драйвер не запускался.
Размеры (arm-none-eabi-gcc 12.2.1, -Os, Cortex-M4): rng43a.o —
376 байт кода (rng_get с встроенным rng_wait 240, rng_init 52,
rng_fill 48, mbedtls_hardware_poll 36); вся прошивка с модулем
энтропии, CTR_DRBG, AES и SHA-256 из mbedTLS — 9972 байта кода,
104 данных, 472 ОЗУ; из них контекст энтропии 100 байт (источников 4
вместо 20), контекст CTR_DRBG 320.
43а.4 F103: свой сборщик из АЦП и LSI
У F103 блока RNG нет. Шум есть в аналоговой части: младшие разряды АЦП на датчике температуры дрожат от теплового шума, а внутренний генератор LSI (около 40 кГц, по RM0008 от 30 до 60) уходит по фазе относительно тактовой процессора. Ни то, ни другое производитель не обещает как источник случайности: сколько в них энтропии, зависит от платы, питания и температуры — это меряют на своём приборе (43а.5), а числа ниже — модели.
АЦП: датчик температуры — канал 16, включается битом TSVREFE вместе с
ADON. Для измерения температуры RM0008 советует выборку 17,1 мкс; нам
точность не нужна, нужен шум — берём самую короткую, 1,5 такта. Такт АЦП
не выше 14 МГц: делитель 6, 12 МГц при 72.
void adc_noise_init(void)
{
F1_RCC_CFGR = (F1_RCC_CFGR & ~RCC_CFGR_ADCPRE) | RCC_CFGR_ADCPRE_6; /* АЦП не быстрее 14 МГц */
F1_RCC_APB2ENR |= RCC_APB2ENR_ADC1EN;
ADC1_CR2 = ADC_CR2_ADON | ADC_CR2_TSVREFE; /* АЦП и датчик включать вместе (RM0008, 11.10) */
for (volatile int i = 0; i < 200; i++) { /* tSTAB и два такта АЦП до калибровки */
}
ADC1_CR2 |= ADC_CR2_RSTCAL;
while (ADC1_CR2 & ADC_CR2_RSTCAL) {
}
ADC1_CR2 |= ADC_CR2_CAL;
while (ADC1_CR2 & ADC_CR2_CAL) {
}
ADC1_SMPR1 = 0; /* канал 16: 1,5 такта — для температуры мало, для шума то, что нужно */
ADC1_SQR1 = 0; /* одно преобразование в группе */
ADC1_SQR3 = 16; /* первым — канал 16, датчик температуры */
ADC1_CR2 |= ADC_CR2_EXTSEL_SW | ADC_CR2_EXTTRIG;
}
uint16_t adc_noise_sample(void)
{
ADC1_CR2 |= ADC_CR2_SWSTART;
while (!(ADC1_SR & ADC_SR_EOC)) {
}
return (uint16_t)ADC1_DR; /* чтение DR сбрасывает EOC */
}
Дрожание LSI меряет TIM5: бит TIM5CH4_IREMAP в AFIO_MAPR заводит LSI на
вход канала 4 — так RM0008 предлагает калибровать LSI. Это есть только у
F103 high-density (F103xE) и старше; у F103C8 TIM5 нет, там остаётся
АЦП. Захват каждого восьмого фронта: восемь периодов LSI при таймере на
72 МГц — около 14 400 тактов; в отсчёт идёт разность двух захватов.
void lsi_jitter_init(void)
{
F1_RCC_CSR |= RCC_CSR_LSION;
while (!(F1_RCC_CSR & RCC_CSR_LSIRDY)) {
}
F1_RCC_APB2ENR |= RCC_APB2ENR_AFIOEN;
F1_RCC_APB1ENR |= RCC_APB1ENR_TIM5EN;
F1_AFIO_MAPR |= AFIO_MAPR_TIM5CH4_IREMAP; /* LSI на вход канала 4 (как для калибровки LSI) */
TIM5_PSC = 0;
TIM5_ARR = 0xFFFFu;
TIM5_CCMR2 = TIM_CCMR2_CC4S_TI4 | TIM_CCMR2_IC4PSC_8;
TIM5_CCER = TIM_CCER_CC4E;
TIM5_CR1 = TIM_CR1_CEN;
}
uint16_t lsi_jitter_sample(void)
{
uint16_t a;
(void)TIM5_CCR4; /* выбросить старый захват */
while (!(TIM5_SR & TIM_SR_CC4IF)) {
}
a = (uint16_t)TIM5_CCR4;
while (!(TIM5_SR & TIM_SR_CC4IF)) {
}
return (uint16_t)((uint16_t)TIM5_CCR4 - a);
}
Модели для опытов: АЦП — 1850 плюс нормальный шум с отклонением σ = 2 единицы младшего разряда («исправный»), σ = 0,45 («тихий вход»), без шума («залип»), медленный дрейф плюс шум («дрейф»: каждое значение — 0,99 предыдущего плюс шум σ = 1); LSI — 14 400 плюс шум σ = 3 такта плюс медленный уход частоты. Шум моделей задан нами, а не измерен.
43а.5 Сколько энтропии в одном отсчёте
Мерой служит минимальная энтропия: H = −log₂ p, где p — вероятность
самого частого значения. Это то, сколько бит нужно угадывающему с первой
попытки. Своя оценка по 100 000 отсчётов: доля самого частого значения,
сверху запас 2,576 сигмы (99 %), — и так трижды: по самим отсчётам, по
парам соседних (делённая на 2) и по разностям соседних. Берём наименьшую.
Пары и разности ловят зависимость между отсчётами, которую по одному
отсчёту не видно. «Правда» модели — её наибольшая вероятность при
известном прошлом, посчитанная по формуле.
| Модель | По отсчётам | По парам | По разностям | Берём | Правда модели | Отсчётов на 256 бит | В прошивке, ×2 |
|---|---|---|---|---|---|---|---|
| АЦП исправный | 2,325 | 2,323 | 2,829 | 2,323 | 2,341 | 111 | 222 |
| АЦП тихий | 0,441 | 0,441 | 0,793 | 0,441 | 0,447 | 581 | 1162 |
| АЦП залип | 0 | 0 | 0 | 0 | 0 | — | — |
| АЦП с дрейфом | 4,096 | 2,731 | 1,421 | 1,421 | 1,385 | 181 | 362 |
| LSI | 3,666 | 3,305 | 3,373 | 3,305 | 2,921 | 78 | 156 |
Что видно.
- У дрейфа оценка по отсчётам — 4,096 бит, почти втрое больше правды: значения расползлись широко, но каждое почти предсказуемо по соседу. Разности это видят: 1,421.
- У LSI все три оценки выше правды (3,305 против 2,921): медленный уход частоты оценке неотличим от шума. Поэтому в прошивке отсчётов вдвое больше расчётного: 156 отсчётов LSI по правде модели дают 455,7 бита, 222 отсчёта исправного АЦП — 519,7.
- Оценку делают на своей плате, на записи сырых отсчётов, а не на модели.
Из оценки H — пороги тестов здоровья (43а.6), свои: допустимая частота
ложной тревоги — 2⁻³⁰ на отсчёт. Повторы подряд: вероятность C одинаковых
отсчётов подряд у исправного источника не больше 2^(−H·(C−1)), отсюда
C = 1 + ⌈30/H⌉. Доля в окне из 512: наименьший порог, у которого
вероятность набрать его совпадений с первым отсчётом окна не больше 2⁻³⁰
(биномиальный хвост). Для исправного АЦП это 14 и 162, для LSI —
11 и 99; для тихого входа было бы 70 и 435, для дрейфа — 23 и 260.
Пороги и число отсчётов записаны в ent_f103.c константами.
43а.6 Тесты, хеш и одинаковые ключи
Тесты идут на каждом сыром отсчёте, до хеша. Отказ «залипает»: после него источник не отдаёт ничего до перезагрузки.
int ent_health(ent_src_t *s, uint16_t x)
{
if (s->failed)
return s->failed;
/* 1. Повторы подряд: застрявший АЦП, остановившийся генератор */
if (s->started && x == s->last) {
if (++s->run >= s->rct_cut)
return s->failed = ENT_FAIL_REPEAT;
} else {
s->run = 1;
}
s->last = x;
/* 2. Доля в окне: первый отсчёт окна — образец, считаем его повторы до конца окна */
if (!s->started || s->pos >= s->apt_win) {
s->ref = x;
s->pos = 1;
s->hits = 1;
} else {
s->pos++;
if (x == s->ref && ++s->hits >= s->apt_cut)
return s->failed = ENT_FAIL_SHARE;
}
s->started = 1;
return ENT_OK;
}
Кондиционирование — SHA-256 по need отсчётам целиком, по два байта на
отсчёт (не «младшие биты»: хеш сам выжмет то, что есть):
int ent_block(ent_src_t *s, unsigned char out[32])
{
mbedtls_sha256_context sha;
unsigned char b[2];
int r = ENT_OK;
mbedtls_sha256_init(&sha);
mbedtls_sha256_starts(&sha, 0);
for (uint16_t i = 0; i < s->need; i++) {
uint16_t x = s->sample();
s->taken++;
if ((r = ent_health(s, x)) != ENT_OK)
break;
b[0] = (unsigned char)x;
b[1] = (unsigned char)(x >> 8);
mbedtls_sha256_update(&sha, b, 2);
}
if (r == ENT_OK)
mbedtls_sha256_finish(&sha, out);
mbedtls_sha256_free(&sha);
return r;
}
int ent_poll(void *data, unsigned char *output, size_t len, size_t *olen)
{
unsigned char blk[32];
size_t n = len < sizeof blk ? len : sizeof blk;
*olen = 0;
if (ent_block((ent_src_t *)data, blk) != ENT_OK)
return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
memcpy(output, blk, n);
mbedtls_platform_zeroize(blk, sizeof blk);
*olen = n;
return 0;
}
Подключение к mbedTLS у F103: АЦП — через mbedtls_hardware_poll, LSI —
вторым сильным источником со своим порогом:
int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t *olen)
{
(void)data;
return ent_poll(&adc_src, output, len, olen);
}
int ent_f103_add_lsi(mbedtls_entropy_context *ctx)
{
return mbedtls_entropy_add_source(ctx, ent_poll, &lsi_src, 32, MBEDTLS_ENTROPY_SOURCE_STRONG);
}
Тесты с порогами прошивки (14 и 162 из 512) на моделях АЦП, по 10 миллионов отсчётов:
| Модель | Тревога | Тест |
|---|---|---|
| АЦП исправный | нет за 10 000 000 | — |
| АЦП тихий | на 190-м отсчёте | повторы |
| АЦП залип | на 14-м | повторы |
| исправный, на 20 000-м отсчёте залип | через 14 отсчётов | повторы |
| исправный, на 20 000-м стал тихим | через 543 отсчёта | повторы |
| АЦП с дрейфом | на 937 235-м | повторы |
| LSI (свои пороги 11 и 99) | нет за 10 000 000 | — |
Тесты ловят поломку: залипание и затихание. Дрейф они почти не видят — это ошибка не поломки, а замысла, и её ловит только оценка 43а.5 на этапе разработки. В этих моделях первым всякий раз срабатывал тест повторов; тест доли нужен источникам, которые чередуют несколько значений, — такой модели в опыте не было.
Хеш прячет отказ. Залипший АЦП, 1000 блоков: SHA-256 от номера блока и 512 одинаковых отсчётов. На выходе 32 000 байт, доля единиц 0,4990, хи-квадрат по 256 значениям байта 205,2 — простые тесты выхода отказа не видят. Поэтому тесты стоят до хеша, а не после.
Сев CTR_DRBG через ent_f103.c на моделях: исправный АЦП — сев прошёл,
взято 444 отсчёта АЦП и 312 LSI (два сбора по 222 и 156); залипший — сев
вернул −0x0034 после 14 отсчётов АЦП, LSI даже не спросили.
Зерно во флеше. Для прибора без источника в mbedTLS есть
MBEDTLS_ENTROPY_NV_SEED: 32 байта во флеше, перед первой выдачей модуль
записывает туда новое зерно. Опыт nv43a.c — ключ ECDSA P-256 из CTR_DRBG,
первые 8 байт координаты X открытого ключа:
| Что | Прибор A | Прибор B |
|---|---|---|
| одно зерно в образе прошивки, 1-я загрузка | 2a9ecbd087b2482e |
2a9ecbd087b2482e |
| то же, 2-я загрузка | 09c0614805a5aeb4 |
09c0614805a5aeb4 |
| своё зерно у каждого (записано на производстве) | 75b9e1d97cdfadc1 |
d9bd7adc7989fadf |
| общее зерно + номер кристалла как персонализация | f2af54d830a4f6f0 |
509104bf84676132 |
Обновление зерна при каждой загрузке (4 записи за 4 загрузки) делает ключ
новым от загрузки к загрузке, но два прибора с общим заводским зерном
идут в ногу навсегда. Номер кристалла в строке персонализации ключи
различает, но не делает тайными: третий прибор с тем же образом прошивки
и прочитанным номером A получил f2af54d830a4f6f0 — ключ A. Зерно должно
быть своим у каждого прибора и записываться из хорошего источника, а
лучше — вместе с живым источником, а не вместо него.
Размеры для F103 (Cortex-M3, -Os): ent43a.o — 274 байта
(ent_health 102, ent_block 108, ent_poll 64), ent_f103.o — 348
байт кода и 56 данных (два описателя источника по 28 байт); вся прошивка с
mbedTLS — 9892 байта кода, 160 данных, 440 ОЗУ — на F103C8 с 64 КБ
место остаётся. Работа на плате не проверялась.
43а.7 Грабли
Сев CTR_DRBG возвращает −0x0034, хотя RNG включён. Не настроена ветка
48 МГц (PLLQ): CECS, слов нет. Или блок не тактируется битом
RNGEN в RCC_AHB2ENR: SR читается нулём, драйвер выходит по
тайм-ауту — так же ведёт себя QEMU.
Ключи у всех приборов одинаковые, хотя RNG «подключён». Сев вернул
−0x0034, а код этого не проверил: mbedtls_ctr_drbg_random после
неудачного сева возвращает 0 и выдаёт одни и те же байты
(530f8afbc74536b9… в опыте). Без нулевого кода сева — никаких ключей.
Программа висит на старте. Цикл while (!(RNG->SR & DRDY)) без
ограничения на блоке без тактирования или в эмуляторе ждёт вечно. Опросы
ограничивают и возвращают ошибку.
Числа «случайные», а блок сбоил. Код ждёт только DRDY и не смотрит
SECS/SEIS. Слово после сбоя источника RM0090 запрещает использовать;
нужен сброс SEIS и перезапуск RNGEN.
mbedtls_hardware_poll отдаёт сырые отсчёты АЦП. mbedTLS считает
каждый байт полным, а в отсчёте исправного АЦП модели — 2,323 бита из 16.
Сначала оценка и сжатие хешем, потом отдача.
Тесты на выходе хеша проходят — источник «хороший». Хеш прячет даже залипание: доля единиц 0,4990 при постоянном АЦП. Тесты — на сырых отсчётах.
Оценка по отсчётам завышена. Зависимость между соседними отсчётами делает их предсказуемее, чем кажется: у дрейфа 4,096 бита по отсчётам при правде 1,385. Оценивать ещё и по парам и разностям, брать меньшее и держать запас.
У всей партии одинаковые ключи. Зерно лежит в образе прошивки, а номер кристалла — не секрет. Зерно своё у каждого прибора, и не единственным источником.
На F103C8 нет TIM5. Дрожание LSI через захват — только у F103xE и старше; на младших остаётся АЦП (или RTC на LSI — в разделе не разбиралось).
43а.8 Итоги
- mbedTLS 3.6.4:
MBEDTLS_ENTROPY_HARDWARE_ALT+MBEDTLS_NO_PLATFORM_ENTROPY, свояmbedtls_hardware_poll; порог 32 байта, просит до 128, сев — 32 + 16 байт; ошибка источника — сев −0x0034, аmbedtls_ctr_drbg_randomпосле него всё равно возвращает 0 и одни и те же байты — код сева проверять. - Драйвер RNG F407: флаги до данных, первое слово — для сравнения, до трёх
перезапусков после
SECS, 1000 опросов вместо вечного ожидания; 376 байт кода, прошивка с mbedTLS — 9972. - QEMU 7.2 RNG не моделирует:
SR= 0, драйвер — тайм-аут, сев −0x0034. - F103: шум АЦП на датчике температуры и дрожание LSI против TIM5 (только F103xE); сколько в них энтропии — мерить на своей плате.
- Оценка по самому частому значению — по отсчётам, парам и разностям; исправный АЦП модели — 2,323 бита, 111 отсчётов на 256 бит, в прошивке 222; дрейф по отсчётам завышен втрое (4,096 против 1,385).
- Тесты повторов (порог 14) и доли (162 из 512) ловят залипание за 14 отсчётов и затихание за 543; на исправной модели — ни одной тревоги за 10 миллионов; хеш поверх залипшего источника выглядит случайным.
- Одно зерно во флеше на партию — одинаковые ключи у всех приборов; номер кристалла их различает, но не скрывает.
Полные исходники
rng43a.c — аппаратный RNG STM32F4 и источник для mbedTLS.
/* rng43a.c — раздел 43а: аппаратный RNG STM32F4 (RM0090, глава 24) и источник для mbedTLS.
* Условие: ветка PLL48CK настроена на 48 МГц (PLLQ), иначе CECS. */
#include <string.h>
#include "mbedtls/entropy.h"
#include "regs43a.h"
#include "rng43a.h"
rng_stat_t rng_stat;
static uint32_t prev; /* предыдущее слово — для сравнения */
static uint8_t have_prev; /* 0: следующее слово только запомнить */
/* Ошибка источника: сбросить SEIS, выключить и включить блок; слово в DR не брать. */
static void rng_restart(void)
{
RNG_SR = ~RNG_SR_SEIS; /* SEIS сбрасывается записью нуля, остальные биты только читаются */
RNG_CR &= ~RNG_CR_RNGEN;
RNG_CR |= RNG_CR_RNGEN;
have_prev = 0;
rng_stat.restarts++;
}
int rng_init(void)
{
RCC_AHB2ENR |= RCC_AHB2ENR_RNGEN;
(void)RCC_AHB2ENR; /* дождаться включения тактирования */
RNG_CR |= RNG_CR_RNGEN;
have_prev = 0;
return RNG_OK;
}
/* Одно слово из DR: флаги ошибок — до данных. */
static int rng_wait(uint32_t *w)
{
for (uint32_t n = 0; n < RNG_POLL_MAX; n++) {
uint32_t sr = RNG_SR;
if (sr & (RNG_SR_SECS | RNG_SR_SEIS))
return RNG_ERR_SEED;
if (sr & RNG_SR_CECS) {
RNG_SR = ~RNG_SR_CEIS;
rng_stat.clock_err++;
return RNG_ERR_CLOCK;
}
if (sr & RNG_SR_CEIS) /* тактовая была ниже нормы, но восстановилась: */
RNG_SR = ~RNG_SR_CEIS; /* прежние слова годны (RM0090, 24.3.2) */
if (sr & RNG_SR_DRDY) {
*w = RNG_DR;
return RNG_OK;
}
}
rng_stat.timeouts++;
return RNG_ERR_TIMEOUT;
}
int rng_get(uint32_t *out)
{
unsigned tries = 0;
for (;;) {
uint32_t w;
int r = rng_wait(&w);
if (r == RNG_ERR_SEED) {
rng_stat.seed_err++;
if (++tries > RNG_SEED_RETRY)
return RNG_ERR_SEED;
rng_restart();
continue;
}
if (r != RNG_OK)
return r;
if (!have_prev) { /* первое слово после включения не выдаём */
prev = w;
have_prev = 1;
continue;
}
if (w == prev) { /* непрерывный тест: два одинаковых слова подряд */
rng_stat.repeats++;
return RNG_ERR_REPEAT;
}
prev = w;
rng_stat.words++;
*out = w;
return RNG_OK;
}
}
int rng_fill(unsigned char *out, size_t len)
{
while (len) {
uint32_t w;
int r = rng_get(&w);
if (r != RNG_OK)
return r;
size_t n = len < 4 ? len : 4;
memcpy(out, &w, n);
out += n;
len -= n;
}
return RNG_OK;
}
/* MBEDTLS_ENTROPY_HARDWARE_ALT: mbedTLS зовёт эту функцию сама (data == NULL). */
int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t *olen)
{
(void)data;
*olen = 0;
if (len > 32)
len = 32; /* порог источника — 32 байта, больше за раз не нужно */
if (rng_fill(output, len) != RNG_OK)
return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
*olen = len;
return 0;
}
ent_f103.c — источники энтропии STM32F103 и подключение к mbedTLS.
/* ent_f103.c — раздел 43а: источники энтропии STM32F103 (своего RNG нет) и подключение к mbedTLS.
* АЦП: датчик температуры, канал 16, — есть у всех F103.
* Дрожание LSI против тактовой таймера: TIM5, канал 4 — только F103xE (high-density) и старше.
* Пороги и число отсчётов — из оценки на модели (sim43a, строки EST/CUT); на своей плате их пересчитать. */
#include "mbedtls/entropy.h"
#include "ent_f103.h"
#include "regs43a.h"
#define ADC_RCT_CUT 14u
#define ADC_APT_CUT 162u
#define ADC_NEED 222u
#define LSI_RCT_CUT 11u
#define LSI_APT_CUT 99u
#define LSI_NEED 156u
#if !defined(HOST43A)
void adc_noise_init(void)
{
F1_RCC_CFGR = (F1_RCC_CFGR & ~RCC_CFGR_ADCPRE) | RCC_CFGR_ADCPRE_6; /* АЦП не быстрее 14 МГц */
F1_RCC_APB2ENR |= RCC_APB2ENR_ADC1EN;
ADC1_CR2 = ADC_CR2_ADON | ADC_CR2_TSVREFE; /* АЦП и датчик включать вместе (RM0008, 11.10) */
for (volatile int i = 0; i < 200; i++) { /* tSTAB и два такта АЦП до калибровки */
}
ADC1_CR2 |= ADC_CR2_RSTCAL;
while (ADC1_CR2 & ADC_CR2_RSTCAL) {
}
ADC1_CR2 |= ADC_CR2_CAL;
while (ADC1_CR2 & ADC_CR2_CAL) {
}
ADC1_SMPR1 = 0; /* канал 16: 1,5 такта — для температуры мало, для шума то, что нужно */
ADC1_SQR1 = 0; /* одно преобразование в группе */
ADC1_SQR3 = 16; /* первым — канал 16, датчик температуры */
ADC1_CR2 |= ADC_CR2_EXTSEL_SW | ADC_CR2_EXTTRIG;
}
uint16_t adc_noise_sample(void)
{
ADC1_CR2 |= ADC_CR2_SWSTART;
while (!(ADC1_SR & ADC_SR_EOC)) {
}
return (uint16_t)ADC1_DR; /* чтение DR сбрасывает EOC */
}
void lsi_jitter_init(void)
{
F1_RCC_CSR |= RCC_CSR_LSION;
while (!(F1_RCC_CSR & RCC_CSR_LSIRDY)) {
}
F1_RCC_APB2ENR |= RCC_APB2ENR_AFIOEN;
F1_RCC_APB1ENR |= RCC_APB1ENR_TIM5EN;
F1_AFIO_MAPR |= AFIO_MAPR_TIM5CH4_IREMAP; /* LSI на вход канала 4 (как для калибровки LSI) */
TIM5_PSC = 0;
TIM5_ARR = 0xFFFFu;
TIM5_CCMR2 = TIM_CCMR2_CC4S_TI4 | TIM_CCMR2_IC4PSC_8;
TIM5_CCER = TIM_CCER_CC4E;
TIM5_CR1 = TIM_CR1_CEN;
}
/* Длина восьми периодов LSI в тактах таймера: около 14 400 при 72 МГц и 40 кГц. */
uint16_t lsi_jitter_sample(void)
{
uint16_t a;
(void)TIM5_CCR4; /* выбросить старый захват */
while (!(TIM5_SR & TIM_SR_CC4IF)) {
}
a = (uint16_t)TIM5_CCR4;
while (!(TIM5_SR & TIM_SR_CC4IF)) {
}
return (uint16_t)((uint16_t)TIM5_CCR4 - a);
}
#endif
ent_src_t adc_src = {.rct_cut = ADC_RCT_CUT, .apt_win = 512, .apt_cut = ADC_APT_CUT,
.need = ADC_NEED, .sample = adc_noise_sample};
ent_src_t lsi_src = {.rct_cut = LSI_RCT_CUT, .apt_win = 512, .apt_cut = LSI_APT_CUT,
.need = LSI_NEED, .sample = lsi_jitter_sample};
/* MBEDTLS_ENTROPY_HARDWARE_ALT: основной источник — АЦП. */
int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t *olen)
{
(void)data;
return ent_poll(&adc_src, output, len, olen);
}
/* Второй источник добавляется вручную; mbedTLS ждёт от каждого свой порог байт. */
int ent_f103_add_lsi(mbedtls_entropy_context *ctx)
{
return mbedtls_entropy_add_source(ctx, ent_poll, &lsi_src, 32, MBEDTLS_ENTROPY_SOURCE_STRONG);
}
ent43a.c — тесты здоровья, кондиционирование SHA-256 и источник для mbedTLS.
/* ent43a.c — раздел 43а: тесты здоровья, кондиционирование SHA-256 и источник для mbedTLS.
* Один и тот же файл — в прошивке F103 и в опытах на компьютере. */
#include <string.h>
#include "mbedtls/entropy.h"
#include "mbedtls/platform_util.h"
#include "ent43a.h"
/* Два теста на каждом сыром отсчёте, до хеша. Отказ «залипает»: источник больше не выдаёт ничего. */
int ent_health(ent_src_t *s, uint16_t x)
{
if (s->failed)
return s->failed;
/* 1. Повторы подряд: застрявший АЦП, остановившийся генератор */
if (s->started && x == s->last) {
if (++s->run >= s->rct_cut)
return s->failed = ENT_FAIL_REPEAT;
} else {
s->run = 1;
}
s->last = x;
/* 2. Доля в окне: первый отсчёт окна — образец, считаем его повторы до конца окна */
if (!s->started || s->pos >= s->apt_win) {
s->ref = x;
s->pos = 1;
s->hits = 1;
} else {
s->pos++;
if (x == s->ref && ++s->hits >= s->apt_cut)
return s->failed = ENT_FAIL_SHARE;
}
s->started = 1;
return ENT_OK;
}
/* need сырых отсчётов → SHA-256 → 32 байта. Хешируется весь отсчёт, а не «младшие биты». */
int ent_block(ent_src_t *s, unsigned char out[32])
{
mbedtls_sha256_context sha;
unsigned char b[2];
int r = ENT_OK;
mbedtls_sha256_init(&sha);
mbedtls_sha256_starts(&sha, 0);
for (uint16_t i = 0; i < s->need; i++) {
uint16_t x = s->sample();
s->taken++;
if ((r = ent_health(s, x)) != ENT_OK)
break;
b[0] = (unsigned char)x;
b[1] = (unsigned char)(x >> 8);
mbedtls_sha256_update(&sha, b, 2);
}
if (r == ENT_OK)
mbedtls_sha256_finish(&sha, out);
mbedtls_sha256_free(&sha);
return r;
}
/* Источник в формате mbedTLS: data — ent_src_t. Отдаёт не больше 32 байт за вызов. */
int ent_poll(void *data, unsigned char *output, size_t len, size_t *olen)
{
unsigned char blk[32];
size_t n = len < sizeof blk ? len : sizeof blk;
*olen = 0;
if (ent_block((ent_src_t *)data, blk) != ENT_OK)
return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
memcpy(output, blk, n);
mbedtls_platform_zeroize(blk, sizeof blk);
*olen = n;
return 0;
}
Дальше: раздел 43б «Защищённый элемент ATECC608: ключ, который не читается из прошивки» — где хранить закрытый ключ, если прошивку можно прочитать.