Skip to content

fix: bind public inputs to Fiat-Shamir transcript#5

Open
jiayaoqijia wants to merge 1 commit intoleanEthereum:mainfrom
jiayaoqijia:fix/bind-public-inputs-to-transcript
Open

fix: bind public inputs to Fiat-Shamir transcript#5
jiayaoqijia wants to merge 1 commit intoleanEthereum:mainfrom
jiayaoqijia:fix/bind-public-inputs-to-transcript

Conversation

@jiayaoqijia
Copy link

Summary

Severity: HIGH (F-03)

ProverState::new() in src/prover.rs:27-35 creates a fresh DuplexChallenger without absorbing any public inputs into the sponge state. This means the Fiat-Shamir transcript is not bound to the statement being proved.

An adversary can exploit this by reusing a valid proof for statement S1 as a "proof" for a different statement S2, since the challenge derivation is identical when the same witness structure is used. This breaks the soundness of any protocol built on top of this Fiat-Shamir implementation.

Fix

Modify ProverState::new() to accept a public_inputs: &[PF<EF>] parameter and absorb it into the challenger via add_base_scalars() before any proof generation begins:

pub fn new(permutation: P, public_inputs: &[PF<EF>]) -> Self {
    // ...
    if !public_inputs.is_empty() {
        state.add_base_scalars(public_inputs);
    }
    state
}

BREAKING CHANGE: ProverState::new() now requires a second argument. Callers with no public inputs can pass &[] to preserve the previous behavior. The test in tests/grinding.rs has been updated accordingly.

Test plan

  • Verify existing tests pass with &[] for no public inputs
  • Verify that two ProverStates initialized with different public inputs produce different challenges
  • Verify that two ProverStates initialized with the same public inputs produce the same challenges
  • Update downstream callers (e.g., leanMultisig/crates/utils/src/wrappers.rs, leanMultisig/crates/whir/tests/run_whir.rs) to pass appropriate public inputs

Found during security audit of leanEthereum repositories.

ProverState::new() creates a fresh challenger without absorbing any
public inputs into the sponge state. This means the Fiat-Shamir
transcript is not bound to the statement being proved: an adversary
can reuse a valid proof for statement S1 as a "proof" for a different
statement S2 that shares the same witness structure, since the
challenge generation is identical for both.

Modify new() to accept a public_inputs slice and absorb it into the
challenger via add_base_scalars() before any proof generation begins.
This ensures all challenges are derived from a transcript that
includes the public statement, preventing cross-statement proof
reuse.

BREAKING CHANGE: ProverState::new() now requires a second argument
`public_inputs: &[PF<EF>]`. Callers with no public inputs can pass
`&[]` to preserve the previous behavior.

Severity: HIGH (F-03)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant