核心要点
即使攻击者没有读到存储中的私钥,也可能将其恢复出来。六起事件揭示了弱随机数、熵损失和签名 nonce 缺陷如何危及受保护的密钥。支付机构需要核验实际实现是否保持了密钥生成时的不可预测性,以及签名时的保密性。
预计阅读时长 16 分钟
Beyond Secure Storage 第一篇讨论了攻击者如何滥用支付系统自身的签名能力。第二篇转向另一条获得未经授权签名的路径:利用密码学实现缺陷恢复私钥,再自行签名。这里要回答的是:攻击者不读取受保护的密钥副本,能否重建密钥?
第 1 节先说明密钥生成、存储和签名各自需要满足的安全要求。第 2、3 节分别分析四起密钥生成事件和两起签名事件,解释实现错误及其后果。第 4 节据此总结设计与实现原则,再讨论如何验证相关安全属性。
1. 密钥生命周期中的安全属性
一把密钥从生成、存储到用于签名,各阶段都有不同的安全要求,如下图所示。

在使用 BIP-39 助记词和 BIP-32 密钥派生的分层确定性钱包中,密码学安全的随机数据被编码为助记词,再转换为种子,用于派生私钥、公钥和钱包地址 [1, 2]。
种子和私钥生成后,需要通过加密或硬件隔离等方式保存。签名时,系统使用私钥生成可供公开验证的签名。根据签名方案的要求,这一步还可能需要一个安全生成的随机 nonce,或一个由秘密数据确定性派生的 nonce。
三个阶段分别需要满足以下要求:
生成时不可预测。攻击者不能预测密钥,也不能以可行的成本枚举出控制钱包的秘密信息。
存储时保密。未经授权的人不能通过软件、物理接口、备份、日志或远程服务读到秘密信息。
使用时不泄漏。即使收集到大量公开签名,攻击者也无法从中获得足以重建密钥的信息。
硬件钱包、安全元件(Secure Element,SE)和可信执行环境(TEE)能够加强存储保护,但不能仅凭使用了这些措施,就认定另外两项要求也已满足。
密钥如果在生成时就可以被预测,存入安全硬件后也不会变得不可预测;签名如果泄漏了可用于推算密钥的信息,将私钥与主机隔离也无法收回这些信息。因此,攻击者可能绕过受保护的存储副本,恢复密钥并控制相应资产。
接下来两节分析实现如何违背这些安全要求:密钥生成缺陷可能让私钥变得可预测,签名缺陷则可能让攻击者从公开签名中恢复私钥。这些案例将为第 4 节归纳实现原则与验证目标提供依据。
2. 密钥生成缺陷:可预测的私钥
密钥生成的每一步都要保留不可预测性。熵源需要适合密码学用途,攻击者不能以可行的成本枚举其内部状态;随机字节转换为种子或私钥时,也不能损失应有的不确定性。如果目标是达到 128 位安全性,实现错误就不应把搜索范围缩小到攻击者能够穷举的程度。
下面四个案例分别涉及依赖库中的弱随机数生成器、错误的随机数生成器(RNG)绑定、丢失信息的输入转换,以及初始状态空间过小的生成器。表中影响数据来自不同报告,统计时间也不同,不能作为同一时点的损失总额比较。

2.1 Ill Bloom:钱包依赖库中的弱随机数
在 Ill Bloom 事件中,一些软件钱包使用有漏洞的 crypto-js 版本生成 BIP-39 助记词。
其 CryptoJS.lib.WordArray.random() 没有从密码学安全的随机源获取熵,而是使用由 Math.random() 初始化的自定义 Multiply-With-Carry(MWC)生成器。
钱包一旦依赖这个函数生成熵,后续即使正确执行了助记词编码和密钥派生,也无法弥补随机源的缺陷 (来源:CryptoJS Security Advisory, GHSA-rg76-677x-56q9)。
crypto-js v3.1.9-1 的相关实现 (来源:CryptoJS core.js source, version 3.1.9-1) 如下:

这里的 r() 是自定义 MWC 生成器,Math.random() 参与初始化或影响其状态。这个伪随机数生成器(PRNG)虽然输出看似随机的字节,却不具备密码学所需的不可预测性。标称 128 位和 256 位的输出,实际候选空间分别仅约为 2³⁹ 和 2⁴⁷ (来源:CryptoJS Security Advisory, GHSA-rg76-677x-56q9)。
搜索范围缩小后,攻击者就可以枚举候选助记词,派生钱包地址,再与公开链上活动比对。这样,即使不从受害者设备中提取数据,也可能恢复秘密信息。助记词符合标准,无法弥补旧版依赖库带来的弱随机性。
2.2 Coldcard:代码没有使用的硬件 RNG
Coldcard 的问题在于,设备配备了安全的随机源,应用却没有调用到它。
构建阶段的集成错误使种子生成过程中的 rng_get() 实际调用了 MicroPython 的软件 PRNG Yasmarang,而不是硬件随机数生成器。
这个不适用于密码学用途的 PRNG,以设备固定的唯一 ID 和有限的计时信息初始化;其初始状态较容易预测,导致钱包种子的有效熵大幅下降 (来源:Coldcard firmware advisory and release notes)。

种子生成过程还混入了第二个 Yasmarang 生成器的输出。较老的 Mk2/Mk3 设备用公开常量初始化第二个生成器,因此没有增加熵。较新型号虽然引入安全元件提供的随机数,但重新设置种子时只保留了 32 位。
因此,输出虽然是 256 位钱包种子,实际搜索空间却小得多。按照 Coinkite 给定的攻击假设,其初步估计为:Mk2/Mk3 约有 2⁴⁰ 个候选,Mk4/Q/Mk5 约有 2⁷² 个候选 [6, 7]。
攻击者因而可以离线尝试候选种子,计算对应地址,再与公开链上数据比对。搜索成本取决于设备型号和攻击假设,不能把 2⁷² 的搜索难度等同于 2⁴⁰。一旦找到匹配的种子,就可以派生对应私钥。
2.3 XRPH Wallet:输入格式错误压缩密钥空间
随机数生成之后,还需要按钱包 API 要求的格式传入数据。XRPH Wallet 就在这一步丢失了大部分输入。
其 Android 应用使用 JavaScript 的 Math.random() 生成若干组十进制数字,将其拼接为字符串,再传给 Wallet.fromEntropy()。接收端把各字符转换成数值较小的字节,并只保留前 16 个位置。
其中两个位置是固定的空格分隔符,最终只有 14 个可变的十进制数字提供了熵 (来源:XRPL.to, The XRPH Wallet Hack: 4,011 Wallets Emptied, and Every Key Was Guessable)。

这一步转换后,钱包输入只剩约 10¹⁴ ≈ 2⁴⁶ 种可能,远少于均匀采样 16 字节所应有的约 2¹²⁸ 种可能。这里计算的是该转换方式能够产生多少种候选输入,并不是在证明 Math.random() 的统计熵。
这个案例叠加了两个问题:随机源不适合密码学用途,传入 API 的数据表示和编码又有错误。功能测试可能漏掉二者,因为钱包仍能生成看似正常的地址,也能生成有效签名。公开调查说明密钥存在被枚举恢复的可能,但并未单凭这一点证明攻击者实际通过哪种方式取得了密钥。
2.4 Qubic Wallet:55 字符种子背后的弱随机性
生成器的初始状态也可能限制种子的取值范围,无论输出字符串有多长。
Qubic 披露的钱包事件涉及使用 JavaScript 非密码学函数 Math.random() 生成的种子,受影响环境包括 Apple 浏览器和部分基于 JavaScriptCore 的旧版 iOS 应用 [9, 10]。
以下代码概括了原有问题和修复方法 (来源:Qubic Wallet, secure seed-generation fix 09dd288):

调用 Math.random() 55 次,并不会获得 55 个独立熵源。在两次重新设置种子之间,PRNG 的每次调用都按确定性规则更新内部状态。
固定调用位置后,如果初始种子是 S,生成的完整 55 字符钱包种子就是确定性函数 W=F(S)。知道 S 和调用位置,就能重现全部 55 个字符;多调用几次并不会增加熵。
初始状态的取值范围取决于具体运行时的初始化方式。我们在 2026 年 10 月 29 日获取并检查的 JavaScriptCore 源码快照中,PRNG 使用 32 位种子初始化,因此固定调用位置后,最多只有 2³² 种输出序列。
此次检查的 V8 源码使用 64 位种子,相应上限为 2⁶⁴ 种。这些上限只适用于所检查的初始化路径和固定调用位置,不能据此确定事件中受害者使用的具体浏览器构建版本,也不能据此断定实际攻击成本。
修复后的代码改用 crypto.getRandomValues() 获取密码学安全随机数,并通过拒绝采样(byte < 234)避免将字节映射为 26 个字母时引入偏差 [12, 13, 14]。
3. 签名实现缺陷:从公开签名恢复私钥
密钥安全生成后,还需要在签名过程中保持秘密性。每次签名都会产生可供外界观察的数据;如果实现存在缺陷,这些公开数据就可能提供足以恢复私钥的信息。
ECDSA 和 Schnorr 为每次签名引入一个秘密标量 nonce,用它掩蔽私钥在公开签名计算中的贡献。
nonce 必须保密,同一密钥不能为不同消息复用 nonce,其取值还应满足相应方案的分布要求。确定性派生也可以满足这些要求:仅知道公钥和已签消息的人,不应能够预测 nonce。
以简化的 Schnorr 关系为例:
s=k-rd (mod n),
其中 d 是私钥,k 是 nonce,r 是根据公钥、消息等公开信息计算的挑战值,s 是公开的签名标量,n 是子群阶。当 r 可逆时,已知 nonce 就能直接得到 d=(k-s)r⁻¹ (mod n)。
攻击者并不一定需要知道完整的 nonce。多份签名中 nonce 的取值约束或偏差,也可能提供足够的信息,使攻击者能够通过格方法恢复密钥。是否可行,取决于暴露了多少信息,以及能够获得哪些签名。
下面两个案例分别展示了这两种情况:Zilliqa 的 nonce 取值受到部分限制,SecondFi 的 nonce 则可以完全由公开信息计算出来。表中影响数据仍采用各报告自己的统计时间范围。

