From f155013e3cd5ea4d3d12e5711a96f3473279e8c9 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Wed, 5 Aug 2026 14:31:53 -0300 Subject: [PATCH] fix(verifier): reject unexpected precomputed commitment for normal AIRs A proof for a non-preprocessed AIR could include an optional precomputed trace commitment. The verifier neither rejected nor absorbed that root before sampling Fiat-Shamir challenges, yet later authenticated and used the corresponding openings in verify_trace_openings and the DEEP composition reconstruction. A malicious prover could therefore choose security-critical trace columns (e.g. bitwise) after learning the challenges. The normal-table branch of the round-1 replay now rejects any proof carrying a precomputed commitment. Honest provers never set it for non-preprocessed AIRs, so rejection is safe. The fix lives in multi_verify_views, the single implementation shared by multi_verify and multi_verify_archived (recursion guest). --- crypto/stark/src/tests/air_tests.rs | 39 +++++++++++++++++++++++++++++ crypto/stark/src/verifier.rs | 12 ++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/crypto/stark/src/tests/air_tests.rs b/crypto/stark/src/tests/air_tests.rs index b6a4108f9..a3619108c 100644 --- a/crypto/stark/src/tests/air_tests.rs +++ b/crypto/stark/src/tests/air_tests.rs @@ -58,6 +58,45 @@ fn test_prove_fib() { )); } +/// Soundness regression: a proof for a normal (non-preprocessed) AIR must be +/// rejected if it carries a precomputed trace commitment. That root is never +/// absorbed into the Fiat-Shamir transcript for normal tables, yet its +/// openings would later be authenticated and used — letting a malicious +/// prover choose those columns after learning the challenges. +#[test_log::test] +fn test_reject_unexpected_precomputed_commitment() { + let mut trace = simple_fibonacci::fibonacci_trace([Felt::from(1), Felt::from(1)], 8); + + let proof_options = ProofOptions::default_test_options(); + + let pub_inputs = FibonacciPublicInputs { + a0: Felt::one(), + a1: Felt::one(), + }; + + let air = FibonacciAIR::::new(&proof_options); + assert!(!air.is_preprocessed()); + + let mut proof = Prover::prove( + &air, + &mut trace, + &pub_inputs, + &mut DefaultTranscript::::new(&[]), + ) + .unwrap(); + // An honest proof for a normal AIR carries no precomputed commitment. + assert!(proof.lde_trace_precomputed_merkle_root.is_none()); + + // A malicious prover attaches an unbound precomputed commitment. + proof.lde_trace_precomputed_merkle_root = Some([0x42; 32]); + + assert!(!Verifier::verify( + &proof, + &air, + &mut DefaultTranscript::::new(&[]), + )); +} + #[test_log::test] fn test_prove_fib_2_cols() { let mut trace = fibonacci_2_columns::compute_trace([Felt::from(1), Felt::from(1)], 16); diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 64ae24363..86cdd2648 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -1208,7 +1208,17 @@ pub trait IsStarkVerifier< transcript.append_bytes(&expected_precomputed); transcript.append_bytes(proof.lde_trace_main_merkle_root()); } else { - // Normal table: use commitment from proof + // Normal table: use commitment from proof. A non-preprocessed + // AIR has no precomputed trace, so the proof must not carry a + // precomputed commitment: it is never absorbed into the + // transcript here, yet `verify_trace_openings` and the DEEP + // reconstruction would authenticate and use its openings, + // letting a malicious prover choose those columns after + // learning the Fiat-Shamir challenges. Reject instead. + if proof.lde_trace_precomputed_merkle_root().is_some() { + error!("Normal table {idx} proof carries an unexpected precomputed commitment"); + return false; + } transcript.append_bytes(proof.lde_trace_main_merkle_root()); } }