mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 20:09:17 +00:00
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.
This commit is contained in:
@@ -101,3 +101,5 @@ PSRAM_HANDOFF.md
|
||||
BLE_AUDIT_INDEX.md
|
||||
POWER_AUDIT_HANDOFF.md
|
||||
POWER_AUDIT_INDEX.md
|
||||
CRYPTO_AUDIT_HANDOFF.md
|
||||
CRYPTO_AUDIT_INDEX.md
|
||||
|
||||
@@ -3,18 +3,268 @@
|
||||
*/
|
||||
|
||||
#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)
|
||||
{
|
||||
int ret = sys_csrand_get(dest, sz);
|
||||
if (ret != 0) {
|
||||
printk("ZephyrRNG: CSPRNG failed (%d), using PRNG fallback\n", ret);
|
||||
sys_rand_get(dest, 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 */
|
||||
|
||||
@@ -6,12 +6,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <mesh/RNG.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
class ZephyrRNG : public RNG {
|
||||
public:
|
||||
void random(uint8_t *dest, size_t sz) override;
|
||||
|
||||
/* 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
|
||||
* 3. Optional caller-supplied data — e.g. ADC LSB 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
|
||||
* Conditioned via SHA-512. NIST-style health checks on jitter
|
||||
* samples; reboots on degenerate output.
|
||||
* Blocks for ~250-300ms — only called at first-boot identity gen.
|
||||
*
|
||||
* Output is suitable as an Ed25519 seed regardless of platform
|
||||
* TRNG state at boot. */
|
||||
static void mixIdentitySeed(uint8_t *out, size_t out_len,
|
||||
const uint8_t *extra = nullptr,
|
||||
size_t extra_len = 0);
|
||||
};
|
||||
|
||||
/* Thin wrapper that returns pre-supplied bytes from the mesh::RNG
|
||||
* interface. Used to feed a mixed seed into mesh::LocalIdentity(RNG*)
|
||||
* without changing the LocalIdentity API. One-shot — bytes are
|
||||
* consumed sequentially across calls. */
|
||||
class SeededRNG : public RNG {
|
||||
public:
|
||||
SeededRNG(const uint8_t *seed_data, size_t len)
|
||||
: _data(seed_data), _avail(len) {}
|
||||
|
||||
void random(uint8_t *dest, size_t sz) override {
|
||||
size_t n = (sz <= _avail) ? sz : _avail;
|
||||
if (n > 0) memcpy(dest, _data, n);
|
||||
_data += n;
|
||||
_avail -= n;
|
||||
if (n < sz) memset(dest + n, 0, sz - n);
|
||||
}
|
||||
|
||||
private:
|
||||
const uint8_t *_data;
|
||||
size_t _avail;
|
||||
};
|
||||
|
||||
} /* namespace mesh */
|
||||
|
||||
@@ -470,7 +470,9 @@ bool BLECodeScreen::handleInput(char c)
|
||||
}
|
||||
|
||||
if (c == KEY_ENTER_LONG) {
|
||||
uint32_t pin = 100000UL + (sys_rand32_get() % 900000UL);
|
||||
uint32_t r;
|
||||
sys_csrand_get(&r, sizeof(r));
|
||||
uint32_t pin = 100000UL + (r % 900000UL);
|
||||
NodePrefs *prefs = _task->getPrefs();
|
||||
if (prefs) { prefs->ble_pin = pin; }
|
||||
snprintf(_pin_buf, sizeof(_pin_buf), "%06lu", (unsigned long)pin);
|
||||
|
||||
@@ -22,6 +22,7 @@ LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
|
||||
#include <adapters/clock/ZephyrRTCClock.h>
|
||||
#include <zephyr/bluetooth/bluetooth.h>
|
||||
#include <zephyr/drivers/hwinfo.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
#include <ZephyrSensorManager.h>
|
||||
#include <helpers/time_sync.h>
|
||||
#include "ui_task.h"
|
||||
@@ -55,7 +56,6 @@ LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
|
||||
* This prints the file/line and reboots so we can actually see what happened.
|
||||
*/
|
||||
#if IS_ENABLED(CONFIG_BT_CTLR_ASSERT_HANDLER)
|
||||
#include <zephyr/sys/reboot.h>
|
||||
extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line)
|
||||
{
|
||||
LOG_ERR("!!! BLE CONTROLLER ASSERT: %s:%u !!!", file ? file : "?", line);
|
||||
@@ -78,6 +78,12 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line)
|
||||
#define MESH_EVENT_GPS_ACTION BIT(5) /* GPS state change (must run on main thread!) */
|
||||
#define MESH_EVENT_TX_DRAIN BIT(6) /* Outbound packet delay expired, run checkSend */
|
||||
#define MESH_EVENT_PREFS_DIRTY BIT(8) /* Prefs mutated off-main; main flushes to flash */
|
||||
|
||||
#ifdef ZEPHCORE_LORA
|
||||
/* Forward decl — data_store + companion_mesh_ptr statics are defined further
|
||||
* down in the file, so mesh_event_loop() can't reference them directly. */
|
||||
static void save_prefs_to_flash(void);
|
||||
#endif
|
||||
#define MESH_EVENT_BASE (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | \
|
||||
MESH_EVENT_BLE_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_UI_ACTION | \
|
||||
MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_PREFS_DIRTY)
|
||||
@@ -354,8 +360,8 @@ static void mesh_event_loop(void)
|
||||
* to flash here so the synchronous LittleFS write doesn't block
|
||||
* the originating thread. Multiple posts coalesce into one
|
||||
* write of the latest _prefs values — desired behaviour. */
|
||||
if (events & MESH_EVENT_PREFS_DIRTY) {
|
||||
data_store.savePrefs(companion_mesh.prefs);
|
||||
if ((events & MESH_EVENT_PREFS_DIRTY) && companion_mesh_ptr) {
|
||||
save_prefs_to_flash();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -451,6 +457,14 @@ static mesh::SimpleMeshTables mesh_tables;
|
||||
static mesh::StaticPoolPacketManager packet_mgr;
|
||||
static CompanionMesh companion_mesh(lora_radio, ms_clock, zephyr_rng, rtc_clock,
|
||||
packet_mgr, mesh_tables, data_store);
|
||||
|
||||
/* Defined here (after data_store + companion_mesh_ptr statics) and
|
||||
* forward-declared near the top of the file so mesh_event_loop() can
|
||||
* call it without the static decls being in scope. */
|
||||
static void save_prefs_to_flash(void)
|
||||
{
|
||||
data_store.savePrefs(companion_mesh_ptr->prefs);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* GPS enable callback - logs state changes
|
||||
@@ -646,17 +660,51 @@ int main(void)
|
||||
LOG_INF("Added default Public channel");
|
||||
}
|
||||
|
||||
/* Load or generate identity */
|
||||
/* Load or generate identity.
|
||||
*
|
||||
* First-boot keygen uses ZephyrRNG::mixIdentitySeed() — a layered
|
||||
* entropy mixer combining sys_csrand_get + HWINFO unique ID + ADC
|
||||
* LSB noise + CPU cycle-counter jitter, conditioned via SHA-512.
|
||||
* This compensates for ESP32's hardware TRNG being only seeded
|
||||
* after WiFi/BT radio init (identity gen happens before bt_enable). */
|
||||
mesh::LocalIdentity self_identity;
|
||||
if (!data_store.loadMainIdentity(self_identity)) {
|
||||
self_identity = mesh::LocalIdentity(&zephyr_rng);
|
||||
/* Ensure pub_key[0] is not reserved (0x00 or 0xFF in MeshCore protocol) */
|
||||
int count = 0;
|
||||
while (count < 10 && (self_identity.pub_key[0] == 0x00 || self_identity.pub_key[0] == 0xFF)) {
|
||||
self_identity = mesh::LocalIdentity(&zephyr_rng);
|
||||
count++;
|
||||
/* Sample ADC LSB noise — best-effort independent physical
|
||||
* source. Boards without battery ADC return 0; jitter remains
|
||||
* the primary entropy source either way. */
|
||||
uint8_t adc_noise[32] = {0};
|
||||
for (size_t i = 0; i < sizeof(adc_noise); i++) {
|
||||
adc_noise[i] = (uint8_t)zephyr_board.getBattMilliVolts();
|
||||
k_msleep(1);
|
||||
}
|
||||
|
||||
uint8_t seed[32];
|
||||
mesh::ZephyrRNG::mixIdentitySeed(seed, sizeof(seed),
|
||||
adc_noise, sizeof(adc_noise));
|
||||
{
|
||||
mesh::SeededRNG seed_rng(seed, sizeof(seed));
|
||||
self_identity = mesh::LocalIdentity(&seed_rng);
|
||||
}
|
||||
|
||||
/* Ensure pub_key[0] is not reserved (0x00 or 0xFF in MeshCore protocol).
|
||||
* With a properly mixed seed this almost never triggers; the
|
||||
* cap+reboot is a safety net against pathological entropy failure. */
|
||||
int attempt = 0;
|
||||
while (self_identity.pub_key[0] == 0x00 || self_identity.pub_key[0] == 0xFF) {
|
||||
if (++attempt > 100) {
|
||||
LOG_ERR("Identity gen stuck on reserved prefix; rebooting");
|
||||
k_msleep(2000);
|
||||
sys_reboot(SYS_REBOOT_COLD);
|
||||
}
|
||||
mesh::ZephyrRNG::mixIdentitySeed(seed, sizeof(seed));
|
||||
mesh::SeededRNG retry_rng(seed, sizeof(seed));
|
||||
self_identity = mesh::LocalIdentity(&retry_rng);
|
||||
}
|
||||
|
||||
data_store.saveMainIdentity(self_identity);
|
||||
|
||||
memset(seed, 0, sizeof(seed));
|
||||
memset(adc_noise, 0, sizeof(adc_noise));
|
||||
}
|
||||
companion_mesh.self_id = self_identity;
|
||||
|
||||
|
||||
@@ -454,19 +454,55 @@ int main(void)
|
||||
lora_radio.setTxDoneCallback(lora_tx_done_callback, nullptr);
|
||||
repeater_mesh.setTxQueuedCallback(tx_queued_callback, nullptr);
|
||||
|
||||
/* Load or generate identity BEFORE begin() */
|
||||
/* Load or generate identity BEFORE begin().
|
||||
*
|
||||
* First-boot keygen uses ZephyrRNG::mixIdentitySeed() — a layered
|
||||
* entropy mixer combining sys_csrand_get + HWINFO unique ID + ADC
|
||||
* LSB noise + CPU cycle-counter jitter, conditioned via SHA-512.
|
||||
* This compensates for ESP32's hardware TRNG being only seeded
|
||||
* after WiFi/BT radio init (which on a repeater is on-demand for
|
||||
* WiFi OTA — there's no guaranteed radio activity at boot). */
|
||||
mesh::LocalIdentity self_identity;
|
||||
if (!data_store.loadIdentity(self_identity)) {
|
||||
LOG_INF("No identity found, generating new keypair...");
|
||||
self_identity = mesh::LocalIdentity(&zephyr_rng);
|
||||
/* Ensure pub_key[0] is not reserved (0x00 or 0xFF) */
|
||||
int count = 0;
|
||||
while (count < 10 && (self_identity.pub_key[0] == 0x00 || self_identity.pub_key[0] == 0xFF)) {
|
||||
self_identity = mesh::LocalIdentity(&zephyr_rng);
|
||||
count++;
|
||||
|
||||
/* Sample ADC LSB noise — best-effort independent physical
|
||||
* source. Boards without battery ADC return 0; jitter remains
|
||||
* the primary entropy source either way. */
|
||||
uint8_t adc_noise[32] = {0};
|
||||
for (size_t i = 0; i < sizeof(adc_noise); i++) {
|
||||
adc_noise[i] = (uint8_t)zephyr_board.getBattMilliVolts();
|
||||
k_msleep(1);
|
||||
}
|
||||
|
||||
uint8_t seed[32];
|
||||
mesh::ZephyrRNG::mixIdentitySeed(seed, sizeof(seed),
|
||||
adc_noise, sizeof(adc_noise));
|
||||
{
|
||||
mesh::SeededRNG seed_rng(seed, sizeof(seed));
|
||||
self_identity = mesh::LocalIdentity(&seed_rng);
|
||||
}
|
||||
|
||||
/* Ensure pub_key[0] is not reserved (0x00 or 0xFF in MeshCore protocol).
|
||||
* With a properly mixed seed this almost never triggers; the
|
||||
* cap+reboot is a safety net against pathological entropy failure. */
|
||||
int attempt = 0;
|
||||
while (self_identity.pub_key[0] == 0x00 || self_identity.pub_key[0] == 0xFF) {
|
||||
if (++attempt > 100) {
|
||||
LOG_ERR("Identity gen stuck on reserved prefix; rebooting");
|
||||
k_msleep(2000);
|
||||
sys_reboot(SYS_REBOOT_COLD);
|
||||
}
|
||||
mesh::ZephyrRNG::mixIdentitySeed(seed, sizeof(seed));
|
||||
mesh::SeededRNG retry_rng(seed, sizeof(seed));
|
||||
self_identity = mesh::LocalIdentity(&retry_rng);
|
||||
}
|
||||
|
||||
data_store.saveIdentity(self_identity);
|
||||
LOG_INF("New identity saved");
|
||||
|
||||
memset(seed, 0, sizeof(seed));
|
||||
memset(adc_noise, 0, sizeof(adc_noise));
|
||||
}
|
||||
repeater_mesh.self_id = self_identity;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user