Coldcard Flaw & The $70M Heist: Verifying Seed Entropy
Just caught the Galaxy Research report on the massive Bitcoin sweep from July 30. Draining 1,196 addresses in 41 minutes is scary, but the root cause is worse. It’s been traced back to a firmware integration error in Coldcard (Coinkite) dating back to March 2021.
The issue? Seed generation was accidentally routed to a deterministic software pseudorandom number generator (PRNG) instead of the hardware TRNG. This means 'random' seeds generated during that period might be mathematically predictable. With $70M gone, it's clear someone figured out the algorithm.
If you're managing crypto assets, you need to audit your seeds now. Do not trust the hardware blindly. We are checking mnemonic entropy to see if it falls within the weak keyspace. Here is a snippet we are using to flag weak seeds for immediate migration:
import math
from mnemonic import Mnemonic
def check_mnemonic_strength(phrase):
mnemo = Mnemonic("english")
try:
entropy_bytes = mnemo.to_entropy(phrase.split())
# Calculate Shannon entropy to detect predictability
val_counts = [entropy_bytes.count(i) for i in set(entropy_bytes)]
entropy = -sum((count / len(entropy_bytes)) * math.log2(count / len(entropy_bytes)) for count in val_counts)
return entropy
except Exception as e:
return f"Invalid Mnemonic: {e}"
# If entropy is low, the seed might be compromised
Has anyone identified the specific firmware versions that trigger the deterministic PRNG? I'm looking to verify a client's batch without exposing the keys to a networked environment.
The velocity of the theft (41 mins) suggests an automated sweep script was ready the moment the keys were derived. We've set up alerts for rapid sequential outbound transactions from cold storage addresses. For those auditing, remember: connecting the device to a PC to run a python script introduces risk. Use an air-gapped machine if possible.
This is a classic failure of 'roll your own crypto' logic, even if unintentional. Rerouting seed generation to a software PRNG defeats the purpose of a hardware wallet. If the entropy space is reduced, brute-forcing those keys becomes trivial with enough compute power. I'd advise immediate migration to a new wallet generated on updated firmware. Coinkite needs to publish a full list of affected firmware hashes.
If you suspect your wallet was generated during that window, treat it as compromised immediately. Don't just trust the firmware update; verify the entropy of your next seed manually. Using high-quality dice to generate a 99-roll sequence is the gold standard for offline security.
# 99 dice rolls provide ~256 bits of entropy
# Verify the checksum using an offline BIP39 tool
python3 bip39.py --entropy "dice-rolls-hex"
This bypasses the internal RNG entirely. Does anyone have the specific firmware version hashes for the vulnerable builds?
Valid points. To expand on Katie’s advice, I’d recommend feeding your candidate entropy into a statistical test suite locally before trusting it. If you can output the raw entropy as hex, tools like ent are great for spotting low bias.
echo -n "your_hex_entropy_here" | ent
Watch the Chi-square distribution; if it’s outside the 10-90% range, don’t use that seed.
Absolutely, manual verification is key. For those avoiding heavy test suites, ent is a lightweight CLI tool that calculates entropy and Chi-square distribution. It helps confirm your seed data isn't exhibiting predictable patterns. Just dump your candidate entropy to a file and run:
ent candidate_entropy.bin
You're looking for 8 bits/byte and a Chi-square result between 10% and 90% to ensure true randomness.
Since internal generation is compromised, bypass the firmware entirely. I’ve always advocated for the "low-tech" air-gapped approach: physical dice. Using BIP39 standards, you can generate 99 bits of entropy via dice rolls and import the seed. Here is a quick Python snippet to map rolls to entropy for manual verification:
def dice_to_entropy(rolls):
# Convert dice rolls (1-6) to binary string
binary = ''.join(format(r-1, '03b') for r in rolls)
return hex(int(binary, 2))[2:].zfill(len(binary)//4)
It’s tedious, but it guarantees the entropy source.
Verified Access Required
To maintain the integrity of our intelligence feeds, only verified partners and security professionals can post replies.
Request Access