Merge pull request #655 from Foundation-Devices/sft-7320-entropy-hardening
What changed, and why it matters
This firmware update hardens the way Passport generates random numbers. Previously, a failing or stuck hardware random-number generator could silently produce weak or repeated values, which is dangerous for creating secret keys. The patch makes the device detect bad randomness and either recover or permanently stop with a clear error screen, instead of continuing with potentially guessable secrets. It also routes all MicroPython randomness through the same checked generator.
Treat this as a security-hardening fix and include it in the next firmware release. Review whether any other callers still use raw HAL RNG or bypass rng_sample, and ensure rng_fatal_error() behavior is acceptable in factory/test modes. Consider adding tests that simulate RNG fault injection to verify the fail-closed path.
Security signals we found
Fail-closed RNG error handling: persistent seed/clock errors now trigger a fatal handler instead of returning potentially weak values
ST-recommended seed-error recovery (RM0433 section 34.3.7): clear SEIS and flush 12 discard words, with bounded retry attempts
Duplicate and zero-value rejection in rng_try_sample to avoid returning stuck or invalid RNG output
Bounded polling loop replaces infinite busy-wait, preventing silent hangs and enabling failure detection
MicroPython RNG hook MICROPY_BOARD_RNG_GET redirected to checked rng_sample, broadening coverage
Bootloader displays a permanent 'Entropy source failure' fatal error when RNG fails early
Evidence from the diff
The commit replaces a minimal STM32 RNG setup with a fail-closed driver: rng_setup() enables the RNG, clears error flags, runs ST’s seed-error recovery sequence, and requires two successful samples before use. rng_try_sample() polls with a bounded loop, clears clock-error flags, recovers from seed errors, discards zero/duplicate values, and returns false on failure. Callers invoke rng_fatal_error(), which resets in the firmware or shows a fatal UI in the bootloader. The change also wires MICROPY_BOARD_RNG_GET to rng_sample so MicroPython’s rng_get() uses the checked path, and updates noise.random_bytes to call rng_fatal_error on failure rather than returning false.
Changed components
STM32 RNG peripheral driver (ports/stm32/boards/Passport/common/pprng.c)Passport board initialization (ports/stm32/boards/Passport/board_init.c)Passport bootloader (ports/stm32/boards/Passport/bootloader/main.c)MicroPython noise module (ports/stm32/boards/Passport/modpassport-noise.h, ports/stm32/boards/Passport/noise.c)Seed generation task (ports/stm32/boards/Passport/modules/tasks/new_seed_task.py)MicroPython board RNG hook (ports/stm32/boards/Passport/mpconfigboard.h, ports/stm32/rng.c)Inspect captured patch +150 / −37
### ports/stm32/boards/Passport/board_init.c
@@ -11,8 +11,14 @@
#include "camera-ovm7690.h"
#include "frequency.h"
#include "gpio.h"
+#include "pprng.h"
#include "se.h"
+void rng_fatal_error(void) {
+ // Entropy checks can fail before the display is initialized.
+ passport_reset();
+}
+
#ifndef PASSPORT_DEBUG_STACK
#define PASSPORT_DEBUG_STACK 0
#endif
@@ -34,6 +40,7 @@ void Passport_board_init(void) {
gpio_init();
frequency_turbo(true);
+ rng_setup();
display_init(false);
camera_init();
adc_init();
### ports/stm32/boards/Passport/bootloader/main.c
@@ -578,6 +578,21 @@ void random_boot_delay() {
delay_ms(ms_to_delay);
}
+void rng_fatal_error(void) {
+ // The first entropy checks run before the normal display initialization.
+ // Bring up only the UI hardware required to show a permanent fatal error.
+ display_init(true);
+ gpio_init();
+ keypad_init();
+ backlight_init();
+ backlight_intensity(100);
+ ui_show_fatal_error("Entropy source failure.");
+
+ // ui_show_fatal_error() does not return, but retain a hard fail-safe if its
+ // implementation ever changes.
+ LOCKUP_FOREVER();
+}
+
void do_verify_current_firmware() {
// Validate the internal firmware
secresult result = verify_current_firmware(true);
### ports/stm32/boards/Passport/common/pprng.c
@@ -8,67 +8,142 @@
* (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
* and is covered by GPLv3 license found in COPYING.
*/
+#include <stdbool.h>
#include <string.h>
-#include "stm32h7xx_hal_conf.h"
+#include "stm32h7xx_hal.h"
#include "delay.h"
#include "pprng.h"
#include "utils.h"
-void rng_setup(void) {
- if (RNG->CR & RNG_CR_RNGEN) {
- // already setup
- return;
+// Bound the number of polling attempts.
+// Firmware and bootloader configure a 480 MHz CPU. Target roughly 10 ms using
+// an unmeasured estimate of 10 CPU cycles per no-data poll: 480 MHz * 10 ms / 10.
+// This is not a calibrated timeout; MMIO stalls, interrupts, and the longer
+// zero/duplicate retry path affect elapsed time.
+#define RNG_MAX_POLL_ATTEMPTS 480000U
+#define RNG_MAX_RECOVERY_ATTEMPTS 3U
+
+static bool rng_recover_seed_error(uint32_t* recovery_attempts) {
+ while (*recovery_attempts < RNG_MAX_RECOVERY_ATTEMPTS) {
+ (*recovery_attempts)++;
+
+ // ST's seed-error recovery sequence (RM0433 section 34.3.7): clear
+ // SEIS and flush 12 words. These are raw discard reads; do not wait
+ // for DRDY or consume any of the values.
+ RNG->SR &= ~RNG_SR_SEIS;
+ for (unsigned int i = 0; i < 12; i++) {
+ (void)RNG->DR;
+ }
+
+ // SEIS must remain clear after flushing. If it is set again, retry
+ // recovery within the remaining budget before reporting failure.
+ if (!(RNG->SR & RNG_SR_SEIS)) {
+ return true;
+ }
}
+ return false;
+}
- // Enable the RNG clock
+void rng_setup(void) {
+ // Enable the peripheral clock even if an earlier boot stage left RNGEN set.
__HAL_RCC_RNG_CLK_ENABLE();
- // Enable the RNG
+ // Restart the generator at image startup.
+ RNG->CR &= ~RNG_CR_RNGEN;
RNG->CR |= RNG_CR_RNGEN;
- // Sample twice to be sure that we have a
- // valid RNG result.
- uint32_t chk = rng_sample();
- uint32_t chk2 = rng_sample();
+ RNG->SR &= ~RNG_SR_CEIS;
+ uint32_t recovery_attempts = 0;
+ // Persistent seed errors leave the RNG output untrustworthy. Stop if
+ // the startup recovery budget is exhausted.
+ if (!rng_recover_seed_error(&recovery_attempts)) {
+ rng_fatal_error();
+ }
- // die if we are clearly not getting random values
- if (chk == 0 || chk == ~0 || chk2 == 0 || chk2 == ~0 || chk == chk2) {
- while (1)
- ;
+ // Always sample twice, even if an earlier boot stage enabled the
+ // peripheral, so each image verifies the source before using it.
+ uint32_t sample;
+ if (!rng_try_sample(&sample) || !rng_try_sample(&sample)) {
+ rng_fatal_error();
}
}
-uint32_t rng_sample(void) {
- static uint32_t last_rng_result;
+bool rng_try_sample(uint32_t* result) {
+ static uint32_t last_rng_result = 0;
+
+ if (result == NULL) {
+ return false;
+ }
+ const uint32_t seed_error_mask = RNG_SR_SECS | RNG_SR_SEIS;
+ uint32_t recovery_attempts = 0;
+
+ for (uint32_t attempt = 0; attempt < RNG_MAX_POLL_ATTEMPTS; attempt++) {
+ uint32_t status = RNG->SR;
+ // Clock errors do not invalidate available data (RM0433 section
+ // 34.3.7). Clear CEIS; CECS clears in hardware when the clock recovers.
+ if (status & RNG_SR_CEIS) {
+ RNG->SR &= ~RNG_SR_CEIS;
+ }
+ if (status & seed_error_mask) {
+ if (!rng_recover_seed_error(&recovery_attempts)) {
+ return false;
+ }
+ continue;
+ }
- while (1) {
- // Check if data register contains valid random data
- while (!(RNG->SR & RNG_SR_DRDY)) {
- // busy wait; okay to get stuck here... better than failing.
+ if (!(status & RNG_SR_DRDY)) {
+ continue;
}
// Get the new number
uint32_t rv = RNG->DR;
- if (rv != last_rng_result && rv) {
+ // Recheck status for errors that arrived during the data read.
+ status = RNG->SR;
+ if (status & RNG_SR_CEIS) {
+ RNG->SR &= ~RNG_SR_CEIS;
+ }
+
+ // On STM32H753, zero from RNG_DR indicates invalid data and can signal
+ // a late seed error (RM0433 section 34.7.3). Discard the sample and
+ // recover on either indication, sharing the same per-call budget.
+ if (rv == 0 || (status & seed_error_mask)) {
+ if (!rng_recover_seed_error(&recovery_attempts)) {
+ return false;
+ }
+ continue;
+ }
+
+ // Never return the same value twice in succession.
+ if (rv != last_rng_result) {
last_rng_result = rv;
+ *result = rv;
- return rv;
+ return true;
}
- // keep trying if not a new number
+ // A duplicate may be transient. Keep trying within the same
+ // polling limit; a stuck source will exhaust it and fail closed.
}
- // NOT-REACHED
+ return false;
+}
+
+uint32_t rng_sample(void) {
+ uint32_t result;
+ if (!rng_try_sample(&result)) {
+ rng_fatal_error();
+ }
+ return result;
}
void rng_buffer(uint8_t* result, int len) {
while (len > 0) {
- uint32_t t = rng_sample();
+ uint32_t sample = rng_sample();
- memcpy(result, &t, MIN(4, len));
+ memcpy(result, &sample, MIN(4, len));
len -= 4;
result += 4;
### ports/stm32/boards/Passport/include/pprng.h
@@ -10,8 +10,11 @@
*/
#pragma once
+#include <stdbool.h>
#include <stdint.h>
void rng_setup(void);
+bool rng_try_sample(uint32_t* result);
uint32_t rng_sample(void);
void rng_buffer(uint8_t* result, int len);
+void rng_fatal_error(void) __attribute__((noreturn));
### ports/stm32/boards/Passport/modpassport-noise.h
@@ -6,6 +6,7 @@
#include "adc.h"
#include "noise.h"
+#include "pprng.h"
#include "stm32h7xx_hal.h"
/// package: passport
@@ -38,7 +39,7 @@ STATIC mp_obj_t mod_passport_Noise_make_new(const mp_obj_type_t* type,
/// directly as a tuple of two ints.
/// """
STATIC mp_obj_t mod_passport_Noise_read(mp_obj_t self) {
- HAL_StatusTypeDef ret = 0;
+ int ret = 0;
uint32_t noise1 = 0;
uint32_t noise2 = 0;
mp_obj_t tuple[2] = {0};
@@ -53,9 +54,10 @@ STATIC mp_obj_t mod_passport_Noise_read(mp_obj_t self) {
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_passport_Noise_read_obj, mod_passport_Noise_read);
-/// def random_bytes(self, buf: buffer, sources: int) -> (int, int):
+/// def random_bytes(self, buf: buffer, sources: int) -> None:
/// """
-/// Read random bytes from multiple noise sources.
+/// Fill buf with random bytes from the selected noise sources.
+/// Entropy failure invokes the fatal handler and does not return.
/// """
STATIC mp_obj_t mod_passport_Noise_random_bytes(mp_obj_t self,
mp_obj_t buf_obj,
@@ -67,10 +69,10 @@ STATIC mp_obj_t mod_passport_Noise_random_bytes(mp_obj_t self,
sources = mp_obj_get_int(sources_obj);
if (!noise_get_random_bytes(sources, buf_info.buf, buf_info.len)) {
- return mp_const_false;
+ rng_fatal_error();
}
- return mp_const_true;
+ return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_3(mod_passport_Noise_random_bytes_obj, mod_passport_Noise_random_bytes);
@@ -99,4 +101,4 @@ const mp_obj_type_t mod_passport_Noise_type = {
.name = MP_QSTR_Noise,
.make_new = mod_passport_Noise_make_new,
.locals_dict = (void*)&mod_passport_Noise_locals_dict,
-};
\ No newline at end of file
+};
### ports/stm32/boards/Passport/modules/tasks/new_seed_task.py
@@ -10,6 +10,7 @@
async def new_seed_task(on_done, seed_length):
seed = bytearray(32)
+ # Entropy failures invoke the fatal handler before this call can return.
common.noise.random_bytes(seed, common.noise.ALL)
# Hash to mitigate any potential bias in RNG sources
### ports/stm32/boards/Passport/mpconfigboard.h
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
+#include <stdint.h>
+
#define MICROPY_HW_BOARD_NAME "Passport"
#define MICROPY_HW_MCU_NAME "STM32H753"
@@ -49,6 +51,10 @@ void Passport_board_early_init(void);
#define MICROPY_BOARD_INIT Passport_board_init
void Passport_board_init(void);
+// Use Passport's checked RNG for MicroPython's random-number consumers.
+#define MICROPY_BOARD_RNG_GET rng_sample
+uint32_t rng_sample(void);
+
/**
* The following two macros disable interrupts preserving interrupt state
* and then properly handle getting the keypad controller to pulse the
### ports/stm32/boards/Passport/noise.c
@@ -38,10 +38,10 @@ void noise_disable() {
}
bool noise_get_random_uint16(uint16_t* result) {
- HAL_StatusTypeDef ret;
- uint32_t noise1 = 0;
- uint32_t noise2 = 0;
- uint16_t r = 0;
+ int ret;
+ uint32_t noise1 = 0;
+ uint32_t noise2 = 0;
+ uint16_t r = 0;
for (int i = 0; i < 4; i++) {
r = r << 4;
### ports/stm32/rng.c
@@ -32,6 +32,9 @@
#define RNG_TIMEOUT_MS (10)
uint32_t rng_get(void) {
+ #ifdef MICROPY_BOARD_RNG_GET
+ return MICROPY_BOARD_RNG_GET();
+ #else
// Enable the RNG peripheral if it's not already enabled
if (!(RNG->CR & RNG_CR_RNGEN)) {
#if defined(STM32H7)
@@ -53,6 +56,7 @@ uint32_t rng_get(void) {
// Get and return the new random number
return RNG->DR;
+ #endif
}
// Return a 30-bit hardware generated random number.Why this scored 55/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.