AI meets Cryptography 3: What AI Found in Bron Labs's bron-crypto - ZK/SEC Quarterly
Back to all posts
This is the third post in the series.<br>If you have not read the first one on Cloudflare's CIRCL or the second on OpenVM's zkVM, we recommend reading them first.<br>They have more context on why we run these experiments and how the pipeline is set up.<br>Above all, because they are fun.<br>This time, we pointed our AI audit pipeline at bron-crypto, Bron Labs's Go library for MPC and threshold signatures, and detected some zero-day vulnerabilities.
bron-crypto is Bron Labs's cryptography library.<br>While it mostly focuses on MPC protocols, it also supports many other primitives that are reusable by other applications.<br>That makes it a good target for our experiment, since, as we said earlier,<br>auditing multiple isolated modules can be coordinated effectively by the standard harnesses in popular coding agents, such as Codex and Claude Code.
Clarification , same as in the previous posts: Our automated process produced a lot of "candidate" findings.<br>Experts on our team still validated each issue, confirmed exploitability, minimized the proof of concept, and handled responsible disclosure.
We ran bron-crypto through a dual-agent pipeline, with Claude Opus 4.6 as the primary auditor and Codex 5.3 as an independent validator (and vice versa), and ended up with 30 findings.<br>The four below are the ones worth walking through in detail.<br>The rest were fixed before we could allocate time to validate them.<br>We come back to the aggregate at the end, because it says something about how consistent LLM bug-finding has become.
Besides that, we earned some bounties and were acknowledged for those bugs under Bron's bug bounty program.
Severities and fixes at a glance
As in the first post, the severity an AI assigns its own finding is still very noisy.<br>The AI tends to rate library code higher than the maintainer does, because impact depends on downstream callers the model cannot see.<br>Therefore we think it just assumes the worst scenario.<br>Here is each bug as the AI rated it and as Bron Labs confirmed it once fixed.
Bug<br>AI severity<br>Bron severity<br>Fix<br>Found by
Lindell17 DKG corrupts encrypted shares via swapped operands<br>Critical<br>High<br>e80a2ea (#197)<br>Opus 4.6 + skills
Poseidon hash.Hash implementation breaks the Go contract<br>High<br>Low<br>8d203d0 (#202)<br>Opus 4.6 + skills
IsOnCurve inverted on k256, Pallas, and Vesta<br>High<br>Medium<br>e070f8f (#196)<br>Opus 4.6 + skills
BLS12-381 G2 IsZero() calls IsOne()<br>Low<br>Low<br>4601a36 (#222)<br>Opus 4.6 + skills
Now let's go through each one in detail.
Bug 1: a swapped operand that corrupts threshold-ECDSA key shares
Background
This one is in the distributed key generation (DKG) for Lindell17 threshold ECDSA.<br>The whole point of threshold signing is that no single party ever holds the private key.<br>The key is split into shares, and signing happens jointly without any party reconstructing the secret.<br>DKG is the protocol that produces those shares in the first place, without a trusted dealer.
To understand the bug, the only piece you need to keep in mind is how the shares are stored and processed.<br>Each party chooses a private share $x \in \mathbb{Z}_q$, then publishes the public share as an elliptic curve point $Q = x\cdot G$.<br>Additionally, each party also publishes the encrypted share $c = \text{Enc}(x)$, where $\text{Enc}(\cdot)$ is Paillier encryption, so we will consider $(c, Q)$ as the public share for simplicity.
The reason to use Paillier encryption is its additively homomorphic property, i.e., $\text{Enc}(a)\cdot\text{Enc}(b)=\text{Enc}(a + b)$.<br>Specifically, the protocol decomposes each private share $x \in \mathbb{Z}_q$ into two pieces $x'$ and $x''$ in $\{\dfrac{q}{3},...,\dfrac{2q}{3}\}$, such that $x = 3x' + x''$.<br>The reason for this decomposition is that, given the public share $(c, Q)$ of a party,<br>we need to verify a "zero-knowledge proof of a Paillier encryption of a discrete log" (which we call the $L_{PDL}$ proof), which proves that there exists an $x \in \mathbb{Z}_q$ satisfying $c = \text{Enc}(x)$ and $Q = xG$.<br>But the paper relaxes the soundness of this proof to hold only for $x \in \{\dfrac{q}{3},...,\dfrac{2q}{3}\}$ for efficiency.<br>So the workaround is that, instead of directly giving $Q = xG$ with $x$ in the full range, each party splits $x$ into $x'$ and $x''$ in shorter range, and provides the $L_{PDL}$ proofs for $(c' = \text{Enc}(x'), Q' = x'G)$ and $(c'' = \text{Enc}(x''), Q'' = x''G)$.<br>Then the other parties verify the proofs, and reconstruct $c = \text{Enc}(x')\cdot\text{Enc}(x')\cdot\text{Enc}(x')\cdot\text{Enc}(x'')$ and $Q = 3Q' + Q''$.
The final public key of the DKG protocol is the sum of those $Q$ values from all parties.
The bug
The implementation built the stored ciphertext like this:
// pkg/mpc/tsig/tecdsa/lindell17/keygen/dkg/round.go<br>p.state.theirPaillierEncryptedShares[id]...