Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103, STM32F407 · уровень средний · 28 мин чтения
Открыть в интерактивном справочнике

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 в режиме счётчика. Из одного и того же зерна он выдаёт одну и ту же последовательность, поэтому вся стойкость ключей — в зерне. Зерно делает модуль энтропии: он опрашивает источники, складывает их байты в хеш-аккумулятор и, когда каждый источник дал свой порог, выдаёт хеш.

Цепочка случайности в прошивке: у F407 аппаратный RNG через драйвер с проверкой флагов, у F103 шум АЦП и дрожание LSI через тесты на сыром отсчёте и SHA-256, дальше mbedtls_hardware_poll, пул энтропии mbedTLS, CTR_DRBG и ключи; одно зерно во флеше на всю партию даёт приборам одинаковый ключ

Что в mbedTLS 3.6.4 устроено так (сверено с исходниками и прогоном):

Отсюда правило всего раздела: при любом сомнении в источнике функция возвращает ошибку, а программа, получив ошибку сева, не делает ключей вовсе. Это лучше, чем ключ из предсказуемых байт.


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

Что видно.

Из оценки 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 Итоги

Полные исходники

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: ключ, который не читается из прошивки» — где хранить закрытый ключ, если прошивку можно прочитать.

Открыть в интерактивном справочнике