在过去的几年里,零知识证明已成为 web3 行业的明星,尤其是在那些将安全和隐私放在首位的协议中。虽然开发者们仍在完善这一技术概念的不完善之处,但其实施已经提升了整个行业的标准。
在 Galxe 身份协议的安全审查中,我们发现 snarkjs 库中存在一个可能被认为是关键的安全漏洞——这是一个 zkSNARK 方案的 JavaScript 实现,也是生产中最受欢迎的零知识证明库。这个安全漏洞被描述为一个漏洞,可能会给协议带来不可预见的风险和威胁,导致恶意方利用资金或数据。
在这篇文章中,我们将剖析这个漏洞的根本原因以及 Galxe 如何为 snarkjs 库实施解决方案。
零知识证明(ZKPs)依赖于证明系统算法,这是一组结构化的计算,用于生成验证声明真实性的证明。这允许验证者确认其有效性,而无需访问底层数据。然而,基于 ZKP 的系统安全性不仅取决于加密原理,还取决于系统的实现方式。
在此次实现的核心是电路编程,这是一种特殊的证明代码形式。与硬件电路类似,零知识证明(ZKP)电路强制执行严格的约束:不允许递归、不允许复杂循环,并且必须预先确定固定次数的迭代。这种设计选择防止了低效和过度计算,但也引入了潜在的限制和风险。
一个广泛采用的实现零知识证明(ZKPs)的库是 snarkjs ,它被 Tornado Cash、Semaphore 和 Dark Forest 等协议在生产中使用。它有两个关键原因受到青睐:
经过验证的可靠性:许多以隐私为重点的协议都依赖它,使其成为生产中最经得起考验的选项。
基于浏览器的证明生成:与一些替代方案不同,snarkjs 支持在浏览器中生成证明,这对于通过保持计算本地化而不是依赖外部服务器来维护用户隐私至关重要。
然而,尽管它具有优势,但电路编程的限制以及对特定证明库(如 snarkjs)的依赖引入了一个关键漏洞——如果不妥善解决,可能会被利用。
在我们的零知识证明(ZKP)系统探索中,我们发现了广泛使用的 snarkjs 库中的一个微妙但关键漏洞。这个问题尤其狡猾,因为它源于一个单字符的打字错误——一个容易忽略的小错误。鉴于 snarkjs 在 Tornado Cash、Semaphore 和 Dark Forest 等项目中的广泛采用,开发者通常对其可靠性抱有隐含的信任,可能会忽视彻底验证的需要。
这种漏洞是输入别名问题的表现,该问题已在各种 ZKP 实现中进行了记录。在 Solidity 智能合约中,证明的公共输入 X(X 是 Fq 类型)使用 uint256 类型存储,可以表示大于 q 的值。这导致在模运算后,多个整数映射到相同的 Fq 值,这种现象称为“输入别名”。例如,s、s+q 和 s+2q 都表示同一个点。由于 q 是循环群的阶,任何通过添加 q 的倍数形成的整数仍然满足验证。在 uint256 中,最多有 uint256_max/q 个不同的整数可以对应同一个点,这意味着证明集可以有最多五个匹配的哈希值通过合约验证。这意味着用户可以使用一组值生成一个证明,并错误地将其作为对另一组值的有效证明接受,这可能导致金融或基于身份的攻击。
Snarkjs 生成了错误的 Groth16Verifier 合约,该合约未能正确验证公共信号是否在预期的标量字段范围内。具体来说,错误的代码检查公共信号是否小于代码中称为 q 的常量变量(基字段大小),而不是代码中称为 r 的常量变量(标量字段大小)。这种错误的范围验证导致某些值被错误地解释。
潜在风险包括:
别名证明:攻击者可以生成看似有效但实际上是欺诈的证明。
双重支出:在资产赎回场景中,例如 Tornado Cash 中,这一缺陷可能允许恶意用户多次提取资金。
身份证明被篡改:依赖于数字证明的系统,如年龄或分数验证,可能会被伪造数据欺骗。
为了说明,考虑一个基于零知识证明(ZKP)的系统,用于验证用户的 100 美元余额。由于这个漏洞,攻击者可以构建一个别名证明,错误地声称拥有 100 美元 + R,其中 R 是一个天文数字。由于验证合约没有正确验证别名输入,这个证明被接受,允许攻击者提取比他们实际拥有的多得多的金额。
这个问题特别影响数值小于 q - r 的公共信号,其中:
q−r=147946756881789318990833708069417712966
鉴于这一范围的广阔,众多现实世界应用可能存在漏洞。
例如,考虑以下 Circom 电路,用于验证输入是否小于 100:
pragma circom 2.1.5;
include "circomlib/circuits/comparators.circom";
template Main() {
signal input v;
signal output tinyVal;
tinyVal <== v;
component lte = LessEqThan(252);
lte.in[0] <== v;
lte.in[1] <== 100;
lte.out === 1;
}
component main = Main();
在这个电路中,verifyProof 函数生成了一段 Solidity 代码,该代码错误地返回了公共输入 1 和 1 + r 都为 true。因此,攻击者可以构造一个别名证明,其中将 1 + 21888242871839275222246405745257275088548364400416034343698204186575808495617 视为小于 100,从而有效地破坏了证明验证逻辑。
此漏洞为恶意行为者伪造证据的通道,导致:
重复交易:利用资产赎回协议。
Sybil 攻击:启用创建多个欺诈身份。
无法量化的财务风险:随着时间的推移,潜在损失评估变得越来越困难。
甚至作为以太坊隐私与扩展探索(PSE)下的一项重要项目,Semaphore v4 也受到了类似类别的漏洞影响。
认识到这一漏洞的严重性,我们在官方发布前提交了一个拉取请求来解决这个问题。修复已合并到 snarkjs 的最新更新中,缓解了我们识别出的特定漏洞。然而,暴露程度取决于每个项目如何使用 snarkjs 以及如何实现其证明验证逻辑。依赖于该库的协议应进行彻底审查,以确保它们不会受到类似风险的影响。
尽管我们在发布前提交了修复,但这一事件凸显了一个更广泛的安全问题:即使是广泛使用的加密工具也可能包含多年未被注意到的细微缺陷。使用 snarkjs 生成其 Solidity 验证器合约的项目必须保持警惕,因为这个漏洞展示了即使是证明验证中的微小疏忽也可能产生深远的影响。
Galxe 首席技术官夏宇民补充道:“安全通常被比作一个木桶,整个系统的稳健性取决于其最薄弱的环节。在 ZK 应用中,前所未有的详细安全审查至关重要。ZK 算法、电路设计、应用架构或工程实践中的任何弱点都可能对生产造成灾难性的后果。”
Galxe 是一个去中心化超级应用和 web3 最大的链上分发平台。通过其强大的基础设施和产品套件,包括模块化的 AI、数字身份、区块链技术——Quest、Passport、Score、Compass 和 Alva —— Galxe 支持开发先进、用户友好的 web3 应用,重点在于安全且自主的数字身份管理。最近推出的 Gravity,一个利用 Galxe 基础设施的 Layer 1 全链平台,使开发者能够利用 Galxe 的 2000 万用户,并创建帮助全球用户进入 web3 的新产品。
Gravity 是一个为大规模采用而构建的高性能 Layer 1 区块链,为超过 2500 万用户提供生态系统。由 Galxe 构建,Gravity 实现了超过每秒1千兆的吞吐量、亚秒级最终确定性,同时通过管道化的 AptosBFT 共识引擎和 Grevm(Gravity EVM),一个并行 EVM 运行时,通过再质押技术保持 PoS 安全性。