{"slug": "ai-meets-cryptography-3-what-ai-found-in-bron-labs-s-bron-crypto", "title": "AI meets Cryptography 3: What AI Found in Bron Labs's bron-crypto", "summary": "Bron Labs's bron-crypto Go library for MPC and threshold signatures was found to contain zero-day vulnerabilities by an AI audit pipeline using Claude Opus 4.6 and Codex 5.3, yielding 30 findings including a swapped operand in threshold-ECDSA key generation that corrupts key shares. The bugs were responsibly disclosed and fixed under Bron's bug bounty program.", "body_md": "This is the third post in the series.\nIf you have not read the [first one on Cloudflare's CIRCL](../circl-bugs) or the [second on OpenVM's zkVM](../openvm-bugs), we recommend reading them first.\nThey have more context on why we run these experiments and how the pipeline is set up.\nAbove all, because they are fun.\nThis time, we pointed our AI audit pipeline at [bron-crypto](https://github.com/bronlabs/bron-crypto), Bron Labs's Go library for MPC and threshold signatures, and detected some zero-day vulnerabilities.\n\n[bron-crypto](https://github.com/bronlabs/bron-crypto) is Bron Labs's cryptography library.\nWhile it mostly focuses on MPC protocols, it also supports many other primitives that are reusable by other applications.\nThat makes it a good target for our experiment, since, as we said earlier,\nauditing multiple isolated modules can be coordinated effectively by the standard harnesses in popular coding agents, such as Codex and Claude Code.\n\nClarification, same as in the previous posts: Our automated process produced a lot of \"candidate\" findings. Experts on our team still validated each issue, confirmed exploitability, minimized the proof of concept, and handled responsible disclosure.\n\nWe 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. The four below are the ones worth walking through in detail. The rest were fixed before we could allocate time to validate them. We come back to the aggregate at the end, because it says something about how consistent LLM bug-finding has become.\n\nBesides that, we earned some bounties and were acknowledged for those bugs under [Bron's bug bounty program](https://bugbounty.bron.org/hall-of-fame).\n\n## Severities and fixes at a glance\n\nAs in the first post, the severity an AI assigns its own finding is still very noisy. The AI tends to rate library code higher than the maintainer does, because impact depends on downstream callers the model cannot see. Therefore we think it just assumes the worst scenario. Here is each bug as the AI rated it and as Bron Labs confirmed it once fixed.\n\n| # | Bug | AI severity | Bron severity | Fix | Found by |\n|---|---|---|---|---|---|\n| 1 |\n|\n\n[(#197)](https://github.com/bronlabs/bron-crypto/pull/197)`e80a2ea`\n\n[Poseidon](#bug-2-poseidon-hashhash-implementation-breaks-the-go-contract)`hash.Hash`\n\nimplementation breaks the Go contract[(#202)](https://github.com/bronlabs/bron-crypto/pull/202)`8d203d0`\n\n`IsOnCurve`\n\ninverted on k256, Pallas, and Vesta[(#196)](https://github.com/bronlabs/bron-crypto/pull/196)`e070f8f`\n\n[BLS12-381 G2](#bug-4-iszero-that-checks-for-one)`IsZero()`\n\ncalls `IsOne()`\n\n[(#222)](https://github.com/bronlabs/bron-crypto/pull/222)`4601a36`\n\nNow let's go through each one in detail.\n\n## Bug 1: a swapped operand that corrupts threshold-ECDSA key shares\n\n### Background\n\nThis one is in the distributed key generation (DKG) for [Lindell17 threshold ECDSA](https://eprint.iacr.org/2017/552.pdf).\nThe whole point of threshold signing is that no single party ever holds the private key.\nThe key is split into shares, and signing happens jointly without any party reconstructing the secret.\nDKG is the protocol that produces those shares in the first place, without a trusted dealer.\n\nTo understand the bug, the only piece you need to keep in mind is how the shares are stored and processed. 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$. 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.\n\nThe reason to use Paillier encryption is its additively homomorphic property, i.e., $\\text{Enc}(a)\\cdot\\text{Enc}(b)=\\text{Enc}(a + b)$. 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''$. The reason for this decomposition is that, given the public share $(c, Q)$ of a party, 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$. But the paper relaxes the soundness of this proof to hold only for $x \\in \\{\\dfrac{q}{3},...,\\dfrac{2q}{3}\\}$ for efficiency. 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)$. 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''$.\n\nThe final public key of the DKG protocol is the sum of those $Q$ values from all parties.\n\n### The bug\n\nThe implementation built the stored ciphertext like this:\n\n```\n// pkg/mpc/tsig/tecdsa/lindell17/keygen/dkg/round.go\np.state.theirPaillierEncryptedShares[id] =\n    theirCKeyPrime.HomAdd(theirCKeyDoublePrime).HomAdd(theirCKeyDoublePrime).HomAdd(theirCKeyDoublePrime)\n```\n\nRead it through the homomorphism. It starts from $x'$, then adds $x''$ three times. The result is $\\text{Enc}(x' + 3x'')$, not $\\text{Enc}(3x' + x'')$. The two operands were swapped. The stored ciphertext encrypts a value that is not the participant's share.\n\nThis corrupts the encrypted key share stored during DKG. Subsequent threshold ECDSA signing operations using this encrypted share will produce incorrect partial signatures, leading to signing failures.\n\n### The fix\n\nAs you might guess, the fix is trivial.\nSwap the operands back so that the tripled term is `x'`\n\n:\n\n``` php\n// Fixed: start from x'', add x' three times -> Enc(3x' + x'')\np.state.theirPaillierEncryptedShares[id] =\n    theirCKeyDoublePrime.HomAdd(theirCKeyPrime).HomAdd(theirCKeyPrime).HomAdd(theirCKeyPrime)\n```\n\nIt landed in PR [#197](https://github.com/bronlabs/bron-crypto/pull/197) (merge commit [ e80a2ea](https://github.com/bronlabs/bron-crypto/commit/e80a2eae9786fe9a21297212fc91dc8db33527a5)).\n\n### Impact\n\nThreshold ECDSA sessions that use DKG-generated shards, as opposed to trusted-dealer shards, can produce incorrect signatures or fail to sign at all. For a custody system this is an availability problem, not a key-compromise one. The secret is not leaked, but the parties may be unable to produce a valid signature. For a wallet, that means funds that cannot be moved.\n\n## Bug 2: Poseidon `hash.Hash`\n\nimplementation breaks the Go contract\n\n### Background\n\n[Poseidon](https://eprint.iacr.org/2019/458) is a hash function designed to be cheap inside arithmetic circuits and ZK proofs.\nLike most modern hashes, it is a sponge: you *absorb* input into an internal state, then *squeeze* a digest out of it.\nbron-crypto exposes Poseidon in two ways: through its own `Update`\n\n/`Digest`\n\nsponge API, and through Go's standard `hash.Hash`\n\ninterface as a drop-in hash.\n\nThat standard interface comes with a contract that the whole Go ecosystem relies on. Two parts of it matter here:\n\n`Write`\n\n*accumulates*. Calling`Write(a)`\n\nthen`Write(b)`\n\nmust hash`a`\n\nfollowed by`b`\n\n, exactly as if you had written`a || b`\n\nin one call.`Sum(b)`\n\nis*non-mutating and prefix-appending*. It returns`append(b, digest...)`\n\n: the bytes`b`\n\nare a prefix glued in front of the digest, not more input to the hash, and calling it must not disturb the hasher's state.\n\n### The bug\n\nThe `hash.Hash`\n\nadapter broke both halves of that contract.\n\n`Write`\n\nwas implemented on top of the one-shot `Hash()`\n\nentry point, which resets the sponge before absorbing.\nSo every call to `Write`\n\nwill clear whatever had been written before it:\n\n```\n// pkg/hashing/poseidon/poseidon.go\nfunc (p *Poseidon) Write(data []byte) (n int, err error) {\n    // ... converts bytes to field elements ...\n    p.Hash(elems...) // Hash() resets the sponge, so earlier writes are discarded\n    return len(data), nil\n}\n...\nfunc (p *Poseidon) Sum(data []byte) []byte {\n    _, err := p.Write(data)\n    if err != nil {\n        panic(err)\n    }\n    return p.Digest().Bytes()\n}\n```\n\n`Write(a); Write(b)`\n\ntherefore hashes only `b`\n\n.\nAnd `Sum`\n\ncalled straight into `Write`\n\n.\nThat reset the state, violating the non-mutating rule, and treated the argument as hash input rather than an output prefix.\n`Write`\n\nalso only accepts input whose length is a multiple of 32 bytes, so `Sum`\n\nwould panic on a prefix that was not 32-byte aligned.\n\n### The fix\n\nMake `Write`\n\nabsorb without resetting, and make `Sum`\n\nappend without mutating:\n\n```\nfunc (p *Poseidon) Write(data []byte) (n int, err error) {\n    // ... converts bytes to field elements ...\n    p.Update(elems...) // Update absorbs into the current state, no reset\n    return len(data), nil\n}\n\nfunc (p *Poseidon) Sum(b []byte) []byte {\n    return append(b, p.Digest().Bytes()...) // non-mutating, prefix-appending\n}\n```\n\nFixed in PR [#202](https://github.com/bronlabs/bron-crypto/pull/202) (merge commit [ 8d203d0](https://github.com/bronlabs/bron-crypto/commit/8d203d0000b241b54a933f73419e773aa8b07c89)).\n\n### Impact\n\nAny caller that reached Poseidon through the standard `hash.Hash`\n\ninterface got wrong results.\nStreaming a message across several `Write`\n\ncalls hashed only the final chunk, and `Sum`\n\npanicked on a prefix that was not 32-byte aligned.\nFortunately, bron-crypto's own protocols use the direct `Update`\n\n/`Digest`\n\nsponge API rather than this adapter, so the internal MPC code was not affected.\n\n## Bug 3: an on-curve check that says yes when it means no, and vice versa\n\n### Background\n\nBefore a library touches an elliptic-curve point that came from outside, it should check that the point actually lies on the curve.\nSkip that check and you open the door to invalid-curve attacks: an attacker hands you a point on a weaker related curve, and your scalar multiplications with the secret leak information about it.\nThe check that catches this is called `IsOnCurve`\n\nin Go's standard library.\n\n### The bug\n\n`bron-crypto`\n\nwraps several of its curves in Go's `elliptic.Curve`\n\ninterface.\nFunnily enough, the `IsOnCurve`\n\nadapter for k256, Pallas, and Vesta returned the negation of what it should:\n\n```\n// pkg/base/curves/k256/elliptic.go (and pasta/elliptic.go for Pallas, Vesta)\nreturn err != nil // BUG: true means \"error\", i.e. NOT on curve. Inverted.\n```\n\nThe underlying routine reports an error when the point is *not* on the curve, so a valid point gives `err == nil`\n\n.\nReturning `err != nil`\n\nflips the whole thing on its head.\nValid points, including the curve generator, get rejected, and arbitrary off-curve points get accepted.\n\nBecause `elliptic.Unmarshal`\n\nleans on `IsOnCurve`\n\ninternally, the inversion propagates: deserializing a valid encoded point fails, and deserializing a bad one succeeds.\n\n### The fix\n\nOne character per curve, `!=`\n\nto `==`\n\n, in three places:\n\n```\nreturn err == nil\n```\n\nFixed in PR [#196](https://github.com/bronlabs/bron-crypto/pull/196) (merge commit [ e070f8f](https://github.com/bronlabs/bron-crypto/commit/e070f8f5e98322a85e8dc83c08e381a2f597e091)).\n\n### Impact\n\nValid points get rejected, so nothing that relies on them works. Invalid points get accepted, which can lead to private key extraction due to invalid-curve attacks. Fortunately, this check lives in a Go adapter for k256, Pallas, and Vesta that bron-crypto's own protocols do not use. So the ones actually at risk were outside callers who unmarshal points through Go's standard path.\n\n## Bug 4: `IsZero`\n\nthat checks for one\n\nThe last one is the simplest.\nIn the shared finite-field trait, `IsZero`\n\nwas wired to the wrong predicate:\n\n```\n// pkg/base/curves/impl/traits/finite_field.go\n// IsZero reports whether the element is zero.\nfunc (fe *FiniteFieldElementTrait[FP, F, WP, W]) IsZero() bool {\n    return FP(&fe.V).IsOne() != 0 // BUG: calls IsOne, copied from the method above\n}\n```\n\nIt is a copy-paste from the `IsOne`\n\nmethod directly above it.\nThe result is that `IsZero`\n\nreturns true for the element `1`\n\nand false for the actual `0`\n\n.\nThat trait is embedded by the BLS12-381 G2 field element, so everything built on top inherits the inversion.\nIdentity-point detection (`IsOpIdentity`\n\n) reports `1`\n\nas the identity and the real identity as non-identity, and the Euclidean valuation used in field arithmetic comes out wrong.\n\nIn practice this sat below the reachable surface, which is why both we and Bron Labs scored it Low. The fix is to just call the right method:\n\n```\nfunc (fe *FiniteFieldElementTrait[FP, F, WP, W]) IsZero() bool {\n    return FP(&fe.V).IsZero() != 0\n}\n```\n\nIt went in as part of the broader \"Robustness fixes\" PR [#222](https://github.com/bronlabs/bron-crypto/pull/222) (merge commit [ 4601a36](https://github.com/bronlabs/bron-crypto/commit/4601a36658ca0e8d74e529b10ab6dbacdb044dc1)).\n\n## A few things we learned\n\nWe don't want to exaggerate the complexity of these bugs.\nThey are essentially \"typo\" bugs that popular harnesses can detect nowadays, even with older models.\nTo find deeper, more \"cryptographic-looking\" bugs, you may need a more thorough harness, such as [zkao](https://zkao.io/).\nWe list these bugs mainly to prove a more interesting pattern about LLM outputs, which people still think of as very unpredictable:\n\n**The set of bugs an LLM can find is more stable than it looks.**\nThe most interesting thing about our experiment is not any individual bug, but the repetition.\nUnder a fixed model and a similar prompt, repeated runs converge on roughly the same set of findings.\nWe see it from our side: the same issues resurface across runs.\nWe suspect the maintainers saw it from the other side too.\nSeveral of our independent findings were resolved together inside a PR, like \"Robustness fixes\" ([#222](https://github.com/bronlabs/bron-crypto/pull/222)).\nThat is exactly the pattern you would expect if the team had run its own model pass, gotten an overlapping set, and swept the results into one PR.\nIt also matches what everyone running a bug bounty already feels: duplicate reports keep getting more and more common,\nas the same model-reachable bugs surface again and again from independent participants.\n\nThe practical takeaway is that after a few runs, re-rolling the same setup does not get you more bugs.\nWhat actually moves the set of bugs you can reach is the harness around the model: how expert knowledge is embedded, how the code is decomposed, how context is fed in, and how subagents are coordinated.\nThat is the whole bet behind [zkao](https://zkao.io/), and it is why we are not satisfied with \"run the LLM a few more times.\"\nWe are also running a more careful study on the obvious follow-up question: how many runs do you need before you can be confident an LLM has found everything it is going to find on a given target?\nWe will write that up separately.\n\nAnother lesson from these experiments, and from building zkao, is that model performance varies significantly depending on the harness and prompting strategy. When using LLMs to discover vulnerabilities, this becomes especially important. Maintaining an effective setup requires continuous iteration, as the strongest models evolve and respond differently to different forms of guidance. We plan to share more of our experiments aimed at understanding why these differences emerge, as well as the trade-offs involved in designing and maintaining an effective harness.\n\nWe also have another \"stronger\" opinion: whenever you find a bug using an LLM through a highly optimized custom harness, the \"secret sauce\" is unlikely to remain exclusive for long. Depending on each provider's policies and user settings, the harness may eventually be part of future training. Even when it does not, similar techniques and findings are likely to be shared publicly and become part of the broader body of online knowledge. As a result, a vulnerability that initially required a sophisticated harness may eventually be rediscovered with a much simpler prompt:\n\n```\n“Find all the bugs. MAKE NO MISTAKES!”\n```\n\nThis is why, even when we find significant vulnerabilities with zkao, we don't claim to have solved security or to outperform every security researcher in the field. Our goal is to build a continuously improving autonomous security platform that evolves as new models emerge, our understanding of LLM-based analysis improves, and we incorporate the feedback of our clients and the knowledge of our security researchers. It is a continuous effort trying to stay ahead of hackers.\n\n## What's next\n\nOur thanks to the Bron Labs team, who triaged and fixed all four issues promptly. This is the third post in the series, and we will keep publishing confirmed bugs from other projects as they are resolved.\n\nIf you maintain a cryptography or MPC project and this sounds interesting, we would love to look at it with you, whether through zkao or a manual audit.\nReach out at [zksecurity.xyz/contact](https://zksecurity.xyz/contact).", "url": "https://wpnews.pro/news/ai-meets-cryptography-3-what-ai-found-in-bron-labs-s-bron-crypto", "canonical_source": "https://blog.zksecurity.xyz/posts/bron-bugs/", "published_at": "2026-07-22 00:00:00+00:00", "updated_at": "2026-07-22 13:31:04.883977+00:00", "lang": "en", "topics": ["ai-tools", "ai-safety"], "entities": ["Bron Labs", "bron-crypto", "Claude Opus 4.6", "Codex 5.3", "Cloudflare", "CIRCL", "OpenVM"], "alternates": {"html": "https://wpnews.pro/news/ai-meets-cryptography-3-what-ai-found-in-bron-labs-s-bron-crypto", "markdown": "https://wpnews.pro/news/ai-meets-cryptography-3-what-ai-found-in-bron-labs-s-bron-crypto.md", "text": "https://wpnews.pro/news/ai-meets-cryptography-3-what-ai-found-in-bron-labs-s-bron-crypto.txt", "jsonld": "https://wpnews.pro/news/ai-meets-cryptography-3-what-ai-found-in-bron-labs-s-bron-crypto.jsonld"}}