1. 引言
Trail of Bits 正在公开披露 elliptic 中的两个漏洞。elliptic 是一个被广泛使用的 JavaScript 椭圆曲线密码学库,每周下载量超过 1000 万次,被近 3000 个项目所使用。这些漏洞源于缺失的模约减以及缺失的长度检查,分别可能导致攻击者伪造签名,或导致合法签名无法被正确验证。
其中一个漏洞在 90 天的披露期(于 2024 年 10 月结束)后仍未被修复。截至本文发布时,该问题仍然未得到解决。
- https://github.com/indutny/elliptic/(JavaScript,上次更新时间为2024年11月)
- https://github.com/C2SP/wycheproof(Go,更新活跃)
这些漏洞是通过使用 Wycheproof 发现的。
Wycheproof 是一组测试向量集合,旨在针对已知漏洞测试各种密码算法,更多详情可参看Wycheproof指南。
本文将介绍Trail of Bits团队是如何使用 Wycheproof 测试 elliptic 库的,这些漏洞的工作原理,以及它们如何被利用来伪造签名或阻止签名验证。
2. 方法论(Methodology)
在 Trail of Bits 实习期间,Markus Schiffermuller 为《Testing Handbook》中新增的密码学测试章节撰写了一份详细指南,介绍如何使用 Wycheproof(参见密码学测试章节)。Markus Schiffermuller决定将 elliptic 库作为该指南中的一个真实案例研究,从而得以发现本文讨论的这些漏洞。
按照Wycheproof指南,为 elliptic 包编写了一个 Wycheproof 测试框架。随后,分析了 Wycheproof 提供的各种失败测试用例所覆盖的源代码,以判断它们是误报还是真实漏洞。在理解这些测试用例失败原因的基础上,为每个漏洞编写了概念验证(PoC)代码。在确认这些问题确实存在之后,启动了协调披露流程。
3. 发现的问题(Findings)
总计识别出了五个漏洞,对应五个 CVE。其中三个漏洞是较为轻微的解析问题。通过向仓库提交公开的 Pull Request 披露了这些问题,并随后申请了 CVE 编号以便进行跟踪。
另外两个问题则更为严重。通过 GitHub 的安全公告(advisory)功能对它们进行了私下披露。以下是这些漏洞的部分细节。
- 1)CVE-2024-48949:EdDSA 签名可变性(Signature Malleability)
- 2)CVE-2024-48948:带前导零哈希值导致的 ECDSA 签名验证错误
3.1 CVE-2024-48949:EdDSA 签名可变性(Signature Malleability)
该问题源于缺失的越界检查,而这一检查在 NIST FIPS 186-5 的第 7.8.2 节 "HashEdDSA Signature Verification" 中有明确规定:
将签名的前一半解码为点 R R R,将签名的后一半解码为整数 s s s。验证整数 s s s 是否落在范围 0 ≤ s < n 0 \leq s < n 0≤s<n 内。
在 elliptic 库中,用于验证 s s s 是否满足 0 ≤ s < n 0 \leq s < n 0≤s<n(即不超出生成元点阶 n n n)的检查从未执行。这一漏洞允许攻击者伪造新的有效签名 s i g ′ sig' sig′,不过前提是攻击者已知一组有效的消息与签名对 ( m s g , s i g ) (msg, sig) (msg,sig)。
Signature = ( m s g , s i g ) s i g = ( R ∣ ∣ s ) s ′ m o d n = s \begin{aligned} \text{Signature} &= (msg, sig) \\ sig &= (R||s) \\ s' \bmod n &= s \end{aligned} Signaturesigs′modn=(msg,sig)=(R∣∣s)=s
为防止该伪造攻击,需要实现如下检查:
javascript
if (sig.S().gte(sig.eddsa.curve.n) || sig.S().isNeg()) {
return false;
}
伪造的签名可能会破坏协议共识。一些协议会正确地将伪造的消息-签名对判定为无效,而使用 elliptic 库的系统却可能会接受它们。
3.2 CVE-2024-48948:带前导零哈希值导致的 ECDSA 签名验证错误
第二个问题涉及 ECDSA 实现:合法的签名可能无法通过验证。
以下是失败的 Wycheproof 测试用例:
-
testvectors_v1/ecdsa_secp192r1_sha256_test.json\]\[tc296\] 特殊哈希用例
这两个测试用例失败的原因在于:它们使用了一个精心构造的哈希值,该哈希值包含 4 个前导零字节,这是对十六进制字符串 343236343739373234 进行 SHA-256 哈希后的结果:
00000000690ed426ccf17803ebe2bd0884bcd58a1bb5e7477ead3645f356e7a9
以 secp192r1 曲线的测试用例为例,说明为何签名验证会失败。
负责椭圆曲线签名验证的函数位于 lib/elliptic/ec/index.js 中:
c
EC.prototype.verify = function verify(msg, signature, key, enc) {
msg = this._truncateToN(new BN(msg, 16));
...
}
消息必须在传入 verify 函数之前进行哈希处理,这一步发生在 elliptic 库之外。根据 FIPS 186-5 第 6.4.2 节 "ECDSA Signature Verification Algorithm" 的规定,消息哈希值需要根据椭圆曲线基点的阶 n n n 进行调整:
如果 log 2 ( n ) ≥ hashlen \log_2(n) \ge \text{hashlen} log2(n)≥hashlen,则令 E = H E = H E=H。
否则,令 E E E 等于 H H H 的最左侧 log 2 ( n ) \log_2(n) log2(n) 位。
为了实现这一点,会调用 _truncateToN 函数来执行必要的调整。在调用该函数之前,已经哈希过的消息 msg 会通过 new BN(msg, 16) 从十六进制字符串或数组转换为一个大整数对象。
c
EC.prototype._truncateToN = function _truncateToN(msg, truncOnly) {
var delta = msg.byteLength() * 8 - this.n.bitLength();
if (delta > 0)
msg = msg.ushrn(delta);
...
};
变量 delta 用于计算哈希值的位长度与当前曲线生成元阶 n n n 的位长度之间的差值。如果 msg 占用的位数多于 n n n,就会按该差值向右移位。
在这个特定测试用例中,使用的是 secp192r1 曲线(192 位)以及 SHA-256(256 位)。因此,哈希值应当右移 64 位,以保留最左侧的 192 位。
elliptic 库中的问题在于:new BN(msg, 16) 这一转换会移除前导零,导致得到的哈希值长度变短,占用的字节数减少。
690ed426ccf17803ebe2bd0884bcd58a1bb5e7477ead3645f356e7a9
在进行 delta 计算时,msg.byteLength() 因而返回的是 28 字节,而不是原本的 32 字节。
javascript
EC.prototype._truncateToN = function _truncateToN(msg, truncOnly) {
var delta = msg.byteLength() * 8 - this.n.bitLength();
...
};
这种错误计算导致得到的 delta 为 32 = ( 28 ∗ 8 − 192 ) 32 = (28*8 − 192) 32=(28∗8−192),而不是正确的 64 = ( 32 ∗ 8 − 192 ) 64 = (32*8 − 192) 64=(32∗8−192)。
因此,消息哈希并未被正确右移,从而导致签名验证失败。当消息哈希中包含足够多的前导零时,这个问题会使有效签名被错误地拒绝,其发生概率约为 2 − 32 2^{-32} 2−32。
为修复该问题,应当在验证函数中增加一个额外参数,用于显式传入哈希长度:
javascript
EC.prototype.verify = function verify(msg, signature, key, enc, msgSize) {
msg = this._truncateToN(new BN(msg, 16), undefined, msgSize);
...
}
EC.prototype._truncateToN = function _truncateToN(msg, truncOnly, msgSize) {
var size = (typeof msgSize === 'undefined')
? (msg.byteLength() * 8)
: msgSize;
var delta = size - this.n.bitLength();
...
};
4. 持续测试的重要性
这些漏洞很好地说明了持续测试对于确保被广泛使用的密码学工具在安全性和正确性方面的重要意义。尤其是 Wycheproof 以及其他持续维护的密码学测试向量集合,都是保障高质量密码库的优秀工具。
建议将这些测试向量(以及其他相关测试)纳入 CI/CD 流水线中,使其在每次代码变更时都会重新运行。这可以确保代码库在当前和未来都能抵御这些特定的密码学问题。
5. 协调披露时间线
在漏洞披露过程中,Trail of Bits团队 使用了 GitHub 集成的安全通告功能进行私下披露,并以该 报告模板 作为报告结构的参考。
- 2024 年 7 月 9 日:在使用 Wycheproof 对 elliptic 库进行测试时,发现了失败的测试向量。
- 2024 年 7 月 10 日:确认 ECDSA 和 EdDSA 模块均存在问题,并编写了 PoC 脚本及修复方案。
5.1 针对 CVE-2024-48949的协调披露时间线
针对 CVE-2024-48949的协调披露时间线:
- 2024 年 7 月 16 日:通过 GitHub 安全通告功能,向 elliptic 库维护者私下披露了 EdDSA 签名可变性问题,并创建了一个包含修复方案的私有 Pull Request。
- 2024 年 7 月 16 日:elliptic 库维护者确认了该 EdDSA 问题,合并了Trail of Bits团队所提出的 修复,并发布了一个未公开披露该问题的新版本。
- 2024 年 10 月 10 日:Trail of Bits团队向 MITRE 申请了 CVE 编号。
- 2024 年 10 月 15 日:由于自私下披露起已过去 90 天,该漏洞正式公开。
5.2 关于 CVE-2024-48948的协调披露时间线
关于 CVE-2024-48948的协调披露时间线:
- 2024 年 7 月 17 日:通过 GitHub 的安全通告(security advisory)功能,向 elliptic 库的维护者私下披露了 ECDSA 签名验证问题,并创建了一个包含拟议修复方案的私有 Pull Request。
- 2024 年 7 月 23 日:联系维护者,希望在 ECDSA 的 GitHub 安全通告中添加一名额外的协作者,但未收到任何回应。
- 2024 年 8 月 5 日:再次联系维护者,请求确认该 ECDSA 问题,并再次请求添加一名额外的协作者到 GitHub 安全通告中,仍然未收到回复。
- 2024 年 8 月 14 日:第三次联系维护者,请求确认 ECDSA 问题,并再次请求在 GitHub 安全通告中添加额外协作者,依旧没有得到回应。
- 2024 年 10 月 10 日:向 MITRE 申请了一个 CVE 编号。
- 2024 年 10 月 13 日:Wycheproof 测试集的开发者 Daniel Bleichenbacher 独立发现并披露了与本次发现相关的 issue #321。
- 2024 年 10 月 15 日:由于自私下披露以来已过去 90 天,该漏洞正式公开。
参考资料
1\] Trail of Bits团队2025年11月博客 [We found cryptography bugs in the elliptic library using Wycheproof](https://blog.trailofbits.com/2025/11/18/we-found-cryptography-bugs-in-the-elliptic-library-using-wycheproof/)