feat(core): detect PRNG presence in hw builds
What changed, and why it matters
This commit adds a new automated check that scans Trezor hardware wallet firmware binaries for leftover 'insecure PRNG' test markers. These markers indicate places where the real random-number generator was replaced with a predictable mock for testing. The check ensures these markers are present in emulator builds (used for testing) but absent from real production hardware builds, where they could weaken security. It is a defensive build-pipeline hardening change, not a fix for an active vulnerability in shipped code.
Treat this as a positive hardening control. Review whether the marker strings are documented in the secure-development guide, ensure the check cannot be trivially bypassed, and confirm that any prior release binaries were scanned retroactively for these markers. No immediate patch is required because the change adds detection rather than fixing a runtime bug.
Security signals we found
New build-time scanner for insecure PRNG mock markers
CI now fails if production hardware binaries contain PRNG mock markers
Emulator builds are checked to confirm expected PRNG markers remain present
Multiple hardware models have model-specific marker expectations
No changelog entry despite security-relevant tooling addition
Evidence from the diff
The commit introduces tools/check-insecure-prng.py, a Python script that searches firmware binaries for byte-string markers such as
Changed components
tools/check-insecure-prng.py.github/workflows/core.ymlbuild-docker.shcore build pipeline for boardloader, bootloader, prodtest, firmwareInspect captured patch +99 / −7
### .github/workflows/core.yml
@@ -85,15 +85,19 @@ jobs:
with:
submodules: recursive
- uses: ./.github/actions/environment
- - run: nix-shell --run "uv run make -C core build_boardloader"
- if: matrix.coins == 'universal' && matrix.type != 'debuglink'
- - run: nix-shell --run "uv run make -C core build_bootloader"
- if: matrix.coins == 'universal' && matrix.type != 'debuglink'
+ - if: matrix.coins == 'universal' && matrix.type != 'debuglink'
+ run: |
+ nix-shell --run "uv run make -C core build_boardloader"
+ nix-shell --run "uv run ./tools/check-insecure-prng.py --absent core/build-xtask/artifacts/latest/boardloader.bin"
+ nix-shell --run "uv run make -C core build_bootloader"
+ nix-shell --run "uv run ./tools/check-insecure-prng.py --absent core/build-xtask/artifacts/latest/bootloader.bin"
+ nix-shell --run "uv run make -C core build_prodtest"
+ nix-shell --run "uv run ./tools/check-insecure-prng.py --absent core/build-xtask/artifacts/latest/prodtest.bin"
- run: nix-shell --run "uv run make -C core build_bootloader_ci"
if: matrix.coins == 'universal' && matrix.type != 'debuglink' && matrix.model == 'T2T1'
- - run: nix-shell --run "uv run make -C core build_prodtest"
- if: matrix.coins == 'universal' && matrix.type != 'debuglink'
- - run: nix-shell --run "uv run make -C core build_firmware"
+ - run: |
+ nix-shell --run "uv run make -C core build_firmware"
+ nix-shell --run "uv run ./tools/check-insecure-prng.py --absent core/build-xtask/artifacts/latest/firmware.bin"
- run: nix-shell --run "uv run ./tools/print-rust-stack-sizes.py | sort -k1 -n | tail -n 50"
# - run: nix-shell --run "uv run ./tools/print-rust-type-sizes.sh core/build/firmware/rust-type-sizes.log" | awk '$1 >= 1000' | sort -k1 -n
- run: nix-shell --run "uv run ./tools/check-bitcoin-only core/build-xtask/artifacts/latest/firmware.bin"
@@ -144,6 +148,7 @@ jobs:
- run: nix-shell --run "uv run make -C core build_bootloader_emu"
if: matrix.coins == 'universal' && matrix.asan == 'noasan'
- run: nix-shell --run "uv run make -C core build_unix_frozen"
+ - run: nix-shell --run "uv run ./tools/check-insecure-prng.py --present --model $TREZOR_MODEL core/build-xtask/artifacts/latest/firmware-emu"
- run: nix-shell --run "uv run make -C core test_emu_sanity"
- run: cp core/build-xtask/artifacts/latest/firmware-emu core/build-xtask/artifacts/latest/firmware-emu-${{ matrix.model }}-${{ matrix.coins }}${{ matrix.n1w1 && '-n1w1' || '' }}
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # actions/upload-artifact@v7.0.0
### build-docker.sh
@@ -353,6 +353,9 @@ for TREZOR_MODEL in ${MODELS[@]}; do
$GIT_CLEAN_REPO
rm -rf /build/*
uv run make clean vendor $MAKE_TARGETS QUIET_MODE=1
+ for binary in build-xtask/artifacts/$TREZOR_MODEL/*.bin; do
+ uv run ../tools/check-insecure-prng.py --absent "\$binary"
+ done
for item in bootloader secmon kernel firmware prodtest; do
# Append the labeled fingerprint, preceded by '# <artifact name>'.
if [ "\$item" != kernel ] && [ -s build-xtask/artifacts/$TREZOR_MODEL/\$item.bin ]; then
### tools/check-insecure-prng.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+"""Check a binary for insecure PRNG mock markers."""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Iterable
+
+import click
+
+MCU_MARKER = b"<PRNG-MCU>"
+OPTIGA_MARKER = b"<PRNG-Optiga>"
+TROPIC_MARKER = b"<PRNG-Tropic>"
+
+MODEL_MARKERS = {
+ "T2T1": (MCU_MARKER,),
+ "T2B1": (MCU_MARKER, OPTIGA_MARKER),
+ "T3B1": (MCU_MARKER, OPTIGA_MARKER),
+ "T3T1": (MCU_MARKER, OPTIGA_MARKER),
+ "T3W1": (MCU_MARKER, OPTIGA_MARKER, TROPIC_MARKER),
+}
+
+
+def find_markers(path: Path, markers: Iterable[bytes]) -> set[bytes]:
+ data = path.read_bytes()
+ return {marker for marker in markers if marker in data}
+
+
+def format_markers(markers: Iterable[bytes]) -> str:
+ return ", ".join(marker.decode("ascii") for marker in sorted(markers))
+
+
+@click.command()
+@click.option(
+ "--present",
+ is_flag=True,
+ help="Require the PRNG markers expected for the selected model.",
+)
+@click.option(
+ "--absent",
+ is_flag=True,
+ help="Require that no insecure PRNG marker is present.",
+)
+@click.option(
+ "-m",
+ "--model",
+ type=click.Choice(sorted(MODEL_MARKERS)),
+ help="Firmware model, required with --present.",
+)
+@click.argument(
+ "filename",
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
+)
+def main(present: bool, absent: bool, model: str | None, filename: Path) -> None:
+ if present == absent:
+ raise click.UsageError("provide exactly one of --present or --absent")
+ if present:
+ if model is None:
+ raise click.UsageError("--model is required with --present")
+ markers = MODEL_MARKERS[model]
+ else:
+ markers = (b"<PRNG-",)
+
+ found = find_markers(filename, markers)
+
+ if present:
+ missing = set(markers) - found
+ if missing:
+ formatted = format_markers(missing)
+ click.echo(
+ f"{filename}: missing insecure PRNG marker(s): {formatted}",
+ err=True,
+ )
+ raise click.exceptions.Exit(1)
+ elif found:
+ formatted = format_markers(found)
+ click.echo(
+ f"{filename}: contains insecure PRNG marker(s): {formatted}", err=True
+ )
+ raise click.exceptions.Exit(1)
+
+
+if __name__ == "__main__":
+ main()Why this scored 59/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.