AI与密码学相遇之三:AI在Bron Labs的bron-crypto中发现了什么

zksecurity 发布于 2026-07-23 阅读 25

本文介绍了使用AI审计管道对Bron Labs的Go加密库bron-crypto进行安全审计的结果,发现了4个零日漏洞,包括:Lindell17 DKG中操作数交换导致阈值ECDSA密钥分享损坏、Poseidon哈希的Go标准接口实现违反合约(Write重置状态、Sum变异)、k256/Pallas/Vesta曲线的IsOnCurve检查逻辑取反、BLS12-381 G2的IsZero误调用IsOne。文章详细描述了每个漏洞的背景、细节、修复和影响,并总结了AI审计的发现模式:重复运行后结果趋于稳定,提高效果的关键在于优化提示工程和代码分解策略。

缩略图

这是该系列的第三篇文章。 如果你还没有读过第一篇关于 Cloudflare CIRCL 的文章第二篇关于 OpenVM zkVM 的文章,我们建议你先阅读它们。 这些文章提供了更多背景信息,说明我们为何进行这些实验以及流水线的设置方式。 最重要的是,它们很有趣。 这次,我们将 AI 审计流水线指向了 bron-crypto——Bron Labs 的 Go 语言 MPC 和门限签名库,并检测到了一些零日漏洞。

bron-crypto 是 Bron Labs 的密码学库。 虽然它主要关注 MPC 协议,但也支持许多其他原语,可供其他应用复用。 这使其成为我们实验的好目标,因为正如我们之前所说, 通过主流编码 Agent(如 Codex 和 Claude Code)中的标准夹具,可以有效地协调对多个独立模块的审计。

澄清,与之前的文章相同:我们的自动化流程产生了大量“候选”发现。 我们团队中的专家仍然验证了每个问题,确认了可利用性,最小化了概念证明,并负责了负责任的披露。

我们通过双 Agent 流水线运行了 bron-crypto,以 Claude Opus 4.6 作为主要审计员,Codex 5.3 作为独立验证者(反之亦然),最终得到了 30 个发现。 以下四个值得详细讲解。 其余的在我们可以抽出时间验证之前就已修复。 我们将在最后回到汇总结果,因为它说明了 LLM 漏洞发现的一致性已达到何种程度。

此外,我们还获得了一些赏金,并因这些漏洞在 Bron 的漏洞赏金计划 中得到了认可。

严重性和修复一览

与第一篇文章一样,AI 对其自身发现分配的严重性仍然非常不准确。 AI 倾向于对库代码给出比维护者更高的严重性评分,因为影响取决于模型无法看到的下游调用者。 因此,我们认为 AI 只是假设了最坏情况。 以下是每个漏洞按 AI 评定的严重性以及 Bron Labs 在修复后确认的严重性。

