返回博客

超越安全存储,第2部分:钱包密码学的安全实现

Code Auditing
2026年11月11日
阅读约 18 分钟
核心要点
  • 即使不读取存储的私钥副本,也可能恢复私钥。

  • 六起事件表明,随机性不足、熵损失和签名随机数缺陷会使受到保护的私钥失去安全保障。

  • 支付机构需要验证其实现能否在密钥生成过程中保持不可预测性,并在签名过程中确保私钥保密。

超越安全存储第 1 部分探讨了支付系统自身签名能力被滥用的问题。第二部分考察了另一条获取未经授权签名的途径:密码学实现缺陷使攻击者能够恢复私钥并独立签名。核心问题是:攻击者能否在不读取受保护的私钥副本的情况下重建私钥?

第 1 节确立了密钥生成、存储和签名的安全要求。第 2 节和第 3 节分析了四起密钥生成事件和两起签名事件,解释其中的实现错误及其后果。第 4 节从这些案例中提炼设计和实现原则,并说明如何验证所需的安全属性。

1. 密钥生命周期各阶段的安全属性

密钥需要经过生成、存储和用于签名三个阶段。如下图所示,每个阶段都有不同的安全要求。

在使用 BIP-39 助记词和 BIP-32 密钥派生的分层确定性钱包中,具备密码学安全性的随机数据会被编码为助记词,再转换为种子。该种子用于派生私钥、公钥和钱包地址 [1, 2]。

种子和私钥生成后,需要通过加密或硬件隔离加以保护。随后,签名过程使用私钥生成可公开验证的签名。根据所用方案,签名还可能需要安全生成的随机 nonce,或从秘密材料确定性派生的 nonce。

这些阶段的安全要求各不相同:

  1. 生成时的不可预测性。 攻击者不得能够预测或以可行的方式穷举控制钱包的秘密。
  2. 存储时的保密性。 未经授权的主体不得通过软件、物理接口、备份、日志或远程服务读取秘密材料。
  3. 使用时不泄露秘密。 即使存在大量可公开观察的签名,签名过程也不得泄露足以重建秘密的信息。

硬件钱包、安全元件和可信执行环境(TEE)可以增强保密性。但仅有这些措施,并不能保证另外两项属性。可预测的密钥放入安全存储后仍然可预测;即使私钥字节与主机隔离,签名所暴露的数学信息仍然可被利用。因此,攻击者可能在不读取存储的私钥副本的情况下,恢复受影响的密钥并访问其控制的资产。

接下来两节将分析实现如何违反这些要求:生成缺陷可能使密钥变得可预测,而签名缺陷可能使密钥能够从公开签名中被恢复。这些案例为第 4 节的实现原则和验证目标提供了依据。

2. 密钥生成缺陷:可预测的私钥

密钥生成必须在每一步都保持不可预测性。熵源必须适合密码学用途,其状态不能在实际条件下被穷举,而从随机字节转换为种子或私钥的过程必须保留预期的不确定性。128 位的安全目标要求秘密空间不能因实现缺陷而缩小到可行的搜索范围。

下表概述了生成过程丧失这一属性的四种方式:弱随机性的库生成器、错误的随机数生成器(RNG)绑定、造成信息损失的输入转换,以及初始状态受限的生成器。影响金额来自涵盖不同时段的报告,并非在同一日期统计的总额。

事件 相关时段 报告的影响金额(约) 下文分析的失效方式
Ill Bloom 2026年5月至7月 截至7月 13 日,下限约为 500万 USD 生成助记词时使用了弱随机性
Coldcard 自 2026年7月30日 起;8月 14 日的统计 1.1270亿 USD 固件中的 RNG 路径错误
XRPH Wallet 2026年9月3日 45万 USD 钱包生成输入可预测且受到严重限制
Qubic Wallet 2026年10月17日–2026年10月18日 130万 USD 使用 Math.random() 不安全地生成种子

2.1 Ill Bloom:钱包依赖项中的弱随机性

在 Ill Bloom 事件中,一些软件钱包使用存在漏洞的 crypto-js 版本生成 BIP-39 恢复助记词。该库的 CryptoJS.lib.WordArray.random() 没有从具备密码学安全性的来源获取熵,而是使用以 Math.random() 初始化的自定义乘法进位(MWC)生成器。将此函数用作安全敏感熵源的钱包继承了该生成器的弱点,即使后续的助记词和密钥派生步骤正确遵循了相关标准也是如此 [3]。

存在漏洞的 crypto-js v3.1.9-1 实现 [4] 包含以下典型逻辑:

  random: function (nBytes) {
      var words = [];

      var r = (function (m_w) {
          var m_w = m_w;
          var m_z = 0x3ade68b1;
          var mask = 0xffffffff;

          return function () {
              m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask;
              m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask;
              var result = ((m_z << 0x10) + m_w) & mask;
              result /= 0x100000000;
              result += 0.5;
              return result * (Math.random() > .5 ? 1 : -1);
          }
      });

      for (var i = 0, rcache; i < nBytes; i += 4) {
          var _r = r((rcache || Math.random()) * 0x100000000);

          rcache = _r() * 0x3ade67b7;
          words.push((_r() * 0x100000000) | 0);
      }

      return new WordArray.init(words, nBytes);
  }

其中,r() 是基于 MWC 的自定义生成器,而 Math.random() 用于帮助初始化其状态或影响其状态。该伪随机数生成器(PRNG)产生的字节看起来与普通熵无异,但这一实现不具备密码学意义上的不可预测性。对于名义上的 128 位输出,有效候选空间约为 2 USD^{39};对于名义上的256位输出,则约为2USD47;对于名义上的 256 位输出,则约为 2 USD^{47} [3]。

这些较小的空间使攻击者能够在可行的条件下重建候选恢复助记词、派生相应的钱包地址,并与公开的区块链活动进行匹配。因此,无需从受害者设备中提取任何内容,也可能恢复秘密。遵循助记词标准无法弥补从旧版依赖项继承的弱随机性。

2.2 Coldcard:代码未使用的硬件随机数生成器

Coldcard 表明,即使设备具备安全的随机数来源,只要应用调用了另一个生成器,就仍然不够安全。虽然该设备配有硬件随机数生成器,但构建时的一处集成错误,导致种子生成路径中的 rng_get() 实际调用的是 MicroPython 的软件伪随机数生成器 Yasmarang。这个非密码学安全的伪随机数生成器使用设备固定的唯一 ID 和有限的时间信息作为种子,使其初始状态具有足够的可预测性,生成的钱包种子的有效熵因此大幅降低 [5]。

种子生成流程还混入了第二个 Yasmarang 生成器。在较旧的 Mk2/Mk3 设备上,第二个生成器从公开常量开始运行,没有贡献额外的熵。在较新的型号上,虽然引入了安全元件提供的随机性,但重新播种时仅保留了 32 位。因此,尽管生成的是 256 位的钱包种子,有效搜索空间仍大幅缩小。在其声明的攻击假设下,Coinkite 初步估计,Mk2/Mk3 上约有 2 USD^{40}$ 个候选种子,Mk4/Q/Mk5 上约有 2 USD^{72}$ 个 [6, 7]。

这一弱点使种子恢复变成了离线搜索:攻击者可以从候选种子推导地址,再与公开的区块链数据比较。实际成本取决于设备型号和攻击假设;2 USD^{72}$ 的估计值并不意味着其可行性与 2 USD^{40}$ 的情况相同。一旦匹配成功,对应的私钥便会暴露。

2.3 XRPH Wallet:缩小密钥空间的输入格式错误

生成路径还包括将随机输出转换为钱包 API 所要求的输入。在 XRPH Wallet 中,这一步转换丢弃了所生成输入的大部分内容。其 Android 应用使用 JavaScript 的 Math.random() 生成十进制数字块,将它们拼接成文本字符串,再把该字符串传给 Wallet.fromEntropy()。接收端代码将字符解释为较小的整数字节值,且只保留前 16 个位置。由于其中两个位置是固定的空格分隔符,最终只有 14 个可变十进制数字为生成的钱包贡献了熵 [8]。

padlock = { A, B, C, D, E, F, G, H };     // each = 6 digits from the line above
r6 =  A.join('') + ' ' + B.join('') + ' ' + C.join('') + ' ' + D.join('')
    + ' ' + E.join('') + ' ' + F.join('') + ' ' + G.join('') + ' ' + H.join('');
r5 = Wallet.fromEntropy;
r3 = r5.bind(Wallet)(r6);                 // a 55-char STRING is passed; fromEntropy wants 16 bytes

// compute entropy be like
const input = "528875 921568 528239 ...";  // decimal blocks
const entropy = Uint8Array.from(input).slice(0, 16);
// Result: [5,2,8,8,7,5,0,9,2,1,5,6,8,0,5,2]

经过这一转换,可生成的钱包输入约有 10 USD^{14}\approx 2^{46}$ 个;相比之下,按预期均匀采样 16 字节熵时,空间约为 2 USD^{128}$。前一个数字描述的是观察到的转换过程下可生成的候选输入,并非对 Math.random() 的统计熵所作的密码学证明。

两个问题叠加在一起:使用了不合适的随机数来源,以及 API 边界处的表示形式/编码错误。功能测试可能发现不了这两类问题,因为钱包仍能生成看似正常的地址并签署有效交易。公开调查证实密钥空间可被搜索以恢复密钥,但调查本身并未证明攻击者采用了哪一种获取密钥的途径。

2.4 Qubic Wallet:55 字符种子背后的弱随机性

无论输出字符串看起来有多长,生成器的初始状态都可能限制种子的可能取值。Qubic 说明了这一问题:据报道,其钱包失陷事件涉及使用 JavaScript 的非密码学安全随机数生成器 Math.random() 生成的种子。受影响的环境包括 Apple 浏览器以及某些基于 JavaScriptCore 的较旧 iOS 应用版本 [9, 10]。

存在漏洞的生成逻辑及其修复方式可概括如下 [11]:

// Vulnerable seed generation (abridged)
seed += letters[Math.floor(Math.random() * 26)];

// Corrected approach (schematic excerpt of the fix)
crypto.getRandomValues(bytes);
if (byte < 234) seed += letters[byte % 26];

调用 Math.random() 55 次,并不会提供 55 个独立的熵来源。在两次重新播种之间,每次调用伪随机数生成器都会以确定的方式推进其内部状态。对于固定的调用位置,如果初始种子为 SS,那么整个 55 字符的钱包种子就是确定性函数 W=F(S)W=F(S) 的结果。知道 SS 和该调用位置,就足以复现全部 55 个字符;多调用几次伪随机数生成器不会产生额外的熵。

可能的初始状态范围取决于运行时的初始化方式。在 2026年10月29日 获取的 JavaScriptCore 源代码快照显示,其伪随机数生成器使用 32 位种子初始化,因此对于固定的调用位置,最多有 2 USD^{32}$ 种序列。此处审查的 V8 源代码使用 64 位种子,因此这类序列最多有 2 USD^{64}$ 种。这些上限适用于所检查的初始化路径以及固定的调用位置。这些快照无法确定事件中使用的确切浏览器版本,也无法确定利用漏洞的成本。修复方案则改用 crypto.getRandomValues() 获取密码学安全的随机数,并使用拒绝采样(byte < 234),避免将字节映射为 26 个字母时产生偏差 [12, 13, 14]。

3. 签名实现缺陷:从公开签名恢复私钥

密钥安全生成后,在用于签名时也必须保持私密。每个签名都会向观察者公开数据,而实现缺陷可能使这些公开数据足以恢复私钥。ECDSA 和 Schnorr 使用每次签名专用的秘密标量(称为 nonce),来掩盖私钥对公开签名的影响。

nonce 必须保持私密、避免在同一密钥签署不同消息时重复使用,并满足相应方案对其分布的要求。确定性推导可以符合这些要求:仅凭公钥和已签名的消息,不能预测 nonce。

以简化的 Schnorr 关系式为例:

s=k−rd(modn),s=k-rd\pmod n,

其中,dd 是私钥,kk 是 nonce,rr 是根据公开信息(例如公钥和消息)计算的公开挑战值,ss 是公开的签名标量,nn 是子群的阶。如果 rr 可逆,已知 nonce 就会立即暴露 d=(k−s)r−1(modn)d=(k-s)r^{-1}\pmod n。

此外,即使无法完全得知 nonce,密钥仍可能泄露。多个 nonce 之间的约束或偏差,可能提供足够的信息,使人能够利用基于格的方法恢复密钥。其可行性取决于暴露的信息和可获得的签名。

下表区分了本节分析的两种随机数故障:Zilliqa 的随机数受到部分限制,而 SecondFi 的随机数可由公开数据计算得出。所报告的影响分别采用不同的历史统计时间范围。

事件 相关时期 报告的影响(约) 下文分析的故障模式
Zilliqa Ledger Wallet 2026年3月至7月 683,130,969.66 ZIL 存在偏差的 EC-Schnorr 签名随机数
SecondFi 2026年6月 所引用的代码证据未能确定 Ed25519 随机数派生错误

3.1 Zilliqa Ledger Wallet:每个 Schnorr 随机数都缺少 64 位

Zilliqa Ledger Wallet 事件涉及用于旧版 Zilliqa EC-Schnorr 交易的应用程序中的一个缺陷 [15]。该应用程序生成了随机数材料,却将错误的部分复制到最终的签名缓冲区中。它保留了八个字节的零填充,丢弃了八个字节的实际随机数据。因此,每个受影响的随机数都满足 0 USD \le k < 2^{192},$,而不是在约 256 位的 secp256k1 群阶范围内正常取值。

签名方程说明了限制随机数范围为何重要:

P=dG,Q=kG,r=H(compress⁡(Q)∥compress⁡(P)∥m)  n,s=k−rd(modn).\begin{aligned} P &= dG,\\ Q &= kG,\\ r &= H(\operatorname{compress}(Q)\|\operatorname{compress}(P)\|m)\bmod n,\\ s &= k-rd\pmod n. \end{aligned}

这里,GG 是群的生成元。整理可得

k=s+rd(modn).k=s+rd\pmod n.

因此,每个签名都提供了一个关于未知数 dd 的模运算关系,其中随机数以 2 USD^{192}$ 为上界。结合多个独立的关系,就形成了一个隐藏数问题(HNP)实例,可通过格约化求解。官方事后分析报告称,利用四个或更多受影响的签名即可恢复私钥。

安全元件无需泄露私钥字节。存在偏差的随机数使攻击者能够从已发布到链上的签名中恢复该密钥。

3.2 SecondFi:可由公开数据计算的签名随机数

在 SecondFi 事件中,错误的 Ed25519 随机数派生造成的问题不只是随机数受到部分限制:它使随机数可由公开数据计算得出。攻击者因此能够从一个公开可得的交易签名中恢复签名私钥 [16]。

对于 Ed25519 类签名,预期的构造方式是从秘密密钥材料中派生秘密标量 aa 和私有的随机数前缀。这里,A=aBA=aB 是公钥,BB 是 Ed25519 基点,MM 是被签名的消息,LL 是子群阶,HH 表示 SHA-512。示意如下:

r=H(secret nonce prefix∥M)  L,R=rB,h=H(R∥A∥M)  L,S=r+ha(modL).\begin{aligned} r &= H(\text{secret nonce prefix}\|M)\bmod L,\\ R &= rB,\\ h &= H(R\|A\|M)\bmod L,\\ S &= r+ha\pmod L. \end{aligned}

存在缺陷的实现却将 rr 设为 H(M)  LH(M)\bmod L。任何知道被签名消息的人都可以推导出 rr。由于 RR、AA 和 SS 都是公开的,观察者随后便可计算

a=(S−r)h−1(modL),a=(S-r)h^{-1}\pmod L,

前提是 hh 对 LL 取模后的值不为零。

与 Zilliqa 的随机数部分泄露不同,一个受影响的签名就足以恢复签名标量。这并不能自动证明原始助记词或分层钱包中的每个同级密钥也能被恢复。故障发生在密钥派生与签名之间的接口:所需的秘密信息本已可用,却从未传给随机数派生例程。

