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()); } }