AI News HubLIVE
In-site rewrite6 min read

AI Meets Cryptography 1: What AI Found in Cloudflare's Circl

zkSecurity's AI audit pipeline uncovered seven real bugs in Cloudflare's CIRCL cryptography library, ranging from a critical float64 precision loss in threshold RSA to a complete access-control break in attribute-based encryption. All seven are now fixed upstream. This is the first post in a series on bugs found by AI across open source cryptography.

SourceHacker News AIAuthor: duha

We pointed our AI audit pipeline at Cloudflare's CIRCL experimental cryptography library and confirmed seven real bugs, from a critical float64 precision loss in threshold RSA to a complete access-control break in attribute-based encryption. All seven are now fixed upstream. This is the first post in a series on bugs our agents found across open source cryptography.

At zkSecurity we are building zkao, an AI audit agent. The goal is simple to state and hard to do: keep an AI looking at your code, continuously, until no bugs remain that other AI tools can find. We wrote about why that approach matters in zkao: Security That Compounds.

Building zkao has been an iterative process, with the end goal of creating an automated auditor capable of finding all bugs that can be detected by AI. This has involved brainstorming new ideas and techniques, systematically encoding the expertise of zkSecurity's security researchers into zkao, ensuring that it detects the latest and most severe vulnerabilities without being biased toward benchmarks, and, importantly, continuously conducting experiments to understand what works, what does not, how models evolve, and to deepen our understanding of bug finding with AI. Some of those experiments produce things worth sharing on their own, independent of the product, and that is what this post series is about.

There is also a second motivation. These experiments are how we build a benchmark suite for zkao, and along the way they keep surfacing insights into how LLMs actually reason about cryptography: where they are sharp, where they are blind, and how to amplify the former and contain the latter. While the bugs are the visible output, the reasoning patterns are the part we care about most.

A few months ago, we started running experiments on selected codebases. We used LLMs to scan a few open source cryptography projects, in two configurations:

LLM only, with a simple prompt.

LLM with skills, where the skills were maintained by experts on our team.

Then, for the important projects where the LLMs found real vulnerabilities, we also ran zkao to see whether it could have detected the same issues on its own. In most cases, zkao not only found all of them but also identified more complex and more severe ones.

The results were good enough that we decided to write them up. We are starting this series with Cloudflare's CIRCL, a library of advanced and post-quantum cryptography. On CIRCL, our pipeline produced many candidate findings, and seven of them are worth reporting here. All seven are now fixed upstream. Most of them were confirmed and awarded bounties under Cloudflare's program on HackerOne.

Clarification: the AI produced candidate findings, not final reports. Humans on our team still validated each issue, checked exploitability, minimized the POC where needed, and handled disclosure. That human-in-the-loop step still matters a lot, because AI candidate findings are cheap while trustworthy reports are not.

Minimizing that step is one of the main things zkao is built to do, and while it is still a work in progress, the current version already takes on much of this validation work itself.

Severities and fixes at a glance

One thing worth flagging before the details: the severity an AI assigns to its own finding is noisy. Here is each bug as the AI rated it and as Cloudflare confirmed it once fixed. We also checked that all seven are consistently reproducible by the current version of zkao.

# Bug AI severity Cloudflare severity Fix commit Found by

1 float64 precision loss in TSS/RSA polynomial eval Critical Low f7d2180 Opus 4.6 + skills

2 qndleq forgery via prover-controlled SecParam High Low 757dde4 Opus 4.6 + skills

3 BLS aggregate missing message distinctness Medium High 9798df7 Opus 4.6 + skills

4 DLEQ soundness break via FillBytes sign collision High Low 19848a5 Opus 4.6 + skills

5 HPKE PSK validation bypass via bitwise-OR switch Medium Medium (Duplicate) a3b4fa3 GPT-5.3 + skills

6 TSS/RSA Lagrange coefficients in int64 High Medium 751e372 Opus 4.6 + skills

7 CP-ABE access-control break via AND-share bug Critical Critical def2fd3 zkao

The gap between the AI severity and the confirmed severity is itself an interesting insight, and we come back to it at the end. Now the seven bugs, one at a time.

Bug 1: polynomial evaluation in float64

This one lives in CIRCL's threshold RSA implementation (tss/rsa). Threshold signing splits a secret across n players using Shamir-style secret sharing. Deal() evaluates a secret polynomial at each player's index. The coefficients are big.Int, as they should be, but the term x^i was computed like this:

// tss/rsa/rsa_threshold.go xi := int64(math.Pow(float64(x), float64(i)))

float64 has a 53-bit mantissa. The moment $x^i$ exceeds $2^{53}$ (roughly $9 \times 10^{15}$), the result is silently rounded before it is ever cast back to an integer. For example, with 100 players and a threshold of 27, evaluating at $x = 100$ with $i = 26$ asks for $100^{26} = 10^{52}$, which overshoots $2^{53}$ by 36 orders of magnitude. Even $x = 20$, $i = 16$ already breaks it.

The consequence is that the polynomial is evaluated incorrectly, so the key shares handed to players are wrong. Depending on the parameters, signature combination either fails outright or produces shares that look fine but do not reconstruct the intended key. Our agent flagged this as critical, since it results in the generation of incorrect key shares, compromising the correctness of the protocol. Cloudflare ultimately assessed the issue as Low severity, based on the low likelihood of the affected conditions occurring in practice.

The fix replaces the floating-point exponentiation with the Horner's-method evaluation the code's own TODO comment had been suggesting all along, keeping everything in big.Int. Commit f7d2180.

Bug 2: a DLEQ proof forgery via a prover-controlled security parameter

