Repository navigation
Fix the findings of security audit run 2 in the Noir and fflonk tools - #24
Merged
Merged
Conversation
…nd public layouts noir-cli would leak Three findings of security audit run 2, and its lead on the noir-cli pin: - The artifact converted is the one whose ACIR was checked. nargo compiles into a fresh --target-dir, and the tool converts and keeps the one artifact that run wrote. It no longer takes a name from Nargo.toml by regex, nor reads the circuit's target/, where a planted file could be keyed while the tool reported the reviewed circuit. - The public signals are the public parameters. noir-cli makes the first ACIR witnesses public, whatever their visibility, so compile refuses unless func 0's public parameters are w0..w(k-1) and nothing is returned, and checks the R1CS header (nPubOut 0, nPubIn k). A private parameter before a public one, or a return value, would otherwise be published in the spend. - nargo's warnings are fatal: --deny-warnings, and any bug/error/warning on stderr. nargo reports an unconstrained Brillig output as `bug:` and exits 0. --allow-nargo-warnings prints them instead, for a reviewed circuit (the check is a heuristic and flags some constrained hints). - noir-cli by its full commit, 4b7caace1f2128e454c8d0fe50cac1ec46b1e272: cargo resolves a short rev as a name, which a tag of that name would capture. docs/noir.md states the layout rule and what the tool checks; the docstring's two-limb Groth16 binding is corrected. The toy circuit's R1CS is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is not 1 From security audit run 2: - fflonk_proof promised evaluations reduced mod r but kept int(). With --pad (no hand run) fflonk_snarkjs.py reported "the proof verifies" for a + r or a - r and wrote witness words the program refuses or the harness cannot load. Evaluations are now decimal strings below 2^256, reduced mod r, and the proof's curve is checked. dsl.wv refuses a value outside [0, 2^bits). - An out-of-range --pad is refused before the witness file is opened, so an earlier witness is left as it was. - g1/g2 refuse z != 1. snarkjs writes affine points and reads any other z as Jacobian (X/Z^2, Y/Z^3); dividing once gave a different point for the same file, so an edited key could verify in snarkjs and compile to another key. Also two names left stale by the refactor (docs/fflonk.md, test_fflonk.py). Every program and witness the tools write from the examples is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Security audit run 2 (Cloudflare security-audit skill,
standardprofile, one562c3b) found no soundness or binding gap in the generated Groth16 and fflonk programs. It confirmed four findings in the off-chain tools and left one lead needing validation. Each record was reproduced by a fresh verifier and checked again by a second one. This PR fixes all five. The full reports are inbuild/security-audit-2/.Generated programs and witnesses are unchanged: every program and witness the tools write from the committed examples hashes the same before and after.
Noir:
tools/noir_r1cs.pyThe converted artifact is the checked one (medium). The opcode check read the ACIR that
nargo compile --print-acirprinted. The tool then convertedtarget/<name>.json, with the name taken by a regex overNargo.toml. Aname = "..."line inside a multi-line description, plus a plantedtarget/file, got a trivial circuit keyed and turned into a program, while the tool reported the reviewed circuit's opcodes. nargo now compiles into a fresh temporary--target-dir, and the tool converts and keeps the one artifact that run wrote. It no longer reads the circuit'starget/or parsesNargo.toml.Public signals are the public parameters (medium). noir-cli makes the first ACIR witnesses the R1CS public signals, whatever their visibility. So a circuit following the old rule ("the sighash first among the public inputs") could still publish a private parameter in the spend: for example
fn main(sighash: pub Field, a: Field, c: pub Field, b: Field)gave public signals[sighash, a]. A return value had the same effect.compilenow refuses unless func 0's public parameters arew0..w(k-1)and nothing is returned. It also checks that the R1CS header has nPubOut 0 and nPubIn k. The rule indocs/noir.mdis now: public parameters first, the sighash first of all, no return value. The docstring's stale two-limb /--sighash-inputstext is corrected.nargo's warnings are fatal (low). nargo 1.0.0-beta.19 reports a Brillig output left unconstrained as
bug:on stderr and exits 0, and the tool discarded stderr on success.compilenow passes--deny-warningsand also refuses on anybug/error/warningline. nargo's check is a heuristic: it flags some constrained hints, such as one checked under anif. For a reviewed circuit,--allow-nargo-warningsprints the warnings instead of refusing. With--deny-warnings, anunsafeblock needs nargo's// Safety:comment. The toy circuit has none and is unaffected: its R1CS is byte-identical toexamples/noir/multiplier.r1cs.noir-cli by its full commit (needs validation). The install pinned
--rev 4b7caac. cargo resolves a short rev as a name, and libgit2 1.9.2 lets a tag of that name take precedence. The docs and the tool now give4b7caace1f2128e454c8d0fe50cac1ec46b1e272.fflonk:
groth/core/snarkjs.py,tools/fflonk_snarkjs.pyEvaluations are read as snarkjs reads them (low).
fflonk_proofpromised evaluations "reduced mod r" but only appliedint(). With--pad N, which skips thehandrun, the tool printed "the proof verifies" fora + r,a - randa + 6r, and wrote witness words the program refuses or the harness cannot load. Evaluations are now decimal strings below 2^256, reduced mod r.fflonk_proofcheckscurveas the Groth16 loader does.dsl.wvrefuses a value outside [0, 2^bits). An out-of-range--padno longer truncates an earlier witness file before it is refused.Affine points only (hardening from the same audit). snarkjs writes every point with z = 1, and reads any other z as Jacobian (X/Z², Y/Z³). The loaders divided once, (X/Z, Y/Z). A key edited with z ≠ 1 could verify in snarkjs yet compile to a different IC0, with the same program-time report.
g1/g2now refuse z ≠ 1.Tests
tests/test_noir.py:target/; abug:on stderr with exit 0 is refused, as are zero or two built programs;--allow-nargo-warningsprints instead of refusing.examples/noir/multiplier.r1csis read correctly.tests/test_tools.py:a + randa + 4rload asa;a - r,a + 6r, hex,' 1','1_2', JSON numbers and booleans are refused, and so is a proof labelledbls12381;wv(256, -1)andwv(256, 2**256)are refused;--pad 512leaves an earlier witness file as it was.(sighash, a, c, b)layout is refused;--allow-nargo-warnings.hand) pass.Not in this PR
The audit's hardening notes are listed in
build/security-audit-2/REPORT.md§6. They include witness malleability (re-padding an fflonk spend cuts its feerate), the unchecked fflonk key constants, degenerate-setup refusals, program-time spendability estimates, and the circom toolchain identity.🤖 Generated with Claude Code