mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 20:38:19 +00:00
525 lines
20 KiB
C++
525 lines
20 KiB
C++
/*
|
||
* SPDX-License-Identifier: MIT
|
||
*/
|
||
|
||
#include "ZephyrRNG.h"
|
||
#include <zephyr/kernel.h>
|
||
#include <zephyr/random/random.h>
|
||
#include <zephyr/sys/reboot.h>
|
||
#include <zephyr/sys/printk.h>
|
||
#include <zephyr/drivers/hwinfo.h>
|
||
#include <psa/crypto.h>
|
||
#include <string.h>
|
||
#include <mesh/Utils.h>
|
||
|
||
#if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32)
|
||
/* 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>
|
||
#endif
|
||
|
||
BUILD_ASSERT(IS_ENABLED(CONFIG_CSPRNG_ENABLED),
|
||
"ZephyrRNG requires CONFIG_CSPRNG_ENABLED for cryptographic key derivation");
|
||
|
||
namespace mesh {
|
||
|
||
void ZephyrRNG::random(uint8_t *dest, size_t sz)
|
||
{
|
||
/* Retry handles transient TRNG-warmup races; cold-reboot on persistent
|
||
* failure. Fabricating entropy here would silently produce weak keys
|
||
* forever (cf. Debian-OpenSSL 2008). k_msleep is illegal from ISR —
|
||
* all current callers run on main thread or syswq. */
|
||
for (int attempt = 0; attempt < 4; attempt++) {
|
||
if (sys_csrand_get(dest, sz) == 0) return;
|
||
k_msleep(10);
|
||
}
|
||
Utils::cryptoPanicReboot("CSPRNG unavailable after retries");
|
||
}
|
||
|
||
/* ===== Jitter sampling + online 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.
|
||
*
|
||
* 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.
|
||
*
|
||
* 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. */
|
||
|
||
/* 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. */
|
||
#define JITTER_HIST_SLOTS 16
|
||
|
||
struct jitter_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. 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. */
|
||
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;
|
||
|
||
/* 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);
|
||
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;
|
||
}
|
||
|
||
/* 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
|
||
* failure without meaningfully describing the bytes themselves. */
|
||
static int distinct_bytes(const uint8_t *buf, size_t len)
|
||
{
|
||
bool seen[256] = {false};
|
||
int n = 0;
|
||
for (size_t i = 0; i < len; i++) {
|
||
if (!seen[buf[i]]) { seen[buf[i]] = true; n++; }
|
||
}
|
||
return n;
|
||
}
|
||
|
||
/* One line per jitter 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)
|
||
{
|
||
printk("[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) {
|
||
printk("[RNG] entropy est: n/a (no samples)\n");
|
||
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);
|
||
|
||
printk("[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]" : "");
|
||
}
|
||
|
||
/* ===== Entropy extraction via AES-256-CTR ================================
|
||
*
|
||
* Per crypto consultant (MeshCore upstream PR#2280 author): the
|
||
* conditioning step is most correctly an XOF or stream cipher, not a
|
||
* truncated hash. For our 32-byte Ed25519-seed output the difference
|
||
* is design hygiene rather than security, but the cost is the same
|
||
* order of magnitude (~one SHA-512 vs SHA-256 + two AES-ECB blocks).
|
||
*
|
||
* Construction (NIST SP 800-108 KDF-in-Counter-Mode style):
|
||
* 1. Extract: SHA-256(pool) → 32-byte AES-256 key.
|
||
* 2. Expand: AES-256-ECB(counter_i) for counter_i = 0, 1, 2 ...
|
||
* output = concatenation of ciphertext blocks.
|
||
* Plaintext-XOR (true CTR mode) is omitted because plaintext would be
|
||
* all-zero — we want just the keystream.
|
||
*
|
||
* Uses PSA crypto API (already enabled via PSA_WANT_KEY_TYPE_AES +
|
||
* PSA_WANT_ALG_ECB_NO_PADDING in zephcore_common.conf).
|
||
*/
|
||
static int extract_via_aes_ctr(const uint8_t *pool, size_t pool_len,
|
||
uint8_t *out, size_t out_len)
|
||
{
|
||
psa_status_t status;
|
||
uint8_t key[32];
|
||
size_t key_len = 0;
|
||
|
||
/* PSA is idempotent — already initialized via mbedTLS but a defensive
|
||
* call here costs nothing if it returns PSA_ERROR_ALREADY_EXISTS. */
|
||
(void)psa_crypto_init();
|
||
|
||
/* Extract: SHA-256(pool) → AES key. Open-coded here (NOT Utils::sha256)
|
||
* on purpose: that wrapper returns void and silently zeroes its output on
|
||
* PSA failure. A zeroed key imports fine and AES-ECB(key=0) derives a
|
||
* fixed, device-independent seed that the all-zero/all-FF degenerate check
|
||
* cannot catch — every affected unit would share one Ed25519 identity. We
|
||
* must hard-fail so the caller (mixIdentitySeed) cryptoPanicReboots. */
|
||
status = psa_hash_compute(PSA_ALG_SHA_256, pool, pool_len,
|
||
key, sizeof(key), &key_len);
|
||
if (status != PSA_SUCCESS || key_len != sizeof(key)) {
|
||
Utils::secureZeroize(key, sizeof(key));
|
||
return -1;
|
||
}
|
||
|
||
/* Import key for AES-256-ECB */
|
||
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
|
||
psa_set_key_type(&attr, PSA_KEY_TYPE_AES);
|
||
psa_set_key_algorithm(&attr, PSA_ALG_ECB_NO_PADDING);
|
||
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_ENCRYPT);
|
||
psa_set_key_bits(&attr, 256);
|
||
|
||
psa_key_id_t key_id = 0;
|
||
status = psa_import_key(&attr, key, sizeof(key), &key_id);
|
||
/* Wipe stack-resident AES key — secureZeroize survives -Os DSE. */
|
||
Utils::secureZeroize(key, sizeof(key));
|
||
if (status != PSA_SUCCESS) {
|
||
return -1;
|
||
}
|
||
|
||
/* Expand: AES-ECB(counter_i) for i = 0, 1, ... */
|
||
uint8_t counter[16] = {0};
|
||
size_t pos = 0;
|
||
int ret = 0;
|
||
while (pos < out_len) {
|
||
uint8_t block[16];
|
||
size_t block_out = 0;
|
||
status = psa_cipher_encrypt(key_id, PSA_ALG_ECB_NO_PADDING,
|
||
counter, sizeof(counter),
|
||
block, sizeof(block), &block_out);
|
||
if (status != PSA_SUCCESS || block_out != sizeof(block)) {
|
||
ret = -1;
|
||
break;
|
||
}
|
||
size_t chunk = (out_len - pos < sizeof(block))
|
||
? (out_len - pos) : sizeof(block);
|
||
memcpy(out + pos, block, chunk);
|
||
pos += chunk;
|
||
|
||
/* Increment 128-bit counter, big-endian — overflow rolls over.
|
||
* For our 32-byte output we only ever hit counters 0 and 1. */
|
||
for (int i = sizeof(counter) - 1; i >= 0; i--) {
|
||
if (++counter[i] != 0) break;
|
||
}
|
||
Utils::secureZeroize(block, sizeof(block));
|
||
}
|
||
|
||
psa_destroy_key(key_id);
|
||
Utils::secureZeroize(counter, sizeof(counter));
|
||
return ret;
|
||
}
|
||
|
||
void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len,
|
||
const uint8_t *extra, size_t extra_len)
|
||
{
|
||
uint8_t pool[512];
|
||
memset(pool, 0, sizeof(pool));
|
||
|
||
/* ESP32 only: give the HWRNG a real entropy source for the duration of
|
||
* this function.
|
||
*
|
||
* WDEV_RANDOM is a PRNG that receives hardware entropy only "provided
|
||
* Wi-Fi or BT are enabled" (Zephyr drivers/entropy/entropy_esp32.c), and
|
||
* sys_csrand_get() maps straight to it. Every caller of this function
|
||
* runs before RF is up, and repeater / room-server builds never enable
|
||
* RF at all — so stages 1 and 5 below contributed NOTHING on ESP32,
|
||
* leaving CPU jitter as the only real source. That was observed failing
|
||
* its health check on ThinkNode M9 hardware while deriving a permanent
|
||
* identity key.
|
||
*
|
||
* bootloader_random_enable() puts the SAR ADC into continuous sampling
|
||
* and mixes its noise into the HWRNG; Espressif's header explicitly
|
||
* sanctions calling it from app code when RF is not up. It must be
|
||
* disabled again before anything else touches the ADC or RF — done at
|
||
* the end of the collection phase, before AES extraction, so the ADC is
|
||
* held for as short a window as possible.
|
||
*
|
||
* WARNING for future callers: this is unsafe if RF or the ADC is already
|
||
* in use. Do not call mixIdentitySeed() after bt_enable() or alongside a
|
||
* 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");
|
||
#else
|
||
printk("[RNG] === identity seed health ===\n");
|
||
printk("[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",
|
||
rc1, distinct_bytes(pool, 64));
|
||
|
||
/* Stage 2: HWINFO unique device ID — uniqueness across devices */
|
||
uint8_t devid[16] = {0};
|
||
ssize_t devid_len = hwinfo_get_device_id(devid, sizeof(devid));
|
||
for (ssize_t i = 0; i < devid_len && i < (ssize_t)sizeof(devid); i++) {
|
||
pool[64 + i] ^= devid[i];
|
||
}
|
||
/* 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",
|
||
(int)devid_len);
|
||
|
||
/* Stage 3: caller-supplied entropy (e.g. ADC LSB noise) */
|
||
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");
|
||
}
|
||
|
||
/* Stage 4: CPU cycle-counter jitter, 200ms.
|
||
*
|
||
* 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. */
|
||
#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);
|
||
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);
|
||
if (!health_ok) {
|
||
printk("[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 */
|
||
int rc5 = sys_csrand_get(pool + 368, 64);
|
||
printk("[RNG] stage5 csrand : rc=%d distinct=%d/64\n",
|
||
rc5, distinct_bytes(pool + 368, 64));
|
||
|
||
/* Stage 6: second jitter sample, independent timing 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);
|
||
#endif /* CONFIG_ARCH_POSIX */
|
||
|
||
/* Collection done — release the SAR ADC before anything else needs it.
|
||
* Unconditional: every path below this point either returns normally or
|
||
* 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");
|
||
#endif
|
||
printk("[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
|
||
* AES key via SHA-256(pool), then expands to out_len bytes via
|
||
* AES-ECB on a 128-bit counter. Per crypto consultant guidance —
|
||
* see extract_via_aes_ctr() for full rationale. */
|
||
if (extract_via_aes_ctr(pool, sizeof(pool), out, out_len) != 0) {
|
||
Utils::cryptoPanicReboot("AES-CTR seed extraction failed");
|
||
}
|
||
|
||
/* Output sanity check — reject all-zero / all-0xFF (catastrophic
|
||
* failure of every source). */
|
||
bool all_zero = true, all_ff = true;
|
||
for (size_t i = 0; i < out_len; i++) {
|
||
if (out[i] != 0x00) all_zero = false;
|
||
if (out[i] != 0xFF) all_ff = false;
|
||
}
|
||
if (all_zero || all_ff) {
|
||
Utils::cryptoPanicReboot("degenerate seed output (all-zero / all-FF)");
|
||
}
|
||
|
||
/* Wipe sensitive intermediate buffers — secureZeroize survives the
|
||
* -Os dead-store-elimination that would silently elide plain memset
|
||
* on stack locals that are never read again. */
|
||
Utils::secureZeroize(pool, sizeof(pool));
|
||
Utils::secureZeroize(devid, sizeof(devid));
|
||
}
|
||
|
||
void ZephyrRNG::generateFirstBootIdentity(LocalIdentity &out_identity)
|
||
{
|
||
uint8_t seed[32];
|
||
|
||
mixIdentitySeed(seed, sizeof(seed));
|
||
out_identity.fromSeed(seed);
|
||
|
||
/* Reserved-prefix guard — MeshCore protocol treats pub_key[0] of
|
||
* 0x00/0xFF as reserved markers. With a working CSPRNG the first
|
||
* attempt almost always passes (P(reserved) = 2/256); the cap +
|
||
* panic-reboot is a stuck-source backstop. */
|
||
int attempt = 0;
|
||
while (out_identity.pub_key[0] == 0x00 || out_identity.pub_key[0] == 0xFF) {
|
||
if (++attempt > 100) {
|
||
Utils::cryptoPanicReboot("identity gen stuck on reserved prefix");
|
||
}
|
||
mixIdentitySeed(seed, sizeof(seed));
|
||
out_identity.fromSeed(seed);
|
||
}
|
||
|
||
Utils::secureZeroize(seed, sizeof(seed));
|
||
}
|
||
|
||
} /* namespace mesh */
|