3.1 Zilliqa Ledger Wallet:每个 Schnorr nonce 缺失 64 位
Zilliqa Ledger Wallet 的问题出在处理旧版 Zilliqa EC-Schnorr 交易的应用中 (来源:Zilliqa, Post-mortem: Legacy Schnorr Nonce Bias)。应用虽然生成了 nonce 所需的随机数据,却向最终签名缓冲区复制了错误的片段:保留八字节零填充,丢弃八字节实际随机数。这样,每个受影响的 nonce 都满足 0 ≤ k < 2¹⁹², 而不能正常覆盖 secp256k1 约 256 位的群阶范围。
签名方程可以解释,为什么 nonce 的取值受限会带来风险:

其中 G 为群的生成元。整理可得:
k=s+rd (mod n).
每份签名都给出一个包含未知私钥 d 的模方程,同时 nonce 的上界已知为 2¹⁹²。将多个独立方程组合起来,可以构造隐藏数问题(Hidden Number Problem,HNP),再尝试通过格约简求解。
官方事后分析报告称,利用四份或更多受影响签名即可恢复私钥。
这一过程不需要安全元件导出私钥。nonce 的偏差让已经公开在链上的签名提供了恢复私钥所需的信息。
3.2 SecondFi:可由公开信息算出的签名 nonce
SecondFi 的 Ed25519 nonce 派生错误更为直接:nonce 不只是存在部分信息泄漏,而是可以完全由公开数据计算。攻击者因此能够从单份公开交易签名恢复签名密钥 (来源:SecondFi implementation analysis and reproduction)。
在 Ed25519 类签名中,正确的构造会从秘密密钥数据派生秘密标量 a 和一个保密的 nonce 前缀。
以下公式中,A=aB 是公钥,B 是 Ed25519 基点,M 是待签消息,L 是子群阶,H 表示 SHA-512:

