Embedded HandbookApps
All sections › III. Peripherals
STM32F103, STM32F407 · level intermediate · 28 min read
Open in the interactive handbook

43а. Where TLS gets its randomness: the hardware RNG, your own entropy source and quality checks

Section 43.2 covered the RNG block on its own: ring oscillator noise, the SECS and CECS flags, discarding the first word. But a TLS key (107.7, 37.4), an AES nonce (48г) or a signing key (48а) is made not by the block but by the crypto library, and it takes its randomness from the CTR_DRBG generator. On a PC it is seeded by the operating system. On a microcontroller without an OS there is nothing to seed it with until the program itself tells the library where to get noise.

This section covers: how mbedTLS collects entropy and what it needs from our function; an F407 RNG driver that returns no numbers when the block fails; the F103 has no RNG of its own — a custom collector from ADC noise and LSI oscillator jitter; how to estimate how much randomness is in one sample, and two simple tests that catch a broken source at run time; why a hash on top of a bad source does not help, and what happens when two devices are seeded identically.

Sources: RM0090 Rev 19 (chapter 24 — RNG: 24.3.1 operation and the continuous test, 24.3.2 errors, 24.4 registers), RM0008 Rev 17 (calibrating LSI through TIM5 and the TIM5CH4_IREMAP bit; temperature sensor — 11.10; ADC clock no higher than 14 MHz, calibration), CMSIS headers stm32f407xx.h and stm32f10x.h from the Keil packs, the source code of mbedTLS 3.6.4 (github.com/Mbed-TLS/mbedtls, tag v3.6.4; license Apache-2.0 or GPL-2.0-or-later): entropy.h, entropy.c, entropy_poll.h, ctr_drbg.c, mbedtls_config.h. The health tests and the entropy estimate in this section are our own, simple ones; they have not been checked against the standards for entropy sources. The F103 noise sources and their numbers are models on a PC, not board measurements.

Terms used in the documentation

Term Meaning
entropy source source of entropy, of physical noise
min-entropy minimum entropy: based on the most probable value
conditioning compressing raw samples with a hash
health test run-time health test of the source
repetition count test test for repeats in a row
adaptive proportion test test of the share of one value within a window
DRBG, CTR_DRBG deterministic generator, based on AES in counter mode
seed, reseed, nonce seed, reseeding, one-time addition
personalization string personalization string
NV seed seed stored in non-volatile memory
MBEDTLS_ENTROPY_HARDWARE_ALT your own source via mbedtls_hardware_poll()
continuous random number generator test continuous test: adjacent words are not equal

43а.1 The chain from noise to key

The CTR_DRBG generator is a formula: AES in counter mode. From the same seed it produces the same sequence, so all the strength of the keys lies in the seed. The seed is made by the entropy module: it polls the sources, feeds their bytes into a hash accumulator and, once every source has delivered its threshold, outputs the hash.

The randomness chain in firmware: on the F407 the hardware RNG through a driver that checks the flags, on the F103 ADC noise and LSI jitter through tests on the raw sample and SHA-256, then mbedtls_hardware_poll, the mbedTLS entropy pool, CTR_DRBG and the keys; one seed in flash for a whole batch gives the devices the same key

How this works in mbedTLS 3.6.4 (checked against the source code and a test run):

Hence the rule for the whole section: at any doubt about the source the function returns an error, and a program that gets a seeding error makes no keys at all. That is better than a key made of predictable bytes.


43а.2 The F407 RNG: a driver that does not hand out bad numbers

The RNG block is three registers: RNG_CR (bit RNGEN), RNG_SR (DRDY, CECS, SECS, CEIS, SEIS), RNG_DR. The addresses and bits are our own in regs43a.h, checked against stm32f407xx.h: the block is at 0x50060800, its clock is bit 6 of RCC_AHB2ENR. Per RM0090 a word is ready no later than 40 cycles of RNG_CLK; the clock is the 48 MHz branch from the PLL (PLLQ). If RNG_CLK is below HCLK/16, CECS is set.

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;
}

