Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ports/stm32/boards/Passport/board_init.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@
#include "camera-ovm7690.h"
#include "frequency.h"
#include "gpio.h"
#include "pprng.h"
#include "se.h"

extern void __attribute__((noreturn)) __fatal_error(const char* msg);

void rng_fatal_error(void) {
__fatal_error("Entropy source failure");

@badicsalex badicsalex Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just freezes the screen on WFI and doesn't actually display anything. I'm not sure that's friendly.

A straight reboot is probably better. (trying to display something would be a much larger change)

}

#ifndef PASSPORT_DEBUG_STACK
#define PASSPORT_DEBUG_STACK 0
#endif
Expand All @@ -34,6 +41,7 @@ void Passport_board_init(void) {

gpio_init();
frequency_turbo(true);
rng_setup();
display_init(false);
camera_init();
adc_init();
Expand Down
20 changes: 18 additions & 2 deletions ports/stm32/boards/Passport/bootloader/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -574,8 +574,24 @@ static void microsd_firmware_recovery(void) {

void random_boot_delay() {
// Random delay to make cold-boot stepping attacks harder: 0 - 50ms
uint32_t ms_to_delay = rng_sample() % 50;
delay_ms(ms_to_delay);
uint32_t random_delay = 0;
(void)rng_try_sample(&random_delay);
delay_ms(random_delay % 50);
}

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() {
Expand Down
102 changes: 75 additions & 27 deletions ports/stm32/boards/Passport/common/pprng.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,67 +8,115 @@
* (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;
#define RNG_TIMEOUT_MS 10U

static bool rng_cycle_counter_setup(void) {
if (DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) {
return true;
}

// Enable the RNG clock
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->LAR = 0xc5acce55;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

return (DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0;
}

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
// Start each image from a known peripheral state. Clearing the latched
// interrupt flags and restarting the generator is the recovery sequence
// recommended by ST after a seed error. A persistent current error is

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documented sequence is clear-SEIS then discard 12 words. Either do the discard part or drop the claim.

// still caught by rng_try_sample() below and fails closed.
RNG->SR &= ~(RNG_SR_SEIS | RNG_SR_CEIS);
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();

// 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) {
bool rng_try_sample(uint32_t* result) {
static uint32_t last_rng_result;
static bool have_last_rng_result;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed, just initialize last_rng_result to 0. The first value shouldn't be 0 anyway.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the diff drops the previous ==0 checks that Linux also does.


if (result == NULL) {
return false;
}
if (!rng_cycle_counter_setup()) {
return false;
}

const uint32_t error_mask = RNG_SR_SECS | RNG_SR_CECS | RNG_SR_SEIS | RNG_SR_CEIS;
const uint32_t timeout_cycles = (SystemCoreClock / 1000U) * RNG_TIMEOUT_MS;
const uint32_t start_cycle = DWT->CYCCNT;

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.
while ((DWT->CYCCNT - start_cycle) < timeout_cycles) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: I'd just use a fixed cycle count for simplicity. The internals of the new rng_cycle_counter_setup function is very arcane, and the registers might not even be available in production units.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or use HAL_GetTick()

// Check both current error status and latched error flags. A flagged
// sample is a hard failure; callers must not silently degrade.
uint32_t status = RNG->SR;
if (status & error_mask) {
return false;

@badicsalex badicsalex Aug 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: The error bits are latching, and it's been reported to happen spuriously. There is a documented recovery procedure from it, and it might be worth implementing for stability (on SEIS/SECS, clear SEIS, discard 12 words from RNG_DR, re-check; retry up to 3x before failing)

Also Linux and ST code considers CECS/CEIS to be non-fatal, they just clear it and continue.

}

if (!(status & RNG_SR_DRDY)) {
continue;
}

// Get the new number
uint32_t rv = RNG->DR;

if (rv != last_rng_result && rv) {
// Catch an error that arrived between the status check and the data
// read. The value must not be used in that case.
if (RNG->SR & error_mask) {
return false;
}

// Continuous test: never return the same value twice in succession.
if (!have_last_rng_result || rv != last_rng_result) {
last_rng_result = rv;
have_last_rng_result = true;
*result = rv;

return rv;
return true;
}

// keep trying if not a new number
// A duplicate may be transient. Keep trying within the same bounded
// interval; a stuck source will time out 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;
Expand Down
5 changes: 3 additions & 2 deletions ports/stm32/boards/Passport/dispatch.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ int se_dispatch(
// printf("se_dispatch() method_num=%d\n", method_num);

// Random small delay to make cold-boot stepping attacks harder: 0 - 10,000us
uint32_t us_to_delay = rng_sample() % 10000;
delay_us(us_to_delay);
uint32_t us_to_delay = 0;
(void)rng_try_sample(&us_to_delay);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This change, and it's friend in bootloader/main.c is not really needed IMO. If the rng has an issue, the device will die microseconds after this by running the actual rng_sample. In fact, this lets an attacker glitch the clock and disable the boot stepping mitigation for free.

delay_us(us_to_delay % 10000);

switch (method_num) {
case CMD_IS_BRICKED:
Expand Down
3 changes: 3 additions & 0 deletions ports/stm32/boards/Passport/include/pprng.h
Original file line number Diff line number Diff line change
Expand Up @@ -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));
7 changes: 4 additions & 3 deletions ports/stm32/boards/Passport/modpassport-noise.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "adc.h"
#include "noise.h"
#include "pprng.h"
#include "stm32h7xx_hal.h"

/// package: passport
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -67,7 +68,7 @@ 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;
Expand Down Expand Up @@ -99,4 +100,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,
};
};
4 changes: 3 additions & 1 deletion ports/stm32/boards/Passport/modules/tasks/new_seed_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

async def new_seed_task(on_done, seed_length):
seed = bytearray(32)
common.noise.random_bytes(seed, common.noise.ALL)
if not common.noise.random_bytes(seed, common.noise.ALL):
await on_done(None, 'Unable to collect entropy for the new seed.')
return

# Hash to mitigate any potential bias in RNG sources
seed = trezorcrypto.sha256(seed).digest()
Expand Down
8 changes: 4 additions & 4 deletions ports/stm32/boards/Passport/noise.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions ports/stm32/rng.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,20 @@
#include "rtc.h"
#include "rng.h"

#if defined(MICROPY_PASSPORT)
#include "pprng.h"
#endif

#if MICROPY_HW_ENABLE_RNG

#define RNG_TIMEOUT_MS (10)

uint32_t rng_get(void) {
#if defined(MICROPY_PASSPORT)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: this plumbing has port code including a board header (pprng.h), which is backwards from everything else here. boardctrl.h already has the convention for this, and boards/Passport/mpconfigboard.h already uses two of those hooks. Same shape works:

/* mpconfigboard.h */
#define MICROPY_BOARD_RNG_GET rng_sample
uint32_t rng_sample(void);

/* rng.c, instead of this #if */
#ifdef MICROPY_BOARD_RNG_GET
return MICROPY_BOARD_RNG_GET();
#else
same as now
#endif

Same behaviour.

// Keep pyb.rng(), os.urandom(), and MicroPython's initial PRNG seed on the
// same status-checked hardware path as Passport's cryptographic consumers.
return rng_sample();
#else
// Enable the RNG peripheral if it's not already enabled
if (!(RNG->CR & RNG_CR_RNGEN)) {
#if defined(STM32H7)
Expand All @@ -53,6 +62,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.
Expand Down