This one is in zk/qndleq, CIRCL's DLEQ (discrete-log-equality) proof for the subgroup of squares in $(\mathbb{Z}/n\mathbb{Z})^*$. A DLEQ proof attests that two pairs share the same discrete log; if an attacker can make the verifier accept a proof for a false statement, the proof system is broken.

The challenge in this proof is derived Fiat-Shamir style, and its bit-length is governed by a SecParam. The problem was that SecParam lived inside the Proof struct itself:

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

During verification, the code recomputed the challenge using the proof's own SecParam. That field is attacker-controlled. Set SecParam = 1 and the challenge collapses to a single bit, value $0$ or $1$: a coin flip per forgery attempt. Set SecParam = 8 and brute force is about $2^8 = 256$ attempts. Either way, soundness is gone.

This is a clean instance of a recurring pattern: a security parameter that must be fixed by the verifier instead being read out of prover-supplied data. The fix removes SecParam from the proof and makes Verify take it as an explicit argument, so the verifier sets it. Commit 757dde4.

Bug 3: BLS aggregate verification without message distinctness

This is the one bug in the batch the AI underrated. The agent labeled it medium. It is in fact a textbook rogue key attack, a widely known critical-class flaw; we reported it as critical, and Cloudflare confirmed it as high.

VerifyAggregate in sign/bls implements the BLS BASIC aggregation mode. That mode is only secure if all messages in the batch are distinct, which is its defense against rogue key attacks. The function checked the aggregate pairing equation but never checked that the messages were distinct, leaving that critical requirement to the caller.

Without it, the standard rogue key attack applies. An adversary who sees a victim's public key $\mathsf{pk}_v$ and a message $m$ can register $\mathsf{pk}_a = g^{\mathsf{sk}_a} - \mathsf{pk}_v$ and forge an aggregate signature over $(\mathsf{pk}_v, m)$ and $(\mathsf{pk}_a, m)$ without ever knowing the victim's secret key. CIRCL ships no proof-of-possession infrastructure to fall back on, which makes the missing check more dangerous.

Why did the AI call this medium? We don't know. Reading its reasoning, it correctly spotted the missing distinctness check and even named the rogue key attack, but it then anchored on the fact that the BASIC mode's contract puts the distinctness requirement on the caller. It treated "the caller is supposed to handle this" as a mitigation and marked the severity down.

The fix makes VerifyAggregate reject batches built with duplicated messages. Commit 9798df7.

Bug 4: a DLEQ soundness break via a FillBytes sign collision

Back to zk/qndleq for the subtlest, and frankly most interesting, bug in the batch. It does not require touching the proof at all.

Take an honest, valid proof pi for the statement $S_1 = (g, g_x, h, h_x)$, which attests that $\log_g(g_x) = \log_h(h_x) = x$. An attacker who does not know $x$ presents the very same pi to the verifier, but pairs it with a different statement, $S_2 = (g, -g_x, h, h_x)$, where $-g_x$ is the negative big.Int new(big.Int).Neg(gx).

The forged statement is accepted whenever the challenge $c$ is even, because two things line up at once.

Algebraic cancellation. The verifier recomputes its value from $-g_x$, and the sign factors straight out:

$$(-g_x)^c \bmod N = (N - g_x)^c \bmod N = (-1)^c \cdot g_x^c \bmod N.$$

When $c$ is even, $(-1)^c = 1$, so the verifier reconstructs exactly the same intermediate values the honest prover did.

A sign collision in the hash. The challenge is derived by hashing the statement, and the hashing uses FillBytes, which writes the absolute value of a big.Int and strips the sign. So doChallenge(..., -gx, ...) and doChallenge(..., gx, ...) hash to the same thing.

Here, $c$ is even with probability at least $1/2$ (it is just the low bit of the hash output), so the attack succeeds on roughly half of all honestly generated proofs. The verifier walks away convinced that $\log_g(-g_x) = \log_h(h_x)$, which is false. Here is the core of the proof-of-concept:

// honest proof for (g, gx, h, hx), selected to have an even challenge c gxNeg := new(big.Int).Neg(gx) // -gx, attacker needs no knowledge of x forgedAccepted := proof.Verify(g, gxNeg, h, hx, N) // accepted!

What makes this one stand out is that it is not a single sloppy line. It is the interaction between an algebraic identity ($(-1)^{\text{even}} = 1$) and an innocent-looking serialization choice (FillBytes dropping the sign). Neither is wrong on its own. Together they break soundness. Reasoning across that kind of boundary is exactly where we have been most surprised by what the models surface.

On severity, the agent rated this high because it is a soundness break, but Cloudflare confirmed it as low due to its high attack complexity.

The fix adds a checkBounds step to challenge computation that requires every input to satisfy 0 < x < N. A negative -gx has a negative sign and is rejected before it can do any damage. Commit 19848a5.

Bug 5: HPKE PSK validation bypassed by a bitwise-OR switch

This one is almost a language-level trap. In HPKE's verifyPSKInputs, the switch labels were written with bitwise-OR:

// hpke/util.go case modeBase | modeAuth: // 0x00 | 0x02 == 0x02, i.e. only modeAuth case modePSK | modeAuthPSK: // 0x01 | 0x03 == 0x03, i.e. only modeAuthPSK

In Go, a case a | b: is a single case whose value is the OR of the two constants, not two cases. So case modePSK | modeAuthPSK is really case 0x03, and modePSK (0x01) matches no case at all. The branch that is supposed to reject a missing PSK in PSK mode is simply skipped.

The effect: SetupPSK(..., nil, nil) proceeds with an empty PSK instead of being rejected. PSK mode is supposed to require PSK material; this silently drops the authentication precondition and lets a deployment run in a weaker mode than it was configured for. The fix is the one-character-class change f

[truncated for AI cost control]