improve RNG

This commit is contained in:
liquidraver
2026-07-22 11:00:38 +02:00
parent 076fbb7ff3
commit 94d7aab142
8 changed files with 831 additions and 65 deletions
+236 -57
View File
@@ -8,6 +8,7 @@
#include <zephyr/sys/reboot.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/hwinfo.h>
#include <zephyr/timing/timing.h> /* portable CPU cycle counter for the beat */
#include <psa/crypto.h>
#include <string.h>
#include <mesh/Utils.h>
@@ -16,8 +17,12 @@
/* Pre-RF entropy for the ESP32 HWRNG — see esp32_entropy_begin() below.
* Source file is added to the build by CMakeLists.txt (ESP32 only). */
#include <bootloader_random.h>
/* RTC-slow clock read for the two-clock beat entropy source (sample_rtc_beat).
* esp_rtc_get_time_us() links in an app build (verified via the selftest). */
#include <esp_rtc_time.h>
#endif
BUILD_ASSERT(IS_ENABLED(CONFIG_CSPRNG_ENABLED),
"ZephyrRNG requires CONFIG_CSPRNG_ENABLED for cryptographic key derivation");
@@ -42,11 +47,13 @@ void ZephyrRNG::random(uint8_t *dest, size_t sz)
* Generator" — Linux kernel's jitterentropy_rng. NIST SP 800-90B has
* a compliance class for this entropy source type.
*
* Per-sample min-entropy on simple in-order embedded CPUs (Cortex-M,
* RISC-V, Xtensa) is conservatively 0.1-0.3 bits per cycle-counter
* delta. At 200ms × 160MHz / 1000 cycles per sample = 32,000 samples
* × 0.1 bits = 3,200 estimated bits. 256 needed for Ed25519 → 12×
* margin even pessimistically.
* IMPORTANT — this source only works where k_cycle_get_32() is CROSS-DOMAIN
* from the CPU. On nRF it is the 32.768 kHz RTC read from a 64 MHz core, so
* the read latency itself jitters and this carries real entropy. On ESP32
* k_cycle_get_32() is CCOUNT, the CPU's own cycle counter — same domain, so
* the loop is deterministic and this yields ~0 bits (measured on hardware:
* thousands of identical deltas). ESP32 therefore uses sample_rtc_beat()
* instead and never calls this function.
*
* Health check (NIST SP 800-90B style): online repetition count + a
* distinct-value check tracked across all samples in the window with
@@ -58,6 +65,27 @@ void ZephyrRNG::random(uint8_t *dest, size_t sz)
/* Health statistics for one jitter window. Timing statistics only — never
* pool contents or derived key material. Reporting these is standard practice
* for a NIST SP 800-90B style noise source; reporting the bytes would not be. */
/* Per-stage health reporting can be silenced. The node wants it — it fires
* once, at first-boot identity generation, and is the only record of what the
* entropy sources actually did. The selftest tool calls mixIdentitySeed
* thousands of times and must be able to shut it up after the first few, or
* the summary drowns in ~12 lines x N. */
static bool s_seed_report_quiet;
#define RNG_RPT(...) do { if (!s_seed_report_quiet) printk(__VA_ARGS__); } while (0)
void ZephyrRNG::setSeedHealthQuiet(bool quiet)
{
s_seed_report_quiet = quiet;
}
#if defined(ZEPHCORE_RNG_TEST_HOOKS)
/* When set, the HWRNG contribution to mixIdentitySeed is zeroed after each
* draw — see the header. Test scaffolding, compiled out of production. */
static bool s_test_kill_hwrng;
void ZephyrRNG::setTestKillHWRNG(bool kill) { s_test_kill_hwrng = kill; }
#endif
#define JITTER_HIST_SLOTS 16
struct jitter_stats {
@@ -115,11 +143,14 @@ static bool sample_cpu_jitter(uint8_t *pool, size_t pool_size,
while (k_uptime_get() < deadline) {
uint32_t t1 = k_cycle_get_32();
/* Variable-time work. The iteration count comes from the
* accumulator, which carries the PREVIOUS measurement (see the
* feedback step below), so the amount of work done here depends
* on observed hardware nondeterminism rather than on a fixed
* sequence. */
/* Variable-time work iteration count depends on the accumulator,
* so timing depends on hardware nondeterminism (cache, branch
* prediction, ISR firing). This ONLY carries entropy where the
* cycle counter is cross-domain from the CPU: on nRF k_cycle_get_32
* is the 32.768 kHz RTC read from the 64 MHz core, so the read
* latency itself jitters. On ESP32 it is CCOUNT — same domain, no
* jitter, deltas repeat by the thousand (measured) — which is why
* ESP32 uses sample_rtc_beat() instead and never calls this. */
volatile uint32_t a = accum;
uint32_t iters = (accum & 0x7f);
for (uint32_t i = 0; i < iters; i++) {
@@ -129,25 +160,6 @@ static bool sample_cpu_jitter(uint8_t *pool, size_t pool_size,
uint32_t t2 = k_cycle_get_32();
uint32_t delta = t2 - t1;
/* FEEDBACK — the load-bearing line. Without it `accum` evolves
* as a pure LCG from a single seed: the iteration count becomes a
* fixed sequence, and on any CPU where reading the cycle counter
* is cheap and same-domain (ESP32 CCOUNT) a warm cache makes each
* `iters` value yield an identical `delta`. Deltas then repeat
* and the health check below correctly fails — which is exactly
* what was observed on every ESP32 board.
*
* Folding the measurement back in closes the loop, so timing
* variation propagates into subsequent work. This is the
* mechanism the cited jitterentropy design relies on and which
* the original implementation omitted.
*
* nRF was unaffected: its k_cycle_get_32() is a 32.768 kHz RTC in
* a different clock domain, so the read latency itself varies
* with domain phase and supplied the nondeterminism this line
* now provides everywhere. */
accum ^= delta;
/* Mix into entropy pool */
pool[idx++ % pool_size] ^= (uint8_t)delta;
pool[idx++ % pool_size] ^= (uint8_t)(delta >> 8);
@@ -220,6 +232,136 @@ static bool sample_cpu_jitter(uint8_t *pool, size_t pool_size,
return ok;
}
/* ===== Universal two-clock beat entropy ==================================
*
* One physical entropy source for every board: count CPU cycles elapsed across
* a fixed interval of an INDEPENDENT low-frequency oscillator. The two clocks
* come from different sources, so the count fluctuates with the slow
* oscillator's phase noise — real physical entropy, not the deterministic
* same-domain loop that CPU-jitter degenerates to where the cycle counter and
* CPU share a clock (measured dead on ESP32: thousands of identical deltas).
*
* FAST counter = timing_counter_get() — portable CPU cycle counter (DWT on
* Cortex-M, CCOUNT on Xtensa; both at CPU frequency). Needs
* CONFIG_TIMING_FUNCTIONS and a one-time timing_init()/timing_start().
*
* SLOW clock = an oscillator in a DIFFERENT domain from the CPU, selected by a
* principled rule so the choice is coherent across boards:
* - ESP32: the RTC-slow oscillator via esp_rtc_get_time_us() (internal
* ~136 kHz RC, independent of the XTAL->PLL CPU path).
* - Any board whose Zephyr system timer runs < 1 MHz: that timer IS a
* low-frequency oscillator cross-domain from the CPU (e.g. nRF's
* 32.768 kHz RTC off LFXO/LFRC), so k_cycle_get_32() is a valid slow
* clock. REQUIRES the LF clock to be LFXO/LFRC, not synthesised from
* HFCLK — true for every BLE-capable nRF config; the health check below
* catches it if a board ever violates that.
* - Otherwise (system timer at CPU frequency, e.g. bare SysTick): no
* independent slow clock is identified, and sample_platform_entropy()
* falls back to CPU-jitter (unchanged behaviour, no regression).
*
* Window = 500 us, from an on-hardware ESP32 sweep (memory/findings.md):
* 120/250/500/1000 us gave 1.85/3.04/3.81/5.14 bits/sample; 500 us is the knee.
* Full 32-bit delta is mixed. On ESP32 this is a SECONDARY source (the
* bootloader_random-seeded HWRNG is primary); on nRF it is the strong
* non-HWRNG leg. Reuses jitter_stats + report_jitter.
*
* CAVEAT (memory/findings.md): the health stats show the beat VARIES, not that
* it is random — the selftest output-diversity run is what validates it. */
#if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
#define BEAT_SLOW_HZ 1000000ULL
static inline uint64_t beat_slow_ticks(void) { return esp_rtc_get_time_us(); }
#define HAVE_TWO_CLOCK_BEAT 1
#elif (CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC < 1000000)
#define BEAT_SLOW_HZ ((uint64_t)CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC)
static inline uint64_t beat_slow_ticks(void) { return k_cycle_get_32(); }
#define HAVE_TWO_CLOCK_BEAT 1
#endif
#ifdef HAVE_TWO_CLOCK_BEAT
#define BEAT_WINDOW_US 500
/* slow-clock ticks per window; >= 1 guaranteed for any BEAT_SLOW_HZ >= 2 kHz */
#define BEAT_WINDOW_TICKS ((uint32_t)((BEAT_SLOW_HZ * BEAT_WINDOW_US) / 1000000ULL))
static bool sample_two_clock_beat(uint8_t *pool, size_t pool_size,
size_t pool_offset, uint32_t duration_ms,
struct jitter_stats *st = nullptr)
{
/* Enable the CPU cycle counter once (DWT on Cortex-M; no-op-ish on
* Xtensa where CCOUNT always runs). */
static bool timing_ready;
if (!timing_ready) {
timing_init();
timing_start();
timing_ready = true;
}
int64_t deadline = k_uptime_get() + duration_ms;
size_t idx = pool_offset;
uint32_t min_delta = UINT32_MAX, max_delta = 0;
uint32_t prev_delta = 0;
int cur_consec = 0, max_consec = 0;
uint32_t distinct[8] = {0};
int n_distinct = 0, n_samples = 0;
while (k_uptime_get() < deadline) {
uint64_t s0 = beat_slow_ticks();
uint32_t f0 = (uint32_t)timing_counter_get();
while ((beat_slow_ticks() - s0) < BEAT_WINDOW_TICKS) {
/* CPU cycle counter advances while the independent slow
* oscillator defines the window; the two drift */
}
uint32_t f1 = (uint32_t)timing_counter_get();
uint32_t delta = f1 - f0;
/* Mix the FULL delta — entropy spans ~11 bits, not the low byte. */
pool[idx++ % pool_size] ^= (uint8_t)delta;
pool[idx++ % pool_size] ^= (uint8_t)(delta >> 8);
pool[idx++ % pool_size] ^= (uint8_t)(delta >> 16);
pool[idx++ % pool_size] ^= (uint8_t)(delta >> 24);
/* Health stats on the low 14 bits (where the beat lives): a
* frozen/domain-locked slow clock freezes the delta and trips
* the repetition count. */
uint32_t d14 = delta & 0x3FFF;
if (n_samples > 0 && d14 == prev_delta) {
if (++cur_consec > max_consec) max_consec = cur_consec;
} else {
cur_consec = 1;
}
prev_delta = d14;
if (n_distinct < 8) {
bool found = false;
for (int j = 0; j < n_distinct; j++) {
if (distinct[j] == d14) { found = true; break; }
}
if (!found) distinct[n_distinct++] = d14;
}
if (d14 < min_delta) min_delta = d14;
if (d14 > max_delta) max_delta = d14;
n_samples++;
}
bool ok = (n_samples >= 16) /* enough samples */
&& (max_consec < 32) /* slow clock not frozen */
&& (n_distinct >= 5); /* beat actually varies */
if (st) {
st->n_samples = n_samples;
st->n_distinct = n_distinct;
st->max_consec = max_consec;
st->min_delta = (n_samples > 0) ? min_delta : 0;
st->max_delta = max_delta;
st->mcv_count = 0; /* span/distinct are the beat's health */
st->untracked = 0;
st->ok = ok;
}
return ok;
}
#endif /* HAVE_TWO_CLOCK_BEAT */
/* Count distinct byte values in a buffer — a repetition/adaptive-proportion
* style health indicator for a CSPRNG draw. A stuck source collapses this to
* 1. Deliberately coarse: one integer per draw, which detects catastrophic
@@ -241,7 +383,7 @@ static int distinct_bytes(const uint8_t *buf, size_t len)
* and no amount of sampling will help. */
static void report_jitter(const char *label, const struct jitter_stats *st)
{
printk("[RNG] %s: samples=%d distinct=%d/8 maxrep=%d "
RNG_RPT("[RNG] %s: samples=%d distinct=%d/8 maxrep=%d "
"delta=[%u..%u] -> %s\n",
label, st->n_samples, st->n_distinct, st->max_consec,
st->min_delta, st->max_delta, st->ok ? "PASS" : "FAIL");
@@ -258,7 +400,11 @@ static void report_jitter(const char *label, const struct jitter_stats *st)
* any input look uniform, so output statistics would read perfect even
* for a near-zero-entropy seed. Entropy is a property of the source. */
if (st->n_samples <= 0 || st->mcv_count == 0) {
printk("[RNG] entropy est: n/a (no samples)\n");
/* mcv_count == 0 is the two-clock beat: its health is the
* span/distinct/maxrep line above, not an MCV estimate (that
* would need a 16k-slot histogram in the key path). Per-sample
* entropy is characterised offline by the selftest window sweep.
* Nothing meaningful to print here. */
return;
}
@@ -270,7 +416,7 @@ static void report_jitter(const char *label, const struct jitter_stats *st)
uint64_t total_mb = (uint64_t)per_mb * (uint64_t)st->n_samples;
uint32_t total_bits = (uint32_t)(total_mb / 1000u);
printk("[RNG] entropy est: %u.%03u bits/sample x %d = ~%u bits "
RNG_RPT("[RNG] entropy est: %u.%03u bits/sample x %d = ~%u bits "
"(need 256)%s\n",
per_mb / 1000u, per_mb % 1000u, st->n_samples, total_bits,
st->untracked ? " [histogram overflowed — est. is optimistic]" : "");
@@ -365,6 +511,29 @@ static int extract_via_aes_ctr(const uint8_t *pool, size_t pool_len,
return ret;
}
/* Platform entropy sampler + its label. Every board with an identified
* independent slow clock uses the SAME two-clock beat (ESP32 RTC-slow, nRF and
* other <1 MHz-systimer boards their RTC). Boards without one fall back to
* CPU-jitter, which only carries entropy where k_cycle_get_32 is itself cross-
* domain. Both yield jitter_stats, so the stages and report are identical. */
#ifdef HAVE_TWO_CLOCK_BEAT
#define ENTROPY_SRC_LABEL "beat "
#else
#define ENTROPY_SRC_LABEL "jitter"
#endif
static inline bool sample_platform_entropy(uint8_t *pool, size_t pool_size,
size_t pool_offset,
uint32_t duration_ms,
struct jitter_stats *st)
{
#ifdef HAVE_TWO_CLOCK_BEAT
return sample_two_clock_beat(pool, pool_size, pool_offset, duration_ms, st);
#else
return sample_cpu_jitter(pool, pool_size, pool_offset, duration_ms, st);
#endif
}
void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len,
const uint8_t *extra, size_t extra_len)
{
@@ -395,17 +564,20 @@ void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len,
* battery read on ESP32. */
#if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
bootloader_random_enable();
printk("[RNG] === identity seed health ===\n");
printk("[RNG] esp32 pre-RF entropy (bootloader_random): ENABLED\n");
RNG_RPT("[RNG] === identity seed health ===\n");
RNG_RPT("[RNG] esp32 pre-RF entropy (bootloader_random): ENABLED\n");
#else
printk("[RNG] === identity seed health ===\n");
printk("[RNG] platform TRNG is radio-independent (no pre-RF workaround needed)\n");
RNG_RPT("[RNG] === identity seed health ===\n");
RNG_RPT("[RNG] platform TRNG is radio-independent (no pre-RF workaround needed)\n");
#endif
/* Stage 1: early CSPRNG (strong on nRF/MG24; on ESP32 this is only real
* because bootloader_random_enable() above is feeding the HWRNG) */
int rc1 = sys_csrand_get(pool, 64);
printk("[RNG] stage1 csrand : rc=%d distinct=%d/64\n",
#if defined(ZEPHCORE_RNG_TEST_HOOKS)
if (s_test_kill_hwrng) memset(pool, 0, 64); /* simulate dead HWRNG */
#endif
RNG_RPT("[RNG] stage1 csrand : rc=%d distinct=%d/64\n",
rc1, distinct_bytes(pool, 64));
/* Stage 2: HWINFO unique device ID — uniqueness across devices */
@@ -416,50 +588,57 @@ void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len,
}
/* NOT secret — this is the efuse/FICR serial, public and printed at boot.
* It contributes uniqueness between devices, never unpredictability. */
printk("[RNG] stage2 hwinfo id : %d bytes (public — uniqueness only)\n",
RNG_RPT("[RNG] stage2 hwinfo id : %d bytes (public — uniqueness only)\n",
(int)devid_len);
/* Stage 3: caller-supplied entropy (e.g. ADC LSB noise) */
/* Stage 3: caller-supplied entropy. A hook for a caller that has its own
* physical noise (e.g. an externally sampled ADC/RF value); unused today,
* so normally a no-op. Kept because it costs nothing when null and gives
* a board a way to inject a source without touching this file. The
* internal battery-ADC experiment was removed — a driven divider yielded
* no reliable entropy and did not justify the complexity in the key path. */
if (extra && extra_len > 0) {
size_t n = (extra_len < 32) ? extra_len : 32;
for (size_t i = 0; i < n; i++) pool[80 + i] ^= extra[i];
printk("[RNG] stage3 extra : %d bytes\n", (int)n);
} else {
printk("[RNG] stage3 extra : none\n");
RNG_RPT("[RNG] stage3 extra : %d bytes\n", (int)n);
}
/* Stage 4: CPU cycle-counter jitter, 200ms.
/* Stage 4: hardware-timing entropy, 200ms. RTC two-clock beat on ESP32,
* CPU cycle-counter jitter elsewhere (sample_platform_entropy).
*
* Skipped on POSIX arch (native_sim / Linux): the simulated clock only
* advances when Zephyr threads yield, so k_uptime_get() is frozen while
* this loop spins → infinite loop. On Linux we have /dev/urandom (via
* sys_csrand_get in stages 1 and 5) which is a far stronger source than
* jitter sampling anyway. */
* this sampling anyway. */
#ifndef CONFIG_ARCH_POSIX
struct jitter_stats js = {};
bool health_ok = sample_cpu_jitter(pool, sizeof(pool), 112, 200, &js);
report_jitter("stage4 jitter 200ms", &js);
bool health_ok = sample_platform_entropy(pool, sizeof(pool), 112, 200, &js);
report_jitter("stage4 " ENTROPY_SRC_LABEL " 200ms", &js);
if (!health_ok) {
printk("[RNG] stage4 FAILED — resampling at 400ms\n");
health_ok = sample_cpu_jitter(pool, sizeof(pool), 112, 400, &js);
report_jitter("stage4 jitter 400ms", &js);
RNG_RPT("[RNG] stage4 FAILED — resampling at 400ms\n");
health_ok = sample_platform_entropy(pool, sizeof(pool), 112, 400, &js);
report_jitter("stage4 " ENTROPY_SRC_LABEL " 400ms", &js);
if (!health_ok) {
printk("[RNG] stage4 STILL FAILING — continuing with mixed sources\n");
RNG_RPT("[RNG] stage4 STILL FAILING — continuing with mixed sources\n");
}
}
#endif /* CONFIG_ARCH_POSIX */
/* Stage 5: late CSPRNG — catches any mid-boot radio init that
* warmed the TRNG during the 200ms jitter window */
* warmed the TRNG during the stage-4 window */
int rc5 = sys_csrand_get(pool + 368, 64);
printk("[RNG] stage5 csrand : rc=%d distinct=%d/64\n",
#if defined(ZEPHCORE_RNG_TEST_HOOKS)
if (s_test_kill_hwrng) memset(pool + 368, 0, 64); /* simulate dead HWRNG */
#endif
RNG_RPT("[RNG] stage5 csrand : rc=%d distinct=%d/64\n",
rc5, distinct_bytes(pool + 368, 64));
/* Stage 6: second jitter sample, independent timing window */
/* Stage 6: second hardware-timing sample, independent window */
#ifndef CONFIG_ARCH_POSIX
struct jitter_stats js6 = {};
(void)sample_cpu_jitter(pool, sizeof(pool), 432, 50, &js6);
report_jitter("stage6 jitter 50ms", &js6);
(void)sample_platform_entropy(pool, sizeof(pool), 432, 50, &js6);
report_jitter("stage6 " ENTROPY_SRC_LABEL " 50ms", &js6);
#endif /* CONFIG_ARCH_POSIX */
/* Collection done — release the SAR ADC before anything else needs it.
@@ -467,9 +646,9 @@ void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len,
* reboots, so there is no path that leaves it enabled. */
#if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
bootloader_random_disable();
printk("[RNG] esp32 pre-RF entropy: DISABLED (ADC released)\n");
RNG_RPT("[RNG] esp32 pre-RF entropy: DISABLED (ADC released)\n");
#endif
printk("[RNG] === end (extracting %u bytes via AES-256-CTR) ===\n",
RNG_RPT("[RNG] === end (extracting %u bytes via AES-256-CTR) ===\n",
(unsigned)out_len);
/* Final conditioning: AES-256-CTR over the pool. Extracts a 32-byte
+33 -8
View File
@@ -17,16 +17,20 @@ public:
/* Layered entropy mixer for one-time first-boot identity-key
* generation. Combines:
* 1. sys_csrand_get (early) — CSPRNG, strong on nRF/MG24
* 2. HWINFO unique device ID — per-device uniqueness
* 1. sys_csrand_get (early) — CSPRNG; on ESP32 only real
* because bootloader_random seeds
* the HWRNG (else pre-RF = 0 bits)
* 2. HWINFO unique device ID — per-device uniqueness (public)
* 3. Optional caller-supplied data — e.g. external noise samples
* 4. CPU cycle-counter jitter — main entropy source
* (NIST SP 800-90B class)
* 5. sys_csrand_get (late) — catches mid-boot radio init
* 6. CPU cycle-counter jitter #2 — independent timing window
* 4. Hardware-timing entropy — RTC two-clock beat on ESP32,
* CPU-jitter on nRF (NIST 90B class)
* 5. sys_csrand_get (late) — second HWRNG draw
* 6. Hardware-timing entropy #2 — independent window
* Conditioned via AES-256-CTR (SHA-256(pool) → key, ECB on counter).
* NIST-style health checks on jitter samples; reboots on degenerate
* output. Blocks for ~250ms — only called at first-boot identity gen.
* On ESP32 the primary source is the bootloader_random-seeded HWRNG
* (stages 1+5); the beat (4+6) is a physical second source. On nRF the
* CSPRNG and CPU-jitter are both independently strong. Health-checked;
* reboots on degenerate output. Blocks ~450ms — first-boot identity only.
*
* Output is suitable as an Ed25519 seed regardless of platform
* TRNG state at boot. */
@@ -34,6 +38,27 @@ public:
const uint8_t *extra = nullptr,
size_t extra_len = 0);
#if defined(ZEPHCORE_RNG_TEST_HOOKS)
/* TEST ONLY — enabled by a compile define, set only by tools/rng_selftest
* (never by production builds). When active, mixIdentitySeed zeroes the
* HWRNG (sys_csrand_get) contribution to the pool, so a diversity test
* measures the two-clock beat alone. Answers "if the hardware RNG returns
* nothing, does ZephyrRNG still produce diverse keys?" On a single device
* the device-id (stage 2) is constant across runs, so the beat is then the
* ONLY varying source. */
static void setTestKillHWRNG(bool kill);
#endif
/* Silence mixIdentitySeed's per-stage entropy health report.
*
* Default OFF — the node prints it once, at first-boot identity
* generation, and it is the only record of what the entropy sources
* actually did for a key that is then permanent. Do not silence it there.
*
* Exists for tools/rng_selftest, which calls mixIdentitySeed thousands of
* times: without this the ~12 lines per seed bury the summary. */
static void setSeedHealthQuiet(bool quiet);
/* End-to-end first-boot identity generation. Mixes a fresh seed,
* derives the Ed25519 keypair, and retries (up to 100×) if the
* MeshCore protocol-reserved 0x00/0xFF public-key prefix happens
@@ -35,6 +35,11 @@ CONFIG_ADC=y
CONFIG_HWINFO=y
CONFIG_REBOOT=y
# Portable CPU cycle counter (DWT on Cortex-M, CCOUNT on Xtensa) — the fast
# clock of ZephyrRNG's universal two-clock beat entropy source. Small: enables
# the DWT counter on ARM; no timekeeping impact.
CONFIG_TIMING_FUNCTIONS=y
# ========== LoRa ==========
CONFIG_LORA=y
CONFIG_LORA_MODULE_BACKEND_NATIVE=y
+117
View File
@@ -0,0 +1,117 @@
# SPDX-License-Identifier: MIT
# ZephCore RNG selftest entropy diversity test for identity-seed generation.
#
# Runs ZephyrRNG::mixIdentitySeed N times and checks the results for exact
# duplicates and pairwise correlation. Boots, runs, prints, halts. It is a
# separate image ON PURPOSE: each seed costs ~250 ms of jitter sampling, so
# N=1024 pins the CPU for ~4 minutes unacceptable inside a live node, where
# it would starve the mesh loop, BLE and LoRa RX.
#
# Build:
# west build -b rak4631 zephcore/tools/rng_selftest --pristine
# west build -b xiao_esp32s3/esp32s3/procpu zephcore/tools/rng_selftest --pristine
#
# Sample count (default 1024, ~4 min):
# west build ... zephcore/tools/rng_selftest -- -DRNG_SELFTEST_N=5000
#
# Flash: nRF52 drag build/zephyr/zephyr.uf2 onto the UF2 drive
# ESP32 west flash --esp-device COMx
#
# The tool never reads or writes stored identity: a device under test keeps
# whatever key it already had. Reflash normal firmware afterwards.
cmake_minimum_required(VERSION 3.20.0)
# Reuse the main project's custom board definitions
list(APPEND BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..)
# Strip the board qualifier (e.g. "/esp32s3/procpu") to get the base name
string(REPLACE "/" ";" BOARD_PARTS ${BOARD})
list(GET BOARD_PARTS 0 BOARD_BASE)
# Tool-local board overlay ONLY deliberately not the node's board.overlay.
#
# The node overlay declares /fstab, radio, sensor and partition nodes that this
# tool does not enable (CONFIG_FLASH=n, SPI=n, I2C=n), so reusing it fails on
# an undefined lfs_partition label. Boards needing console plumbing get a
# minimal overlay under boards/<board>/ here; everything else just uses the
# upstream board default, which already provides a console.
set(_SELFTEST_OVERLAY
"${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.overlay")
if(EXISTS "${_SELFTEST_OVERLAY}")
set(EXTRA_DTC_OVERLAY_FILE "${_SELFTEST_OVERLAY}" CACHE STRING "" FORCE)
message(STATUS "RNG selftest overlay: ${_SELFTEST_OVERLAY}")
else()
message(STATUS "RNG selftest: using upstream board console (no overlay)")
endif()
# Per-board conf (UF2 output, code partition) nRF52 needs it, ESP32 must not
# have it. See boards/rak4631/board.conf.
set(_SELFTEST_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/${BOARD_BASE}/board.conf")
if(EXISTS "${_SELFTEST_CONF}")
set(EXTRA_CONF_FILE "${_SELFTEST_CONF}" CACHE STRING "" FORCE)
message(STATUS "RNG selftest conf: ${_SELFTEST_CONF}")
endif()
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(zephcore_rng_selftest)
# The point of this tool is to exercise the SHIPPING code, not a copy of it.
# ZephyrRNG.cpp and Utils.cpp are compiled straight from the main tree, so the
# algorithm under test is byte-for-byte the one the node runs.
target_sources(app PRIVATE
src/main.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../adapters/rng/ZephyrRNG.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Utils.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/Identity.cpp
)
# ZephyrRNG.cpp also carries generateFirstBootIdentity(), which derives the
# Ed25519 keypair via LocalIdentity -> monocypher. We only call
# mixIdentitySeed() here, but linking the real dependency is more honest than
# leaning on --gc-sections to make the undefined symbols disappear and it
# keeps the door open to testing derived keypairs, not just seeds.
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../lib/monocypher monocypher)
target_link_libraries(app PRIVATE monocypher)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/../../include
${CMAKE_CURRENT_SOURCE_DIR}/../../adapters/rng
${CMAKE_CURRENT_SOURCE_DIR}/../../lib/monocypher
)
# ESP32 only: the HWRNG needs the SAR-ADC entropy source enabled before RF is
# up. Same source file the main build pulls in see zephcore/CMakeLists.txt.
if(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
set(_ESP_BOOTLOADER_RANDOM
"${ZEPHYR_HAL_ESPRESSIF_MODULE_DIR}/components/bootloader_support/src/bootloader_random_${CONFIG_SOC_SERIES}.c")
if(EXISTS "${_ESP_BOOTLOADER_RANDOM}")
message(STATUS "RNG selftest: ESP32 pre-RF entropy (${CONFIG_SOC_SERIES})")
target_sources(app PRIVATE "${_ESP_BOOTLOADER_RANDOM}")
else()
message(WARNING "RNG selftest: no bootloader_random source for ${CONFIG_SOC_SERIES}")
endif()
endif()
# Sample count. 1024 (~4 min) detects an effective-entropy floor up to ~20 bits
# by the birthday bound comfortably past the ~15-bit Debian OpenSSL failure.
if(NOT DEFINED RNG_SELFTEST_N)
set(RNG_SELFTEST_N 1024)
endif()
target_compile_definitions(app PRIVATE RNG_SELFTEST_N=${RNG_SELFTEST_N})
message(STATUS "RNG selftest: N=${RNG_SELFTEST_N} (~${RNG_SELFTEST_N} x 250ms)")
# "Dead HWRNG" mode: zero the hardware-RNG contribution so the diversity run
# measures the two-clock beat ALONE the definitive answer to "if the HWRNG
# returns nothing, does ZephyrRNG still produce diverse keys?". Needs the
# ZephyrRNG test hook compiled in.
# west build ... zephcore/tools/rng_selftest -- -DRNG_KILL_HWRNG=1
if(RNG_KILL_HWRNG)
# RNG_KILL_HWRNG -> main.cpp calls the setter; ZEPHCORE_RNG_TEST_HOOKS ->
# compiles the setter into ZephyrRNG (both are part of this app target, so
# the define reaches ZephyrRNG.cpp). Node builds define neither -> the hook
# is entirely absent from production.
target_compile_definitions(app PRIVATE RNG_KILL_HWRNG=1 ZEPHCORE_RNG_TEST_HOOKS=1)
message(STATUS "RNG selftest: KILL_HWRNG — beat-alone measurement")
endif()
@@ -0,0 +1,16 @@
# RAK4631 — RNG selftest board config
#
# UF2 output lives here rather than in prj.conf: it is nRF52-with-bootloader
# specific, and on ESP32 uf2conv fails outright (no UF2 family for that SoC).
#
# CONFIG_FLASH is enabled ONLY so the devicetree flash partitions resolve —
# uf2conv derives its base address from CONFIG_FLASH_LOAD_OFFSET, which
# USE_DT_CODE_PARTITION reads from the `zephyr,code-partition` chosen node.
# With flash off the offset comes out empty and uf2conv aborts with
# "argument -b/--base: expected one argument".
#
# This does NOT let the tool touch stored data: CONFIG_FILE_SYSTEM stays off,
# nothing mounts /lfs, and the tool never reads or writes an identity.
CONFIG_FLASH=y
CONFIG_USE_DT_CODE_PARTITION=y
CONFIG_BUILD_OUTPUT_UF2=y
@@ -0,0 +1,49 @@
/*
* RAK4631 — RNG selftest console overlay
* SPDX-License-Identifier: MIT
*
* Minimal ON PURPOSE. The node's own board.overlay is NOT reused here: it
* declares an /fstab entry, radio, sensors and partitions, none of which this
* tool enables (CONFIG_FLASH=n, CONFIG_SPI=n ...) — pulling it in fails the
* build on an undefined lfs_partition label. Same reasoning as the LR1110
* updater's per-board overlays.
*
* RAK4631 connects USB straight to the nRF52840, so the console has to ride
* USB CDC ACM; uart0's physical pins are not reachable on a plain WisBlock
* base board.
*/
/* Same flash layout as the node build.
*
* NOT just for UF2 packaging: RAK4631 boots through the Adafruit nRF52
* bootloader, so the application must be LINKED at the bootloader's app
* offset. Without a code partition the image links at 0 and would not run even
* if uf2conv managed to package it (it does not — the missing
* CONFIG_FLASH_LOAD_OFFSET is what produces "argument -b/--base: expected one
* argument"). SoftDevice v6 layout, matching boards/nrf52840/rak4631.
*
* The delete-nodes are required by that dtsi's own contract (see its header):
* it redefines partitions the upstream board DTS already labels, and without
* removing those first the build fails with a duplicate 'storage_partition'
* label. */
/delete-node/ &boot_partition;
/delete-node/ &slot0_partition;
/delete-node/ &slot1_partition;
/delete-node/ &storage_partition;
#include "../../../../boards/common/nrf52_partitions_sdv6.dtsi"
/ {
chosen {
zephyr,code-partition = &code_partition;
zephyr,console = &cdc_acm_uart;
zephyr,shell-uart = &cdc_acm_uart;
};
};
&zephyr_udc0 {
cdc_acm_uart: cdc_acm_uart {
compatible = "zephyr,cdc-acm-uart";
};
};
+83
View File
@@ -0,0 +1,83 @@
# ZephCore RNG selftest — minimal config.
#
# Exercises the REAL ZephyrRNG::mixIdentitySeed path, so it needs exactly what
# that function needs and nothing else: a CSPRNG, hwinfo, and PSA crypto for
# the AES-256-CTR conditioning step.
# Entropy / CSPRNG — sys_csrand_get().
#
# CONFIG_CSPRNG_ENABLED is NOT set here: it is a promptless result symbol
# ("default y depends on ENTROPY_HAS_DRIVER", subsys/random/Kconfig) that
# reports whether a real hardware entropy driver made it into the build.
# Assigning it directly is a Kconfig error. Ask for the driver instead and let
# it resolve — ZephyrRNG.cpp BUILD_ASSERTs on the result, so a platform without
# one fails loudly at compile time rather than silently testing a stub.
CONFIG_ENTROPY_GENERATOR=y
CONFIG_CSPRNG_NEEDED=y
# hwinfo — stage 2 device ID
CONFIG_HWINFO=y
# Portable CPU cycle counter — fast clock of the universal two-clock beat.
# Must match the node (zephcore_common.conf) so the tool tests the real path.
CONFIG_TIMING_FUNCTIONS=y
# PSA crypto — mirrors boards/common/zephcore_common.conf. mixIdentitySeed
# conditions the pool with SHA-256 + AES-256-ECB via PSA; without these the
# extraction step fails and the function panics.
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_PSA_CRYPTO_C=y
CONFIG_PSA_WANT_ALG_SHA_256=y
CONFIG_PSA_WANT_ALG_ECB_NO_PADDING=y
CONFIG_PSA_WANT_KEY_TYPE_AES=y
# sys_reboot() — Utils::cryptoPanicReboot() calls it when AES-CTR extraction
# fails or the seed comes out degenerate. Kept live rather than stubbed: that
# panic is part of the behaviour under test, and a tool that silently continued
# past it would report a pass on a seed the node would have refused.
CONFIG_REBOOT=y
# C++ — ZephyrRNG and Utils are C++
CONFIG_CPP=y
CONFIG_STD_CPP17=y
CONFIG_REQUIRES_FULL_LIBC=y
# Console only. No log subsystem: mixIdentitySeed reports via printk, which
# keeps the tool's output identical to what the node prints.
CONFIG_SERIAL=y
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_PRINTK=y
CONFIG_LOG=n
# CONFIG_ASSERT stays off: on ESP32 the Espressif blob trips false kswap.h
# assertions, and this tool has no reason to differ from the node.
CONFIG_ASSERT=n
# Main thread needs room for the 512-byte pool, the 256-byte distinct_bytes
# table and the jitter histogram, plus the fingerprint scratch in main().
CONFIG_MAIN_STACK_SIZE=4096
# NOTE: no CONFIG_BUILD_OUTPUT_UF2 here. UF2 is an nRF52-with-bootloader thing
# and needs a code partition to derive its base address; ESP32 has no UF2
# family at all and fails uf2conv outright. It lives in the per-board conf
# (boards/rak4631/board.conf) instead. ESP32 flashes with `west flash`.
# USB CDC ACM console — nRF52840 boards (RAK4631) route console over USB
# because the UART pins are not reachable on a plain base board. Harmless on
# ESP32, which uses its own console and ignores these.
CONFIG_USB_DEVICE_STACK_NEXT=y
CONFIG_UART_LINE_CTRL=y
CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=y
CONFIG_CDC_ACM_SERIAL_ENABLE_AT_BOOT=y
# Explicitly off — this tool must not touch the radio, storage or BLE. In
# particular it must never mount /lfs: it does not read or write any stored
# identity, so a device under test keeps whatever it already had.
CONFIG_BT=n
CONFIG_FLASH=n
CONFIG_FILE_SYSTEM=n
CONFIG_SPI=n
CONFIG_I2C=n
CONFIG_SENSOR=n
CONFIG_GNSS=n
+292
View File
@@ -0,0 +1,292 @@
/*
* ZephCore RNG selftest
* SPDX-License-Identifier: MIT
*
* Generates N identity seeds through the real ZephyrRNG::mixIdentitySeed and
* checks them for the failure mode that actually matters: lack of diversity.
*
* WHY NOT "test one key and see if it looks random":
* mixIdentitySeed ends in AES-256-CTR conditioning, and a cryptographic
* conditioner makes ANY input look uniform. A seed built from one bit of real
* entropy passes every statistical test you can point at it. Entropy is a
* property of the generating process, not of the bytes produced which is why
* NIST SP 800-90B health-tests the noise source and does not accept output
* testing as evidence.
*
* The real-world failure of a weak generator is not "the key looks wrong", it
* is "the key is not unique" Debian OpenSSL 2008 (~15 bits, 32767 possible
* keys) was found by noticing duplicates, and "Mining your Ps and Qs" (2012)
* found thousands of embedded devices sharing keys because they generated them
* at boot before entropy was available. That is exactly our situation, so that
* is exactly what this measures.
*
* WHY REPEAT ON ONE DEVICE rather than compare across devices:
* stage 2 of the mixer folds in the hwinfo device ID, which differs between
* devices but is constant on one. A cross-device comparison would therefore
* look healthy even with zero real entropy the device IDs alone would
* separate the keys and mask the bug. Repeating on a single device holds that
* differentiator constant and isolates the sources under suspicion.
*
* The tool never reads or writes stored identity. A device under test keeps
* whatever key it already had.
*/
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <string.h>
#include <ZephyrRNG.h>
#if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
/* RTC-slow-clock beat diagnostic. ESP32's k_cycle_get_32() is CCOUNT, same
* clock domain as the CPU, so CPU-jitter sampling measures a deterministic
* loop (proven on hardware: maxrep in the thousands, ~0 bits). The RTC slow
* clock is a SEPARATE oscillator (internal ~136 kHz RC, or a 32 kHz crystal)
* that drifts independently of the main-crystal->PLL->240 MHz path so the
* number of CPU cycles that elapse across a fixed RTC interval fluctuates, and
* that fluctuation is real physical entropy. This is what nRF gets for free
* (its system timer IS the 32 kHz RTC). This diagnostic MEASURES that beat; it
* does not touch identity generation. */
#include <esp_rtc_time.h>
#define HAVE_RTC_BEAT 1
#endif
#ifndef RNG_SELFTEST_N
#define RNG_SELFTEST_N 1024
#endif
#define SEED_LEN 32 /* Ed25519 seed size — what identity gen uses */
#define FP_LEN 8 /* fingerprint bytes kept per seed */
/* 8-byte fingerprints, not 4. At N=5000 a 4-byte fingerprint has a ~0.3%
* chance of colliding BY ACCIDENT (5000^2 / 2 / 2^32), which would report a
* spurious duplicate and discredit the whole run. At 8 bytes the accidental
* rate is ~10^-12 any duplicate reported is a real one. */
static uint8_t fingerprints[RNG_SELFTEST_N][FP_LEN];
static int popcount8(uint8_t x)
{
int n = 0;
while (x) { n += x & 1; x >>= 1; }
return n;
}
/* Hamming distance between two seeds, in bits. Two independent 256-bit values
* differ in ~128 bits on average; a distribution pulled well below that means
* the seeds are correlated even when no exact duplicate ever appears. */
static int hamming(const uint8_t *a, const uint8_t *b, size_t len)
{
int d = 0;
for (size_t i = 0; i < len; i++) d += popcount8(a[i] ^ b[i]);
return d;
}
#ifdef HAVE_RTC_BEAT
/* -log2(mcv/n) in milli-bits — SP 800-90B Most Common Value min-entropy,
* integer math. Understates slightly (linear log2 interpolation), the safe
* direction for an entropy claim. */
static uint32_t min_entropy_mb(uint32_t mcv, uint32_t n)
{
if (mcv == 0 || mcv >= n) return 0;
uint32_t ratio_q10 = (uint32_t)(((uint64_t)n * 1024u) / mcv); /* (n/mcv)<<10 */
uint32_t ip = 31u - (uint32_t)__builtin_clz(ratio_q10);
uint32_t base = 1u << ip;
uint32_t frac = (uint32_t)(((uint64_t)(ratio_q10 - base) * 1000u) / base);
uint32_t lg = ip * 1000u + frac; /* log2(ratio_q10)*1000 */
return (lg > 10000u) ? (lg - 10000u) : 0u; /* subtract log2(1024) */
}
/* 14-bit fold of the delta (0x3FFF = 16384 slots). The first run showed a
* ~1681-cycle span, so the entropy lives across ~11 bits, not the low 8 the
* low-byte-only estimate threw most of it away. 14 bits covers spans up to
* 16384 without wrapping; a wider span folds and only ever UNDERcounts
* distinctness (merges values), which understates entropy safe. uint16_t is
* enough since per-window sample count < 65535. */
static uint16_t beat_hist[16384];
/* Sweep several RTC-window sizes and report full-delta min-entropy for each,
* so we can pick the parameter before touching the key path. Characterization
* ONLY nothing here feeds identity generation. */
static void rtc_beat_diagnostic(void)
{
const int K = 4096; /* samples per window */
const uint32_t windows_us[] = { 120, 250, 500, 1000 };
printk("\n");
printk("--------------------------------------------------------\n");
printk(" RTC-beat window sweep (ESP32) — %d samples per window\n", K);
printk(" CPU cycles per fixed RTC-slow interval; full-delta entropy\n");
printk("--------------------------------------------------------\n");
printk(" window span distinct mcv bits/samp bits/sec\n");
printk(" ------ ---- -------- ------- --------- --------\n");
for (size_t w = 0; w < ARRAY_SIZE(windows_us); w++) {
uint32_t win = windows_us[w];
memset(beat_hist, 0, sizeof(beat_hist));
uint32_t cyc_min = UINT32_MAX, cyc_max = 0;
for (int i = 0; i < K; i++) {
uint64_t r0 = esp_rtc_get_time_us();
uint32_t c0 = k_cycle_get_32();
while ((esp_rtc_get_time_us() - r0) < win) {
/* CPU clock advances while the independent RTC
* oscillator defines the window; they drift */
}
uint32_t c1 = k_cycle_get_32();
uint32_t d = c1 - c0;
if (d < cyc_min) cyc_min = d;
if (d > cyc_max) cyc_max = d;
beat_hist[d & 0x3FFF]++;
}
uint32_t mcv = 0;
int distinct = 0;
for (int v = 0; v < 16384; v++) {
if (beat_hist[v]) distinct++;
if (beat_hist[v] > mcv) mcv = beat_hist[v];
}
uint32_t per_mb = min_entropy_mb(mcv, K);
/* bits/sec = bits/sample * 1e6 / window_us */
uint32_t bits_per_sec = (uint32_t)(((uint64_t)per_mb * 1000000u)
/ (win * 1000u));
printk(" %4u us %5u %6d %6u %u.%03u %u\n",
win, cyc_max - cyc_min, distinct, mcv,
per_mb / 1000u, per_mb % 1000u, bits_per_sec);
}
printk("--------------------------------------------------------\n");
printk(" pick: highest bits/sample for cleanest per-sample quality,\n");
printk(" or highest bits/sec if throughput-bound (200ms budget needs\n");
printk(" >256 bits, so any row over ~1300 bits/sec has 10x margin).\n");
printk("\n");
}
#endif /* HAVE_RTC_BEAT */
int main(void)
{
/* Console settle — USB CDC enumerates after boot on some boards. */
k_msleep(2000);
#ifdef HAVE_RTC_BEAT
/* Characterize the candidate second source BEFORE the diversity run,
* so its numbers are the first thing on the console. */
rtc_beat_diagnostic();
#endif
printk("\n");
printk("========================================================\n");
printk(" ZephCore RNG selftest — identity seed diversity\n");
printk(" N=%d seeds x 32 bytes, ~250 ms each (~%d s total)\n",
RNG_SELFTEST_N, (RNG_SELFTEST_N * 250) / 1000);
printk("========================================================\n\n");
#ifdef RNG_KILL_HWRNG
/* Zero the HWRNG contribution: the diversity run below then measures the
* two-clock beat ALONE. On this single device the device-id (stage 2) is
* constant across all seeds, so if this run still shows 0 duplicates and
* Hamming ~128, the beat by itself carries the key i.e. ZephyrRNG
* survives a completely dead hardware RNG. */
mesh::ZephyrRNG::setTestKillHWRNG(true);
printk("*** KILL_HWRNG: hardware RNG zeroed — measuring the beat ALONE ***\n\n");
#endif
uint8_t seed[SEED_LEN];
uint8_t prev[SEED_LEN];
int duplicates = 0;
int ham_min = 10000;
int ham_max = -1;
int64_t ham_sum = 0;
int ham_n = 0;
int first_dup_a = -1, first_dup_b = -1;
int64_t t_start = k_uptime_get();
for (int i = 0; i < RNG_SELFTEST_N; i++) {
/* Per-stage health output comes from mixIdentitySeed itself. Let
* it through for the first two seeds so the sources are on the
* record, then actually silence it at N=1024 that is ~12,000
* lines that would bury the summary. */
if (i == 2) {
printk("\n[selftest] per-stage output silenced from here "
"(%d more seeds); summary follows at the end\n\n",
RNG_SELFTEST_N - 2);
mesh::ZephyrRNG::setSeedHealthQuiet(true);
}
mesh::ZephyrRNG::mixIdentitySeed(seed, sizeof(seed));
/* Fingerprint + duplicate scan against everything so far. */
memcpy(fingerprints[i], seed, FP_LEN);
for (int j = 0; j < i; j++) {
if (memcmp(fingerprints[i], fingerprints[j], FP_LEN) == 0) {
duplicates++;
if (first_dup_a < 0) { first_dup_a = j; first_dup_b = i; }
break;
}
}
/* Consecutive Hamming distance. */
if (i > 0) {
int d = hamming(seed, prev, SEED_LEN);
if (d < ham_min) ham_min = d;
if (d > ham_max) ham_max = d;
ham_sum += d;
ham_n++;
}
memcpy(prev, seed, SEED_LEN);
/* Progress every 10%, so a long run visibly lives. */
int step = RNG_SELFTEST_N / 10;
if (step > 0 && ((i + 1) % step) == 0) {
printk("[selftest] %d/%d seeds (%d%%) dup=%d\n",
i + 1, RNG_SELFTEST_N,
((i + 1) * 100) / RNG_SELFTEST_N, duplicates);
}
}
int64_t elapsed = k_uptime_get() - t_start;
/* Mean to 2 decimals without floating point. */
int ham_mean_x100 = ham_n ? (int)((ham_sum * 100) / ham_n) : 0;
printk("\n");
printk("========================================================\n");
printk(" RESULTS (%d seeds in %lld s)\n", RNG_SELFTEST_N, elapsed / 1000);
printk("--------------------------------------------------------\n");
printk(" exact duplicates : %d\n", duplicates);
if (duplicates) {
printk(" first collision: seed #%d == seed #%d\n",
first_dup_a, first_dup_b);
}
printk(" hamming distance : min=%d max=%d mean=%d.%02d (expect ~128)\n",
ham_min, ham_max, ham_mean_x100 / 100, ham_mean_x100 % 100);
printk("--------------------------------------------------------\n");
/* Verdict. Deliberately conservative wording: passing means "no evidence
* of a problem at this sample count", never "the entropy is good". A
* clean run at N only rules out an effective-entropy floor below about
* 2*log2(N) bits see the note in CMakeLists.txt. */
bool bad_dup = (duplicates > 0);
bool bad_ham = (ham_n > 0) && (ham_mean_x100 < 11000 || ham_mean_x100 > 14500);
if (bad_dup) {
printk(" VERDICT: FAIL — duplicate seeds. The generator is\n");
printk(" producing a small key space. Do NOT ship.\n");
} else if (bad_ham) {
printk(" VERDICT: FAIL — seeds are correlated (mean far from\n");
printk(" 128 bits). Do NOT ship.\n");
} else {
printk(" VERDICT: no evidence of correlation at N=%d.\n",
RNG_SELFTEST_N);
printk(" Rules out an entropy floor below ~%d bits.\n",
2 * (31 - __builtin_clz(RNG_SELFTEST_N)));
printk(" This is NOT proof the entropy is sufficient.\n");
}
printk("========================================================\n");
printk("\n[selftest] done — halting. Reflash normal firmware.\n");
return 0;
}