The order of checks follows RM0090: errors first, then DRDY. SEIS with SECS cleared means "a failure happened and is over", but the word in DR still must not be used — this is also a reason to restart. CEIS without CECS means the clock was below the norm and recovered; RM0090 allows the earlier words to be used. Without a limit on polling, the driver would hang forever on a dead block: 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;
}

The first word after enabling is only used for comparison (the continuous test of RM0090, 24.3.1); every next one is compared with the previous one. After a source failure — up to three restarts:

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;
    }
}

The driver was run on a PC against a model of the block (sim43a.c): the model sets DRDY after 14 register accesses and faithfully implements clearing SEIS/CEIS by writing zero and disabling RNGEN. Six calls of rng_get in a row:

Model scenario Codes What the driver did
healthy block 0 ×6 6 words; the model produced 7 — the first was discarded
one source failure on the 3rd word 0 ×6 1 restart; the model produced 9 words
the source always fails −3 ×6 4 errors and 3 restarts per call
clock below the norm (CECS) −2 ×6 not a single word
the block does not respond (SR = 0) −1 ×6 1000 polls each, then exit
a word repeated 0, 0, 0, −4, 0, 0 the repeat was caught on the 4th call
a clock failure happened and passed (CEIS) 0 ×6 CEIS cleared, 6 words

After a repeat the driver returns words again on the next call: what to do about the failure is up to the caller. mbedTLS decides simply — it stops seeding.


43а.3 Hooking it up to mbedTLS

The configuration contains only what is needed for the seed: the entropy module, CTR_DRBG, AES, SHA-256. For the key experiment on the PC, ECP P-256 is added; for a device without a source, a seed in flash (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

The source itself for the F407 is a thin wrapper over the driver. Per call it returns 32 bytes — exactly the threshold; there is no point asking the block for 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;
}

Seeding on the same block model: mbedtls_ctr_drbg_seed, then 16 bytes from mbedtls_ctr_drbg_random. Calls to mbedtls_hardware_poll were counted by linker interception (--wrap):

Scenario mbedtls_ctr_drbg_seed Source calls Bytes RNG words
healthy block 0 2 (asks for 128 each) 64 16
one source failure 0 2 64 16
a clock failure happened and passed 0 2 64 16
the source always fails −0x0034 1 0 0
clock below the norm −0x0034 1 0 0
the block does not respond −0x0034 1 0 0
a word repeated −0x0034 1 0 3

64 bytes per seeding — 32 + 16 bytes, each with a separate collection.

Always check the seeding return code. After a failed seeding, mbedtls_ctr_drbg_random in the experiment returned 0 and produced bytes — the same ones in all four failures: 530f8afbc74536b9…. The generator does not know it was not seeded, and it produces a sequence anyone can repeat. (For the healthy block and the "clock failure" case the output also matched, 4afe8942f8435c6d… — the block model produces the same words every time; a real block does not.)

In QEMU (7.2, the netduinoplus2 board — an STM32F405 with the same block at the same address) there is no RNG: it is registered as an "unimplemented device". The firmware fw43a.c writes 0x00000004 to RNG_CR, RNG_SR reads as zero, rng_get returns −1 (timeout), seeding returns 0xFFFFFFCC, that is −0x0034; the SR register was read 2001 times — 1000 polls in rng_get, 1000 in seeding and one for printing. So the real RNG numbers in this section come only from the model; the driver has not been run on a board.

Sizes (arm-none-eabi-gcc 12.2.1, -Os, Cortex-M4): rng43a.o is 376 bytes of code (rng_get with inlined rng_wait 240, rng_init 52, rng_fill 48, mbedtls_hardware_poll 36); the whole firmware with the entropy module, CTR_DRBG, AES and SHA-256 from mbedTLS is 9972 bytes of code, 104 of data, 472 of RAM; of that, the entropy context is 100 bytes (4 sources instead of 20) and the CTR_DRBG context 320.


43а.4 F103: a custom collector from the ADC and LSI

The F103 has no RNG block. There is noise in the analog part: the low bits of the ADC on the temperature sensor jitter from thermal noise, and the internal LSI oscillator (about 40 kHz, 30 to 60 per RM0008) drifts in phase relative to the processor clock. The manufacturer promises neither as a source of randomness: how much entropy they contain depends on the board, the supply and the temperature — you measure it on your own device (43а.5), and the numbers below are models.

