feat: add mathematical proof tests for OPRF security properties
- Add test_proof_of_fingerprint_linkability proving split-blinding is broken - Add test_proof_of_linkability proving deterministic r,e is linkable - Add test_proof_of_noise_instability proving fresh random breaks correctness - Add test_proof_of_fingerprint_in_proposed_fix proving r_pk fix is unlinkable - Refactor ntru_lwr_oprf.rs for clarity - Add anyhow dependency for error handling
This commit is contained in:
@@ -33,6 +33,7 @@ thiserror = "2"
|
|||||||
zeroize = { version = "1", features = ["derive"] }
|
zeroize = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
subtle = "2.5"
|
subtle = "2.5"
|
||||||
|
anyhow = "1.0.100"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["full", "test-util"] }
|
tokio = { version = "1", features = ["full", "test-util"] }
|
||||||
|
|||||||
@@ -227,228 +227,101 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_math_step_by_step() {
|
fn test_proof_of_linkability() {
|
||||||
println!("\n=== STEP-BY-STEP MATH DIAGNOSTIC ===\n");
|
println!("\n=== PROOF OF LINKABILITY (CURRENT CONSTRUCTION) ===");
|
||||||
|
let key = ServerKey::generate(b"server-key");
|
||||||
|
let params = ServerPublicParams::from(&key);
|
||||||
|
let password = b"common-password";
|
||||||
|
|
||||||
let key = ServerKey::generate(b"test-key");
|
let (_, blinded_session_1) = client_blind(¶ms, password);
|
||||||
|
let (_, blinded_session_2) = client_blind(¶ms, password);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Blinded C1 (first 5): {:?}",
|
||||||
|
&blinded_session_1.c.coeffs[0..5]
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Blinded C2 (first 5): {:?}",
|
||||||
|
&blinded_session_2.c.coeffs[0..5]
|
||||||
|
);
|
||||||
|
|
||||||
|
let is_linkable = blinded_session_1.c.eq(&blinded_session_2.c)
|
||||||
|
&& blinded_session_1.r_pk.eq(&blinded_session_2.r_pk);
|
||||||
|
|
||||||
|
dbg!(is_linkable);
|
||||||
|
assert!(
|
||||||
|
is_linkable,
|
||||||
|
"Current construction is LINKABLE due to deterministic r,e"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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 params = ServerPublicParams::from(&key);
|
||||||
let password = b"password";
|
let password = b"password";
|
||||||
|
|
||||||
|
let mut outputs = Vec::new();
|
||||||
|
|
||||||
|
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 s = NtruRingElement::hash_to_ring(password);
|
||||||
let r = sample_random_ternary();
|
let r = sample_random_ternary();
|
||||||
let e = sample_random_ternary();
|
let e = sample_random_ternary();
|
||||||
|
|
||||||
println!("--- INPUTS ---");
|
let c = params.a.mul(&r).add(&e).add(&s);
|
||||||
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 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 v_eval = key.k.mul(&c);
|
||||||
let ak = params.a.mul(&key.k);
|
let x_fingerprint = v_eval.sub(&r_pk);
|
||||||
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 ---");
|
fingerprints.push(x_fingerprint);
|
||||||
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()
|
|
||||||
);
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("\n--- WHAT CLIENT ACTUALLY GETS ---");
|
let fingerprint_diff = fingerprints[0].sub(&fingerprints[1]);
|
||||||
let expected_x = k_s.add(&k_e).sub(&r_ek);
|
let fingerprint_diff_norm = fingerprint_diff.l2_norm();
|
||||||
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()
|
|
||||||
);
|
|
||||||
|
|
||||||
println!("\n--- TARGET: k*s ---");
|
dbg!(fingerprint_diff_norm);
|
||||||
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");
|
|
||||||
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<u8> = k_s.coeffs.iter().map(|&c| round_coeff(c)).collect();
|
|
||||||
println!("k*s rounded first 8: {:?}", &k_s_rounded[..8]);
|
|
||||||
|
|
||||||
for session in 1..=2 {
|
|
||||||
println!("\n--- SESSION {} ---", session);
|
|
||||||
|
|
||||||
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 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());
|
|
||||||
|
|
||||||
println!("X first 8 coeffs: {:?}", &x.coeffs[..8]);
|
|
||||||
let x_rounded: Vec<u8> = 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::<f64>()
|
|
||||||
/ P as f64;
|
|
||||||
println!("Avg |noise| per coeff: {:.2}", noise_per_coeff);
|
|
||||||
println!("Bin width (Q/P_LWR): {}", Q / P_LWR);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n=== END SESSION COMPARISON ===");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_correctness() {
|
|
||||||
println!("\n=== NTRU-LWR CORRECTNESS ===");
|
|
||||||
let key = ServerKey::generate(b"test-key");
|
|
||||||
|
|
||||||
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!(
|
assert!(
|
||||||
b1.c.eq(&b2.c),
|
fingerprint_diff_norm > 500.0,
|
||||||
"Same password → same blinded input (deterministic OPRF)"
|
"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]
|
#[test]
|
||||||
|
|||||||
@@ -525,6 +525,32 @@ mod tests {
|
|||||||
println!("[PASS] All outputs identical despite random blinding!");
|
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]
|
#[test]
|
||||||
fn test_revolutionary_summary() {
|
fn test_revolutionary_summary() {
|
||||||
println!("\n=== UNLINKABLE FAST OPRF ===");
|
println!("\n=== UNLINKABLE FAST OPRF ===");
|
||||||
|
|||||||
Reference in New Issue
Block a user