4. 安全实现原则与验证

综合这些案例可以看出,审查随机数源和签名算法时,还必须审查连接它们的代码。不可预测的随机数源必须生成不可预测的密钥,而这些密钥在签名过程中必须保持秘密。转换例程或库接口中的错误可能破坏其中任一要求。

第 4.1 节和第 4.2 节阐述密钥生成与签名的设计和实现原则。第 4.3 节说明,为评估已部署系统是否遵循这些原则,审查和测试应提供哪些证据。

4.1 在密钥生成过程中保持安全性

密钥生成案例表明,实现必须满足三项要求:

  1. 使用密码学安全的随机数源,并在发生故障时停止。 种子和私钥需要合适的密码学安全随机数源,例如密码学安全伪随机数生成器(CSPRNG)。Ill Bloom 和 Qubic 的案例说明,通用随机数生成器无法胜任这一用途。如果预期使用的随机数源不可用或发生故障,必须停止生成,而不是悄然改用安全性较弱的替代来源。

  2. 在数据转换过程中保留熵。 安全随机数源的输出在编码、采样或接口间传递时,必须保留所需的不确定性。XRPH Wallet 案例说明,仅有看似有效的输出并不足够。类型、长度和编码约束必须防止意外截断或有偏差的映射缩小有效密钥空间。

  3. 确保已部署代码使用预期的随机数源。 Coldcard 案例表明,系统中存在安全的随机数源,并不能证明应用程序实际调用的是哪个源。从集成、编译到部署,随机数源的选择都必须保持正确,错误处理和回退路径也不例外。对随机数生成器的设计假设,必须在目标平台上运行的可执行程序中成立。

4.2 在签名过程中保持秘密性

密钥生成要求保护密钥的创建过程。要在使用过程中保持密钥的秘密性,还需要正确处理随机数。具体要求取决于签名方案;除案例分析的 Schnorr 和 Ed25519 方案外,下文也涵盖 ECDSA。

  1. ECDSA: 保持随机数的秘密性和不可预测性,并防止同一密钥针对不同消息重复使用随机数。遵循 RFC 6979 [17] 进行确定性生成,可以避免每次签名都依赖新的外部随机数,但实现仍需防范侧信道泄露和故障攻击。

  2. EdDSA: 保留随机数派生所需的全部秘密输入。SecondFi 展示了遗漏私有前缀、仅使用公开数据的后果。包装层和密钥适配器必须传入 RFC 8032 [18] 要求的完整秘密输入,而不能把签名能够正常生成视为输入正确的证据。

  3. Schnorr: 保持方案要求的随机数分布及表示方式。正如 Zilliqa 案例所示,如果部分随机数据丢失,仅避免随机数重复和完全可预测仍不够。数据处理和标量转换不得引入可被利用的限制或偏差。

经过充分审查的库可以降低实现风险,但无法消除集成错误。如果调用方提供的输入不完整或已损坏,即使签名原语本身正确,使用起来仍不安全。

4.3 测试安全属性

要评估这些要求是否得到满足,审查必须跟踪输入如何流经实际运行的实现。有效的输出可能掩盖集成错误,因此验证还需要覆盖常规功能测试遗漏的路径。下表将密钥生成和签名要求转化为具体的审查目标:

审查领域 验证目标
熵的不可预测性 确定随机数源、初始化方式和有效种子空间能否抵御可行的预测或穷举
熵的保留 验证转换和采样能否保留所需的不确定性,而不发生意外截断或引入偏差
随机数生成器集成 确认已部署的可执行程序使用预期的随机数源,包括错误处理和回退路径
随机数的秘密性 验证随机数的生成或派生能否保持秘密性、包含全部必需的秘密输入,且无法仅凭公开数据重现
随机数的正确性 将随机数处理方式与方案要求进行比较,并在适用时使用已知答案测试和独立实现
依赖项安全性 审查密码学依赖项的版本和集成方式,并检查更新后是否仍保持所需行为