ADC: the temperature sensor is channel 16, enabled by the TSVREFE bit together with ADON. For temperature measurement RM0008 recommends a sampling time of 17.1 µs; we do not need accuracy, we need noise — so we take the shortest, 1.5 cycles. The ADC clock is no higher than 14 MHz: divider 6, 12 MHz at 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 jitter is measured by TIM5: the TIM5CH4_IREMAP bit in AFIO_MAPR routes LSI to the channel 4 input — this is how RM0008 suggests calibrating LSI. It exists only on F103 high-density (F103xE) and above; the F103C8 has no TIM5, so only the ADC is left. Capturing every eighth edge: eight LSI periods with the timer at 72 MHz are about 14400 cycles; the sample is the difference of two captures.

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);
}

Models for the experiments: ADC — 1850 plus normal noise with deviation σ = 2 LSB ("healthy"), σ = 0.45 ("quiet input"), no noise ("stuck"), slow drift plus noise ("drift": each value is 0.99 of the previous one plus noise σ = 1); LSI — 14400 plus noise σ = 3 cycles plus slow frequency drift. The model noise was set by us, not measured.


43а.5 How much entropy is in one sample

The measure is min-entropy: H = −log₂ p, where p is the probability of the most frequent value. That is how many bits an attacker guessing on the first attempt needs. Our own estimate over 100000 samples: the share of the most frequent value with a margin of 2.576 sigma on top (99 %), done three times: over the samples themselves, over pairs of adjacent samples (divided by 2) and over differences of adjacent samples. We take the smallest. Pairs and differences catch dependence between samples that a single sample does not reveal. The model's "truth" is its highest probability given the known past, computed by formula.

Model Over samples Over pairs Over differences Take Model truth Samples per 256 bits In firmware, ×2
healthy ADC 2.325 2.323 2.829 2.323 2.341 111 222
quiet ADC 0.441 0.441 0.793 0.441 0.447 581 1162
stuck ADC 0 0 0 0 0 — —
ADC with drift 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

What this shows.

From the estimate H come the health test thresholds (43а.6), our own: the allowed false alarm rate is 2⁻³⁰ per sample. Repeats in a row: the probability of C identical samples in a row from a healthy source is no more than 2^(−H·(C−1)), hence C = 1 + ⌈30/H⌉. Share in a window of 512: the smallest threshold for which the probability of collecting that many matches with the first sample of the window is no more than 2⁻³⁰ (binomial tail). For the healthy ADC this is 14 and 162, for LSI 11 and 99; for the quiet input it would be 70 and 435, for drift 23 and 260. The thresholds and sample counts are written into ent_f103.c as constants.


43а.6 Tests, the hash and identical keys

The tests run on every raw sample, before the hash. A failure "latches": after it the source returns nothing until a reboot.

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;
}

Conditioning is SHA-256 over all need samples, two bytes per sample (not the "low bits": the hash itself will squeeze out whatever is there):

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;
}

Hooking it up to mbedTLS on the F103: the ADC goes through mbedtls_hardware_poll, LSI is a second strong source with its own threshold:

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);
}

Tests with the firmware thresholds (14 and 162 of 512) on the ADC models, 10 million samples each:

Model Alarm Test
healthy ADC none in 10000000 —
quiet ADC at sample 190 repeats
stuck ADC at sample 14 repeats
healthy, stuck from sample 20000 14 samples later repeats
healthy, went quiet from sample 20000 543 samples later repeats
ADC with drift at sample 937235 repeats
LSI (own thresholds 11 and 99) none in 10000000 —

The tests catch breakage: sticking and going quiet. They barely see drift — that is not a breakage error but a design error, and only the 43а.5 estimate catches it at the development stage. In these models the repeat test fired first every time; the share test is needed for sources that alternate between several values — there was no such model in the experiment.

