Reed's News
← 返回精选

AI审计Cloudflare密码学库:发现7个真实漏洞

AI 84 duha 2026/7/7 3337 字 原文 ↗

缩略图

我们将AI审计管线应用于Cloudflare的CIRCL实验密码学库,确认了7个真实漏洞——从门限RSA算法中严重的float64精度丢失问题,到基于属性加密(ABE)中完全失效的访问控制机制。目前所有漏洞均已在上游代码库修复。本文是系列文章的第一篇,将陆续介绍我们的AI工具在各类开源密码学项目中发现的漏洞。

在zkSecurity,我们正在开发一款AI审计工具zkao。它的目标听起来简单,实现起来却极具挑战:持续用AI监控代码,直至找到其他AI工具所能发现的全部漏洞。我们曾在《zkao:复利式安全防护》zkao: Security That Compounds一文中阐述过这种方案的重要性。

zkao的开发是一个迭代过程,最终目标是打造一款能自动发现所有AI可检测漏洞的审计工具。为此,我们不断构思新方法与新技术,将zkSecurity安全研究员的专业知识系统化地融入zkao,确保它能检测最新、最严重的漏洞,同时避免过度依赖基准测试产生偏见。更重要的是,我们持续开展实验,探索有效与无效的方法、模型演变规律,深化对AI漏洞检测的理解。部分实验成果具备独立分享价值,无需依附产品本身,这便是本系列文章的内容来源。

我们还有第二个发布动机:这些实验同时为zkao构建了一套基准测试套件,过程中不断揭示大语言模型(LLM)对密码学的真实推理逻辑——它们擅长什么、盲区在哪里,以及如何强化优势、弥补不足。漏洞是可见的输出,但推理模式才是我们最关注的核心。

几个月前,我们开始针对特定代码库开展实验,用LLM扫描了若干开源密码学项目,设置了两种扫描模式:

  • 仅用LLM,搭配简单提示词
  • LLM结合专家技能,技能由团队专家维护

对于LLM发现真实漏洞的重要项目,我们还运行了zkao,测试它能否独立检测出相同问题。多数情况下,zkao不仅能复现所有已发现漏洞,还能识别出更复杂、更严重的问题。

实验结果足够有价值,因此我们决定整理成文。系列文章的开篇聚焦Cloudflare的CIRCL——一个包含高级算法与后量子密码学的库。我们的审计管线在CIRCL中发现了大量候选漏洞,其中7个值得重点报告,且均已在上游修复。多数漏洞经确认后,通过Cloudflare在HackerOne上的漏洞悬赏计划获得了赏金。

说明:AI仅生成候选漏洞,而非最终报告。我们团队的人工环节仍不可或缺:验证每个问题的真实性、评估可利用性、按需简化POC(概念验证)、负责漏洞披露。人工参与的价值依然重大,因为AI生成候选漏洞的成本很低,但可信的漏洞报告却需要专业判断。减少人工介入的工作量,正是zkao的核心设计目标之一。尽管它仍在开发中,但当前版本已能独立完成大量验证工作。

漏洞严重程度与修复概览

在深入细节前需要说明:AI对自身发现的漏洞严重程度评级存在误差。以下表格列出了AI的初始评级,以及Cloudflare修复后确认的最终评级。我们同时验证了当前版本的zkao能稳定复现这7个漏洞。

编号 漏洞 AI评级 Cloudflare确认评级 修复提交 发现工具
1 门限RSA(TSS/RSA)多项式求值中的float64精度丢失 严重 f7d2180 Opus 4.6 + 专家技能
2 通过 prover 可控的SecParam实现qndleq伪造 757dde4 Opus 4.6 + 专家技能
3 BLS聚合签名缺失消息唯一性校验 9798df7 Opus 4.6 + 专家技能
4 通过FillBytes签名碰撞破坏DLEQ可靠性 19848a5 Opus 4.6 + 专家技能
5 通过按位或分支绕过HPKE预共享密钥(PSK)验证 中(重复漏洞) a3b4fa3 GPT-5.3 + 专家技能
6 门限RSA(TSS/RSA)中用int64存储拉格朗日系数 751e372 Opus 4.6 + 专家技能
7 通过AND份额漏洞破坏密文策略属性加密(CP-ABE)访问控制 严重 严重 def2fd3 zkao

AI评级与实际确认评级之间的差异本身就是有趣的发现,我们将在文末展开讨论。接下来逐一介绍这7个漏洞。

漏洞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位。一旦$x^i$超过$2^{53}$(约$9 \times 10^{15}$),结果会被自动舍入,再转换为整数。例如,当有100个参与者、门限值为27时,计算$x = 100$、$i = 26$对应的$100^{26} = 10^{52}$,数值超出$2^{53}$达36个数量级。即使是$x = 20$、$i = 16$的情况,也会触发该问题。

这会导致多项式求值错误,进而使分发给参与者的密钥份额失效。根据参数不同,要么签名合并直接失败,要么生成看似正常但无法还原出目标密钥的份额。我们的AI工具将其标记为严重,因为错误的密钥份额会破坏协议的正确性。但Cloudflare最终评估为低严重程度,理由是实际场景中触发该问题的概率极低。

修复方案采用了代码中TODO注释早已建议的霍纳法(Horner's method)进行求值,全程使用big.Int类型,避免浮点运算。提交记录 f7d2180

漏洞2:通过 prover 可控的安全参数实现DLEQ证明伪造

该漏洞存在于zk/qndleq模块,这是CIRCL针对$(\mathbb{Z}/n\mathbb{Z})^*$平方子群实现的DLEQ(离散对数相等)证明。DLEQ证明用于验证两对元素具有相同的离散对数;若攻击者能让验证者接受虚假证明,整个证明系统就会失效。

该证明中的挑战值采用Fiat-Shamir方法生成,其比特长度由SecParam参数控制。问题在于,SecParam被包含在Proof结构体内部:

type Proof struct {
Z, C *big.Int
SecParam uint
}

验证时,代码会使用证明自身携带的SecParam重新计算挑战值。而这个参数完全由攻击者控制:若设置SecParam = 1,挑战值就会退化为单个比特(0或1),攻击者只需尝试两次即可伪造证明;若设置SecParam = 8,暴力破解仅需约$2^8 = 256$次尝试。无论哪种情况,证明的可靠性都会彻底丧失。

这是一类典型的漏洞模式:本应由验证者固定的安全参数,却从 prover 提供的数据中读取。修复方案将SecParam从证明结构体中移除,改为让Verify函数接收显式参数,由验证者自行设置。提交记录 757dde4

漏洞3:BLS聚合签名未校验消息唯一性

这是本次发现中AI低估严重程度的漏洞。AI将其标记为中等,但实际上这是典型的恶意密钥攻击(rogue key attack)——一种广为人知的严重级漏洞。我们提交时将其列为严重,Cloudflare最终确认为高严重程度。

sign/bls中的VerifyAggregate函数实现了BLS的BASIC聚合模式。该模式仅在所有消息唯一时才安全,这是防范恶意密钥攻击的核心前提。但该函数仅验证了聚合配对等式,未检查消息是否唯一,将这一关键要求完全交由调用方处理。

缺少该校验时,标准恶意密钥攻击即可生效:攻击者获取受害者公钥$\mathsf{pk}_v$和消息$m$后,可注册公钥$\mathsf{pk}_a = g^{\mathsf{sk}_a} - \mathsf{pk}_v$,无需知晓受害者私钥,即可伪造针对$(\mathsf{pk}_v, m)$和$(\mathsf{pk}_a, m)$的聚合签名。此外,CIRCL未提供密钥持有证明(proof-of-possession)机制作为兜底,进一步加剧了该漏洞的危险性。

AI为何将其标记为中等?我们无法确定具体原因。从它的推理过程来看,它正确识别了缺失的唯一性校验,甚至提到了恶意密钥攻击,但随后陷入了BASIC模式的契约逻辑——认为消息唯一性是调用方的责任,并将此视为风险缓解因素,从而降低了严重程度评级。

修复方案修改了VerifyAggregate函数,使其直接拒绝包含重复消息的聚合签名。提交记录 9798df7

漏洞4:通过FillBytes签名碰撞破坏DLEQ可靠性

回到zk/qndleq模块,这是本次发现中最隐蔽、也最有趣的漏洞,甚至无需修改证明本身即可利用。

假设存在一个合法证明pi,用于验证声明$S_1 = (g, g_x, h, h_x)$,即证明$\log_g(g_x) = \log_h(h_x) = x$。攻击者无需知晓$x$,只需将同一个证明pi与另一个声明$S_2 = (g, -g_x, h, h_x)$(其中$-g_x$是big.Int类型的负值,通过new(big.Int).Neg(gx)生成)一起提交给验证者。

当挑战值$c$为偶数时,伪造的声明会被验证通过,这源于两个因素的叠加:

代数抵消:验证者基于$-g_x$重新计算值时,符号会直接抵消: 当$c$为偶数时,$(-1)^c = 1$,验证者会得到与诚实 prover 完全相同的中间值。

哈希签名碰撞:挑战值通过哈希声明生成,而哈希过程使用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)^{\text{偶数}} = 1$)与看似无害的序列化选择(FillBytes丢弃符号)共同作用的结果。两者单独存在时均无问题,但结合起来就会破坏证明的可靠性。这种跨领域的逻辑关联推理,正是AI模型展现出的令人意外的能力。

AI将其评为高严重程度(因为破坏了证明可靠性),但Cloudflare因攻击复杂度较高,将其确认为低严重程度。

修复方案在挑战值计算过程中新增了checkBounds步骤,要求所有输入满足0 < x < N。负值-gx会被直接拒绝,无法造成危害。提交记录 19848a5

漏洞5:通过按位或分支绕过HPKE预共享密钥验证

这几乎是一个语言层面的陷阱。在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 modePSK | modeAuthPSK实际上等价于case 0x03,而modePSK0x01)无法匹配任何分支。原本用于拒绝PSK模式下缺失PSK的代码分支被完全跳过。

这会导致SetupPSK(..., nil, nil)在PSK为空的情况下仍能正常执行,而不是被拒绝。PSK模式本应强制要求提供预共享密钥,但该漏洞会悄无声息地跳过身份验证前提,使系统运行在比配置更弱的安全模式下。修复只需将按位或改为逗号分隔的多分支(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 \times 10^{18}$),并发生静默溢出(Go语言不会因整数溢出触发panic)。computeLambda会返回错误的系数,通常为负值。

第二个是截断问题:即使未发生溢出,也会出现错误。代码先计算num / den,再乘以delta。在Shoup的门限签名方案中,$\delta \cdot \text{num}$能被den整除,但当份额索引不连续(这是t-of-n门限方案的常见情况)时,num本身无法被den整除。例如,在3-of-5方案中合并份额${1, 3, 5}$时,某个系数的$\text{num} = (0-3)(0-5) = 15$,$\text{den} = (1-3)(1-5) = 8$。错误的计算顺序会得到$\delta \cdot \lfloor 15/8 \rfloor$,当$\delta = 120$时结果为120;而正确的计算顺序$\delta \cdot 15 / 8$结果应为225。

修复方案将整个计算过程改为使用big.Int类型,并调整乘法与除法的顺序,确保整除性。提交记录 751e372。这两个漏洞彼此独立,但AI将它们合并为一个发现提交,我们尊重AI的判断,未做拆分。

漏洞7:一行代码错误导致CP-ABE访问控制完全失效

这是zkao独立发现的漏洞。在确认上述6个漏洞后,我们用zkao扫描同一代码库,得到了这个发现。它导致CIRCL的密文策略属性加密(abe/cpabe/tkn20)的访问控制机制完全失效,Cloudflare已确认其有效性。

密文策略属性加密(CP-ABE)支持基于策略的加密,例如(地点: 美国 AND 部门: 财务) OR (角色: 管理员)。用户持有与自身属性绑定的密钥,只有当属性满足策略时才能解密。例如,美国财务部员工和管理员可以解密消息,其他人即使拿到相同密文也无法解密。

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被设为0,最后一行代码将In0覆盖为parent - In1(即parent本身)。最终一个子节点获得完整密钥,另一个子节点获得空值。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 - 随机值