有缺陷的实现却令 r=H(M) mod L。这样,只要知道签名消息,就能计算 r。由于 R、A 和 S 也都公开,任何观察者都可以继续计算:
a=(S-r)h⁻¹ (mod L),
条件是 h 模 L 非零。
因此,与 Zilliqa 需要组合多份签名的情况不同,SecondFi 的一份受影响签名就足以恢复签名标量。不过,这不等于同时恢复了原始助记词或分层钱包中的所有同级密钥。
问题位于密钥派生与签名之间的接口:所需秘密数据本来可用,却没有被传给 nonce 派生函数。
4. 安全实现原则与验证
综合这些案例可以看到,审查随机源和签名算法时,还需要检查连接它们的代码。不可预测的随机源应当产生不可预测的密钥,而密钥用于签名时仍需保密。转换函数或库接口中的错误,都可能破坏这些要求。
第 4.1、4.2 节分别总结密钥生成和签名的设计与实现原则。第 4.3 节则说明,审查和测试需要提供哪些证据,才能评估实际部署的系统是否满足这些要求。
4.1 保持密钥生成的安全性
上述密钥生成缺陷对应三项实现要求:
使用密码学安全随机源,出错时停止生成。种子和私钥需要使用适合密码学用途的安全随机源,例如密码学安全的伪随机数生成器(CSPRNG)。Ill Bloom 和 Qubic 说明了为什么一般用途的随机数生成器不能承担这一职责。如果预期随机源不可用或运行失败,应停止生成,不能悄悄改用更弱的替代方案。
在数据转换中保留熵。安全随机源的输出在编码、采样或跨接口传递时,都需要保留足够的不确定性。XRPH Wallet 说明,仅看输出是否正常并不够。输入类型、长度和编码约束应防止意外截断或有偏映射缩小实际密钥空间。
确保部署代码使用预期随机源。Coldcard 说明,系统配备了安全随机源,不代表应用实际调用的就是它。从集成、编译到部署,随机源选择都必须正确,包括出错和回退处理。设计中对 RNG 的假设,需要在目标平台实际运行的程序中成立。
4.2 防止签名过程泄漏秘密
上述要求约束了密钥如何生成。要在使用时继续保持秘密性,还需要正确处理签名 nonce。具体要求取决于签名方案;下面除案例中的 Schnorr 和 Ed25519 外,也列出 ECDSA 的相关要求。
ECDSA:保持 nonce 的秘密性和不可预测性,同一密钥不能为不同消息复用 nonce。遵循 RFC 6979 (来源:RFC 6979, Deterministic Usage of DSA and ECDSA) 的确定性生成方法,可以避免每次签名都依赖新的外部随机数,但实现仍需防范侧信道泄漏和故障攻击。
EdDSA:保留 nonce 派生所需的全部秘密输入。SecondFi 展示了遗漏秘密前缀、仅使用公开数据的后果。封装代码和密钥适配层必须完整传递 RFC 8032 (来源:RFC 8032, Edwards-Curve Digital Signature Algorithm (EdDSA)) 要求的秘密输入,不能仅凭签名能够通过验证就认定输入正确。
Schnorr:保持方案要求的 nonce 分布和数据表示。Zilliqa 说明,即使 nonce 没有重复、也无法被完全预测,只要丢失了部分随机性,仍可能不安全。数据处理和标量转换不能引入可被利用的取值约束或偏差。
经过充分审查的库可以减少实现错误,但不能代替对集成代码的检查。调用方如果传入了不完整或损坏的数据,即使底层签名算法实现正确,系统仍可能不安全。
4.3 测试安全属性
要判断这些要求是否落实,需要沿实际运行的代码检查输入如何被处理。集成错误也可能产生有效输出,因此验证还必须覆盖正常功能测试容易遗漏的路径。下表将密钥生成和签名要求归纳为具体的审查目标:

这些目标需要结合多种方法验证。代码审查追踪秘密数据和随机输入如何到达实际部署的生成与签名函数。异常路径测试和故障注入用于检查正常前提不成立时的行为;已知答案测试与独立实现比对可以发现偏离方案规范的问题。
统计测试能够发现明显偏差,但地址生成成功、签名验证通过或统计测试通过,都不能单独证明密码学实现安全。应结合这些证据,核对随机源、输入处理和方案要求。
第三方安全团队或专家可以通过代码与集成审计补充内部验证,尤其是随机源、密钥派生和签名库之间的接口。审计范围应包括依赖版本、构建配置和实际执行路径。发现问题时,应明确受影响的安全属性,并提供可复现证据和回归检查。
实现或依赖发生变更后,仍需重新验证。
5. 结论
六起事件揭示了实现错误危及受保护密钥的两种方式:生成时缩小密钥的候选范围,或签名时暴露已有密钥的信息。本文据此总结生成与签名的实现要求,并给出支付基础设施中部署代码的审查和测试目标。
对支付机构而言,安全存储保护的是存储中的秘密信息,无法弥补可预测的密钥生成或签名中的信息泄漏。所需安全属性必须在依赖库、适配层和部署过程中保持不变。因此,审查和测试应覆盖完整实现,不能仅凭地址和签名有效就推断系统安全。
这些实现要求与 Beyond Secure Storage 第一篇讨论的流程控制相互补充。第一篇分析流程风险、预防检查,以及检查未能阻止未经授权操作时的损失控制;第二篇关注可能让攻击者独立签名的实现缺陷。
密码学运算正确,不代表业务授权有效;业务审批通过,也不代表签名密钥无法被预测或恢复。支付与签名系统需要同时设计和验证业务授权流程与底层密码学实现。
本文深入剖析六起真实事件,揭示密钥生成与签名过程中因弱随机数、熵损失和nonce缺陷导致私钥可被恢复的风险。重点涵盖Ill Bloom、Coldcard等密钥生成漏洞,以及Zilliqa、SecondFi等签名nonce设计错误,强调密码学安全随机源使用、熵保留、nonce保密性等核心安全属性,并提出覆盖依赖集成、构建配置与运行路径的验证方法。
183

被折叠的 条评论
为什么被折叠?



