Files
ZephCore/zephcore/adapters/rng/ZephyrRNG.cpp
T
liquidraver 515f3610e1 crypto: harden first-boot identity entropy + 3 RNG fixes
Adds a layered entropy mixer for first-boot identity Ed25519 keygen,
primarily to address ESP32 where the hardware TRNG (WDEV_RND_REG) is
only fed real entropy once the internal WiFi/BT radio is enabled —
but identity gen runs before that on companion and indefinitely
before that on a bare repeater. ESP-IDF's bootloader_random_enable()
is not compiled by Zephyr-Espressif HAL, ruling out that workaround.
Design reviewed with nextgens (author of upstream meshcore-dev/
MeshCore#2280 which fixes the same issue via BT/WiFi init/pull/deinit).

ZephyrRNG::random — retry sys_csrand_get up to 4x with k_msleep
backoff; cold-reboot on persistent failure. Previously fell back
silently to sys_rand_get (xoshiro PRNG), which would have produced
a weak Ed25519 seed on CSPRNG error. BUILD_ASSERT enforces
CONFIG_CSPRNG_ENABLED.

ZephyrRNG::mixIdentitySeed — layered entropy mixer for one-shot
identity keygen. Combines sys_csrand_get (early + late),
HWINFO unique device ID, caller-supplied ADC LSB noise, 200ms of
CPU cycle-counter jitter (NIST SP 800-90B class source), and
50ms more jitter in an independent timing window. Conditioned via
AES-256-CTR (NIST SP 800-108 KDF-in-Counter-Mode): SHA-256 of the
pool extracts a 32-byte AES key; AES-256-ECB on an incrementing
128-bit counter expands to the requested output length. Uses PSA
crypto already enabled in zephcore_common.conf. NIST-style
repetition-count + variance health check on jitter samples;
reboot on degenerate output. ~280ms one-time cost at first boot.
LoRa radio TRNG was considered as an additional source but rejected
on expert advice — radio sources are attacker-influenceable
(jamming/spoofing).

ui-joystick BLE passkey — switch from sys_rand32_get (non-crypto
xoshiro) to sys_csrand_get. The 6-digit passkey is the MITM
protection the rest of the BLE config enforces; predictable PINs
weaken it.

Identity reserved-prefix loop — replace the silent 10-attempt cap
(which committed whatever it had on fall-through) with a
bounded-retry-then-reboot pattern.

Also: fix a pre-existing scope bug at main_companion.cpp:357 in
the MESH_EVENT_PREFS_DIRTY handler — data_store was referenced
inside mesh_event_loop() but declared 50+ lines later. Moved the
call into a forward-declared helper defined after the statics.
Unrelated to crypto work but uncovered during build verification;
every companion build was broken.
2026-05-28 09:34:43 +02:00

271 lines
8.9 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SPDX-License-Identifier: Apache-2.0
*/
#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>
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);
}
printk("ZephyrRNG: CSPRNG unavailable after retries — rebooting\n");
k_msleep(2000);
sys_reboot(SYS_REBOOT_COLD);
}
/* ===== Jitter sampling + 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 is the NIST SP 800-90B "repetition count" plus a
* variance check on the first 128 samples. Detects stuck-source
* catastrophic failure (e.g. cycle counter not advancing). Does not
* statistically prove entropy quality — that's what the literature is
* for. */
#define JITTER_TRACKED 128
static bool jitter_health_check(const uint32_t *deltas, size_t n)
{
if (n < 16) return false;
/* Repetition count: 32+ consecutive identical samples is failure. */
int consec = 1, max_consec = 1;
for (size_t i = 1; i < n; i++) {
if (deltas[i] == deltas[i - 1]) {
consec++;
if (consec > max_consec) max_consec = consec;
} else {
consec = 1;
}
}
if (max_consec >= 32) return false;
/* Variance: require at least 5 distinct delta values across the
* tracked window. A perfectly deterministic CPU would produce one
* value; even minimal jitter produces several. */
uint32_t distinct[8] = {0};
int n_distinct = 0;
for (size_t i = 0; i < n && n_distinct < 8; i++) {
bool found = false;
for (int j = 0; j < n_distinct; j++) {
if (distinct[j] == deltas[i]) { found = true; break; }
}
if (!found) distinct[n_distinct++] = deltas[i];
}
return n_distinct >= 5;
}
static bool sample_cpu_jitter(uint8_t *pool, size_t pool_size,
size_t pool_offset, uint32_t duration_ms)
{
uint32_t deltas[JITTER_TRACKED];
size_t tracked_idx = 0;
uint32_t accum = k_cycle_get_32();
int64_t deadline = k_uptime_get() + duration_ms;
size_t idx = pool_offset;
while (k_uptime_get() < deadline) {
uint32_t t1 = k_cycle_get_32();
/* Variable-time work — number of iterations depends on the
* accumulator, so timing depends on hardware nondeterminism
* (cache, branch prediction, ISR firing). */
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);
if (tracked_idx < JITTER_TRACKED) deltas[tracked_idx++] = delta;
}
return jitter_health_check(deltas, tracked_idx);
}
/* ===== 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 */
status = psa_hash_compute(PSA_ALG_SHA_256, pool, pool_len,
key, sizeof(key), &key_len);
if (status != PSA_SUCCESS || key_len != 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);
memset(key, 0, 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;
}
memset(block, 0, sizeof(block));
}
psa_destroy_key(key_id);
memset(counter, 0, 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));
/* Stage 1: early CSPRNG (strong on nRF/MG24, weak on ESP32 pre-radio) */
(void)sys_csrand_get(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];
}
/* 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];
}
/* Stage 4: CPU cycle-counter jitter, 200ms */
bool health_ok = sample_cpu_jitter(pool, sizeof(pool), 112, 200);
if (!health_ok) {
printk("ZephyrRNG: jitter health check failed, resampling 400ms\n");
health_ok = sample_cpu_jitter(pool, sizeof(pool), 112, 400);
if (!health_ok) {
printk("ZephyrRNG: jitter health still failing — continuing with mixed sources\n");
}
}
/* Stage 5: late CSPRNG — catches any mid-boot radio init that
* warmed the TRNG during the 200ms jitter window */
(void)sys_csrand_get(pool + 368, 64);
/* Stage 6: second jitter sample, independent timing window */
(void)sample_cpu_jitter(pool, sizeof(pool), 432, 50);
/* 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) {
printk("ZephyrRNG: AES-CTR extraction failed — rebooting\n");
k_msleep(2000);
sys_reboot(SYS_REBOOT_COLD);
}
/* Output sanity check — reject all-zero / all-0xFF (catastrophic
* failure of every source). Reboot to retry. */
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) {
printk("ZephyrRNG: degenerate seed output — rebooting\n");
k_msleep(2000);
sys_reboot(SYS_REBOOT_COLD);
}
/* Wipe sensitive intermediate buffers */
memset(pool, 0, sizeof(pool));
memset(devid, 0, sizeof(devid));
}
} /* namespace mesh */