评估这些目标需要结合多种方法。代码审查追踪秘密输入和随机输入如何到达已部署的密钥生成与签名例程。异常路径测试和故障注入检验常规假设不成立时的行为;已知答案测试以及与独立实现的比较可以发现对方案的偏离。统计测试可以检测明显的偏差,但仅凭地址生成成功、签名验证通过或统计测试通过,都无法确立密码学安全性。应结合随机数源、输入处理和方案要求,评估各类证据。

第三方安全团队或专家可通过代码和集成审计补充内部验证,尤其应关注随机数源、密钥派生与签名库的交汇处。审查范围应涵盖依赖项版本、构建配置和实际执行的代码路径。审查结果应指出受影响的安全属性,并提供可复现的证据和回归检查。实现或其依赖项发生变更后,仍需进行验证。

卓越的 Web3 安全审计机构

在上线前验证设计、代码和业务逻辑

5. 结论

这六起事件揭示了实现错误可能破坏受保护密钥的两种方式:在生成过程中缩小可能的密钥范围,或通过签名泄露现有密钥的信息。本文将这些故障归纳为密钥生成和签名要求,并提出针对支付基础设施中已部署代码的审查与测试目标。

对于支付机构,安全存储保护的是已存储的秘密信息;它无法弥补可预测的密钥生成过程,也无法消除签名泄露的信息。所需的安全属性必须贯穿依赖项、适配器和部署过程。审查和测试需要检查完整的实现,而不是根据有效的地址和签名推断系统安全。

这些实现要求是《超越安全存储》第 1 部分所分析的工作流程控制措施的补充。第 1 部分分析工作流程风险、预防性检查,以及这些检查未能阻止未经授权的操作时如何控制损失。第 2 部分讨论可能使攻击者获得独立签名能力的实现缺陷。密码学操作正确执行,并不证明业务授权有效;业务审批有效,也不能证明签名密钥不可预测或无法被恢复。安全的支付和签名系统要求授权工作流程与底层密码学实现都经过设计和验证。

参考资料

订阅最新动态
超越安全存储,第 1 部分:设计安全的支付与签名流程
Security Insights

超越安全存储,第 1 部分:设计安全的支付与签名流程

攻击者不窃取私钥,未经授权的操作为何仍能获得有效签名?《Beyond Secure Storage》第 1 部分以 Bitget、Bybit 和 Binance 等事件为例,审视支付请求、交易构建、签名、执行和结算各环节中的授权失效问题。文章据此提出防止未经授权的操作并限制损失的设计原则,随后说明如何通过审查、审计、渗透测试和演练来验证这些防护措施。

NEAR Intents 漏洞利用事件:退款缺陷与安全启示
Security Insights

NEAR Intents 漏洞利用事件:退款缺陷与安全启示

NEAR Intents 漏洞利用事件中,攻击者利用超额退款事件,在存入的资产退还后仍保留内部余额。本文分析重构了交易回执流程及 BNB Chain 上七笔总计 3,865,021 USDT 的提现,区分了这些资金流出与项目方报告的资金已全部追回,并概述了开发和审计 NEAR 智能合约时需要考虑的安全问题。

损失约557万 USD:X100 Freezer、NEM 遭攻击|BlockSec周报
Security Insights

损失约557万 USD:X100 Freezer、NEM 遭攻击|BlockSec周报

本周报告涵盖 2026年10月19日–2026年10月25日 期间以太坊和 BNB Chain 上发生的两起安全事件。报告分析了利用闪电贷资金接管 X100 Freezer 治理权的事件,以及影响 BNB Chain 上一个 NEM 代币流动性池的税费结算缺陷。报告统计范围内的估计损失合计约 557万 USD:其中,具有代表性的 X100 交易造成约 ~475万 USD 的损失,NEM 流动性池事件造成约 ~82.0万 USD 的损失。该合计金额不包括另行报告的后续 X100 资金提取。

Web3 最佳安全审计方

在上线之前验证设计、代码与业务逻辑,对标业内最高安全标准。

BlockSec 审计
超越安全存储,第2部分:钱包密码学的安全实现