本文介绍了使用Noir构建零知识证明(ZKP)应用时可能存在的安全漏洞。主要讨论了有限域算术的陷阱、意图与实现不符以及隐私泄露等常见问题,并提供了审计关注点和示例代码,强调了在设计零知识电路时进行严格约束和持续审查的重要性。
在零知识证明(ZKP)的世界中,Noir 已经成为一种强大的、对开发者友好的语言,用于构建保护隐私的应用程序。Aztec 将 Noir 设计为一种植根于 Rust 原则的领域特定语言(DSL),它简化了复杂算术电路的创建——这些电路定义了要证明的计算,而无需泄露敏感信息。Noir 的设计抽象了大部分密码学复杂性,但安全的电路仍然依赖于仔细的实现、全面的测试和独立的审查。
Noir 抽象了低级别的密码学复杂性,允许开发者专注于逻辑而不是电路优化。与早期的框架(如 Circom 或 ZoKrates)不同,Noir 利用类似 Rust 的语法和工具来减少样板代码和人为错误。然而,这种抽象并不能完全消除风险:Noir 编译器自动将 Noir 电路转换为抽象电路中间表示(ACIR),然后由 Barretenberg、Groth16 和 PLONK 等证明系统处理,以生成和验证证明。这种抽象消除了处理复杂数学细节的负担。但是,开发者仍然有责任确保电路逻辑本身的正确性。
这些电路的安全性基于三个基本属性:
电路中的一个缺陷可能会破坏这些支柱中的一个或多个,从而导致不安全或不正确的证明。
Noir 电路中常见的安全漏洞
下面,我们探讨威胁基于 Noir 的系统的可靠性、完备性或零知识属性的漏洞:
1. 有限域中的算术陷阱
Noir 在有限域上运行,其中对原生 Field 类型的算术运算会以素数为模进行包装。这为系统的可靠性带来了微妙的风险:
审计重点:
例子:
考虑以下示例,其中两个私有 `Field` 输入 $x,y$ 的和被约束为等于公共 `Field` 输入 $z$:
fn main(x : Field, y : Field, z : Field) {
assert(x + y == z);
}
如果目的是对域的素数 $p$ 执行模检查,则上述实现是正确的:$x + y = z \mod p$。但是,如果目的是检查总和 $x+y$ 而不减少 $\mod p$(这将隐式执行),那么上述可能会导致错误的结果。例如,对于 $p=7$ 和 $x=5,y=4$,我们可能想要检查 $5+4=9$,而不是 $5+4=2\mod 7$。为了避免这种歧义,需要确保该域足够大。
作为旁注,与将值约束在预期范围内相关,我们注意到 Noir 为固定大小的输入(如 `Field` 和数组)添加了隐式的 `assert`。因此,显式的 `assert` 仅对于动态对象(如向量、切片等)才是必需的。
2. 意图与实现不匹配
即使是文档完善的电路也可能存在开发者意图与代码之间的差异。例如,一个旨在强制执行“用户 X 拥有 NFT Y”的电路可能会在 X 已经出售 Y 的情况下意外地验证证明。这些不匹配通常源于不明确的规范或对密码学原语的误解。
审计重点:
例子:
考虑一个授权场景,其中用户被授予对某些资源的访问权限,基于对密钥的了解。例如,这可以是一个更大的基于 ZK 的登录系统的一部分,其中授权是在不泄露用户凭据的情况下授予的。
以下实现引入了一个相当简单但具有说明性的漏洞,该漏洞可能源于不够详细的规范或对密码学假设的误解:
fn main(user_id : Field, sk : Field, auth_hash : Field) {
assert(bn254::hash_1([sk]) == auth_hash);
println("access granted to user {}", user_id);
}
虽然该电路通过约束 bn254::hash_1([sk]) == auth_hash 来证明对密钥的了解,但它未能强制执行用户 ID 和密钥之间的任何绑定。因此,任何知道密钥的人都可以为任何用户 ID 生成有效的证明。一个修复方法是使用一些域分离常量从密钥派生用户 ID,如下所示:
fn main(user_id : Field, sk : Field, auth_hash : Field) {
let computed_user_id = bn254::hash_1([sk, 12345]);
assert(computed_user_id == user_id);
assert(bn254::hash_1([sk]) == auth_hash);
println("access granted to user {}", user_id);
}
请注意,所示的漏洞本身并非 Noir 特有的,并且可能适用于其他电路 DSL,例如 Circom。然而,Noir 的高级类 Rust 语法(这也是其无可争议的优势之一)可能使类似的错误更难发现。
3. 隐私泄露
隐私失败会危及零知识属性,并通过以下两个主要途径暴露敏感数据:
pub 的值会意外地对验证者可见。隐式泄露 (Implicit Leaks):公共输出可能与私有输入相关联。
审计重点:
public 或 private?例子:
一个天真地完全泄露假定的私有输入(age)的例子如下:
fn main(age : Field) -> pub Field {
return age;
}
一个修复方法是省略返回私有输入(age):
fn main(age : Field) {
}
另请注意,当一个电路暴露来自小域的密钥输入的公共哈希时,攻击者可以对该值进行暴力破解。例如,考虑上面的示例,其中电路还返回 age 上的公共哈希。尽管该哈希在隔离时是密码学安全的,但可能的年龄范围有限(只有大约 82 = 100-18 个潜在值,假设最大年龄为 100)允许攻击者快速迭代每个候选值,计算其哈希,并将其与公共哈希进行匹配。通过这样做,攻击者可以轻松地恢复实际年龄,从而损害输入的隐私。
零知识电路的成功或失败取决于严格的约束设计。通过将每个业务规则转换为显式断言,防止有限域算术的意外情况,并将每个公共接口视为潜在的侧信道,团队可以同时保持可靠性和隐私。随着 Noir 生态系统的成熟,持续的同行评审和周到的审计(无论是内部还是外部)仍然是值得信赖的 ZK 应用程序最可靠的途径。
对审计 Noir 电路有任何疑问吗?
- 原文链接: openzeppelin.com/news/de...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!