@@ -2039,6 +2039,140 @@ fn test_prove_ethrex_empty_block() {
20392039 assert_eq ! ( proof. public_output. len( ) , 160 ) ;
20402040}
20412041
2042+ // =============================================================================
2043+ // Security: private-input tamper tests
2044+ // =============================================================================
2045+
2046+ /// Verifier must reject when num_private_input_pages is zeroed out.
2047+ /// The proof contains a non-preprocessed PAGE sub-proof for the private input,
2048+ /// but the verifier expects 0 such pages → proof count mismatch.
2049+ #[ test]
2050+ fn test_verify_rejects_tampered_num_private_input_pages_zero ( ) {
2051+ let elf_bytes = crate :: test_utils:: asm_elf_bytes ( "test_private_input_xpage" ) ;
2052+ let input: Vec < u8 > = ( 0u8 ..16 ) . collect ( ) ;
2053+ let vm_proof = crate :: prove_with_inputs ( & elf_bytes, & input) . expect ( "prove should succeed" ) ;
2054+
2055+ // Baseline: untampered proof must verify.
2056+ assert ! (
2057+ crate :: verify( & vm_proof, & elf_bytes) . expect( "verify should not error" ) ,
2058+ "Baseline proof must verify before tampering"
2059+ ) ;
2060+ assert ! (
2061+ vm_proof. num_private_input_pages > 0 ,
2062+ "proof should have private pages"
2063+ ) ;
2064+
2065+ // Tamper: zero out private input pages.
2066+ let tampered = crate :: VmProof {
2067+ num_private_input_pages : 0 ,
2068+ ..vm_proof
2069+ } ;
2070+
2071+ let result = crate :: verify ( & tampered, & elf_bytes) ;
2072+ assert ! (
2073+ result. is_err( ) || result. unwrap( ) == false ,
2074+ "Verifier must reject proof with num_private_input_pages zeroed out"
2075+ ) ;
2076+ }
2077+
2078+ /// Verifier must reject when num_private_input_pages is inflated beyond actual.
2079+ /// The proof has 1 private page but we claim 2 → proof count mismatch.
2080+ #[ test]
2081+ fn test_verify_rejects_inflated_num_private_input_pages ( ) {
2082+ let elf_bytes = crate :: test_utils:: asm_elf_bytes ( "test_private_input_xpage" ) ;
2083+ let input: Vec < u8 > = ( 0u8 ..16 ) . collect ( ) ;
2084+ let vm_proof = crate :: prove_with_inputs ( & elf_bytes, & input) . expect ( "prove should succeed" ) ;
2085+
2086+ assert_eq ! (
2087+ vm_proof. num_private_input_pages, 1 ,
2088+ "16 bytes fits in 1 page"
2089+ ) ;
2090+
2091+ let tampered = crate :: VmProof {
2092+ num_private_input_pages : 2 ,
2093+ ..vm_proof
2094+ } ;
2095+
2096+ let result = crate :: verify ( & tampered, & elf_bytes) ;
2097+ assert ! (
2098+ result. is_err( ) || result. unwrap( ) == false ,
2099+ "Verifier must reject proof with inflated num_private_input_pages"
2100+ ) ;
2101+ }
2102+
2103+ /// Verifier must reject num_private_input_pages that exceeds the max bound.
2104+ /// The early bounds check should catch this before constructing AIRs.
2105+ #[ test]
2106+ fn test_verify_rejects_num_private_input_pages_exceeds_max ( ) {
2107+ let elf_bytes = crate :: test_utils:: asm_elf_bytes ( "test_private_input_xpage" ) ;
2108+ let input: Vec < u8 > = ( 0u8 ..16 ) . collect ( ) ;
2109+ let vm_proof = crate :: prove_with_inputs ( & elf_bytes, & input) . expect ( "prove should succeed" ) ;
2110+
2111+ let tampered = crate :: VmProof {
2112+ num_private_input_pages : 1000 ,
2113+ ..vm_proof
2114+ } ;
2115+
2116+ assert ! (
2117+ crate :: verify( & tampered, & elf_bytes) . is_err( ) ,
2118+ "Verifier must error on num_private_input_pages exceeding max"
2119+ ) ;
2120+ }
2121+
2122+ /// Verifier must reject tampered public_output when private input is used.
2123+ /// Ensures the COMMIT bus balance check still works with non-preprocessed pages.
2124+ #[ test]
2125+ fn test_verify_rejects_private_input_with_tampered_public_output ( ) {
2126+ let elf_bytes = crate :: test_utils:: asm_elf_bytes ( "test_private_input_xpage" ) ;
2127+ let input: Vec < u8 > = ( 0u8 ..16 ) . collect ( ) ;
2128+ let vm_proof = crate :: prove_with_inputs ( & elf_bytes, & input) . expect ( "prove should succeed" ) ;
2129+
2130+ assert ! (
2131+ crate :: verify( & vm_proof, & elf_bytes) . expect( "verify" ) ,
2132+ "Baseline must verify"
2133+ ) ;
2134+
2135+ let mut tampered_output = vm_proof. public_output . clone ( ) ;
2136+ tampered_output[ 0 ] ^= 0x01 ;
2137+ let tampered = crate :: VmProof {
2138+ public_output : tampered_output,
2139+ ..vm_proof
2140+ } ;
2141+
2142+ let verified =
2143+ crate :: verify ( & tampered, & elf_bytes) . expect ( "verify should not error on tampered output" ) ;
2144+ assert ! (
2145+ !verified,
2146+ "Verifier must reject proof with tampered public_output (private input present)"
2147+ ) ;
2148+ }
2149+
2150+ /// VmProof must not contain a field that stores the raw private input bytes.
2151+ /// This is a structural check: the proof struct should only carry
2152+ /// `num_private_input_pages`, not the actual input data.
2153+ #[ test]
2154+ fn test_proof_does_not_contain_private_input_field ( ) {
2155+ let elf_bytes = crate :: test_utils:: asm_elf_bytes ( "test_private_input_xpage" ) ;
2156+ let input: Vec < u8 > = vec ! [
2157+ 0xDE , 0xAD , 0xBE , 0xEF , 0xCA , 0xFE , 0xBA , 0xBE , 0x12 , 0x34 , 0x56 , 0x78 , 0x9A , 0xBC , 0xDE ,
2158+ 0xF0 ,
2159+ ] ;
2160+ let vm_proof = crate :: prove_with_inputs ( & elf_bytes, & input) . expect ( "prove should succeed" ) ;
2161+
2162+ // The VmProof struct should only contain num_private_input_pages (a count),
2163+ // not the actual bytes. Verify the proof's public fields don't contain them.
2164+ assert_eq ! ( vm_proof. num_private_input_pages, 1 ) ;
2165+ // public_output is the committed output, NOT the private input.
2166+ // It should contain bytes [4..12] of the input (what the ASM program commits).
2167+ assert_eq ! ( vm_proof. public_output, input[ 4 ..12 ] . to_vec( ) ) ;
2168+ // No `private_input` field exists — this is enforced by the type system,
2169+ // but explicitly document that the proof carries only the page count.
2170+ assert ! (
2171+ vm_proof. num_private_input_pages <= 1 ,
2172+ "Only the page count is stored, not the bytes"
2173+ ) ;
2174+ }
2175+
20422176/// Regression test: addiw with negative immediate must verify.
20432177/// arg2_sign_bit is the sign bit of rv2 (bit 31), not of arg2, per spec
20442178/// constraint CPU-CE61: MSB16[arg2_sign_bit; rv2[1]].
0 commit comments