A hash hides failure. A stuck ADC, 1000 blocks: SHA-256 of the block number and 512 identical samples. The output is 32000 bytes, the share of ones is 0.4990, chi-square over 256 byte values is 205.2 — simple output tests do not see the failure. That is why the tests sit before the hash, not after it.

Seeding CTR_DRBG through ent_f103.c on the models: healthy ADC — seeding succeeded, 444 ADC samples and 312 LSI samples were taken (two collections of 222 and 156); stuck — seeding returned −0x0034 after 14 ADC samples, LSI was not even asked.

A seed in flash. For a device without a source, mbedTLS has MBEDTLS_ENTROPY_NV_SEED: 32 bytes in flash; before the first output the module writes a new seed there. The experiment nv43a.c — an ECDSA P-256 key from CTR_DRBG, the first 8 bytes of the X coordinate of the public key:

What Device A Device B
one seed in the firmware image, 1st boot 2a9ecbd087b2482e 2a9ecbd087b2482e
the same, 2nd boot 09c0614805a5aeb4 09c0614805a5aeb4
each has its own seed (written in production) 75b9e1d97cdfadc1 d9bd7adc7989fadf
common seed + chip ID as personalization f2af54d830a4f6f0 509104bf84676132

Updating the seed on every boot (4 writes in 4 boots) makes the key new from boot to boot, but two devices with a common factory seed march in step forever. The chip ID in the personalization string makes the keys differ but does not make them secret: a third device with the same firmware image and the ID of A read out got f2af54d830a4f6f0 — the key of A. The seed must be unique to every device and written from a good source, and better still used together with a live source, not instead of it.

Sizes for the F103 (Cortex-M3, -Os): ent43a.o is 274 bytes (ent_health 102, ent_block 108, ent_poll 64), ent_f103.o is 348 bytes of code and 56 of data (two source descriptors of 28 bytes); the whole firmware with mbedTLS is 9892 bytes of code, 160 of data, 440 of RAM — on an F103C8 with 64 KB there is room to spare. Operation on a board has not been tested.


43а.7 Pitfalls

CTR_DRBG seeding returns −0x0034 although the RNG is enabled. The 48 MHz branch (PLLQ) is not configured: CECS, no words. Or the block is not clocked by the RNGEN bit in RCC_AHB2ENR: SR reads as zero, the driver exits on timeout — QEMU behaves the same way.

All devices have the same keys although the RNG is "connected". Seeding returned −0x0034 and the code did not check it: mbedtls_ctr_drbg_random after a failed seeding returns 0 and produces the same bytes (530f8afbc74536b9… in the experiment). No zero seeding code — no keys.

The program hangs at start-up. A loop while (!(RNG->SR & DRDY)) without a limit waits forever on an unclocked block or in an emulator. Limit the polling and return an error.

The numbers look "random", but the block failed. The code waits only for DRDY and does not look at SECS/SEIS. RM0090 forbids using the word after a source failure; you must clear SEIS and restart RNGEN.

mbedtls_hardware_poll returns raw ADC samples. mbedTLS counts every byte as full, while a sample of the healthy model ADC carries 2.323 bits out of 16. First estimate and compress with a hash, then hand over.

Tests on the hash output pass, so the source is "good". A hash hides even a stuck source: share of ones 0.4990 with a constant ADC. Tests belong on the raw samples.

The per-sample estimate is too high. Dependence between adjacent samples makes them more predictable than they seem: for drift, 4.096 bits over samples against a truth of 1.385. Estimate over pairs and differences as well, take the smaller and keep a margin.

The whole batch has the same keys. The seed sits in the firmware image, and the chip ID is not a secret. Each device gets its own seed, and it must not be the only source.

The F103C8 has no TIM5. LSI jitter through capture is only available on F103xE and above; on the smaller parts only the ADC is left (or RTC on LSI — not covered in this section).


43а.8 Summary

Full source code

rng43a.c — the STM32F4 hardware RNG and a source for 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 entropy sources and hooking them up to 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 — health tests, SHA-256 conditioning and a source for 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;
}

Next: section 43б "The ATECC608 secure element: a key that cannot be read from the firmware" — where to keep the private key if the firmware can be read.

Open in the interactive handbook