COLDCARD 熵漏洞专业复盘:卷走 3800 万美元的随机数缺陷

要点总结
- COLDCARD 2021 年后的建种路径可能因编译配置问题静默回退到软件 PRNG。
- Coinkite 当前估计 Mk3 有效搜索空间约 40 位,Mk4/Mk5/Q 约 72 位。
- 受影响用户应在可信路径生成新种子,完成备份、地址和小额交易验证后迁移资产。
- OneKey 硬件钱包不受该漏洞影响。
以下内容由 OneKey 安全实验室 @OneKey_Anzen 撰写校对完成,英文原文见评论。
在密码学里,“随机”是一个可以量化、经得起算力检验的硬指标。
一套 12 个词的助记词,背后是一串 128 位的随机数,按 BIP-39 规范编码成一组人能读写的单词。这串数字的不可预测程度,直接决定钱包能不能扛住暴力破解。128 位意味着 2¹²⁸ 种可能——把全世界的算力加在一起,穷举到宇宙消失也跑不完。
所以硬件钱包理论上应该在这件事很严谨。必须会有专门处理随机数生成的真随机数发生器单元(TRNG),从电路噪声里取物理世界的随机,再配一颗过了 EAL 认证的安全元件(SE),最后用成熟的算法把这些随机搅匀。
2026 年 7 月 30 日,约 594 枚比特币(约 3800 万美元)在不到半小时内,从近 500 个 COLDCARD 钱包里被席卷一空。次日,厂商 Coinkite 在《Technical Deep Dive into the Entropy Issue》中确认了根源[1]:自 2021 年 3 月的一次固件改动起,一处编译配置错误让代码悄悄回退到了 MicroPython 自带的软件伪随机数生成器(PRNG)。
后果按型号不同:官方初步估计,Mk3 的有效搜索空间只剩约 40 位,Mk4、Mk5 和 Q 约 72 位,而设计目标是 128 位。40 位约等于 1.1 万亿种可能,听起来仍是天文数字,但对一个租得起 GPU 集群的攻击者来说,它已经从“物理上不可能”,降级为一个数学期望上可行的工程项目。从 128 到 40,搜索空间缩小了 2⁸⁸ 倍——三百亿亿亿倍。
这次 3800 万美元的失窃,把变成了既成事实。
注:OneKey 用户的资金是安全的,OneKey 的所有代码均不涉及相关问题,也不使用其相关实现和依赖。详见声明:https://x.com/OneKeyCN/status/2083104424764018990?s=20
太长不看?结论:
- 使用 COLDCARD Mk3 生成助记词并使用的用户,立刻在其他钱包重新生成助记词并逐步转移资金到新钱包。
- 使用 COLDCARD Mk4 或 Mk5 生成助记词并使用的用户,如果生成初始助记词的系统版本低于 5.6.0,立刻在其他钱包重新生成助记词并逐步转移资金到新钱包。
- 使用 COLDCARD Q 生成助记词并使用的用户,如果生产初始助记词的系统版本低于 1.5.0Q,立刻在其他钱包重新生成助记词并逐步转移资金到新钱包。
- 使用 OneKey 硬件钱包的用户,都不受同样的漏洞困扰,可以放心使用。
专业硬核开始👇
漏洞根因:一次静默的软件回退
从硬件 RNG 接口迁移到 libNgU
2021 年的加密栈迁移将钱包种子生成从 ckcc.rng_bytes() 改为 ngu.random.bytes()[6]:
ngu.random.bytes()
-> libNgU rng_get()
-> 最终链接到的 rng_get 符号
风险不在函数名或输出长度,而在最后一个 rng_get() 实际由哪个目标文件提供。
#ifndef 只检查“有没有定义”,不检查值
目标板配置把 MICROPY_HW_ENABLE_RNG 定义为 0,表示 MicroPython 的硬件 RNG 路径未启用。
但 libNgU 当时使用[8]:
#ifndef MICROPY_HW_ENABLE_RNG
#error ...
#endif
#ifndef 只判断宏是否存在。一个被定义为 0 的宏依然是“已定义”,所以预期用于阻止错误构建的 #error 没有触发。
同名符号让错误实现静默进入最终固件
MicroPython 在硬件 RNG 未启用时仍提供同名 rng_get(),但它是 Yasmarang 软件 PRNG fallback[9]。
于是发生了完整的失败链:
宏存在但值为 0
-> libNgU 的构建保护未触发
-> 固件仍能成功编译
-> 同名 rng_get 解析到 MicroPython fallback
-> 钱包建种没有获得预期硬件 RNG 输入
这条链上每一环单独看都只是小疏忽,连起来却让整个构建系统的把关形同虚设:安全实现即使存在于源码或二进制中,也不代表安全关键调用真正到达了它。
混合熵源补不回缺失的熵
fallback 初态涉及 UID 的一部分、SysTick、RTC 时间和子秒状态,之后由 Yasmarang 确定性推进。libNgU 还将其与另一个固定初态的 Yasmarang 流混合。
两个可重建或枚举的确定性流,经过异或或 SHA-256 后仍不会获得新的物理不可预测性。哈希可以调理输入,却不能把有限候选集合扩展为真正的 2^256 未知空间。
40 位和 72 位到底意味着什么
Coinkite 使用的有效搜索空间估计,并非经过测量和认证的熵值。这两个数字描述的是攻击者需要遍历的候选状态规模,它不是 min-entropy 的实测结果,也换算不出平均搜索次数,更推不出具体的攻击时间和成本。
实际攻击成本取决于一连串前提:攻击者是否知道或能推测设备 UID,能否收窄 RTC、SysTick 和启动时间的范围,种子生成前发生过多少次 RNG 调用,手上有没有 xpub、地址或公钥可以离线验证候选,以及 BIP-39/BIP-32 派生和并行硬件要花多少钱。
所以准确的说法是:Coinkite 在当前初步攻击假设下,估计 Mk3 的有效搜索空间约为 40 位,Mk4/Q/Mk5 约为 72 位。这不等于“每个 Mk3 钱包恰好只有 40 位熵”,不等于“较新型号经过证明拥有 72 位密码学安全性”,更不等于“任何普通电脑都能在固定时间内恢复所有钱包”。
为什么 Mk4、Mk5 和 Q 也受影响
Mk5 与 Q 电路板上的两颗安全元件(来源:coldcard.com)
Mk5 与 Q 电路板上的两颗安全元件(来源:coldcard.com)
Mk4、Mk5 和 Q 的确会从两个安全元件取得随机材料,但修复前的实现并没有把全部输出注入一个密码学 DRBG。
公开源码显示,它把安全元件材料哈希后,只取四字节传给 ngu.random.reseed(),并仅替换 Yasmarang 的一个 32 位状态字。
因此,在 fallback 其余状态和调用轨迹固定时:
由安全元件 reseed 区分的输出流数量 <= 2^32
这与官方“约 72 位”并不矛盾。72 位模型还计入 UID、计时器、RTC 和调用历史等状态的不确定性。Block Engineering 给出的宽松组合上界低于 2^73.3,并明确指出这不等于 73 位密码学安全认证[10]。
换句话说:Mk3 的问题最严重,因为没有安全 reseed;Mk4/Q/Mk5 的安全元件输入降低了风险,但四字节 reseed 没有恢复设计目标中的 128 位安全性。
官方公告与实际影响范围
Coinkite 当前正式警告从 Mk3 4.0.1 开始。但 v4.0.0 的不可变源码已经包含相关建熵生成路径[7],4.0.0 到 4.0.1 之间也没有对应的 RNG 修复。
Block Engineering 进一步将 Mk2/Mk3 v4.0.0–v4.1.9 一并纳入源码分析[10]。
因此需要区分两层:
- Coinkite 正式范围:Mk3 4.0.1+;
- 源码能够支持的技术范围:Mk2/Mk3 v4.0.0–v4.1.9。
不能说 Coinkite 已经正式确认 Mk2 或 v4.0.0,但也不能因为官方没有点名就推断它们安全。
修复如何工作
Mk4/Mk5 5.6.0 和 Q 1.5.0Q 的热修复不止修改了一个条件表达式,还增加了构建级强制保证[3][4][5]:
- 排除 MicroPython 的 fallback RNG object;
- 确保板级对象提供全局
rng_get(); - 使用
nm检查目标文件的符号; - 如果 fallback 仍导出符号,或板级对象没有正确提供
rng_get(),构建直接失败。
用户应该如何处置
Mk4、Mk5 和 Q
- Mk4/Mk5 先升级到 5.6.0 或更高版本;Q 先升级到 1.5.0Q 或更高版本。
- 在修复后的固件中生成全新种子。
- 记录并完成备份恢复验证。
- 在设备屏幕核对钱包 fingerprint 和接收地址。
- 先进行小额测试交易,再迁移其余资金。
- 在全部迁移确认前保留旧备份,但不要继续向旧钱包收款。
Mk3
Mk3 当前没有已发布的修复固件,4.1.9 不是修复。用户不应等待未来可能出现的固件,因为任何新固件都无法增加旧种子的熵。
按照 Coinkite 当前迁移指引,在可信路径生成新种子,完成备份、地址和小额交易验证后迁移资产。
两个不同的骰子阈值
关于骰子有两个数字:50 和 99。它们服务于两个不同的目标,不能混用。
**原始建种时加入 50+ 次骰子。**如果种子最初生成时加入了至少 50 次公平、独立、私密、未记录或保存且未泄露的六面骰结果,Coinkite 表示不认为该种子仅因本 RNG 问题而处于风险[11]。
50 × log2(6) ≈ 129.25 bits
这是针对本事件的约 128 位门槛,不是整个操作流程的绝对安全证明。
**空白 Mk3 的 Dice Roll Import 流程。**Coinkite 还提供一个不同流程:在空白 Mk3 4.1.9 上选择 Import Existing > Dice Rolls,并输入至少 99 次公平掷骰。该专用路径直接处理骰子序列,不使用有问题的设备生成器。
99 × log2(6) ≈ 255.91 bits
99 次是官方流程的门槛;如果按“至少 256 位”的字面标准倒推,则需要 100 次。
Passphrase 是独立屏障,不是修复
强、唯一、随机且保密的 BIP-39 passphrase 会派生一个不同钱包,可以增加独立搜索因子;设备 PIN 只控制本地访问,不参与 BIP-39 根密钥派生[12]。
但 passphrase 不能补回助记词缺失的熵。短口令、名言、规律模式或复用口令仍可能被猜中。输入错误也会生成一个看似有效但完全不同的钱包,因此必须验证 fingerprint,并建立可靠、经过恢复测试的备份。
结语
这起事件暴露的核心问题,是安全关键实现的实际可达性没有被构建系统强制验证——“一个宏写错了”远不足以概括它。
硬件钱包和其他密钥生成系统至少应做到:
- 安全 API 验证最终链接到的 object 和 symbol,而不只检查函数签名;
- 能力检查同时验证宏的存在和值;
- 安全关键 fallback 必须 fail closed,不能静默提供普通 PRNG;
- CI 检查 object composition、link map、符号来源和端到端数据流;
- 测试证明真实硬件熵进入最终种子,而不只是检查输出长度、非零或无重复;
- 公告明确区分生成版本、当前固件、修复版本和旧种子处置。
参考链接
[1] Coinkite:Technical Deep Dive into the Entropy Issue [2] Coinkite:Mk3 Security Advisory [3] Coldcard 5.6.0 / 1.5.0Q 修复提交 [4] Coldcard 5.6.0 release commit [5] Coldcard 1.5.0Q release commit [6] 2021 libNgU 建种迁移提交 [7] 正式 v4.0.0 源码快照 [8] libNgU random.c [9] MicroPython fallback 引入提交 [10] Block Engineering 技术分析 [11] Coldcard 骰子说明 [12] Coldcard Passphrase 说明 [13] OneKey trezorcrypto.random source 分支 [14] OneKey 建种逻辑 [15] OneKey 标准 Makefile [16] OneKey SCons