修改后,In0保留随机值,In1持有parent - In0,单个AND叶子节点无法单独获取完整密钥。提交记录 def2fd3

几点发现

我们有三点重要观察:

AI对严重程度的判断能力不足,且误差具有不对称性。再看开头的表格,如果以Cloudflare确认的评级为“真值”,多数情况下AI高估了漏洞的影响;但在BLS漏洞上却出现了相反的情况——它将典型的恶意密钥攻击(严重级漏洞)低估为中等。我们目前还没有完整的解释和解决方案,但初步假设是:对于CIRCL这类被广泛应用的库,漏洞影响取决于下游调用场景,而模型无法获取这些信息,因此难以准确判断。我们认为,添加统一的严重程度评估矩阵,并引入明确的威胁建模步骤,能帮助模型从系统层面(及潜在集成场景)而非仅从局部代码推理漏洞影响。目前,严重程度评级仍是我们依赖人工处理的环节。

zkao中,我们暂时通过让开发者在用户配置文件(zkao.md)中明确威胁模型和严重程度偏好来解决这一问题,并持续迭代优化zkao的评级能力。此外,zkao能稳定生成POC的特性也减少了误报,让我们对真实漏洞更有信心。不过,我们仍在系统地改进其默认评级能力,这对开发者至关重要。需要注意的是,Cloudflare的评级是基于其漏洞悬赏计划,重点考量漏洞是否影响其线上服务。例如漏洞2,尽管它完全破坏了证明的可靠性,但仍被评为低严重程度——这仅意味着受影响的代码未被Cloudflare服务使用,或在其环境中影响有限,并不代表在其他部署场景中危害同样小。这一点对于被众多项目依赖的开源库尤为重要。

模型组合并非对称,角色会发生反转。6个漏洞中有5个由Claude Opus 4.6结合专家技能发现。在相同技能和系统提示下,GPT-5.3更多扮演验证角色而非发现者,表格中的HPKE漏洞是它独立发现的唯一漏洞。我们原本认为这种分工是稳定的,但事实并非如此:几周后,我们用新版本组合(Opus 4.7和GPT-5.4)重新扫描,角色完全反转——GPT-5.4发现更多漏洞,而Opus 4.7仅能验证。这提醒我们,不要过度依赖特定模型的表现。AI技术前沿一直在推进,未来还会不断变化,我们将在后续文章中探讨这一话题。

这种模式正是我们打造zkao的初衷——让它具备“模型无关性”,无需猜测下个月哪个模型表现最佳,始终保持最优性能。

AI能收集漏洞,但不擅长关联分析漏洞6就是典型例子:AI将两个完全独立的漏洞(溢出和整数截断)合并为一个发现提交。两者都是真实漏洞,这确实有价值,但AI只是将它们并列呈现,未分析它们之间的关联。我们在其他场景中也见过类似模式:AI会收集多个真实观察结果,但不会尝试将它们关联成更具影响力的完整利用链。

而这正是zkao的核心突破之一:我们设计了全新流程,能够将独立漏洞组合起来,形成完整的端到端利用链。

后续计划

感谢CIRCL的维护者及时修复了所有报告的漏洞。本文是系列文章的第一篇,我们将继续发布其他项目中已修复的真实漏洞。

我们扫描了超过200个密码学项目(从下载量最高的密码学包中选取),得到了超过1000个候选漏洞。目前最大的瓶颈是人工筛选:在提交给项目方之前,每个漏洞都需要我们的专家验证技术有效性,因为我们和所有人一样厌恶AI生成的垃圾报告。这需要大量人力,因为我们尚未完全信任正在开发的自动筛选流程(尽管它在不断改进)。因此我们优先处理了一些最受欢迎且维护良好的项目。

如果你是密码学项目的维护者,对我们的工作感兴趣,我们很乐意与你合作,及时发现并修复严重漏洞。如果你的项目尚未被扫描,或自上次扫描后代码发生重大变更,我们可以重新扫描。这种持续的AI监控正是zkao的设计目标。欢迎通过zksecurity.xyz/contact联系我们。