mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 16:53:42 +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:
@@ -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