# 漏洞 AI 严重性 Bron 严重性 修复 发现者
1 Lindell17 DKG 因操作数交换导致加密份额损坏 严重 e80a2ea (#197) Opus 4.6 + skills
2 Poseidon hash.Hash 实现违反 Go 合约 8d203d0 (#202) Opus 4.6 + skills
3 IsOnCurve 在 k256、Pallas 和 Vesta 上取反 e070f8f (#196) Opus 4.6 + skills
4 BLS12-381 G2 IsZero() 调用了 IsOne() 4601a36 (#222) Opus 4.6 + skills

现在让我们逐一详细讲解。

漏洞 1:操作数交换导致门限 ECDSA 密钥份额损坏

背景

这个漏洞出现在 Lindell17 门限 ECDSA 的分布式密钥生成(DKG)中。 门限签名的全部意义在于没有任何单个参与方持有私钥。 密钥被分割成份额,签名在没有任何参与方重构秘密的情况下联合进行。 DKG 是首先生成这些份额的协议,且无需可信的分发者。

要理解该漏洞,只需记住份额是如何存储和处理的。 每个参与方选择一个私密份额 x∈Zq,然后发布公开份额,即椭圆曲线点 Q=x⋅G。 此外,每个参与方还发布加密份额 c=Enc(x),其中 Enc(·) 是 Paillier 加密,所以为简单起见,我们将 (c,Q) 视为公开份额。

使用 Paillier 加密的原因是它的加法同态性质,即 Enc(a)⋅Enc(b)=Enc(a+b)。 具体来说,协议将每个私密份额 x∈Zq 分解为两个片段 x′ 和 x″,分别在 {q/3,...,2q/3} 范围内,使得 x=3x′+x″。 这样分解的原因是,给定参与方的公开份额 (c,Q), 我们需要验证一个“离散对数的 Paillier 加密的零知识证明”(我们称之为 LPDL 证明),该证明证明存在一个 x∈Zq 满足 c=Enc(x) 且 Q=xG。 但论文为了效率,将该证明的可靠性放宽到仅对 x∈{q/3,...,2q/3} 成立。 因此,变通方法是,不直接提供全范围的 x 对应的 Q=xG,而是每个参与方将 x 拆分为较短范围内的 x′ 和 x″,并提供 (c′=Enc(x′),Q′=x′G) 和 (c″=Enc(x″),Q″=x″G) 的 LPDL 证明。 然后其他参与方验证证明,并重构 c=Enc(x′)⋅Enc(x′)⋅Enc(x′)⋅Enc(x″) 和 Q=3Q′+Q″。

DKG 协议的最终公钥是所有参与方的 Q 值的总和。

漏洞

实现构建了如下存储的密文:

// pkg/mpc/tsig/tecdsa/lindell17/keygen/dkg/round.go
p.state.theirPaillierEncryptedShares[id] =
    theirCKeyPrime.HomAdd(theirCKeyDoublePrime).HomAdd(theirCKeyDoublePrime).HomAdd(theirCKeyDoublePrime)

从同态角度来理解:它从 x′ 开始,然后加三次 x″。 结果是 Enc(x′+3x″),而不是 Enc(3x′+x″)。 两个操作数被交换了。 存储的密文加密的值不是参与方的份额。

这损坏了 DKG 期间存储的加密密钥份额。 随后使用此加密份额进行门限 ECDSA 签名操作将产生不正确的部分签名, 导致签名失败。

修复

如你所料,修复很简单。 交换操作数,使三倍的项是 x′:

// 修复:从 x'' 开始,加三次 x' -> Enc(3x' + x'')
p.state.theirPaillierEncryptedShares[id] =
    theirCKeyDoublePrime.HomAdd(theirCKeyPrime).HomAdd(theirCKeyPrime).HomAdd(theirCKeyPrime)

它被合并在 PR #197 中(合并提交 e80a2ea)。

影响

使用 DKG 生成的分片(而不是可信分发者分片)的门限 ECDSA 会话可能产生不正确的签名或根本无法签名。 对于托管系统来说,这是一个可用性问题,而不是密钥泄露问题。 秘密不会泄露,但参与方可能无法生成有效签名。 对于钱包而言,这意味着资金无法转移。

漏洞 2:Poseidon hash.Hash 实现违反 Go 合约

背景

Poseidon 是一种哈希函数,设计用于在算术电路和 ZK 证明中成本低廉。 与大多数现代哈希函数一样,它是一个海绵结构:你将输入吸收到内部状态,然后从中挤出摘要。 bron-crypto 通过两种方式公开 Poseidon:通过自己的 Update/Digest 海绵 API,以及通过 Go 标准的 hash.Hash 接口作为即插即用的哈希函数。

该标准接口附带了一个整个 Go 生态系统都依赖的合约。 这里涉及两个部分:

  • Write累积的。 调用 Write(a) 然后 Write(b) 必须哈希 a 后跟 b,就像一次写入 a || b 一样。
  • Sum(b)非变异且前缀追加的。 它返回 append(b, digest...):字节 b 是粘在摘要前面的前缀,而不是哈希的更多输入,并且调用它不得干扰哈希器的状态。

漏洞

hash.Hash 适配器破坏了合约的两个部分。

Write 是在一次性 Hash() 入口点上实现的,该入口点在吸收之前会重置海绵。 因此,每次调用 Write 都会清除之前写入的所有内容:

// pkg/hashing/poseidon/poseidon.go
func (p *Poseidon) Write(data []byte) (n int, err error) {
    // ... 将字节转换为域元素 ...
    p.Hash(elems...) // Hash() 重置海绵,因此之前的写入被丢弃
    return len(data), nil
}
...
func (p *Poseidon) Sum(data []byte) []byte {
    _, err := p.Write(data)
    if err != nil {
        panic(err)
    }
    return p.Digest().Bytes()
}

因此 Write(a); Write(b) 只哈希了 b。 而 Sum 直接调用了 Write。 这重置了状态,违反了非变异规则,并将参数视为哈希输入而不是输出前缀。 Write 还只接受长度是 32 字节倍数的输入,因此如果前缀不是 32 字节对齐,Sum 会 panic。

修复

Write 在不重置的情况下吸收,并让 Sum 在不变异的情况下追加:

func (p *Poseidon) Write(data []byte) (n int, err error) {
    // ... 将字节转换为域元素 ...
    p.Update(elems...) // Update 吸收到当前状态,不重置
    return len(data), nil
}

func (p *Poseidon) Sum(b []byte) []byte {
    return append(b, p.Digest().Bytes()...) // 非变异,前缀追加
}

已在 PR #202 中修复(合并提交 8d203d0)。

影响

任何通过标准 hash.Hash 接口访问 Poseidon 的调用者都会得到错误的结果。 跨多个 Write 调用流式传输消息只会哈希最后一个块,而 Sum 会在前缀不是 32 字节对齐时 panic。 幸运的是,bron-crypto 自己的协议使用直接的 Update/Digest 海绵 API,而不是这个适配器,因此内部 MPC 代码未受影响。

漏洞 3:一个曲线检查说“是”时其实意味着“否”,反之亦然

背景

在库处理来自外部的椭圆曲线点之前,应检查该点是否确实在曲线上。 跳过此检查会为无效曲线攻击打开大门:攻击者向你提供一个较弱相关曲线上的点,你的秘密标量乘法会泄露相关信息。 在 Go 的标准库中,捕获此问题的检查称为 IsOnCurve

漏洞

bron-crypto 将其几个曲线包装在 Go 的 elliptic.Curve 接口中。 有趣的是,k256、Pallas 和 Vesta 的 IsOnCurve 适配器返回了应该返回的值的相反值:

// pkg/base/curves/k256/elliptic.go(以及 pasta/elliptic.go 用于 Pallas、Vesta)
return err != nil // 漏洞:true 表示“错误”,即不在曲线上。取反了。

底层例程在点不在曲线上时报告错误,因此有效点给出 err == nil。 返回 err != nil 完全颠倒了整个逻辑。 有效点,包括曲线生成元,被拒绝,而任意非曲线点被接受。

因为 elliptic.Unmarshal 内部依赖于 IsOnCurve,所以这种取反会传播:反序列化有效的编码点失败,而反序列化坏的点成功。

修复

每个曲线改一个字符,将 != 改为 ==,共三处:

return err == nil

已在 PR #196 中修复(合并提交 e070f8f)。

影响

有效点被拒绝,因此任何依赖它们的东西都无法工作。 无效点被接受,这可能导致因无效曲线攻击而泄露私钥。 幸运的是,此检查位于 bron-crypto 自己的协议不使用的 k256、Pallas 和 Vesta 的 Go 适配器中。 因此,真正面临风险的是那些通过 Go 标准路径反序列化点的外部调用者。

漏洞 4:检查一的 IsZero

最后一个是最简单的。 在共享的有限域特质中,IsZero 被连接到了错误的谓词:

// pkg/base/curves/impl/traits/finite_field.go
// IsZero 报告元素是否为零。
func (fe *FiniteFieldElementTrait[FP, F, WP, W]) IsZero() bool {
    return FP(&fe.V).IsOne() != 0 // 漏洞:调用了 IsOne,从上面的方法复制而来
}

它是直接从上面的 IsOne 方法复制粘贴而来的。 结果是 IsZero 对元素 1 返回 true,对实际的 0 返回 false。 该特质被 BLS12-381 G2 域元素嵌入,因此所有构建在其上的东西都继承了这种取反。 单位点检测 (IsOpIdentity) 将 1 报告为单位点,将真正的单位点报告为非单位点,并且域算术中使用的欧几里得估值也会出错。

实际上,它位于可触及表面之下,这就是为什么我们和 Bron Labs 都将其评级为低。 修复方法是直接调用正确的方法:

func (fe *FiniteFieldElementTrait[FP, F, WP, W]) IsZero() bool {
    return FP(&fe.V).IsZero() != 0
}

它作为更广泛的“健壮性修复” PR #222 的一部分被合并(合并提交 4601a36)。

我们学到的一些东西

我们不想夸大这些漏洞的复杂性。 它们本质上是“拼写错误”类型的漏洞,如今即使使用较旧的模型,主流夹具也能检测到。 要找到更深层、更“密码学风格”的漏洞,你可能需要更全面的夹具,例如 zkao。 我们列出这些漏洞主要是为了证明一个关于 LLM 输出的更有趣的模式,人们仍然认为 LLM 输出非常不可预测:

LLM 能发现的漏洞集合比看起来更稳定。 我们实验中最有趣的点不是任何一个单独的漏洞,而是重复性。 在固定模型和类似提示下,重复运行会收敛到大致相同的发现集合。 从我们这边来看,这一点很明显:相同的问题在多次运行中反复出现。 我们怀疑维护者从他们的角度也看到了这一点。 我们的几个独立发现都在同一个 PR 中被一起修复了,例如“健壮性修复”(#222)。 这正好是你所期望的模式:如果团队运行了自己的模型扫描,得到了重叠的发现集合,并将结果合并到一个 PR 中。 这也与每个运行漏洞赏金计划的人的感受一致:重复报告变得越来越普遍, 因为相同的模型可发现的漏洞一次又一次地从独立参与者那里浮现出来。

实际要点在于,经过几次运行后,重新运行相同的设置不会给你带来更多漏洞。 真正能改变你可触及的漏洞集合的是模型周围的夹具:专家知识如何嵌入,代码如何分解,上下文如何输入,以及子 Agent 如何协调。 这正是 zkao 背后的全部赌注,也是我们不满足于“再多运行几次 LLM”的原因。 我们还正在对显而易见的后续问题进行更仔细的研究:在给定目标上,你需要运行多少次才能确信 LLM 已经找到了它将要找到的所有东西? 我们将单独撰文介绍。

这些实验以及构建 zkao 的过程中的另一个教训是,模型性能会根据夹具和提示策略而有显著差异。当使用 LLM 发现漏洞时,这一点尤其重要。维护有效的设置需要持续迭代,因为最强的模型会进化,并对不同形式的指导做出不同反应。我们计划分享更多旨在理解这些差异为何出现的实验,以及设计和维护有效夹具所涉及的权衡。

我们还有另一个“更强”的观点:每当你通过高度优化的自定义夹具使用 LLM 发现一个漏洞时,“秘方”不太可能长期保持独家。根据每个提供商的政策和用户设置,该夹具最终可能会被纳入未来的训练数据中。即使没有,类似的技术和发现也很可能被公开分享,并成为更广泛在线知识的一部分。因此,一个最初需要复杂夹具才能发现的漏洞,最终可能会被一个更简单的提示重新发现:

“找到所有漏洞。不要犯错!”

这就是为什么,即使我们使用 zkao 发现了重大漏洞,我们也不声称解决了安全问题或超越了该领域的每个安全研究人员。我们的目标是构建一个持续改进的自主安全平台,随着新模型的出现、我们对 LLM 分析的深入理解,以及我们整合客户反馈和安全研究人员的知识,该平台将不断进化。这是一项持续的努力,旨在领先于黑客。

下一步

感谢 Bron Labs 团队迅速分类并修复了所有四个问题。 这是本系列的第三篇文章,随着其他项目的漏洞得到解决,我们将继续发布已确认的漏洞。

如果你维护一个密码学或 MPC 项目,并且对此感兴趣,我们很乐意与你一起研究,无论是通过 zkao 还是手动审计。 请通过 zksecurity.xyz/contact 联系我们。

分享本文

在 X 上分享 在 LinkedIn 上分享 通过电子邮件分享

zkSecurity 为密码学系统(包括零知识证明、MPC、FHE、共识协议等)提供审计、研究和开发服务。

了解更多 →

  • 原文链接: blog.zksecurity.xyz/post...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论