What changed, and why it matters
This commit fixes a critical random-number bug in COLDCARD hardware wallets. The device was accidentally using a weak, software-based fallback random generator instead of its secure hardware one when creating Bitcoin seed phrases. Because the fallback generator had very little real randomness, attackers who captured a public transaction or signature could potentially figure out the wallet's private keys and steal funds. The patch forces the build to use the hardware random source and adds a build-time check to make sure the weak fallback is never linked in again. Coinkite issued an emergency hotfix and warned users to move funds from wallets created with affected firmware.
Treat this as a critical security fix. Users should upgrade to vendor-recommended firmware (v5.6.0+ / v1.5.0Q+) and, for any wallet created on affected firmware, create a new wallet with the fixed firmware and move all funds to it. Developers should ensure the rng-code-check build step passes and that upstream stm32/rng.o exports no symbols.
Security signals we found
Weak/fallback PRNG silently linked in place of hardware RNG
Seed phrase entropy reduction allowing private-key recovery
Build-system override of upstream rng.o with empty object
Post-link symbol check to prevent regression
Emergency version bump and vendor hotfix announcement
Vendor disclosure of user-impact/losses
Evidence from the diff
The patch resolves a build-time symbol-resolution failure that caused MicroPython’s fallback PRNG (Yasmarang) to be linked instead of the board-specific hardware RNG. The board rng.c files already provided pyb_rng_get() and random_buffer(), but upstream stm32/rng.c defines rng_get() and a weak pyb_rng_yasmarang() fallback. When MICROPY_HW_ENABLE_RNG was disabled, the linker could satisfy rng_get() calls with the upstream fallback, whose entropy was only a few bytes of timing jitter. The fix: (1) adds an explicit non-static rng_get() wrapper in each board’s rng.c so the upstream symbol is overridden; (2) replaces the upstream rng.o with an empty object in the Makefile for COLDCARD, MK4, and Q1; (3) adds a post-build nm-based check verifying that stm32/rng.o defines no symbols and that boards/$(BOARD)/rng.o exports a global rng_get; (4) bumps versions to 5.5.2 and 1.4.2Q. The vendor blog confirms this was an emergency hotfix for limited-entropy seed generation on Mk3 and that releases 5.6.0 / 1.5.0Q remediated it; Karma-X ties the patch to reported user losses.
Changed components
stm32/COLDCARD/rng.cstm32/COLDCARD_MK4/rng.cstm32/COLDCARD_Q1/rng.c (by shared Makefile pattern)stm32/COLDCARD/mpconfigboard.mkstm32/COLDCARD_MK4/mpconfigboard.mkstm32/COLDCARD_Q1/mpconfigboard.mkstm32/shared.mkCOLDCARD Mk3/Mk4/Q1 firmware build systemInspect captured patch +60 / −7
diff --git a/stm32/COLDCARD/mpconfigboard.mk b/stm32/COLDCARD/mpconfigboard.mk
index fa3560f..6d7e3cf 100644
--- a/stm32/COLDCARD/mpconfigboard.mk
+++ b/stm32/COLDCARD/mpconfigboard.mk
@@ -69,6 +69,13 @@ COPT += -g
# bugfix IIRC
build-COLDCARD/boards/COLDCARD/modckcc.o: COPT = -O0 -DNDEBUG
+# Do not compile MicroPython's fallback PRNG. The board-specific rng.c
+# provides rng_get(), and this empty object satisfies the upstream object list.
+$(BUILD)/rng.o: CFLAGS += -Dpyb_rng_yasmarang=error-do-not-want-this
+$(BUILD)/rng.o:
+ $(ECHO) "SKIP stm32/rng.c"
+ $(Q)$(CC) $(CFLAGS) -x c -c /dev/null -o $@
+
files:
# SRC_C: $(SRC_C)
@echo
diff --git a/stm32/COLDCARD/rng.c b/stm32/COLDCARD/rng.c
index 374cd32..00e3d20 100644
--- a/stm32/COLDCARD/rng.c
+++ b/stm32/COLDCARD/rng.c
@@ -79,6 +79,11 @@ static uint32_t rng_get_or_fault(void)
return last_value;
}
+uint32_t rng_get(void)
+{
+ return rng_get_or_fault();
+}
+
/// \function pyb_rng_get()
//
/// Return a 30-bit hardware generated random number: or fail!
@@ -149,4 +154,3 @@ random_buffer(uint8_t *p, size_t count)
last = next;
}
}
-
diff --git a/stm32/COLDCARD/rng.h b/stm32/COLDCARD/rng.h
index 28775b3..57704d8 100644
--- a/stm32/COLDCARD/rng.h
+++ b/stm32/COLDCARD/rng.h
@@ -3,6 +3,7 @@
*/
#pragma once
+uint32_t rng_get(void);
+
MP_DECLARE_CONST_FUN_OBJ_0(pyb_rng_get_obj);
MP_DECLARE_CONST_FUN_OBJ_1(pyb_rng_get_bytes_obj);
-
diff --git a/stm32/COLDCARD_MK4/mpconfigboard.h b/stm32/COLDCARD_MK4/mpconfigboard.h
index 4933d9c..b3ccb7c 100644
--- a/stm32/COLDCARD_MK4/mpconfigboard.h
+++ b/stm32/COLDCARD_MK4/mpconfigboard.h
@@ -75,6 +75,7 @@
// We have our own version of this code.
+// LATER: when zero, this selected some PRNG code we really didnt want.
#define MICROPY_HW_ENABLE_RNG (0)
extern void ckcc_early_init(void);
diff --git a/stm32/COLDCARD_MK4/mpconfigboard.mk b/stm32/COLDCARD_MK4/mpconfigboard.mk
index 07d5974..b00447e 100644
--- a/stm32/COLDCARD_MK4/mpconfigboard.mk
+++ b/stm32/COLDCARD_MK4/mpconfigboard.mk
@@ -92,7 +92,13 @@ build-COLDCARD_MK4/flashbdev.o: CFLAGS += -Dled_state=led_state_OMIT
build-COLDCARD_MK4/spibdev.o: CFLAGS += -Dled_state=led_state_OMIT
build-COLDCARD_MK4/factoryreset.o: CFLAGS += -Dled_state=led_state_OMIT
build-COLDCARD_MK4/boardctrl.o: CFLAGS += -Dled_state=led_state_OMIT
-
+
+# Do not compile MicroPython's fallback PRNG. The board-specific rng.c
+# provides rng_get(), and this empty object satisfies the upstream object list.
+$(BUILD)/rng.o: CFLAGS += -Dpyb_rng_yasmarang=error-do-not-want-this
+$(BUILD)/rng.o:
+ $(ECHO) "SKIP stm32/rng.c"
+ $(Q)$(CC) $(CFLAGS) -x c -c /dev/null -o $@
files:
# SRC_C: $(SRC_C)
diff --git a/stm32/COLDCARD_MK4/rng.c b/stm32/COLDCARD_MK4/rng.c
index 374cd32..00e3d20 100644
--- a/stm32/COLDCARD_MK4/rng.c
+++ b/stm32/COLDCARD_MK4/rng.c
@@ -79,6 +79,11 @@ static uint32_t rng_get_or_fault(void)
return last_value;
}
+uint32_t rng_get(void)
+{
+ return rng_get_or_fault();
+}
+
/// \function pyb_rng_get()
//
/// Return a 30-bit hardware generated random number: or fail!
@@ -149,4 +154,3 @@ random_buffer(uint8_t *p, size_t count)
last = next;
}
}
-
diff --git a/stm32/COLDCARD_MK4/rng.h b/stm32/COLDCARD_MK4/rng.h
index 28775b3..57704d8 100644
--- a/stm32/COLDCARD_MK4/rng.h
+++ b/stm32/COLDCARD_MK4/rng.h
@@ -3,6 +3,7 @@
*/
#pragma once
+uint32_t rng_get(void);
+
MP_DECLARE_CONST_FUN_OBJ_0(pyb_rng_get_obj);
MP_DECLARE_CONST_FUN_OBJ_1(pyb_rng_get_bytes_obj);
-
diff --git a/stm32/COLDCARD_Q1/mpconfigboard.mk b/stm32/COLDCARD_Q1/mpconfigboard.mk
index a5b1a47..54c26ce 100644
--- a/stm32/COLDCARD_Q1/mpconfigboard.mk
+++ b/stm32/COLDCARD_Q1/mpconfigboard.mk
@@ -89,6 +89,12 @@ build-COLDCARD_Q1/spibdev.o: CFLAGS += -Dled_state=led_state_OMIT
build-COLDCARD_Q1/factoryreset.o: CFLAGS += -Dled_state=led_state_OMIT
build-COLDCARD_Q1/boardctrl.o: CFLAGS += -Dled_state=led_state_OMIT
+# Do not compile MicroPython's fallback PRNG. The board-specific rng.c
+# provides rng_get(), and this empty object satisfies the upstream object list.
+$(BUILD)/rng.o: CFLAGS += -Dpyb_rng_yasmarang=error-do-not-want-this
+$(BUILD)/rng.o:
+ $(ECHO) "SKIP stm32/rng.c"
+ $(Q)$(CC) $(CFLAGS) -x c -c /dev/null -o $@
files:
# SRC_C: $(SRC_C)
diff --git a/stm32/MK-Makefile b/stm32/MK-Makefile
index 8fd079a..029ce6e 100644
--- a/stm32/MK-Makefile
+++ b/stm32/MK-Makefile
@@ -19,7 +19,7 @@ LATEST_RELEASE = $(shell ls -t1 ../releases/*-mk-*.dfu ../releases/*-mk4-*.dfu |
# Our version for this release.
# - caution, the bootrom will not accept version < 3.0.0
-VERSION_STRING = 5.5.1
+VERSION_STRING = 5.5.2
# keep near top, because defined default target (all)
include shared.mk
diff --git a/stm32/Q1-Makefile b/stm32/Q1-Makefile
index 2abb89b..99bd552 100644
--- a/stm32/Q1-Makefile
+++ b/stm32/Q1-Makefile
@@ -16,7 +16,7 @@ BOOTLOADER_DIR = q1-bootloader
LATEST_RELEASE = $(shell ls -t1 ../releases/*-q1-*.dfu | head -1)
# Our version for this release.
-VERSION_STRING = 1.4.1Q
+VERSION_STRING = 1.4.2Q
# Remove this closer to shipping.
#$(warning "Forcing debug build")
diff --git a/stm32/shared.mk b/stm32/shared.mk
index 013f181..f516ff8 100644
--- a/stm32/shared.mk
+++ b/stm32/shared.mk
@@ -28,6 +28,8 @@ MAKE_ARGS = BOARD=$(BOARD) -j 4 EXCLUDE_NGU_TESTS=1 DEBUG_BUILD=$(DEBUG_BUILD)
all: $(BOARD)/file_time.c
cd $(PORT_TOP) && $(MAKE) $(MAKE_ARGS)
+ $(SUBMAKE) rng-code-check
+
clean:
cd $(PORT_TOP) && $(MAKE) $(MAKE_ARGS) clean
@@ -53,6 +55,27 @@ firmware-signed.bin: $(BUILD_DIR)/firmware0.bin $(BUILD_DIR)/firmware1.bin
firmware-signed.dfu: firmware-signed.bin
$(PYTHON_MAKE_DFU) -b $(FIRMWARE_BASE):$< $@
+#
+# Verify correct RNG code was built in.
+#
+NM = arm-none-eabi-nm
+.PHONY: rng-code-check
+rng-code-check:
+ @upstream_symbols="$$($(NM) --defined-only $(BUILD_DIR)/rng.o)" || exit $$?; \
+ if test -n "$$upstream_symbols"; then \
+ echo "ERROR: micropython's stm32/rng.o must not define any symbols"; \
+ printf '%s\n' "$$upstream_symbols"; \
+ exit 1; \
+ fi; \
+ board_symbols="$$($(NM) --defined-only \
+ $(BUILD_DIR)/boards/$(BOARD)/rng.o)" || exit $$?; \
+ if ! printf '%s\n' "$$board_symbols" \
+ | grep -Eq '^[[:xdigit:]]+[[:space:]]+T[[:space:]]+rng_get$$'; then \
+ echo "ERROR: board rng.o does not define global rng_get"; \
+ printf '%s\n' "$$board_symbols"; \
+ exit 1; \
+ fi
+
# make the DFU file which is shared for upgrades
dfu: firmware-signed.dfu
Why this scored 91/100
Evidence and disclosure record
Verified links used to place this patch in context. External claims remain attributed to their publishers.
COLDCARD Mk3 Seed Generation Warning
Vendor hotfix announcement for the limited-entropy seed-generation incident and the v5.6.0 / v1.5.0Q remediation releases.
Coldcard v5.6.0 Post-Hotfix Analysis and 39 Unpatched Findings
Independent analysis tying this patch to the fallback-PRNG symbol-resolution failure, the emergency release, and reported user losses.
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.