mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 20:38:19 +00:00
rng cleanup
This commit is contained in:
@@ -23,9 +23,9 @@
|
||||
|
||||
---
|
||||
|
||||
A maintenance release: an important **admin-password fix**, **GPS standby power savings**, and the
|
||||
removal of **Adaptive Power Control** — plus two new boards, **muzi works R1 Neo** and an experimental
|
||||
first cut at the **ThinkNode M9**.
|
||||
A maintenance release: an important **admin-password fix**, a substantial **identity-key randomness
|
||||
hardening** on ESP32, **GPS standby power savings**, and the removal of **Adaptive Power Control** —
|
||||
plus two new boards, **muzi works R1 Neo** and an experimental first cut at the **ThinkNode M9**.
|
||||
|
||||
## Highlights
|
||||
|
||||
@@ -45,6 +45,41 @@ The one place autocapitalization genuinely hurts — typing commands to the V-Co
|
||||
which capitalizes the first letter of every line — is handled narrowly instead: only character 0 of a
|
||||
V-Contact chat line is folded. No command takes an argument at position 0, so no value can be touched.
|
||||
|
||||
### Identity keys are now generated with hardened, hardware-verified randomness
|
||||
|
||||
Your node's identity — the Ed25519 keypair that *is* your node on the mesh — is generated once, on first
|
||||
boot, and kept for life. An investigation into how the randomness behind that key is gathered found that
|
||||
**ESP32 boards were generating identities with far less entropy than intended.** Two independent problems,
|
||||
both fixed in this release:
|
||||
|
||||
- **The ESP32 hardware RNG was running unseeded.** It only produces true random numbers while the radio
|
||||
(WiFi/BT) is active — and identity generation runs before the radio comes up (on repeaters, the radio
|
||||
never comes up at all). Key generation now feeds the RNG from the chip's ADC noise source for its
|
||||
duration, Espressif's documented method for exactly this pre-radio situation. Verified on hardware:
|
||||
the RNG's output went from a fixed pattern to statistically ideal.
|
||||
- **The timing-jitter backup source measured nothing on ESP32.** It sampled the CPU's cycle counter
|
||||
against work paced by that same clock — deterministic, confirmed dead on hardware. It has been replaced
|
||||
everywhere by a **two-clock beat** source: counting CPU cycles across fixed intervals of an
|
||||
*independent* low-frequency oscillator, whose physical drift is genuinely random. The same mechanism
|
||||
now runs uniformly on ESP32, nRF52 and MG24. (nRF was never in danger — its RNG is radio-independent —
|
||||
this adds an equally strong second source there for defence in depth.)
|
||||
|
||||
This was not signed off by inspection. A new on-device self-test (`tools/rng_selftest`) generated **1024
|
||||
identities per platform through the real generation path**: zero duplicate keys, and pairwise-difference
|
||||
statistics matching ideal randomness — **including a torture run with the hardware RNG deliberately
|
||||
disabled**, where the beat source alone still produced fully diverse keys. A node whose hardware RNG
|
||||
silently dies no longer means a predictable identity.
|
||||
|
||||
> [!TIP]
|
||||
> **Upgrading does not regenerate your identity — the key made at first boot stays.** If your node is an
|
||||
> ESP32 board and its identity was generated by an earlier release, that key was created under the old,
|
||||
> weaker entropy. If you want one generated under the new code, **factory-reset / format the node** (or
|
||||
> erase-flash and reflash) — the next boot creates a fresh identity through the hardened path. The cost is
|
||||
> real: a new public key, so your contacts must re-add you, and contacts, channels and bonds start clean.
|
||||
> Whether that trade is worth it is your call — for a casual node, probably not; for a node whose identity
|
||||
> matters (an admin key, a well-known repeater), it is worth considering. nRF boards have no reason to
|
||||
> regenerate.
|
||||
|
||||
### GPS standby now actually saves power
|
||||
|
||||
`CONFIG_PM_DEVICE` (device power management) is enabled for the first time, carefully scoped: system-managed
|
||||
@@ -170,4 +205,7 @@ ZephCore and are fine left alone.
|
||||
3. **Everything else, from v1.16.2 onward:** just flash — bonds and data survive.
|
||||
4. **From v1.16.1 or older:** just flash — self-migrates on first boot; re-bond once.
|
||||
5. **From Official MeshCore:** just flash — auto-formats on first boot.
|
||||
6. **Anything odd?** Format first.
|
||||
6. **ESP32 node whose identity you care about?** Consider a one-time factory reset so the key is
|
||||
regenerated under the hardened entropy — see the tip in the identity-randomness section. Optional,
|
||||
and it changes your public key.
|
||||
7. **Anything odd?** Format first.
|
||||
|
||||
@@ -41,28 +41,23 @@ void ZephyrRNG::random(uint8_t *dest, size_t sz)
|
||||
Utils::cryptoPanicReboot("CSPRNG unavailable after retries");
|
||||
}
|
||||
|
||||
/* ===== Jitter sampling + online health check =============================
|
||||
/* ===== Timing-entropy health check =======================================
|
||||
*
|
||||
* Stephan Müller "CPU Time Jitter Based Non-Physical True Random Number
|
||||
* Generator" — Linux kernel's jitterentropy_rng. NIST SP 800-90B has
|
||||
* a compliance class for this entropy source type.
|
||||
* Online health check (NIST SP 800-90B style) for the two-clock beat source
|
||||
* below: repetition count + a distinct-value check tracked across all samples
|
||||
* in the window with scalar state — no per-sample buffer needed. Detects
|
||||
* stuck-source catastrophic failure (e.g. a frozen slow clock). Does not
|
||||
* statistically prove entropy quality — that's what the selftest
|
||||
* output-diversity run is for.
|
||||
*
|
||||
* 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
|
||||
* scalar state — no per-sample buffer needed. Detects stuck-source
|
||||
* catastrophic failure (e.g. cycle counter not advancing). Does not
|
||||
* statistically prove entropy quality — that's what the literature is
|
||||
* for. */
|
||||
* (The former CPU-jitter fallback — Stephan Müller style k_cycle_get_32()
|
||||
* delta sampling — was removed: it only ever carried entropy where the cycle
|
||||
* counter was already cross-domain from the CPU, and every such board is
|
||||
* exactly a board the beat covers. Where the beat is unavailable the counter
|
||||
* is same-domain, the loop is deterministic, and jitter yields ~0 bits —
|
||||
* measured dead on ESP32 hardware.) */
|
||||
|
||||
/* Health statistics for one jitter window. Timing statistics only — never
|
||||
/* Health statistics for one beat 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
|
||||
@@ -86,152 +81,15 @@ 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 {
|
||||
struct beat_stats {
|
||||
int n_samples;
|
||||
int n_distinct; /* distinct delta values seen, capped at 8 */
|
||||
int max_consec; /* longest run of identical deltas */
|
||||
uint32_t min_delta;
|
||||
uint32_t max_delta;
|
||||
uint32_t mcv_count; /* occurrences of the most common tracked delta */
|
||||
uint32_t untracked; /* samples whose value missed the histogram */
|
||||
bool ok;
|
||||
};
|
||||
|
||||
/* log2(x) * 1000, integer math (no FP — printk here has no float support).
|
||||
* Integer part from the leading bit; fraction by linear interpolation between
|
||||
* adjacent powers of two. Linear interpolation UNDERSTATES log2 across that
|
||||
* range (log2(1.5)=0.585 vs 0.5 linear), so the derived entropy figure errs
|
||||
* low — the safe direction for an entropy claim. */
|
||||
static uint32_t log2_millibits(uint32_t x)
|
||||
{
|
||||
if (x <= 1) return 0;
|
||||
uint32_t ipart = 31u - (uint32_t)__builtin_clz(x);
|
||||
uint32_t base = 1u << ipart;
|
||||
uint32_t frac = (uint32_t)(((uint64_t)(x - base) * 1000u) / base);
|
||||
return ipart * 1000u + frac;
|
||||
}
|
||||
|
||||
static bool sample_cpu_jitter(uint8_t *pool, size_t pool_size,
|
||||
size_t pool_offset, uint32_t duration_ms,
|
||||
struct jitter_stats *st = nullptr)
|
||||
{
|
||||
uint32_t accum = k_cycle_get_32();
|
||||
int64_t deadline = k_uptime_get() + duration_ms;
|
||||
size_t idx = pool_offset;
|
||||
uint32_t min_delta = UINT32_MAX, max_delta = 0;
|
||||
|
||||
/* Online health stats: 32 bytes total vs. the previous 512-byte
|
||||
* deltas[] array. Tracks every sample, not just the first 128. */
|
||||
uint32_t prev_delta = 0;
|
||||
int cur_consec = 0, max_consec = 0;
|
||||
uint32_t distinct[8] = {0};
|
||||
int n_distinct = 0;
|
||||
int n_samples = 0;
|
||||
|
||||
/* Bounded histogram for the SP 800-90B Most Common Value estimator.
|
||||
* Holds the first JITTER_HIST_SLOTS distinct deltas; anything beyond
|
||||
* that is counted in `untracked`. A value frequent enough to dominate
|
||||
* p_max shows up within the first few distinct observations with
|
||||
* overwhelming probability, so this captures what the estimator needs
|
||||
* — but `untracked` is reported so the assumption stays visible. */
|
||||
uint32_t hist_val[JITTER_HIST_SLOTS] = {0};
|
||||
uint32_t hist_cnt[JITTER_HIST_SLOTS] = {0};
|
||||
int n_hist = 0;
|
||||
uint32_t untracked = 0;
|
||||
|
||||
while (k_uptime_get() < deadline) {
|
||||
uint32_t t1 = k_cycle_get_32();
|
||||
/* 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++) {
|
||||
a = a * 1664525u + 1013904223u;
|
||||
}
|
||||
accum = a;
|
||||
uint32_t t2 = k_cycle_get_32();
|
||||
uint32_t delta = t2 - t1;
|
||||
|
||||
/* Mix into entropy pool */
|
||||
pool[idx++ % pool_size] ^= (uint8_t)delta;
|
||||
pool[idx++ % pool_size] ^= (uint8_t)(delta >> 8);
|
||||
pool[idx++ % pool_size] ^= (uint8_t)accum;
|
||||
pool[idx++ % pool_size] ^= (uint8_t)(accum >> 8);
|
||||
|
||||
/* Online repetition count */
|
||||
if (n_samples > 0 && delta == prev_delta) {
|
||||
if (++cur_consec > max_consec) max_consec = cur_consec;
|
||||
} else {
|
||||
cur_consec = 1;
|
||||
}
|
||||
prev_delta = delta;
|
||||
|
||||
/* Track first 8 distinct delta values */
|
||||
if (n_distinct < 8) {
|
||||
bool found = false;
|
||||
for (int j = 0; j < n_distinct; j++) {
|
||||
if (distinct[j] == delta) { found = true; break; }
|
||||
}
|
||||
if (!found) distinct[n_distinct++] = delta;
|
||||
}
|
||||
|
||||
if (delta < min_delta) min_delta = delta;
|
||||
if (delta > max_delta) max_delta = delta;
|
||||
|
||||
/* MCV histogram */
|
||||
{
|
||||
bool binned = false;
|
||||
for (int j = 0; j < n_hist; j++) {
|
||||
if (hist_val[j] == delta) {
|
||||
hist_cnt[j]++;
|
||||
binned = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!binned) {
|
||||
if (n_hist < JITTER_HIST_SLOTS) {
|
||||
hist_val[n_hist] = delta;
|
||||
hist_cnt[n_hist] = 1;
|
||||
n_hist++;
|
||||
} else {
|
||||
untracked++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
n_samples++;
|
||||
}
|
||||
|
||||
uint32_t mcv_count = 0;
|
||||
for (int j = 0; j < n_hist; j++) {
|
||||
if (hist_cnt[j] > mcv_count) mcv_count = hist_cnt[j];
|
||||
}
|
||||
|
||||
bool ok = (n_samples >= 16) /* enough samples */
|
||||
&& (max_consec < 32) /* not a stuck source */
|
||||
&& (n_distinct >= 5); /* minimal variance */
|
||||
|
||||
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 = mcv_count;
|
||||
st->untracked = untracked;
|
||||
st->ok = ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* ===== Universal two-clock beat entropy ==================================
|
||||
*
|
||||
* One physical entropy source for every board: count CPU cycles elapsed across
|
||||
@@ -256,14 +114,18 @@ static bool sample_cpu_jitter(uint8_t *pool, size_t pool_size,
|
||||
* 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).
|
||||
* independent slow clock is identified and the timing stages are
|
||||
* SKIPPED — a same-domain counter measures a deterministic loop
|
||||
* (~0 bits, measured), so sampling it would only pretend to add
|
||||
* entropy. Such boards (STM32WL SysTick, nRF54L 1 MHz GRTC) rely on
|
||||
* their true TRNG via the CSPRNG stages, which is what the removed
|
||||
* CPU-jitter fallback effectively did anyway.
|
||||
*
|
||||
* 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.
|
||||
* non-HWRNG leg.
|
||||
*
|
||||
* 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. */
|
||||
@@ -284,7 +146,7 @@ static inline uint64_t beat_slow_ticks(void) { return k_cycle_get_32(); }
|
||||
|
||||
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)
|
||||
struct beat_stats *st = nullptr)
|
||||
{
|
||||
/* Enable the CPU cycle counter once (DWT on Cortex-M; no-op-ish on
|
||||
* Xtensa where CCOUNT always runs). */
|
||||
@@ -354,8 +216,6 @@ static bool sample_two_clock_beat(uint8_t *pool, size_t pool_size,
|
||||
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;
|
||||
@@ -376,51 +236,24 @@ static int distinct_bytes(const uint8_t *buf, size_t len)
|
||||
return n;
|
||||
}
|
||||
|
||||
/* One line per jitter window. `distinct` is capped at 8 by the sampler, so 8/8
|
||||
/* One line per beat window. `distinct` is capped at 8 by the sampler, so 8/8
|
||||
* means "at least 8" — the pass threshold is 5. `maxrep` is the longest run of
|
||||
* identical deltas; >=32 fails. min/max delta expose a resolution problem: if
|
||||
* they are 0 and 1, the cycle counter is too coarse to measure the work at all
|
||||
* and no amount of sampling will help. */
|
||||
static void report_jitter(const char *label, const struct jitter_stats *st)
|
||||
* identical deltas; >=32 fails. The span/distinct/maxrep line IS the beat's
|
||||
* health — per-sample entropy is characterised offline by the selftest window
|
||||
* sweep, not estimated here (an in-path MCV estimate would need a 16k-slot
|
||||
* histogram). Deliberately no statistics on the conditioned output:
|
||||
* AES-256-CTR makes any input look uniform, so output statistics would read
|
||||
* perfect even for a near-zero-entropy seed. Entropy is a property of the
|
||||
* source. */
|
||||
#ifdef HAVE_TWO_CLOCK_BEAT
|
||||
static void report_beat(const char *label, const struct beat_stats *st)
|
||||
{
|
||||
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");
|
||||
|
||||
/* Min-entropy estimate, NIST SP 800-90B 6.3.1 Most Common Value:
|
||||
* p_max = mcv_count / n_samples
|
||||
* H_min per sample = -log2(p_max) = log2(n_samples / mcv_count)
|
||||
*
|
||||
* Computed in milli-bits with integer math. This REPLACES the 0.1-0.3
|
||||
* bits/sample the file header used to assume — assumption is only valid
|
||||
* if deltas actually vary, which is the very thing that failed on ESP32.
|
||||
*
|
||||
* Deliberately NOT measured on the conditioned output: AES-256-CTR makes
|
||||
* 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) {
|
||||
/* 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;
|
||||
}
|
||||
|
||||
uint32_t ratio_q10 = (uint32_t)(((uint64_t)st->n_samples * 1024u)
|
||||
/ st->mcv_count);
|
||||
uint32_t per_mb = log2_millibits(ratio_q10);
|
||||
per_mb = (per_mb > 10000u) ? (per_mb - 10000u) : 0u; /* less log2(1024) */
|
||||
|
||||
uint64_t total_mb = (uint64_t)per_mb * (uint64_t)st->n_samples;
|
||||
uint32_t total_bits = (uint32_t)(total_mb / 1000u);
|
||||
|
||||
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]" : "");
|
||||
}
|
||||
#endif /* HAVE_TWO_CLOCK_BEAT */
|
||||
|
||||
/* ===== Entropy extraction via AES-256-CTR ================================
|
||||
*
|
||||
@@ -511,29 +344,6 @@ 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)
|
||||
{
|
||||
@@ -603,27 +413,33 @@ void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len,
|
||||
RNG_RPT("[RNG] stage3 extra : %d bytes\n", (int)n);
|
||||
}
|
||||
|
||||
/* Stage 4: hardware-timing entropy, 200ms. RTC two-clock beat on ESP32,
|
||||
* CPU cycle-counter jitter elsewhere (sample_platform_entropy).
|
||||
/* Stage 4: hardware-timing entropy, 200ms — the two-clock beat.
|
||||
*
|
||||
* 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
|
||||
* this sampling anyway. */
|
||||
#ifndef CONFIG_ARCH_POSIX
|
||||
struct jitter_stats js = {};
|
||||
bool health_ok = sample_platform_entropy(pool, sizeof(pool), 112, 200, &js);
|
||||
report_jitter("stage4 " ENTROPY_SRC_LABEL " 200ms", &js);
|
||||
* Skipped where no independent slow clock exists (see the beat header
|
||||
* comment): a same-domain counter would sample a deterministic loop and
|
||||
* only pretend to add entropy. Those boards rely on their true TRNG via
|
||||
* stages 1 and 5.
|
||||
*
|
||||
* Also 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 this sampling anyway. */
|
||||
#if defined(HAVE_TWO_CLOCK_BEAT) && !defined(CONFIG_ARCH_POSIX)
|
||||
struct beat_stats js = {};
|
||||
bool health_ok = sample_two_clock_beat(pool, sizeof(pool), 112, 200, &js);
|
||||
report_beat("stage4 beat 200ms", &js);
|
||||
if (!health_ok) {
|
||||
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);
|
||||
health_ok = sample_two_clock_beat(pool, sizeof(pool), 112, 400, &js);
|
||||
report_beat("stage4 beat 400ms", &js);
|
||||
if (!health_ok) {
|
||||
RNG_RPT("[RNG] stage4 STILL FAILING — continuing with mixed sources\n");
|
||||
}
|
||||
}
|
||||
#endif /* CONFIG_ARCH_POSIX */
|
||||
#else
|
||||
RNG_RPT("[RNG] stage4/6 skipped — no independent slow clock (TRNG via csrand only)\n");
|
||||
#endif /* HAVE_TWO_CLOCK_BEAT && !CONFIG_ARCH_POSIX */
|
||||
|
||||
/* Stage 5: late CSPRNG — catches any mid-boot radio init that
|
||||
* warmed the TRNG during the stage-4 window */
|
||||
@@ -635,11 +451,11 @@ void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len,
|
||||
rc5, distinct_bytes(pool + 368, 64));
|
||||
|
||||
/* Stage 6: second hardware-timing sample, independent window */
|
||||
#ifndef CONFIG_ARCH_POSIX
|
||||
struct jitter_stats js6 = {};
|
||||
(void)sample_platform_entropy(pool, sizeof(pool), 432, 50, &js6);
|
||||
report_jitter("stage6 " ENTROPY_SRC_LABEL " 50ms", &js6);
|
||||
#endif /* CONFIG_ARCH_POSIX */
|
||||
#if defined(HAVE_TWO_CLOCK_BEAT) && !defined(CONFIG_ARCH_POSIX)
|
||||
struct beat_stats js6 = {};
|
||||
(void)sample_two_clock_beat(pool, sizeof(pool), 432, 50, &js6);
|
||||
report_beat("stage6 beat 50ms", &js6);
|
||||
#endif /* HAVE_TWO_CLOCK_BEAT && !CONFIG_ARCH_POSIX */
|
||||
|
||||
/* Collection done — release the SAR ADC before anything else needs it.
|
||||
* Unconditional: every path below this point either returns normally or
|
||||
|
||||
@@ -22,14 +22,16 @@ public:
|
||||
* 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. Hardware-timing entropy — RTC two-clock beat on ESP32,
|
||||
* CPU-jitter on nRF (NIST 90B class)
|
||||
* 4. Hardware-timing entropy — two-clock beat (ESP32 RTC-slow,
|
||||
* nRF/MG24 32 kHz RTC); skipped on
|
||||
* boards with no independent slow
|
||||
* clock (they rely on their TRNG)
|
||||
* 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).
|
||||
* 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;
|
||||
* CSPRNG and the beat 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
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/LoRaConfig.h>
|
||||
#include <adapters/radio/LoRaRadioBase.h>
|
||||
#include <adapters/rng/ZephyrRNG.h> /* generateFirstBootIdentity (hardened keygen) */
|
||||
#include <helpers/MeshcoreJson.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
@@ -32,10 +33,10 @@ namespace mesh {
|
||||
|
||||
/* ========== Construction ========== */
|
||||
|
||||
ObserverMesh::ObserverMesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc)
|
||||
ObserverMesh::ObserverMesh(Radio &radio, MillisecondClock &ms, RTCClock &rtc)
|
||||
: Dispatcher(radio, ms, _pkt_mgr),
|
||||
_last_rssi(0.0f), _last_score(0.0f), _last_raw_len(0),
|
||||
_store(nullptr), _creds(nullptr), _rng(&rng), _rtc(&rtc), _start_uptime_secs(0)
|
||||
_store(nullptr), _creds(nullptr), _rtc(&rtc), _start_uptime_secs(0)
|
||||
{
|
||||
memset(_pubkey_hex, 0, sizeof(_pubkey_hex));
|
||||
memset(_packets_topic, 0, sizeof(_packets_topic));
|
||||
@@ -62,15 +63,19 @@ void ObserverMesh::begin(RepeaterDataStore *store, struct ObserverCreds *creds)
|
||||
_store->savePrefs(_prefs);
|
||||
}
|
||||
|
||||
/* Load or generate node identity */
|
||||
/* Load or generate node identity.
|
||||
*
|
||||
* Use ZephyrRNG::generateFirstBootIdentity — the SAME hardened path the
|
||||
* companion and repeater use (bootloader_random-seeded HWRNG + two-clock
|
||||
* beat, conditioned via AES-256-CTR) — NOT LocalIdentity(_rng). The old
|
||||
* form drew straight from ZephyrRNG::random() / sys_csrand_get, which on
|
||||
* an ESP32 observer is unseeded (BLE never comes up to seed WDEV_RANDOM),
|
||||
* so it derived a permanent key from a weak PRNG. generateFirstBootIdentity
|
||||
* also owns the reserved-prefix retry (100 attempts + panic backstop),
|
||||
* replacing the weaker 10-try loop that silently kept a reserved prefix. */
|
||||
if (!_store->loadIdentity(_self_id)) {
|
||||
LOG_INF("No identity found — generating new keypair");
|
||||
int attempts = 0;
|
||||
do {
|
||||
_self_id = LocalIdentity(_rng);
|
||||
attempts++;
|
||||
} while (attempts < 10 &&
|
||||
(_self_id.pub_key[0] == 0x00 || _self_id.pub_key[0] == 0xFF));
|
||||
mesh::ZephyrRNG::generateFirstBootIdentity(_self_id);
|
||||
_store->saveIdentity(_self_id);
|
||||
LOG_INF("New observer identity saved");
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <mesh/Dispatcher.h>
|
||||
#include <mesh/StaticPoolPacketManager.h>
|
||||
#include <mesh/Identity.h>
|
||||
#include <mesh/RNG.h>
|
||||
#include <mesh/RTC.h>
|
||||
#include <helpers/MeshTimeSync.h>
|
||||
#include <helpers/NodePrefs.h>
|
||||
@@ -45,7 +44,6 @@ class ObserverMesh : public Dispatcher {
|
||||
NodePrefs _prefs;
|
||||
RepeaterDataStore *_store;
|
||||
struct ObserverCreds *_creds;
|
||||
RNG *_rng;
|
||||
RTCClock *_rtc;
|
||||
|
||||
/* Pre-built MQTT topic strings (set in begin()) */
|
||||
@@ -73,7 +71,7 @@ protected:
|
||||
DispatcherAction onRecvPacket(Packet *pkt) override;
|
||||
|
||||
public:
|
||||
ObserverMesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc);
|
||||
ObserverMesh(Radio &radio, MillisecondClock &ms, RTCClock &rtc);
|
||||
|
||||
/* Initialize: load/generate identity, load/init prefs, start radio RX. */
|
||||
void begin(RepeaterDataStore *store, struct ObserverCreds *creds);
|
||||
|
||||
@@ -245,7 +245,6 @@ static void time_sync_cb(uint32_t unix_ts)
|
||||
|
||||
static mesh::ZephyrBoard s_board;
|
||||
static mesh::ZephyrMillisecondClock s_ms_clock;
|
||||
static mesh::ZephyrRNG s_rng;
|
||||
|
||||
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
|
||||
|
||||
@@ -260,7 +259,7 @@ static mesh::SX127xRadio lora_radio(lora_dev, s_board, &s_radio_prefs);
|
||||
static mesh::SX126xRadio lora_radio(lora_dev, s_board, &s_radio_prefs);
|
||||
#endif
|
||||
|
||||
static mesh::ObserverMesh observer_mesh(lora_radio, s_ms_clock, s_rng, s_rtc_clock);
|
||||
static mesh::ObserverMesh observer_mesh(lora_radio, s_ms_clock, s_rtc_clock);
|
||||
static RepeaterDataStore data_store;
|
||||
|
||||
/* ========== main() ========== */
|
||||
|
||||
@@ -177,7 +177,8 @@ STM32WL caveats — different from every other ZephCore platform:
|
||||
AES tables live in ROM (`MBEDTLS_AES_ROM_TABLES`) to reclaim ~8KB SRAM.
|
||||
- **TRNG only (no HW CSPRNG):** the STM32 TRNG is enabled as the entropy source
|
||||
and `CSPRNG_ENABLED` auto-resolves on top; `ZephyrRNG` further conditions
|
||||
identity seeds with jitter + AES-CTR.
|
||||
identity seeds with AES-CTR (the timing stages are skipped — SysTick has no
|
||||
independent slow clock — so the TRNG-fed CSPRNG stages carry the seed).
|
||||
- **No MCUboot / UF2:** single app partition at flash origin + a LittleFS volume
|
||||
(see `board.overlay`). Flash over SWD/ST-Link with `west flash` (OpenOCD).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user