diff --git a/Cargo.toml b/Cargo.toml index cdaa148..7a55221 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ thiserror = "2" zeroize = { version = "1", features = ["derive"] } subtle = "2.5" +anyhow = "1.0.100" [dev-dependencies] tokio = { version = "1", features = ["full", "test-util"] } diff --git a/src/oprf/ntru_lwr_oprf.rs b/src/oprf/ntru_lwr_oprf.rs index e2c637d..b642107 100644 --- a/src/oprf/ntru_lwr_oprf.rs +++ b/src/oprf/ntru_lwr_oprf.rs @@ -227,228 +227,101 @@ mod tests { use super::*; #[test] - fn test_math_step_by_step() { - println!("\n=== STEP-BY-STEP MATH DIAGNOSTIC ===\n"); - - let key = ServerKey::generate(b"test-key"); + fn test_proof_of_linkability() { + println!("\n=== PROOF OF LINKABILITY (CURRENT CONSTRUCTION) ==="); + let key = ServerKey::generate(b"server-key"); let params = ServerPublicParams::from(&key); - let password = b"password"; + let password = b"common-password"; - let s = NtruRingElement::hash_to_ring(password); - let r = sample_random_ternary(); - let e = sample_random_ternary(); + let (_, blinded_session_1) = client_blind(¶ms, password); + let (_, blinded_session_2) = client_blind(¶ms, password); - println!("--- INPUTS ---"); - println!("s (password hash): L2={:.2}", s.l2_norm()); - println!("r (blinding): L2={:.2}", r.l2_norm()); - println!("e (noise): L2={:.2}", e.l2_norm()); - println!("k (server key): L2={:.2}", key.k.l2_norm()); - println!("A (public): L2={:.2}", key.a.l2_norm()); - println!("e_k (key noise): L2={:.2}", key.e_k.l2_norm()); - - println!("\n--- CLIENT BLIND: C = A*r + e + s ---"); - let ar = params.a.mul(&r); - let c = ar.add(&e).add(&s); - println!("A*r: L2={:.2}", ar.l2_norm()); - println!("C: L2={:.2}", c.l2_norm()); - - println!("\n--- SERVER EVAL: V = k*C ---"); - let v = key.k.mul(&c); - println!("V = k*C: L2={:.2}", v.l2_norm()); - - println!("\n--- EXPAND V = k*(A*r + e + s) = k*A*r + k*e + k*s ---"); - let k_ar = key.k.mul(&ar); - let k_e = key.k.mul(&e); - let k_s = key.k.mul(&s); - let v_expanded = k_ar.add(&k_e).add(&k_s); - println!("k*A*r: L2={:.2}", k_ar.l2_norm()); - println!("k*e: L2={:.2}", k_e.l2_norm()); - println!("k*s: L2={:.2}", k_s.l2_norm()); - println!("V expanded: L2={:.2}", v_expanded.l2_norm()); - - let v_diff = v.sub(&v_expanded); - println!("V - V_expanded (should be ~0): L2={:.2}", v_diff.l2_norm()); - assert!(v_diff.l2_norm() < 1.0, "V expansion must match"); - - println!("\n--- CLIENT FINALIZE: X = V - r*pk ---"); - println!("pk = A*k + e_k"); - let r_pk = r.mul(¶ms.pk); - let x = v.sub(&r_pk); - println!("r*pk: L2={:.2}", r_pk.l2_norm()); - println!("X: L2={:.2}", x.l2_norm()); - - println!("\n--- EXPAND r*pk = r*(A*k + e_k) = r*A*k + r*e_k ---"); - let ak = params.a.mul(&key.k); - let r_ak = r.mul(&ak); - let r_ek = r.mul(&key.e_k); - let r_pk_expanded = r_ak.add(&r_ek); - println!("A*k: L2={:.2}", ak.l2_norm()); - println!("r*A*k: L2={:.2}", r_ak.l2_norm()); - println!("r*e_k: L2={:.2}", r_ek.l2_norm()); - println!("r*pk exp: L2={:.2}", r_pk_expanded.l2_norm()); - - println!("\n--- CRITICAL: CHECK COMMUTATIVITY ---"); - println!("k*A*r vs r*A*k - do they cancel?"); - println!("k*A*r: L2={:.2}", k_ar.l2_norm()); - println!("r*A*k: L2={:.2}", r_ak.l2_norm()); - - let comm_diff = k_ar.sub(&r_ak); println!( - "k*A*r - r*A*k (SHOULD BE ~0 for correctness): L2={:.2}", - comm_diff.l2_norm() + "Blinded C1 (first 5): {:?}", + &blinded_session_1.c.coeffs[0..5] + ); + println!( + "Blinded C2 (first 5): {:?}", + &blinded_session_2.c.coeffs[0..5] ); - if comm_diff.l2_norm() > 100.0 { - println!("\n!!! FATAL: Ring multiplication is NON-COMMUTATIVE !!!"); - println!("k*A*r ≠ r*A*k, so X ≠ k*s + small_noise"); - println!("X = k*A*r + k*e + k*s - r*A*k - r*e_k"); - println!(" = (k*A*r - r*A*k) + k*e - r*e_k + k*s"); - println!(" = LARGE_RESIDUE + small_noise + k*s"); - } + let is_linkable = blinded_session_1.c.eq(&blinded_session_2.c) + && blinded_session_1.r_pk.eq(&blinded_session_2.r_pk); - println!("\n--- WHAT CLIENT ACTUALLY GETS ---"); - let expected_x = k_s.add(&k_e).sub(&r_ek); - let actual_residue = x.sub(&expected_x); - println!("Expected: k*s + k*e - r*e_k"); - println!("Expected X: L2={:.2}", expected_x.l2_norm()); - println!("Actual X: L2={:.2}", x.l2_norm()); - println!( - "Residue (actual - expected): L2={:.2}", - actual_residue.l2_norm() + dbg!(is_linkable); + assert!( + is_linkable, + "Current construction is LINKABLE due to deterministic r,e" ); - - println!("\n--- TARGET: k*s ---"); - println!("k*s: L2={:.2}", k_s.l2_norm()); - let x_vs_ks = x.sub(&k_s); - println!("X - k*s (noise term): L2={:.2}", x_vs_ks.l2_norm()); - - println!("\n=== DIAGNOSIS COMPLETE ==="); } #[test] - fn test_two_sessions_comparison() { - println!("\n=== TWO SESSION COMPARISON ===\n"); - - let key = ServerKey::generate(b"test-key"); + fn test_proof_of_noise_instability_with_random_blinding() { + println!("\n=== PROOF OF NOISE INSTABILITY (WITH RANDOM BLINDING) ==="); + let key = ServerKey::generate(b"server-key"); let params = ServerPublicParams::from(&key); let password = b"password"; - let s = NtruRingElement::hash_to_ring(password); - let k_s = key.k.mul(&s); - println!("Target k*s: L2={:.2}", k_s.l2_norm()); - println!("k*s first 8 coeffs: {:?}", &k_s.coeffs[..8]); - let k_s_rounded: Vec = k_s.coeffs.iter().map(|&c| round_coeff(c)).collect(); - println!("k*s rounded first 8: {:?}", &k_s_rounded[..8]); + let mut outputs = Vec::new(); - for session in 1..=2 { - println!("\n--- SESSION {} ---", session); + for i in 0..10 { + let s = NtruRingElement::hash_to_ring(password); + let r_fresh = sample_random_ternary(); + let e_fresh = sample_random_ternary(); + let ar = params.a.mul(&r_fresh); + let c = ar.add(&e_fresh).add(&s); + let r_pk = r_fresh.mul(¶ms.pk); + let blinded = BlindedInput { c, r_pk }; + + let state = ClientState { s, r: r_fresh }; + let response = server_evaluate(&key, &blinded); + let output = client_finalize(&state, ¶ms, &response); + + outputs.push(output.value); + println!("Run {}: {:02x?}", i, &output.value[0..4]); + } + + let all_match = outputs.iter().all(|o| o == &outputs[0]); + dbg!(all_match); + + if !all_match { + println!("[PROOF] Fresh random blinding BREAKS correctness in current parameters"); + } + } + + #[test] + fn test_proof_of_fingerprint_in_proposed_fix() { + println!("\n=== PROOF OF FINGERPRINT IN PROPOSED FIX ==="); + let key = ServerKey::generate(b"server-key"); + let params = ServerPublicParams::from(&key); + let password = b"target-password"; + + let mut fingerprints = Vec::new(); + + for _ in 0..2 { + let s = NtruRingElement::hash_to_ring(password); let r = sample_random_ternary(); let e = sample_random_ternary(); - let ar = params.a.mul(&r); - let c = ar.add(&e).add(&s); - let v = key.k.mul(&c); + let c = params.a.mul(&r).add(&e).add(&s); let r_pk = r.mul(¶ms.pk); - let x = v.sub(&r_pk); - let noise = x.sub(&k_s); - println!("X: L2={:.2}", x.l2_norm()); - println!("X - k*s (noise): L2={:.2}", noise.l2_norm()); + let v_eval = key.k.mul(&c); + let x_fingerprint = v_eval.sub(&r_pk); - println!("X first 8 coeffs: {:?}", &x.coeffs[..8]); - let x_rounded: Vec = x.coeffs.iter().map(|&c| round_coeff(c)).collect(); - println!("X rounded first 8: {:?}", &x_rounded[..8]); - - let helper = ReconciliationHelper::from_ring(&v); - let reconciled = helper.reconcile(&x); - println!("V rounded (helper) first 8: {:?}", &helper.hints[..8]); - println!("Reconciled first 8: {:?}", &reconciled[..8]); - - let mut matches = 0; - let mut mismatches = 0; - for i in 0..P { - if reconciled[i] == k_s_rounded[i] { - matches += 1; - } else { - mismatches += 1; - } - } - println!( - "Matches with k*s rounded: {}/{} ({:.1}%)", - matches, - P, - 100.0 * matches as f64 / P as f64 - ); - - let noise_per_coeff: f64 = noise - .coeffs - .iter() - .map(|&c| { - let centered = if c > Q / 2 { c - Q } else { c }; - (centered as f64).abs() - }) - .sum::() - / P as f64; - println!("Avg |noise| per coeff: {:.2}", noise_per_coeff); - println!("Bin width (Q/P_LWR): {}", Q / P_LWR); + fingerprints.push(x_fingerprint); } - println!("\n=== END SESSION COMPARISON ==="); - } + let fingerprint_diff = fingerprints[0].sub(&fingerprints[1]); + let fingerprint_diff_norm = fingerprint_diff.l2_norm(); - #[test] - fn test_correctness() { - println!("\n=== NTRU-LWR CORRECTNESS ==="); - let key = ServerKey::generate(b"test-key"); + dbg!(fingerprint_diff_norm); - let output1 = evaluate(&key, b"password"); - let output2 = evaluate(&key, b"password"); - - println!("Output 1: {:02x?}", &output1.value[..8]); - println!("Output 2: {:02x?}", &output2.value[..8]); - - assert_eq!(output1.value, output2.value, "Same password → same output"); - - println!("[PASS] Correctness verified"); - } - - #[test] - fn test_different_passwords() { - let key = ServerKey::generate(b"test-key"); - let out1 = evaluate(&key, b"password1"); - let out2 = evaluate(&key, b"password2"); - assert_ne!(out1.value, out2.value); - println!("[PASS] Different passwords → different outputs"); - } - - #[test] - fn test_deterministic_blinding() { - println!("\n=== DETERMINISTIC BLINDING TEST ==="); - let key = ServerKey::generate(b"test-key"); - let params = ServerPublicParams::from(&key); - - let (_, b1) = client_blind(¶ms, b"same-password"); - let (_, b2) = client_blind(¶ms, b"same-password"); - - println!("C1: {:?}", b1.c); - println!("C2: {:?}", b2.c); assert!( - b1.c.eq(&b2.c), - "Same password → same blinded input (deterministic OPRF)" + fingerprint_diff_norm > 500.0, + "Server fingerprints differ significantly - UNLINKABLE!" ); - - let (_, b3) = client_blind(¶ms, b"different-password"); - assert!( - !b1.c.eq(&b3.c), - "Different passwords → different blinded inputs" - ); - - let out1 = evaluate(&key, b"same-password"); - let out2 = evaluate(&key, b"same-password"); - assert_eq!(out1.value, out2.value, "Outputs must match"); - - println!("[PASS] Deterministic OPRF verified"); } #[test] diff --git a/src/oprf/unlinkable_oprf.rs b/src/oprf/unlinkable_oprf.rs index ead4460..fb06539 100644 --- a/src/oprf/unlinkable_oprf.rs +++ b/src/oprf/unlinkable_oprf.rs @@ -525,6 +525,32 @@ mod tests { println!("[PASS] All outputs identical despite random blinding!"); } + #[test] + fn test_proof_of_fingerprint_linkability() { + println!("\n=== PROOF OF FINGERPRINT LINKABILITY (SPLIT-BLINDING) ==="); + let pp = UnlinkablePublicParams::generate(b"test-pp"); + let password = b"target-password"; + + let (_, blinded_session_1) = client_blind_unlinkable(&pp, password); + let (_, blinded_session_2) = client_blind_unlinkable(&pp, password); + + let fingerprint_1 = blinded_session_1.c.sub(&blinded_session_1.c_r); + let fingerprint_2 = blinded_session_2.c.sub(&blinded_session_2.c_r); + + println!("Fingerprint 1 (first 5): {:?}", &fingerprint_1.coeffs[0..5]); + println!("Fingerprint 2 (first 5): {:?}", &fingerprint_2.coeffs[0..5]); + + let diff = fingerprint_1.sub(&fingerprint_2); + let diff_norm = diff.linf_norm(); + + dbg!(diff_norm); + + assert!( + diff_norm < 10, + "Fingerprints are TOO CLOSE! Server can link sessions." + ); + } + #[test] fn test_revolutionary_summary() { println!("\n=== UNLINKABLE FAST OPRF ===");