Damus

Recent Notes

Contra · 5h
Part 1: Who Peter Gray Is.. Peter D. Gray. CTO and co-founder of Coinkite. Goes by “doc-hex” online. Bachelor of Mathematics from Waterloo. Programmer since before there was a commercial web, by ...
armstrys profile picture
Oh shit… from Kimi K3. Smoking gun. Looks like doc-hex was also responsible for the code that added entropy on newer devices, but STILL fell below a reasonable threshold. This is super sketchy.


—————-
# Peter D. Gray and the Coldcard Entropy Incident

## Executive summary

Peter D. Gray, Coinkite engineer (GitHub: `doc-hex`, email: `[email protected]`), authored the commit that introduced the Coldcard entropy vulnerability and later authored the inadequate mitigation. On March 1, 2021, Gray pushed commit `b18723dd` directly to Coldcard's firmware master branch: a 120-file, 5,488-line change that replaced the hardware TRNG seed generation path with a call through the `libngu` library, which silently resolved to a non-cryptographic software PRNG called Yasmarang. The commit had no pull request, no code review, no CI pipeline, and no GPG signature despite Gray possessing a valid signing key. The same commit deleted the GPLv3 license and replaced it with MIT plus the Commons Clause.

Gray was not a passive consumer of libngu. He was an active contributor with 19 commits to the library starting February 10, 2021, 19 days before the integration commit. The library's primary author, `switck`, had no independent GPG key and signed their own commits using Gray's GPG key (`A2DCD558C2BE5D7C`), verified by raw PGP signature packet inspection. The two were collaborators: Gray was pushing to switck's firmware fork by January 2021, and Coinkite maintained their own fork of switck's library.

Thirteen months after introducing the vulnerability, Gray authored PR #18 to libngu (March 11, 2022): a 32-bit reseed function that brought newer devices (Mk4, Q, Mk5) from ~40 bits of effective entropy to ~72 bits. The design target was 128 bits. The person who introduced the vulnerability also wrote the fix that left users still below the security threshold.

The bug survived five years until July 30, 2026, when an attacker drained approximately 1,367 BTC ($88.6 million) from 4,500+ addresses. The attacker never touched a single device.

---

## Gray's relationship with libngu and switck

### The GPG key

Gray registered a GPG key on his GitHub account:

| Property | Value | Source |
|---|---|---|
| Primary key ID | `A2DCD558C2BE5D7C` | [GitHub API: users/doc-hex/gpg_keys](https://api.github.com/users/doc-hex/gpg_keys) |
| Subkey ID | `F0E6CC6AFC16CF7B` | [GitHub API: users/doc-hex/gpg_keys](https://api.github.com/users/doc-hex/gpg_keys) |
| Created | July 17, 2018 | GitHub API |
| Expired | May 28, 2023 | GitHub API |

switck has no GPG key registered on GitHub ([API returns empty](https://api.github.com/users/switck/gpg_keys)), no SSH keys ([API returns empty](https://api.github.com/users/switck/keys)), and uses only GitHub's noreply email. Location listed as "Cyberspace."

Despite switck having no registered key, switck's commits to libngu are GPG-signed. GitHub marks them `verified=False, reason=unknown_key` because the signing key is not registered to switck's account. Raw inspection of the PGP signature packets reveals the key ID:

- switck commit `f7711c5522` (2024-03-30): raw PGP packet contains `A2DCD558C2BE5D7C`. This is Gray's primary key.
- doc-hex commit `61ffc74af1` (2022-03-11): raw PGP packet contains `F0E6CC6AFC16CF7B`. This is Gray's subkey.

Both key IDs belong to the same GPG key registered to doc-hex. switck does not have their own GPG key. switck is signing commits with Peter Gray's key.

### The collaboration timeline

| Date | Event | Evidence |
|---|---|---|
| 2020-08-07 | switck creates GitHub account | [GitHub API: users/switck](https://api.github.com/users/switck) |
| 2020-10-15 | switck creates libngu repo, forks Coldcard/firmware, forks micropython | [GitHub API: repos/switck/libngu](https://api.github.com/repos/switck/libngu) |
| 2021-01-08 | Gray commits appear in switck's firmware fork (release signing, bug fixes) | [GitHub API: repos/switck/firmware/commits](https://api.github.com/repos/switck/firmware/commits) — commits by "Peter D. Gray" |
| 2021-02-10 | Coinkite forks switck's libngu. Gray makes first commit to libngu same day. | [GitHub API: repos/coinkite/libngu](https://api.github.com/repos/coinkite/libngu) (created 2021-02-10), [GitHub API: libngu commits by doc-hex](https://api.github.com/repos/switck/libngu/commits?author=doc-hex) |
| 2021-02-10 to 02-26 | Gray makes multiple commits to libngu (AES clone, env var tweaks, bip39 bugfix) | [GitHub API: libngu commits by doc-hex](https://api.github.com/repos/switck/libngu/commits?author=doc-hex) |

Gray had 19 total commits to libngu. He knew the library he was integrating.

### The `peter-conalgo` identity

The parent commit's `.gitmodules` referenced `github.com/peter-conalgo/trezor-crypto.git` as the `external/crypto` submodule. The GitHub account `peter-conalgo` was created April 22, 2021. The account name is "peter," no other identity. Gray's email is `[email protected]`. The commit removed this submodule (Gray's personal trezor-crypto fork) and replaced it with switck's libngu.

Sources: [GitHub API: users/peter-conalgo](https://api.github.com/users/peter-conalgo), [parent commit .gitmodules](https://raw.githubusercontent.com/Coldcard/firmware/f456a88/.gitmodules)

---

## The fateful commit: `b18723dd`

**Repository:** `github.com/Coldcard/firmware`
**SHA:** `b18723dd`
**Author:** Peter D. Gray (`[email protected]`)
**Date:** March 1, 2021, 14:03:03 UTC
**Message:** "First pass w/ libNgU"
**Parent:** `f456a88` (single parent: direct push, not a merge commit)
**Files changed:** 120
**Lines:** +2,766 / -2,722
**Pull request:** None
**Code review:** None
**CI:** None (no `.github` directory in repo)
**GPG signature:** Unsigned, despite Gray having a valid key (created 2018, valid through May 2023)

Sources: [GitHub API: commits/b18723dd](https://api.github.com/repos/Coldcard/firmware/commits/b18723dd), [GitHub commit page](https://github.com/Coldcard/firmware/commit/b18723dd), [GitHub API: contents/.github](https://api.github.com/repos/Coldcard/firmware/contents/.github) (Not Found)

### What Gray changed

| File | Change | Significance |
|---|---|---|
| `shared/random.py` | Changed `from ckcc import rng` to `import ngu`; set `bytes = ngu.random.bytes`. Comment still says "crypto-quality rng." | Call path that resolved to Yasmarang instead of the hardware TRNG |
| `shared/seed.py` | Changed `make_new_wallet()` from `rng_bytes(seed)` to `random.bytes(32)`. Changed `import tcc` to `import ngu` throughout. Changed `tcc.sha256(seed)` to `ngu.hash.sha256s(seed)`. | Seed generation function. Old path reached STM32 hardware TRNG. New path reached Yasmarang. |
| `shared/drv_entro.py` | Changed `import stash, tcc, hmac, chains` to `import stash, ngu, chains` | BIP-85 entropy derivation path migrated |
| `COPYING` | Deleted (674 lines, GPLv3) | Removed GPLv3 license |
| `COPYING-CC` | Added (MIT + Commons Clause) | Replaced GPLv3 with source-available license restricting commercial use |
| `.gitmodules` | Removed `external/crypto` (`peter-conalgo/trezor-crypto`), removed `external/modcryptocurrency`, added `external/libngu` (`switck/libngu`) | Removed Gray's personal trezor-crypto fork, added switck's libngu |

Sources: [GitHub API file diffs from commit b18723dd](https://api.github.com/repos/Coldcard/firmware/commits/b18723dd)

### The specific seed generation change

Before (firmware v3.2.2):
```python
seed = bytearray(32)
rng_bytes(seed) # reaches ckcc.rng_bytes -> STM32 hardware TRNG
```

After (commit b18723dd, shipped in v4.0.0):
```python
seed = random.bytes(32) # reaches ngu.random.bytes -> libngu -> rng_get() -> Yasmarang
```

Source: [Block Engineering analysis](https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware), section 5

### How the bug worked

The bug required two pieces: a faulty preprocessor check in libngu (switck's code) and Gray's seed generation change that routed through it (Gray's code).

**libngu's faulty check** (switck's code):
```c
#ifndef MICROPY_HW_ENABLE_RNG
#error "get a HW TRNG plz"
#endif
```
`#ifndef` tests whether the macro exists, not whether it is enabled. Coldcard's board config defines `MICROPY_HW_ENABLE_RNG` as `0`. The macro exists. Its value is zero. The check passes. The build links Yasmarang.

**Coldcard's board config** (Gray's code):
```c
#define MICROPY_HW_ENABLE_RNG (0)
```
Coinkite disabled MicroPython's built-in RNG because they provide their own wrapper. But Gray's seed generation change called `ngu.random.bytes` instead of `ckcc.rng_bytes`, routing through the broken libngu path.

Yasmarang was seeded from the MCU's 96-bit unique device identifier (factory metadata, not a secret), the SysTick counter (~80,000 possible values on Mk2/Mk3), and RTC registers (correlated timing values). None are cryptographic entropy sources. Collected once. Never reseeded.

Effective entropy: ~40 bits on Mk2/Mk3 (design target: 128 bits). A 40-bit keyspace is searchable on commodity hardware.

Sources: [Block Engineering analysis](https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware), sections 1-4

---

## The reseed commit: PR #18

Thirteen months after introducing the vulnerability, Gray authored a mitigation.

**Repository:** `github.com/switck/libngu`
**PR:** #18
**Title:** "Add reseed function to ngu.random module"
**Author:** doc-hex (Peter D. Gray)
**Submitted from:** `coinkite/libngu` fork
**Opened:** March 11, 2022
**Merged by:** switck on March 15, 2022

Source: [GitHub API: repos/switck/libngu/pulls/18](https://api.github.com/repos/switck/libngu/pulls/18)

### What the reseed did

```python
a = callgate.read_rng(1) # 32 bytes from SE1
b = callgate.read_rng(2) # 8 bytes from SE2
n = ngu.hash.sha256d(a + b)
n, = ustruct.unpack('I', n[0:4]) # keeps only 4 bytes
ngu.random.reseed(n) # replaces one 32-bit state word
```

The reseed hashes 40 bytes of secure-element output with SHA256d, then truncates to 4 bytes. It replaces one 32-bit Yasmarang state word. Maximum 2^32 distinct reseed values.

### Why the reseed was inadequate

| Property | Value |
|---|---|
| Secure element output | 40 bytes |
| Bytes retained after SHA256d | 4 (ustruct unpack 'I') |
| Maximum distinct reseed values | 2^32 |
| Effective entropy after reseed (Mk4/Q/Mk5) | ~72 bits |
| Design target | 128 bits |
| Gap | 56 bits below target |

The reseed function itself is trivial: `yasmarang_pad = mp_obj_get_int_truncated(arg)`. It does not initialize a cryptographic DRBG. It does not reset other state words. It does not provide periodic reseeding or prediction resistance.

The secure element firmware that provides the reseed entropy is closed source. `shared/callgate.py` is a thin wrapper around `ckcc.gate()` calls. The 72-bit figure rests on trust in a closed system that cannot be independently audited from outside the chip.

Source: [Block Engineering analysis](https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware), section 6

### The same key signed both commits

Gray's commits to libngu, including the reseed PR, are signed with his subkey `F0E6CC6AFC16CF7B`, verified by raw PGP packet inspection. switck's commits are signed with Gray's primary key `A2DCD558C2BE5D7C`. The same GPG key is used across both identities.

The fateful firmware commit `b18723dd` was the one commit Gray did not sign.

---

## The test failures

### What the test suite looked like at the time of the commit

| Issue | Evidence | Source |
|---|---|---|
| Test RNG deliberately seeded | `conftest.py` line 13: `random.seed(42)` | [GitHub: testing/conftest.py](https://github.com/Coldcard/firmware/blob/b18723dd/testing/conftest.py) |
| Only assertion on seed generation | `shared/seed.py`: `assert len(set(seed)) > 4` with comment `# TRNG failure` | GitHub API file diff |
| Entropy test tests derivation, not generation | `testing/test_drv_entro.py` tests BIP-85 derivation from a hardcoded XPRV | [GitHub: testing/test_drv_entro.py](https://github.com/Coldcard/firmware/blob/b18723dd/testing/test_drv_entro.py) |
| No end-to-end path test | No test generates a seed with known entropy input and verifies output | Full test directory inspection (18 test files) |
| No CI/CD | No `.github` directory | [GitHub API: contents/.github](https://api.github.com/repos/Coldcard/firmware/contents/.github) (Not Found) |

The test suite assumed the RNG worked. It never verified it. A seed with 40 bits of real entropy still expands into well-distributed, unique-looking addresses. No statistical test run against the output can distinguish "unpredictable" from "deterministic but unexamined."

The hardware TRNG code existed in every affected binary. It was reviewed. It was intact. It was never called from the seed generation path. Block Engineering's analysis: "reviews did not verify end-to-end symbol resolution and call reachability." Presence was mistaken for function.

Sources: [Block Engineering analysis](https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware), [808bits analysis](https://808bits.com/articles/coldcard-entropy-failure-lessons/)

---

## Timeline of Gray's involvement

| Date | Action | Evidence |
|---|---|---|
| 2018-07-17 | Gray creates GPG key `A2DCD558C2BE5D7C` | [GitHub API: users/doc-hex/gpg_keys](https://api.github.com/users/doc-hex/gpg_keys) |
| 2021-01-08 | Gray commits appear in switck's firmware fork | [GitHub API: repos/switck/firmware/commits](https://api.github.com/repos/switck/firmware/commits) |
| 2021-02-10 | Coinkite forks switck's libngu. Gray makes first commit to libngu. | [GitHub API: repos/coinkite/libngu](https://api.github.com/repos/coinkite/libngu), [GitHub API: libngu commits by doc-hex](https://api.github.com/repos/switck/libngu/commits?author=doc-hex) |
| 2021-02-10 to 02-26 | Gray makes multiple commits to libngu | [GitHub API: libngu commits by doc-hex](https://api.github.com/repos/switck/libngu/commits?author=doc-hex) |
| **2021-03-01** | **Gray pushes `b18723dd` to Coldcard/firmware master. No PR, no review, no CI, no GPG signature. Changes seed generation to route through libngu (Yasmarang). Deletes GPLv3, replaces with MIT + Commons Clause.** | [GitHub API: commits/b18723dd](https://api.github.com/repos/Coldcard/firmware/commits/b18723dd) |
| 2021-03-02 to 03-09 | Gray continues committing to libngu | [GitHub API: libngu commits by doc-hex](https://api.github.com/repos/switck/libngu/commits?author=doc-hex) |
| 2021-03-17 | Firmware v4.0.0 ships containing the vulnerability | [Block Engineering analysis](https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware), section 5 |
| 2022-01-12 | Gray opens PR #17 to libngu (hardware accelerated SHA256) from `coinkite/libngu` | [GitHub API: repos/switck/libngu/pulls/17](https://api.github.com/repos/switck/libngu/pulls/17) |
| **2022-03-11** | **Gray opens PR #18 to libngu: 32-bit reseed function** | [GitHub API: repos/switck/libngu/pulls/18](https://api.github.com/repos/switck/libngu/pulls/18) |
| 2022-03-15 | switck merges PR #18 | [GitHub API: repos/switck/libngu/pulls/18](https://api.github.com/repos/switck/libngu/pulls/18) |
| 2023-05-28 | Gray's GPG key expires | [GitHub API: users/doc-hex/gpg_keys](https://api.github.com/users/doc-hex/gpg_keys) |
| **2026-07-30** | **Attacker drains ~1,367 BTC ($88.6M) from 4,500+ addresses** | [808bits](https://808bits.com/articles/coldcard-entropy-failure-lessons/) |

---

## Blame allocation: Peter D. Gray

1. **Authored the vulnerability.** Commit `b18723dd` changed seed generation from the hardware TRNG path to libngu, routing through Yasmarang. Verified by GitHub API commit metadata.

2. **Shipped it with no process.** Direct push to master. No pull request. No code review. No CI. No GPG signature despite having a valid key. A 120-file, 5,488-line change to a system protecting billions of dollars, pushed with the message "First pass w/ libNgU." Verified by GitHub commit page (single parent, no PR, no review elements, verification: unsigned).

3. **Knew the library.** 19 commits to libngu starting 19 days before the integration. Coinkite maintained their own fork. He was not importing an unknown dependency. He was integrating a collaborator's code that he had been actively modifying.

4. **Introduced the license change in the same commit.** Deleted GPLv3, replaced with MIT + Commons Clause. The FSF considers this "source-available," not open source. The restriction on commercial use could have limited independent security auditors and tools. Verified by GitHub API file diffs.

5. **Authored the inadequate mitigation.** PR #18 (March 2022) added a 32-bit reseed that left Mk4/Q/Mk5 at ~72 bits, 56 bits below the 128-bit target. The reseed truncated 40 bytes of secure-element output to 4 bytes. The person who introduced the vulnerability also wrote the fix that left users still below the security threshold. Verified by GitHub API PR metadata.

6. **The one commit that needed signing was the one that was not signed.** Gray signed his libngu commits. switck signed their commits using Gray's key. But the firmware commit that integrated the vulnerable library, changed the license, and shipped to production was unsigned.

---

## Sources

- Block Engineering root-cause analysis: https://engineering.block.xyz/blog/predictable-rng-fallback-and-32-bit-reseed-in-coldcard-firmware
- 808bits operational analysis: https://808bits.com/articles/coldcard-entropy-failure-lessons/
- GitHub API: commit b18723dd: https://api.github.com/repos/Coldcard/firmware/commits/b18723dd
- GitHub commit page: https://github.com/Coldcard/firmware/commit/b18723dd
- GitHub API: doc-hex profile: https://api.github.com/users/doc-hex
- GitHub API: doc-hex GPG keys: https://api.github.com/users/doc-hex/gpg_keys
- GitHub API: switck profile: https://api.github.com/users/switck
- GitHub API: switck GPG keys (empty): https://api.github.com/users/switck/gpg_keys
- GitHub API: switck SSH keys (empty): https://api.github.com/users/switck/keys
- GitHub API: libngu repo: https://api.github.com/repos/switck/libngu
- GitHub API: libngu commits by doc-hex: https://api.github.com/repos/switck/libngu/commits?author=doc-hex
- GitHub API: libngu PR #17: https://api.github.com/repos/switck/libngu/pulls/17
- GitHub API: libngu PR #18: https://api.github.com/repos/switck/libngu/pulls/18
- GitHub API: coinkite/libngu fork: https://api.github.com/repos/coinkite/libngu
- GitHub API: switck/firmware fork: https://api.github.com/repos/switck/firmware
- GitHub API: switck/firmware commits: https://api.github.com/repos/switck/firmware/commits
- GitHub API: peter-conalgo profile: https://api.github.com/users/peter-conalgo
- GitHub API: Coldcard/.github (Not Found): https://api.github.com/repos/Coldcard/firmware/contents/.github
- GitHub: conftest.py: https://github.com/Coldcard/firmware/blob/b18723dd/testing/conftest.py
- GitHub: test_drv_entro.py: https://github.com/Coldcard/firmware/blob/b18723dd/testing/test_drv_entro.py
- GitHub: COPYING-CC: https://github.com/Coldcard/firmware/blob/master/COPYING-CC
- Parent commit .gitmodules: https://raw.githubusercontent.com/Coldcard/firmware/f456a88/.gitmodules
- GPG signature verification: switck commit `f7711c5522` contains key `A2DCD558C2BE5D7C` in raw PGP packet; doc-hex commit `61ffc74af1` contains key `F0E6CC6AFC16CF7B` in raw PGP packet
❤️3
Kingbee · 1h
Over my head. Just going to spread coins to multiple wallets, exchanges, and etfs. Might lose some, but it won't be all.
daniele · 1d
I've been thinking about reviewing the onboarding process of CC and similar devices, to see how seed generation is handled. My suspicion is that developers, being hardcore tech people, take dice as t...
armstrys profile picture
Generally agree, but camera entropy is not a good idea to use for real funds. I believe seedsigner says this themselves.

For example, the seed signer software doesn’t have any threshold for entropy so a plain black image would potentially generate a low entropy seed.
1
daniele · 1d
Any source? SS takes 50 preview frames AND the final pic, it is a lot of entropy. Also in "full" blackness the sensor registers very different levels, you cannot have two similar entropy patterns.
Neo ⚡️ · 4d
I wonder how much multisig in case of bitkey helps if it’s all the same vendor/tech stack that provides the keys 🤔
armstrys profile picture
This. This wasn’t a cold card problem… this was a putting your trust in the vendor software problem. I would argue leaning heavily on something like bitkey actually makes this worse.

Yes it solves other problems, but this same exploit can, and will, happen to other vendors. Actually… didn’t it already happen to jade? Roll dice, use multiple vendors.
note1e60r7...
armstrys profile picture
For sure. Definitely a trade off between doing everything on your own, but there are some spots (like rolling dice) where you can easily reduce risk without increasing your risk of making a mistake. Only way you can do that is to understand the potential risks and where you are putting trust in a vendor
armstrys profile picture
Seems like a good time to remind folks of the great multisig guide from seedsigner. Even if you don’t have a seedsigner there are some great bitcoin security tidbits in here like the section on “Creating Secure Private Keys in a Trust-Minimized Way”.

The ColdCard exploit is a great reminder of the importance of minizing vendor risk. The same exploit could have been found in SeedSigner or any other vendor. If you aren’t making an effort to take vendor risk out of each part of the process then you should re-evaluate your strategy.

https://seedsigner.com/seedsigner-independent-custody-guide/
1❤️1
tuco · 4d
Sorry https://blossom.primal.net/bafb54138b3827f5415d5a88c3d29edd9cb750179785fe60486694e45e5f4553.png
Will Cole · 4d
No issues I’m aware of. I just have no personal experience with the product .
Will Cole · 2d
Just saw this from nunchuck on Twitter. Not sure what your setup is, but it seems some of their paid users were exposed to Coldcard keys whether you own one or not. https://x.com/nunchuk_io/status/2083357949297414435?s=46&t=OhFuTtv-xPcwkmJSTd2Ovg
ALLCOSTS · 4d
I gotta buy fucking dice