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_ALTyour 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.
How this works in mbedTLS 3.6.4 (checked against the source code and a test run):
MBEDTLS_ENTROPY_HARDWARE_ALTin the configuration — and the entropy module itself adds the sourcembedtls_hardware_poll()with the thresholdMBEDTLS_ENTROPY_MIN_HARDWARE= 32 bytes and the "strong" mark. The program writes the function; it must acceptNULLas the first argument.MBEDTLS_NO_PLATFORM_ENTROPY— otherwise the library looks for/dev/urandomor CryptoAPI and will not build on a bare chip.- Per call the module asks for up to
MBEDTLS_ENTROPY_MAX_GATHER= 128 bytes; you may return less by writing the count to*olen. - The module counts each source byte as a full byte of entropy. So
mbedtls_hardware_pollmust return already compressed, full-value bytes, not raw ADC samples. - When only SHA-256 is enabled, the accumulator is SHA-256, the block is 32 bytes, and CTR_DRBG takes 32 bytes of entropy when seeding plus 16 for the nonce — two calls to the module.
- A nonzero code from a source aborts the collection: seeding returns
MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED= −0x0034. A source that "successfully" returns zero bytes is asked up toENTROPY_MAX_LOOPtimes: 257 in the experiment, then −0x003C.
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.
- For drift, the estimate over samples is 4.096 bits, almost three times the truth: the values are widely spread, but each is almost predictable from its neighbour. The differences see this: 1.421.
- For LSI all three estimates are above the truth (3.305 against 2.921): slow frequency drift is indistinguishable from noise for the estimate. That is why the firmware takes twice as many samples as calculated: 156 LSI samples give 455.7 bits by the model truth, 222 samples of the healthy ADC give 519.7.
- The estimate is made on your own board, on a recording of raw samples, not on a model.
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
- mbedTLS 3.6.4:
MBEDTLS_ENTROPY_HARDWARE_ALT+MBEDTLS_NO_PLATFORM_ENTROPY, your ownmbedtls_hardware_poll; threshold 32 bytes, asks for up to 128, seeding takes 32 + 16 bytes; a source error makes seeding return −0x0034, andmbedtls_ctr_drbg_randomafter that still returns 0 and the same bytes — check the seeding code. - F407 RNG driver: flags before data, the first word only for comparison, up to three
restarts after
SECS, 1000 polls instead of waiting forever; 376 bytes of code, firmware with mbedTLS 9972. - QEMU 7.2 does not model the RNG:
SR= 0, the driver times out, seeding returns −0x0034. - F103: ADC noise on the temperature sensor and LSI jitter against TIM5 (F103xE only); how much entropy they hold must be measured on your own board.
- Estimate by the most frequent value — over samples, pairs and differences; the healthy model ADC gives 2.323 bits, 111 samples per 256 bits, 222 in the firmware; for drift the per-sample estimate is three times too high (4.096 against 1.385).
- The repeat test (threshold 14) and the share test (162 of 512) catch sticking within 14 samples and going quiet within 543; on the healthy model there is not a single alarm in 10 million; a hash over a stuck source looks random.
- One seed in flash for a batch means identical keys on all devices; the chip ID makes them differ but does not hide them.
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.