构建安全Noir电路的开发者指南

本文介绍了使用Noir构建零知识证明(ZKP)应用时可能存在的安全漏洞。主要讨论了有限域算术的陷阱、意图与实现不符以及隐私泄露等常见问题,并提供了审计关注点和示例代码,强调了在设计零知识电路时进行严格约束和持续审查的重要性。

介绍

在零知识证明(ZKP)的世界中,Noir 已经成为一种强大的、对开发者友好的语言,用于构建保护隐私的应用程序。Aztec 将 Noir 设计为一种植根于 Rust 原则的领域特定语言(DSL),它简化了复杂算术电路的创建——这些电路定义了要证明的计算,而无需泄露敏感信息。Noir 的设计抽象了大部分密码学复杂性,但安全的电路仍然依赖于仔细的实现、全面的测试和独立的审查。

Noir 和零知识系统的核心原则

Noir 抽象了低级别的密码学复杂性,允许开发者专注于逻辑而不是电路优化。与早期的框架(如 Circom 或 ZoKrates)不同,Noir 利用类似 Rust 的语法和工具来减少样板代码和人为错误。然而,这种抽象并不能完全消除风险:Noir 编译器自动将 Noir 电路转换为抽象电路中间表示(ACIR),然后由 Barretenberg、Groth16 和 PLONK 等证明系统处理,以生成和验证证明。这种抽象消除了处理复杂数学细节的负担。但是,开发者仍然有责任确保电路逻辑本身的正确性。

这些电路的安全性基于三个基本属性:

  1. 可靠性 (Soundness): 恶意的证明者无法伪造错误声明的证明。
  2. 完备性 (Completeness): 诚实的证明者总是可以为正确的声明生成有效的证明。
  3. 零知识性 (Zero-Knowledge): 证明除了显式声明的公共输出外,不会泄露任何关于私有输入的信息。

电路中的一个缺陷可能会破坏这些支柱中的一个或多个,从而导致不安全或不正确的证明。

Noir 电路中常见的安全漏洞

下面,我们探讨威胁基于 Noir 的系统的可靠性、完备性或零知识属性的漏洞:

1. 有限域中的算术陷阱

Noir 在有限域上运行,其中对原生 Field 类型的算术运算会以素数为模进行包装。这为系统的可靠性带来了微妙的风险:

  • 溢出/下溢 (Overflow/Underflow):如果 x+y 超过域模数,则像 x+y==z 这样的检查可能会错误地通过。
  • 缺少范围检查 (Missing Range Checks):旨在模仿“真实世界”整数(例如,8 位无符号整数)的值需要显式的边界约束。

审计重点:

  • 运算是否能抵抗回绕行为?
  • 变量是否具有手动强制执行的边界,例如 assert(x.lt(256)),或者是否转换为具有自动边界检查的类型,例如整数

例子:

考虑以下示例,其中两个私有 `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. 隐私泄露

隐私失败会危及零知识属性,并通过以下两个主要途径暴露敏感数据:

  • 意外的公共输入 (Accidental Public Inputs):一个无意中标记为 pub 的值会意外地对验证者可见。
  • 隐式泄露 (Implicit Leaks):公共输出可能与私有输入相关联。

    • 例如,如果与辅助数据结合使用,投票系统的公共“总票数”可能会泄露个人偏好。
    • 另一个例子是将哈希后的发送者地址(hashed sender address)错误地用作零知识证明的无效化器(nullifier)。这可以防止重放攻击,但隐式地泄露了发送者的身份。

审计重点:

  • 输入/输出是否被正确标记为 publicprivate
  • 公共输出是否泄露关于私有输入的间接信息?

例子:

一个天真地完全泄露假定的私有输入(age)的例子如下:

fn main(age : Field) -> pub Field {
    return age;
}

一个修复方法是省略返回私有输入(age):

fn main(age : Field) {

}

另请注意,当一个电路暴露来自小域的密钥输入的公共哈希时,攻击者可以对该值进行暴力破解。例如,考虑上面的示例,其中电路还返回 age 上的公共哈希。尽管该哈希在隔离时是密码学安全的,但可能的年龄范围有限(只有大约 82 = 100-18 个潜在值,假设最大年龄为 100)允许攻击者快速迭代每个候选值,计算其哈希,并将其与公共哈希进行匹配。通过这样做,攻击者可以轻松地恢复实际年龄,从而损害输入的隐私。

结论

零知识电路的成功或失败取决于严格的约束设计。通过将每个业务规则转换为显式断言,防止有限域算术的意外情况,并将每个公共接口视为潜在的侧信道,团队可以同时保持可靠性和隐私。随着 Noir 生态系统的成熟,持续的同行评审和周到的审计(无论是内部还是外部)仍然是值得信赖的 ZK 应用程序最可靠的途径。

对审计 Noir 电路有任何疑问吗?

与我们的 ZK 团队交谈 →

  • 原文链接: openzeppelin.com/news/de...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。