AI与密码学相遇1:AI在Cloudflare CIRCL中的发现
zkSecurity团队使用AI审计工具zkao扫描Cloudflare的CIRCL密码学库,发现7个真实漏洞,包括阈值RSA中的float64精度丢失、DLEQ证明伪造、BLS聚合签名缺少消息唯一性检查、HPKE PSK验证绕过、Lagrange系数整数溢出、CP-ABE访问控制完全突破等。所有漏洞已被修复,其中CP-ABE漏洞为zkao独立发现。文章还总结了AI在严重性评估上的偏差、模型配对的不稳定性以及漏洞组合能力的不足。
![]()
我们针对 Cloudflare 的 CIRCL 实验性加密库运行了 AI 审计流水线,并确认了七个真实漏洞,从阈值 RSA 中的关键的 float64 精度损失到属性基加密中的完整访问控制破坏。所有七个漏洞现在均已在上游修复。这是关于我们的 AI Agent 在开源加密领域发现的漏洞系列文章中的第一篇。
在 zkSecurity,我们正在构建 zkao,一个 AI 审计 Agent。目标说起来简单做起来难:让 AI 持续不断地查看你的代码,直到其他 AI 工具能发现的漏洞都被清除。我们在 zkao:持续累积的安全 中解释了这种方法为何重要。
构建 zkao 是一个迭代过程,最终目标是创建一个能够发现所有 AI 可检测漏洞的自动化审计员。这包括构思新想法和技术,系统地将 zkSecurity 安全研究人员的专业知识编码到 zkao 中,确保它能检测到最新和最严重的漏洞而不偏向于基准测试,并且重要的是,持续进行实验以了解什么有效、什么无效、模型如何演变,并加深我们对利用 AI 发现漏洞的理解。 其中一些实验本身就产生了值得分享的成果,与产品无关,这正是本系列文章的内容。
还有第二个动机。这些实验是我们为 zkao 构建基准套件的方式,在此过程中,它们持续揭示 LLM 实际如何推理加密问题:它们在哪些方面敏锐,在哪些方面盲目,以及如何放大前者并控制后者。虽然漏洞是可见的输出,但推理模式是我们最关心的部分。
几个月前,我们开始在选定的代码库上进行实验。我们使用 LLM 扫描了几个开源加密项目,采用两种配置:
- 仅使用 LLM,配合简单的提示词。
- 使用 LLM 配合 skills(技能),这些 skills 由我们团队的专家维护。
然后,对于 LLM 发现了真实漏洞的重要项目,我们还运行了 zkao,以查看它是否能自行检测到同样的问题。在大多数情况下,zkao 不仅发现了所有漏洞,还识别出更复杂和更严重的漏洞。
结果足够令人满意,我们决定将其记录下来。我们以 Cloudflare 的 CIRCL 作为本系列的开篇,这是一个先进和后量子密码学库。在 CIRCL 上,我们的流水线产生了许多候选发现,其中有七个值得在此报告。所有七个漏洞现在均已在上游修复。其中大多数漏洞已根据 Cloudflare 在 HackerOne 上的计划得到确认并获得了赏金。
澄清:AI 生成的是候选发现,而非最终报告。我们团队的人员仍然验证了每个问题,检查了可利用性,必要时最小化了 PoC,并处理了披露。这个人工复核环节仍然非常重要,因为 AI 候选发现成本低廉,而可信赖的报告则不然。
最小化这一环节正是 zkao 旨在完成的主要任务之一,虽然它仍在进行中,但当前版本已经承担了大部分这种验证工作。
严重性和修复一览
在详细说明之前,有一点值得指出:AI 对其自身发现的严重性评估是有噪声的。以下是每个漏洞的 AI 评级以及 Cloudflare 在修复后确认的严重性。我们还检查了所有七个漏洞均可由当前版本的 zkao 一致复现。
| 序号 | 漏洞 | AI 严重性 | Cloudflare 严重性 | 修复提交 | 发现者 |
|---|---|---|---|---|---|
| 1 | TSS/RSA 多项式求值中的 float64 精度损失 | 严重 | 低 | f7d2180 |
Opus 4.6 + skills |
| 2 | 通过证明者控制的 SecParam 进行 qndleq 伪造 |
高 | 低 | 757dde4 |
Opus 4.6 + skills |
| 3 | BLS 聚合验证缺少消息唯一性检查 | 中 | 高 | 9798df7 |
Opus 4.6 + skills |
| 4 | 通过 FillBytes 符号碰撞破坏 DLEQ 可靠性 |
高 | 低 | 19848a5 |
Opus 4.6 + skills |
| 5 | 通过按位或开关绕过 HPKE PSK 验证 | 中 | 中(重复) | a3b4fa3 |
GPT-5.3 + skills |
| 6 | TSS/RSA 拉格朗日系数使用 int64 | 高 | 中 | 751e372 |
Opus 4.6 + skills |
| 7 | 通过 AND 共享错误导致 CP-ABE 访问控制破坏 | 严重 | 严重 | def2fd3 |
zkao |
AI 严重性与已确认严重性之间的差距本身就是一个有趣的见解,我们将在最后回顾这一点。现在,逐一介绍这七个漏洞。
漏洞 1:多项式求值中的 float64 精度损失
这个问题存在于 CIRCL 的阈值 RSA 实现(tss/rsa)中。阈值签名使用 Shamir 风格秘密共享将秘密分割给 n 个参与者。Deal() 在每个参与者的索引处计算一个秘密多项式。系数应该是 big.Int,但项 x^i 的计算方式如下:
// tss/rsa/rsa_threshold.go
xi := int64(math.Pow(float64(x), float64(i)))
float64 有 53 位尾数。一旦 xi 超过 2^53(大约 9×10^15),结果在被转换回整数之前就会静默舍入。例如,有 100 个参与者和 27 的阈值,在 x=100 且 i=26 时,要求 100^26=10^52,这超出了 2^53 多达 36 个数量级。甚至 x=20, i=16 时也已破坏。
后果是多项式求值错误,因此分发给参与者的密钥分片是错误的。根据参数,签名组合要么完全失败,要么产生看起来正常但无法重建预期密钥的分片。我们的 Agent 将其标记为 严重,因为它导致生成错误的密钥分片,损害了协议的正确性。Cloudflare 最终将问题评估为低严重性,基于受影响条件在实际中发生的可能性较低。
修复方法是用 Horner 求值法替换浮点指数运算,这正是代码自身的 TODO 注释一直建议的方法,将所有计算保持在 big.Int 中。提交 f7d2180。
漏洞 2:通过证明者控制的安全参数进行 DLEQ 证明伪造
这个问题存在于 zk/qndleq 中,这是 CIRCL 针对 (Z/nZ)* 中平方子群的 DLEQ(离散对数相等)证明。DLEQ 证明证明两个对共享相同的离散对数;如果攻击者能使验证者接受一个错误陈述的证明,那么证明系统就被破坏了。
该证明中的挑战是以 Fiat-Shamir 风格推导的,其比特长度由 SecParam 控制。问题在于 SecParam 存在于 Proof 结构体本身中:
type Proof struct {
Z, C *big.Int
SecParam uint
}
在验证过程中,代码使用证明 自身 的 SecParam 重新计算挑战。该字段是攻击者控制的。设置 SecParam = 1,挑战就塌缩为单个比特,值为 0 或 1:每次伪造尝试等于抛Coin。设置 SecParam = 8,暴力破解大约需要 2^8=256 次尝试。无论哪种方式,可靠性都不复存在。
这是一个常见模式的典型实例:一个必须由验证者固定的安全参数,却从证明者提供的数据中读取。修复方法是从证明中移除 SecParam,并使 Verify 将其作为显式参数接受,这样验证者来设置它。提交 757dde4。
漏洞 3:BLS 聚合验证缺少消息唯一性检查
这是本次批量中 AI 低估 的一个漏洞。Agent 将其标记为中。实际上这是一个教科书式的流氓密钥攻击,是一种广为人知的关键级缺陷;我们报告为严重,Cloudflare 确认为高。
sign/bls 中的 VerifyAggregate 实现了 BLS BASIC 聚合模式。该模式仅在批次中所有消息都不同时才安全,这是其对流氓密钥攻击的防御。该函数检查了聚合配对方程,但从未检查消息是否不同,将这一关键要求留给了调用者。
没有这个检查,标准的流氓密钥攻击就适用。一个看到受害者公钥 pkv 和消息 m 的攻击者可以注册 pka = g^ska - pkv,并伪造一个关于 (pkv, m) 和 (pka, m) 的聚合签名,而无需知道受害者的私钥。CIRCL 没有提供备用证明拥有的基础设施,这使得缺失的检查更加危险。
为什么 AI 称此为中?我们不知道。阅读其推理,它正确地发现了缺失的唯一性检查,甚至命名了流氓密钥攻击,但它随后锚定在 BASIC 模式的契约将唯一性要求放在调用者身上这一事实上。它将“调用者应该处理这个”视为一种缓解措施,并降低了严重性。
修复使 VerifyAggregate 拒绝包含重复消息的批次。提交 9798df7。
漏洞 4:通过 FillBytes 符号碰撞破坏 DLEQ 可靠性
回到 zk/qndleq,这是本次批量中最微妙、坦白说也是最有意思的漏洞。它根本不需要触及证明本身。
取一个诚实的、有效的证明 pi,用于陈述 S1=(g, g^x, h, h^x),它证明 log_g(g^x) = log_h(h^x) = x。一个不知道 x 的攻击者将完全相同的 pi 呈现给验证者,但将其与一个不同的陈述 S2=(g, -g^x, h, h^x) 配对,其中 -g^x 是负的 big.Int new(big.Int).Neg(gx)。
当挑战 c 为偶数时,伪造的陈述会被接受,因为两件事同时发生。
代数抵消。 验证者从 -g^x 重新计算其值,符号直接提出来:
(-g^x)^c mod N = (N - g^x)^c mod N = (-1)^c * g^(xc) mod N。
当 c 为偶数时,(-1)^c = 1,因此验证者重构出与诚实证明者完全相同的中间值。
哈希中的符号碰撞。 挑战是通过哈希陈述来推导的,哈希使用 FillBytes,它写入 big.Int 的绝对值并去掉符号。所以 doChallenge(..., -gx, ...) 和 doChallenge(..., gx, ...) 哈希到相同的结果。
这里,c 为偶数的概率至少为 1/2(它只是哈希输出的低位),因此攻击在大约一半的诚实生成的证明上成功。验证者确信 log_g(-g^x) = log_h(h^x),而这是错误的。以下是概念验证的核心:
// 针对 (g, gx, h, hx) 的诚实证明,选择使得挑战 c 为偶数
gxNeg := new(big.Int).Neg(gx) // -gx,攻击者不需要知道 x
forgedAccepted := proof.Verify(g, gxNeg, h, hx, N) // 被接受!
这个漏洞之所以突出,是因为它不是一行粗心的代码。它是代数恒等式 ((-1)^even=1) 和一个看似无害的序列化选择(FillBytes 丢弃符号)之间的相互作用。单独来看,两者都没有错。但它们结合在一起就破坏了可靠性。跨越这种边界进行推理正是模型最让我们惊讶的地方。
在严重性上,Agent 将其评为高,因为它是可靠性破坏,但 Cloudflare 由于攻击复杂度高而确认为低。
修复在挑战计算中添加了 checkBounds 步骤,要求每个输入满足 0 < x < N。负的 -gx 有负号,在造成任何损害之前就被拒绝了。提交 19848a5。
漏洞 5:通过按位或开关绕过 HPKE PSK 验证
这几乎是一个语言级别的陷阱。在 HPKE 的 verifyPSKInputs 中,switch 标签是用按位或编写的:
// hpke/util.go
case modeBase | modeAuth: // 0x00 | 0x02 == 0x02,即仅 modeAuth
case modePSK | modeAuthPSK: // 0x01 | 0x03 == 0x03,即仅 modeAuthPSK
在 Go 中,case a | b: 是一个单一 case,其值是两个常量的或,而不是两个 case。所以 case modePSK | modeAuthPSK 实际上是 case 0x03,而 modePSK(0x01)不匹配任何 case。本应在 PSK 模式下拒绝缺失 PSK 的分支直接被跳过。
效果:SetupPSK(..., nil, nil) 继续进行,使用空的 PSK 而不是被拒绝。PSK 模式本应 要求 PSK 材料;这静默地丢弃了身份验证前提条件,并允许部署以比配置的更弱模式运行。修复是将 OR 改为逗号分隔的 case(case modePSK, modeAuthPSK:)的一次字符级变化。提交 a3b4fa3。它被确认为重复。
漏洞 6:拉格朗日系数使用 int64
回到 tss/rsa,一旦分片被分发,组合签名需要拉格朗日插值。computeLambda 在 int64 中构建每个拉格朗日系数的分子和分母:
// tss/rsa/rsa_threshold.go
num := int64(1)
den := int64(1)
for _, s := range S {
jprime := int64(s.Index)
if jprime == j { continue }
num *= i - jprime // 对于中等数量的参与者,溢出 int64
den *= j - jprime
}
lambda.Div(big.NewInt(num), big.NewInt(den)) // 截断整数除法
lambda.Mul(delta, &lambda)
实际上有两个完全独立的漏洞,每个都足以破坏签名。第一个是溢出:大约 21 个参与者时,乘积超过 int64 上限(约 9.2×10^18)并静默回绕,因为 Go 在整数溢出时不会恐慌。computeLambda 随后返回一个错误、通常是负的系数。
第二个是截断,即使没有溢出也会发生。代码先计算 num / den,然后才乘以 delta。在 Shoup 方案中,δ * num 保证能被 den 精确整除,但每当分片索引不连续时(这是 t-of-n 子集的正常情况),num 本身就不能。以 3-of-5 方案组合分片 {1,3,5} 为例:对于一个系数,num=(0-3)(0-5)=15,den=(1-3)(1-5)=8。错误的顺序计算 δ * floor(15/8),当 δ=120 时得到 120;正确的值 δ * 15/8 是 225。
修复将整个计算移至 big.Int,并重新排序乘法和除法,以便精确整除性保证成立。提交 751e372。这两个问题不相关,但 Agent 将它们作为一个发现一起报告,我们尊重 Agent 的工作,就这样提交了。
漏洞 7:通过一行 AND 共享错误导致 CP-ABE 访问控制破坏
这是 zkao 自行发现的漏洞。在确认了上述六个问题后,我们将它指向同一库,看它能发现什么,它报告了这个漏洞。这是对 CIRCL 的密文策略属性基加密(abe/cpabe/tkn20)中访问控制保证的完全破坏,Cloudflare 确认了其有效性。
密文策略属性基加密(CP-ABE)允许你根据策略加密消息,例如 (location: usa AND department: finance) OR (role: admin)。用户持有一个与其自身属性绑定的密钥,该方案保证只有当这些属性满足策略时才能解密。美国财务部的员工和管理员可以读取消息,其他人则不能,即使每个人都收到相同的密文。
在内部,tkn20 将策略转换为由 AND 和 OR 门组成的树,属性在叶节点,并将一个秘密(保护消息的密钥)向下分享到树中。分享必须遵循布尔逻辑:
- OR 门将完整秘密给予两个子节点,因为满足任一分支就足够了。
- AND 门分割秘密,因此需要两个子节点来重建它。一个子节点获得随机值
r,另一个获得parent - r,只有r + (parent - r)才能恢复父节点。
要理解这个漏洞,只需记住 AND 门:每个子节点必须获得部分份额,且单独任何一个子节点都不应重构父节点。
以下是 share 实际处理 AND 情况的方式:
// abe/cpabe/tkn20/internal/tkn/formula.go
case Andgate:
shares[gate.In0], err = randomMatrixZp(rand, k.rows, k.cols) // In0 = 随机值 r
...
shares[gate.In1] = newMatrixZp(k.rows, k.cols) // In1 = 0
shares[gate.In0].sub(shares[gate.Out], shares[gate.In1]) // In0 = parent - 0
生成了随机份额,然后立即被丢弃。In1 被设为零,最后一行用 parent - In1 覆盖了 In0,而这只是 parent。所以一个子节点接收了整个秘密,另一个什么也没得到。AND 门不再是 AND:其第一个叶节点自己就重建了父节点。
注意,这并没有破坏正确性。两个份额仍然加起来等于父节点(parent + 0 = parent),因此任何满足策略的密钥仍然可以解密,旧代码生成的密文仍然兼容。它破坏的是保密性:一个 AND 叶节点现在恢复了本应需要两者的秘密。
将其变成完全破坏的是这个被破坏的 AND 门所在的位置。为了实现 CCA 安全,tkn20 应用了一个 Boneh-Katz 变换,将每个策略包装在一个新的外部 AND 门中,而该门的左子节点是一个内部的“通配符”叶节点。权威机构颁发的每个属性密钥都带有该通配符,因此每个密钥都满足那个叶节点。现在将两个事实结合起来:通配符叶节点是一个 AND 门的第一个子节点(In0),而 In0 正是接收完整秘密的子节点。因此,每个密钥都持有一个单独重建消息密钥的叶节点,与策略无关!
虽然看起来像一个简单的笔误漏洞,但给我们留下深刻印象的是 zkao 推理像 CP-ABE 这样的复杂概念并正确评估其影响的能力。许多 LLM 仍然会识别出笔误,但将其视为“纵深防御”或“代码整洁”问题而不进一步推理。这可能导致开发者低估或忽视该漏洞。
修复是一行代码。正确地分享父节点,使随机份额保留在 In0 中,In1 成为补集:
shares[gate.In1].sub(shares[gate.Out], shares[gate.In0]) // In1 = parent - random
现在 In0 保持其随机值,In1 持有 parent - In0,因此没有一个 AND 叶节点单独携带秘密。提交 def2fd3。
我们学到的一些事情
有三点观察让我们印象深刻。
AI 不擅长评估严重性,而且这种不擅长是不对称的。 再次看看顶部的表格。如果我们依赖 Cloudflare 确认的严重性作为基准真值,那么在大多数情况下,Agent 高估了它发现的影响。但在 BLS 漏洞 上,它却走向了相反的方向,低估了一个广为人知的关键缺陷,将一个干净的流氓密钥攻击标记为仅仅是中。我们还没有完整的解释或解决方案。我们的工作假设是,当目标是像 CIRCL 这样被许多不同应用程序使用的库时,这确实很难,因为影响取决于模型无法看到的下游调用者。我们认为添加一个一致的严重性矩阵加上一个显式的威胁建模步骤,将有助于模型在整体系统(及其潜在集成)的级别上推理影响,而不是局部代码。目前,严重性仍然是我们信任人类进行评估的部分。
在 zkao 中,我们通过让开发者通过用户配置(
zkao.md)澄清他们的威胁模型和严重性偏好来暂时解决这个问题,并持续迭代改进 zkao 的严重性设置。zkao 能够一致地生成 PoC 也减少了误报,因此我们对真正的问题有信心。尽管如此,我们仍在努力以更系统的方式改进其默认严重性分配,因为这对开发者至关重要。同样值得注意的是,Cloudflare 通过其赏金计划的视角评估严重性,该计划考虑一个漏洞是否影响其在线服务。例如,漏洞 2 被评为低,尽管它完全破坏了证明的可靠性。这个评级仅表明受影响的代码未被 Cloudflare 服务使用,或者在 Cloudflare 环境中影响有限。这并不意味着在其他部署中影响同样小,这对于许多项目可能依赖的库尤其重要。
模型配对不是对称的,而且角色可以翻转。 六个漏洞中有五个是由 Claude Opus 4.6 配合我们的 skills 发现的。在相同的 skills 和相同的系统提示下,GPT-5.3 主要是验证而非发现;表格中的 HPKE 漏洞 是它自己发现的。我们没有预料到这种分工保持稳定,事实也确实没有。几周后,我们使用当时最新的配对 Opus 4.7 和 GPT-5.4 重新运行扫描,角色基本上颠倒了:GPT-5.4 发现了更多漏洞,而 Opus 4.7 只能验证它们。这是一个很好的提醒,不要过于依赖任何特定模型名称的结论。前沿领域已经再次前进,并且将继续前进。我们将在另一篇文章中探讨这个问题。
这种模式正是我们构建 zkao 为“模型无关”的确切原因,使其性能保持在最佳状态,而你无需猜测下个月哪个模型最好。
AI 收集问题,但并不总是将它们串联起来。 漏洞 6 就是一个例子。Agent 将两个完全独立的漏洞(溢出和整数截断)打包成一个发现。两者都是真实的,所以这确实是实用的工作。但它将它们并列呈现,而不是推理它们之间的关系,我们在其他地方也看到了同样的模式:几个真实的观察被收集在一起,没有任何解释或试图将它们串联成更具影响力的利用。
正是在这里,在 zkao 中,我们构建了新颖的流程,以实现将单独问题串联成真实端到端利用的漏洞组合。
下一步计划
感谢 CIRCL 维护者,他们迅速修复了所有报告的问题。这是本系列的第一篇文章;我们将继续发布其他项目中已解决的已确认漏洞。
我们扫描了超过 200 个加密项目(从下载量最高的加密 crate/包中选择),最终得到超过一千个候选发现。结果,目前最大的瓶颈是分类。每个报告的漏洞在到达项目之前都必须由我们的专家检查技术有效性,因为我们和其他人一样讨厌 AI 垃圾信息。这需要大量人力,因为我们还没有完全信任我们正在构建的自动化分类流程(它正在变得更好)。因此,我们优先考虑了一些维护得最好的最流行项目。
如果你维护一个加密项目并且对此感兴趣,我们很乐意与你一起验证,以便严重漏洞得到及时发现和修复。如果还没有扫描过,或者你的代码库自上次扫描以来发生了重大变化,我们很乐意运行一次新的扫描。这种持续的 AI 覆盖正是 zkao 的设计目标。请通过 zksecurity.xyz/contact 联系我们。
分享本文
在 X 上分享 在 LinkedIn 上分享 通过电子邮件分享
zkSecurity 为加密系统提供审计、研究和开发服务,包括零知识证明、MPC、FHE、共识协议等。
- 原文链接: blog.zksecurity.xyz/post...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~