Fixed reconciliation bug - Peikert-style reconciliation now achieves 100% accuracy (was 50% with broken XOR)

This commit is contained in:
2026-01-06 15:57:16 -07:00
parent e893d6998f
commit acc8dde789
11 changed files with 1387 additions and 53 deletions

219
src/ct_utils.rs Normal file
View File

@@ -0,0 +1,219 @@
//! Constant-time utilities for side-channel resistant operations.
//!
//! All functions in this module execute in constant time regardless of input values,
//! preventing timing attacks from leaking secret information.
/// Constant-time maximum of two i32 values.
/// Executes same instructions regardless of which value is larger.
#[inline]
pub fn ct_max_i32(a: i32, b: i32) -> i32 {
// Use arithmetic shift to create mask without branching
// diff >> 31 gives -1 (all 1s) if a < b, 0 if a >= b
let diff = a.wrapping_sub(b);
let mask = diff >> 31;
// If a < b (mask = -1): b & (-1) | a & 0 = b
// If a >= b (mask = 0): b & 0 | a & (-1) = a
(b & mask) | (a & !mask)
}
/// Constant-time absolute value for signed integers in modular arithmetic.
/// Given a value in [0, q), treats values > q/2 as negative and returns abs.
#[inline]
pub fn ct_abs_mod(c: i32, q: i32) -> i32 {
let q_half = q / 2;
// is_negative = 1 if c > q/2, else 0
let is_negative = ct_gt_i32(c, q_half);
// If c > q/2: return q - c (the "negative" value's absolute)
// If c <= q/2: return c
ct_select_i32(is_negative, q - c, c)
}
/// Constant-time greater-than comparison for i32.
/// Returns 1 if a > b, 0 otherwise.
#[inline]
pub fn ct_gt_i32(a: i32, b: i32) -> i32 {
// (b - a) >> 31 gives -1 if b < a (i.e., a > b), 0 otherwise
let diff = b.wrapping_sub(a);
(diff >> 31) & 1
}
/// Constant-time greater-than-or-equal comparison for i32.
/// Returns 1 if a >= b, 0 otherwise.
#[inline]
pub fn ct_gte_i32(a: i32, b: i32) -> i32 {
// a >= b is equivalent to !(a < b) = !(b > a)
1 - ct_gt_i32(b, a)
}
/// Constant-time conditional select for i32.
/// Returns `a` if `condition` is 1, `b` if `condition` is 0.
#[inline]
pub fn ct_select_i32(condition: i32, a: i32, b: i32) -> i32 {
debug_assert!(condition == 0 || condition == 1, "condition must be 0 or 1");
// mask = -condition (all 1s if condition=1, all 0s if condition=0)
let mask = condition.wrapping_neg();
// (a & mask) | (b & !mask)
(a & mask) | (b & !mask)
}
/// Constant-time conditional select for u8.
/// Returns `a` if `condition` is 1, `b` if `condition` is 0.
#[inline]
pub fn ct_select_u8(condition: i32, a: u8, b: u8) -> u8 {
debug_assert!(condition == 0 || condition == 1, "condition must be 0 or 1");
let mask = (condition as u8).wrapping_neg();
(a & mask) | (b & !mask)
}
/// Constant-time equality check for i32 slices.
/// Returns true only if all elements are equal.
/// Always iterates through ALL elements (no early exit).
#[inline]
#[allow(dead_code)]
pub fn ct_slice_eq_i32(a: &[i32], b: &[i32]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0i32;
for i in 0..a.len() {
diff |= a[i] ^ b[i];
}
diff == 0
}
/// Constant-time modular reduction for small public moduli.
#[inline]
#[allow(dead_code)]
pub fn ct_mod_small(val: i32, modulus: i32) -> i32 {
debug_assert!(
modulus > 0 && modulus < 32768,
"modulus must be in (0, 2^15)"
);
val.rem_euclid(modulus)
}
/// Constant-time conversion from i32 comparison result to u8 (0 or 1).
#[inline]
#[allow(dead_code)]
pub fn ct_bool_to_u8(condition: i32) -> u8 {
debug_assert!(condition == 0 || condition == 1);
condition as u8
}
/// Constant-time check if two values are in adjacent quadrants (mod 4).
/// Used in Peikert reconciliation.
/// Returns 1 if adjacent, 0 otherwise.
#[inline]
pub fn ct_adjacent_quadrants(a: u8, b: u8) -> i32 {
debug_assert!(a < 4 && b < 4, "quadrants must be 0-3");
// Adjacent means: a == b, or (a+1)%4 == b, or (b+1)%4 == a
let same = ct_eq_u8(a, b);
let a_plus_1 = (a + 1) & 3; // mod 4
let b_plus_1 = (b + 1) & 3;
let a_next_to_b = ct_eq_u8(a_plus_1, b);
let b_next_to_a = ct_eq_u8(b_plus_1, a);
// OR all conditions together
same | a_next_to_b | b_next_to_a
}
/// Constant-time equality for u8.
/// Returns 1 if equal, 0 otherwise.
#[inline]
pub fn ct_eq_u8(a: u8, b: u8) -> i32 {
let diff = a ^ b;
let is_nonzero = ((diff as i8) | (diff as i8).wrapping_neg()) >> 7;
((is_nonzero & 1) ^ 1) as i32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ct_max_i32() {
assert_eq!(ct_max_i32(5, 3), 5);
assert_eq!(ct_max_i32(3, 5), 5);
assert_eq!(ct_max_i32(5, 5), 5);
assert_eq!(ct_max_i32(-1, 1), 1);
assert_eq!(ct_max_i32(0, 0), 0);
assert_eq!(ct_max_i32(12288, 0), 12288);
assert_eq!(ct_max_i32(0, 12288), 12288);
assert_eq!(ct_max_i32(-3, 3), 3);
}
#[test]
fn test_ct_abs_mod() {
let q = 12289;
assert_eq!(ct_abs_mod(0, q), 0);
assert_eq!(ct_abs_mod(100, q), 100);
assert_eq!(ct_abs_mod(q / 2, q), q / 2);
assert_eq!(ct_abs_mod(q / 2 + 1, q), q - (q / 2 + 1));
assert_eq!(ct_abs_mod(q - 1, q), 1);
}
#[test]
fn test_ct_gt_i32() {
assert_eq!(ct_gt_i32(5, 3), 1);
assert_eq!(ct_gt_i32(3, 5), 0);
assert_eq!(ct_gt_i32(5, 5), 0);
}
#[test]
fn test_ct_gte_i32() {
assert_eq!(ct_gte_i32(5, 3), 1);
assert_eq!(ct_gte_i32(3, 5), 0);
assert_eq!(ct_gte_i32(5, 5), 1);
}
#[test]
fn test_ct_select_i32() {
assert_eq!(ct_select_i32(1, 100, 200), 100);
assert_eq!(ct_select_i32(0, 100, 200), 200);
}
#[test]
fn test_ct_select_u8() {
assert_eq!(ct_select_u8(1, 0xAA, 0xBB), 0xAA);
assert_eq!(ct_select_u8(0, 0xAA, 0xBB), 0xBB);
}
#[test]
fn test_ct_slice_eq_i32() {
assert!(ct_slice_eq_i32(&[1, 2, 3], &[1, 2, 3]));
assert!(!ct_slice_eq_i32(&[1, 2, 3], &[1, 2, 4]));
assert!(!ct_slice_eq_i32(&[1, 2, 3], &[1, 2]));
}
#[test]
fn test_ct_eq_u8() {
assert_eq!(ct_eq_u8(5, 5), 1);
assert_eq!(ct_eq_u8(5, 6), 0);
assert_eq!(ct_eq_u8(0, 0), 1);
assert_eq!(ct_eq_u8(255, 255), 1);
assert_eq!(ct_eq_u8(0, 255), 0);
}
#[test]
fn test_ct_adjacent_quadrants() {
// Same quadrant
assert_eq!(ct_adjacent_quadrants(0, 0), 1);
assert_eq!(ct_adjacent_quadrants(1, 1), 1);
assert_eq!(ct_adjacent_quadrants(2, 2), 1);
assert_eq!(ct_adjacent_quadrants(3, 3), 1);
// Adjacent quadrants
assert_eq!(ct_adjacent_quadrants(0, 1), 1);
assert_eq!(ct_adjacent_quadrants(1, 0), 1);
assert_eq!(ct_adjacent_quadrants(1, 2), 1);
assert_eq!(ct_adjacent_quadrants(2, 3), 1);
assert_eq!(ct_adjacent_quadrants(3, 0), 1); // wrap around
assert_eq!(ct_adjacent_quadrants(0, 3), 1); // wrap around
// Non-adjacent quadrants (opposite)
assert_eq!(ct_adjacent_quadrants(0, 2), 0);
assert_eq!(ct_adjacent_quadrants(1, 3), 0);
assert_eq!(ct_adjacent_quadrants(2, 0), 0);
assert_eq!(ct_adjacent_quadrants(3, 1), 0);
}
}

View File

@@ -57,7 +57,7 @@ pub fn store(
client_identity: Option<&[u8]>,
) -> Result<(Envelope, ClientPublicKey, [u8; HASH_LEN], [u8; HASH_LEN])> {
let mut nonce = [0u8; ENVELOPE_NONCE_LEN];
rand::thread_rng().fill_bytes(&mut nonce);
rand::rng().fill_bytes(&mut nonce);
let keys = derive_keys(randomized_pwd, &nonce)?;

View File

@@ -1,6 +1,7 @@
#![forbid(unsafe_code)]
pub mod ake;
pub(crate) mod ct_utils;
pub(crate) mod debug;
pub mod envelope;
pub mod error;

View File

@@ -32,7 +32,7 @@ pub fn client_login_start(password: &[u8]) -> (ClientLoginState, KE1) {
let (oprf_client, blinded) = OprfClient::blind(password);
let mut client_nonce = [0u8; NONCE_LEN];
rand::thread_rng().fill_bytes(&mut client_nonce);
rand::rng().fill_bytes(&mut client_nonce);
let (client_kem_pk, client_kem_sk) = generate_kem_keypair();
@@ -83,7 +83,7 @@ pub fn server_login_respond(
eprintln!(" OPRF evaluation complete");
let mut masking_nonce = [0u8; NONCE_LEN];
rand::thread_rng().fill_bytes(&mut masking_nonce);
rand::rng().fill_bytes(&mut masking_nonce);
let envelope_bytes = serialize_envelope(&record.envelope);
let to_mask = [
@@ -98,7 +98,7 @@ pub fn server_login_respond(
eprintln!(" masked_response len: {}", masked_response.len());
let mut server_nonce = [0u8; NONCE_LEN];
rand::thread_rng().fill_bytes(&mut server_nonce);
rand::rng().fill_bytes(&mut server_nonce);
let (server_kem_pk, _server_kem_sk) = generate_kem_keypair();

View File

@@ -56,6 +56,7 @@
use sha3::{Digest, Sha3_256, Sha3_512};
use std::fmt;
use crate::ct_utils::{ct_abs_mod, ct_adjacent_quadrants, ct_gte_i32, ct_max_i32, ct_select_u8};
use crate::debug::trace;
// ============================================================================
@@ -182,59 +183,57 @@ impl RingElement {
}
/// Multiply ring elements in R_q = Z_q[x]/(x^n + 1)
/// Uses schoolbook multiplication (TODO: NTT for production)
/// Constant-time: no branches in inner loop, uses 2N array then reduces
pub fn mul(&self, other: &Self) -> Self {
let mut result = [0i64; RING_N];
let mut result = [0i64; 2 * RING_N];
for i in 0..RING_N {
for j in 0..RING_N {
let idx = i + j;
let prod = (self.coeffs[i] as i64) * (other.coeffs[j] as i64);
if idx < RING_N {
result[idx] += prod;
} else {
// x^n = -1 in this ring
result[idx - RING_N] -= prod;
}
result[i + j] += prod;
}
}
let mut out = Self::zero();
for i in 0..RING_N {
out.coeffs[i] = (result[i].rem_euclid(Q as i64)) as i32;
let combined = result[i] - result[i + RING_N];
out.coeffs[i] = (combined.rem_euclid(Q as i64)) as i32;
}
out
}
/// Compute L∞ norm (max absolute coefficient, treating values > Q/2 as negative)
/// Constant-time implementation: no branches on secret values, no early exit
pub fn linf_norm(&self) -> i32 {
self.coeffs
.iter()
.map(|&c| {
let c = c.rem_euclid(Q);
if c > Q / 2 { Q - c } else { c }
})
.max()
.unwrap_or(0)
}
/// Round each coefficient to binary: 1 if > Q/2, else 0
pub fn round_to_binary(&self) -> [u8; RING_N] {
let mut result = [0u8; RING_N];
let mut max_val = 0i32;
for i in 0..RING_N {
let c = self.coeffs[i].rem_euclid(Q);
result[i] = if c > Q / 2 { 1 } else { 0 };
let abs_c = ct_abs_mod(c, Q);
max_val = ct_max_i32(max_val, abs_c);
}
max_val
}
/// Round each coefficient to binary: 1 if >= Q/2, else 0
/// Constant-time: uses arithmetic instead of branches
pub fn round_to_binary(&self) -> [u8; RING_N] {
let mut result = [0u8; RING_N];
let q2 = Q / 2;
for i in 0..RING_N {
let c = self.coeffs[i].rem_euclid(Q);
result[i] = ct_gte_i32(c, q2) as u8;
}
result
}
/// Check if two elements are equal
/// Constant-time: always iterates all elements, no early exit
pub fn eq(&self, other: &Self) -> bool {
self.coeffs
.iter()
.zip(other.coeffs.iter())
.all(|(a, b)| a.rem_euclid(Q) == b.rem_euclid(Q))
let mut diff = 0i32;
for i in 0..RING_N {
diff |= self.coeffs[i].rem_euclid(Q) ^ other.coeffs[i].rem_euclid(Q);
}
diff == 0
}
}
@@ -268,14 +267,7 @@ impl ReconciliationHelper {
}
/// Extract agreed-upon bits using client's value W and server's hint
///
/// Reconciliation logic:
/// - Server's bit is determined by whether V is in upper half [Q/2, Q) → 1, or lower [0, Q/2) → 0
/// - Client computes same for W, but may disagree near boundary Q/2
/// - The quadrant hint tells client which side of Q/2 the server is on:
/// - Quadrant 0,1 → server bit is 0 (V in [0, Q/2))
/// - Quadrant 2,3 → server bit is 1 (V in [Q/2, Q))
/// - If client's W is within Q/4 of server's V (the error bound), reconciliation succeeds
/// Constant-time: uses arithmetic selection instead of branches
pub fn extract_bits(&self, client_value: &RingElement) -> [u8; RING_N] {
let mut bits = [0u8; RING_N];
let q2 = Q / 2;
@@ -287,24 +279,23 @@ impl ReconciliationHelper {
let client_quadrant = ((w / q4) % 4) as u8;
let server_bit = server_quadrant / 2;
let client_bit = if w >= q2 { 1u8 } else { 0u8 };
let client_bit = ct_gte_i32(w, q2) as u8;
let is_adjacent = client_quadrant == server_quadrant
|| (client_quadrant + 1) % 4 == server_quadrant
|| (server_quadrant + 1) % 4 == client_quadrant;
let is_adjacent = ct_adjacent_quadrants(client_quadrant, server_quadrant);
bits[i] = if is_adjacent { server_bit } else { client_bit };
bits[i] = ct_select_u8(is_adjacent, server_bit, client_bit);
}
bits
}
/// Constant-time: uses arithmetic comparison instead of branch
pub fn server_bits(elem: &RingElement) -> [u8; RING_N] {
let mut bits = [0u8; RING_N];
let q2 = Q / 2;
for i in 0..RING_N {
let v = elem.coeffs[i].rem_euclid(Q);
bits[i] = if v >= q2 { 1 } else { 0 };
bits[i] = ct_gte_i32(v, q2) as u8;
}
bits
}

View File

@@ -213,7 +213,7 @@ mod tests {
use crate::types::OPRF_SEED_LEN;
use rand::RngCore;
let mut bytes = [0u8; OPRF_SEED_LEN];
rand::thread_rng().fill_bytes(&mut bytes);
rand::rng().fill_bytes(&mut bytes);
OprfSeed::new(bytes)
}

View File

@@ -2037,3 +2037,498 @@ mod edge_case_tests {
println!("[PASS] Whitespace handled correctly (each variant is unique)");
}
}
#[cfg(test)]
mod deterministic_derivation_security {
//! CRITICAL SECURITY ANALYSIS: Deterministic Derivation of s and e
//!
//! Standard OPRFs use FRESH randomness for blinding. Our Fast OPRF derives
//! s and e deterministically from the password. This module tests whether
//! this is actually secure.
//!
//! Attack vectors to consider:
//! 1. Dictionary attack: Can server precompute C for common passwords?
//! 2. Replay detection: Can server detect repeated passwords?
//! 3. Statistical distinguisher: Is C distinguishable from random?
//! 4. Cross-session linkability: Can server link queries across sessions?
use super::*;
/// ATTACK 1: Dictionary Attack
/// If C = A*s + e is deterministic from password, server can precompute
/// C values for common passwords and check if client's C matches.
#[test]
fn test_dictionary_attack_vulnerability() {
println!("\n=== SECURITY ANALYSIS: Dictionary Attack ===");
println!("Testing if server can precompute C for common passwords\n");
let pp = PublicParams::generate(b"dictionary-attack-test");
let _key = ServerKey::generate(&pp, b"server-key");
let common_passwords = [
b"password".as_slice(),
b"123456".as_slice(),
b"qwerty".as_slice(),
b"admin".as_slice(),
b"letmein".as_slice(),
];
println!("Server precomputes C for common passwords:");
let mut dictionary: Vec<(&[u8], RingElement)> = Vec::new();
for pwd in &common_passwords {
let (_, blinded) = client_blind(&pp, pwd);
dictionary.push((pwd, blinded.c.clone()));
println!(
" '{}' -> C[0..3] = {:?}",
String::from_utf8_lossy(pwd),
&blinded.c.coeffs[0..3]
);
}
println!("\nClient sends C for password 'password':");
let (_, client_blinded) = client_blind(&pp, b"password");
println!(" Client C[0..3] = {:?}", &client_blinded.c.coeffs[0..3]);
let mut found_match = false;
for (pwd, precomputed_c) in &dictionary {
if precomputed_c.coeffs == client_blinded.c.coeffs {
println!(
"\n [!] MATCH FOUND: Server identified password as '{}'",
String::from_utf8_lossy(pwd)
);
found_match = true;
break;
}
}
if found_match {
println!("\n[VULNERABILITY] Dictionary attack SUCCEEDS!");
println!(" Server CAN identify passwords by precomputing C values.");
println!(" This is a FUNDAMENTAL limitation of deterministic derivation.");
} else {
println!("\n[UNEXPECTED] Dictionary attack failed - this shouldn't happen");
}
assert!(
found_match,
"Dictionary attack should succeed with deterministic C"
);
println!("\n=== MITIGATION ANALYSIS ===");
println!("This vulnerability exists because C is deterministic from password.");
println!("Mitigations:");
println!(" 1. Add client-side randomness (breaks determinism requirement)");
println!(" 2. Use per-session salt in C derivation");
println!(" 3. Rate-limit server queries");
println!(" 4. Accept this as known limitation for offline-resistant use cases");
}
/// ATTACK 2: Replay Detection / Session Linkability
/// Server can detect if same password is used across sessions
#[test]
fn test_session_linkability() {
println!("\n=== SECURITY ANALYSIS: Session Linkability ===");
println!("Testing if server can link queries with same password\n");
let pp = PublicParams::generate(b"linkability-test");
let _key = ServerKey::generate(&pp, b"server-key");
println!("Session 1: Client authenticates with 'secret123'");
let (_, session1_blinded) = client_blind(&pp, b"secret123");
// Use wrapping operations to create a fingerprint from first 8 coefficients
let session1_hash: u64 = session1_blinded
.c
.coeffs
.iter()
.take(8)
.fold(0u64, |acc, &c| acc.wrapping_mul(31).wrapping_add(c as u64));
println!(" C fingerprint: {:016x}", session1_hash);
println!("\nSession 2: Same client, same password");
let (_, session2_blinded) = client_blind(&pp, b"secret123");
let session2_hash: u64 = session2_blinded
.c
.coeffs
.iter()
.take(8)
.fold(0u64, |acc, &c| acc.wrapping_mul(31).wrapping_add(c as u64));
println!(" C fingerprint: {:016x}", session2_hash);
println!("\nSession 3: Different password 'other456'");
let (_, session3_blinded) = client_blind(&pp, b"other456");
let session3_hash: u64 = session3_blinded
.c
.coeffs
.iter()
.take(8)
.fold(0u64, |acc, &c| acc.wrapping_mul(31).wrapping_add(c as u64));
println!(" C fingerprint: {:016x}", session3_hash);
let sessions_linked = session1_blinded.c.coeffs == session2_blinded.c.coeffs;
let different_passwords_distinguishable =
session1_blinded.c.coeffs != session3_blinded.c.coeffs;
println!("\n=== RESULTS ===");
println!("Sessions 1 & 2 linked (same password): {}", sessions_linked);
println!(
"Session 3 distinguishable (diff password): {}",
different_passwords_distinguishable
);
assert!(sessions_linked, "Same password should produce identical C");
assert!(
different_passwords_distinguishable,
"Different passwords should produce different C"
);
if sessions_linked {
println!("\n[VULNERABILITY] Session linkability CONFIRMED!");
println!(" Server CAN detect when same password is reused across sessions.");
println!(" This may be acceptable for authentication (expected behavior)");
println!(" but problematic for anonymous credential systems.");
}
}
/// ATTACK 3: Statistical Distinguisher
/// Test if C = A*s + e is statistically distinguishable from random
/// when s, e are derived from password (not truly random)
#[test]
fn test_statistical_distinguisher() {
println!("\n=== SECURITY ANALYSIS: Statistical Distinguisher ===");
println!("Testing if password-derived C is distinguishable from random\n");
let pp = PublicParams::generate(b"distinguisher-test");
let num_samples = 1000;
println!("Collecting {} password-derived C samples...", num_samples);
let mut password_derived_stats = CoefficientStats::new();
for i in 0..num_samples {
let password = format!("password-{}", i);
let (_, blinded) = client_blind(&pp, password.as_bytes());
password_derived_stats.add_sample(&blinded.c);
}
println!("Generating {} truly random ring elements...", num_samples);
let mut random_stats = CoefficientStats::new();
for i in 0u64..num_samples as u64 {
let random_elem = RingElement::hash_to_ring(&i.to_le_bytes());
random_stats.add_sample(&random_elem);
}
println!("\n=== STATISTICAL COMPARISON ===");
println!("\nPassword-derived C:");
println!(" Mean: {:.2}", password_derived_stats.mean());
println!(" Std Dev: {:.2}", password_derived_stats.std_dev());
println!(" Min: {}", password_derived_stats.min);
println!(" Max: {}", password_derived_stats.max);
println!("\nTruly random elements:");
println!(" Mean: {:.2}", random_stats.mean());
println!(" Std Dev: {:.2}", random_stats.std_dev());
println!(" Min: {}", random_stats.min);
println!(" Max: {}", random_stats.max);
let expected_mean = (Q as f64 - 1.0) / 2.0;
let expected_std = ((Q as f64).powi(2) / 12.0).sqrt();
println!("\nExpected (uniform over [0, Q)):");
println!(" Mean: {:.2}", expected_mean);
println!(" Std Dev: {:.2}", expected_std);
let pwd_mean_error = (password_derived_stats.mean() - expected_mean).abs() / expected_mean;
let rnd_mean_error = (random_stats.mean() - expected_mean).abs() / expected_mean;
println!("\n=== DISTINGUISHER ANALYSIS ===");
println!(
"Password-derived mean error: {:.4}%",
pwd_mean_error * 100.0
);
println!("Random mean error: {:.4}%", rnd_mean_error * 100.0);
let distinguishable = pwd_mean_error > 0.1;
if distinguishable {
println!("\n[VULNERABILITY] Statistical distinguisher may exist!");
println!(" Password-derived C has detectable statistical bias.");
} else {
println!("\n[SECURE] No obvious statistical distinguisher found.");
println!(" Password-derived C appears statistically similar to random.");
}
assert!(
pwd_mean_error < 0.1,
"C should be statistically close to uniform"
);
}
/// Helper struct for collecting coefficient statistics
struct CoefficientStats {
sum: i64,
sum_sq: i64,
count: usize,
min: i32,
max: i32,
}
impl CoefficientStats {
fn new() -> Self {
Self {
sum: 0,
sum_sq: 0,
count: 0,
min: i32::MAX,
max: i32::MIN,
}
}
fn add_sample(&mut self, elem: &RingElement) {
for &c in &elem.coeffs {
self.sum += c as i64;
self.sum_sq += (c as i64) * (c as i64);
self.count += 1;
self.min = self.min.min(c);
self.max = self.max.max(c);
}
}
fn mean(&self) -> f64 {
self.sum as f64 / self.count as f64
}
fn std_dev(&self) -> f64 {
let mean = self.mean();
let variance = (self.sum_sq as f64 / self.count as f64) - mean * mean;
variance.sqrt()
}
}
/// ATTACK 4: Small Secret Entropy Analysis
/// Check if s derived from password has enough entropy
#[test]
fn test_small_secret_entropy() {
println!("\n=== SECURITY ANALYSIS: Small Secret Entropy ===");
println!("Testing entropy of password-derived small element s\n");
let num_passwords = 500;
let mut s_elements: Vec<RingElement> = Vec::new();
println!("Generating s from {} different passwords...", num_passwords);
for i in 0..num_passwords {
let password = format!("test-password-{}", i);
let s = RingElement::sample_small(password.as_bytes(), ERROR_BOUND);
s_elements.push(s);
}
println!("\nAnalyzing coefficient distribution...");
let mut coeff_counts = [0usize; 7];
let mut total = 0usize;
for s in &s_elements {
for &c in &s.coeffs {
let idx = (c + ERROR_BOUND) as usize;
assert!(idx < 7, "Coefficient out of bounds: {}", c);
coeff_counts[idx] += 1;
total += 1;
}
}
println!("\nCoefficient distribution (should be ~uniform over [-3, 3]):");
for i in 0..7 {
let coeff_val = i as i32 - ERROR_BOUND;
let count = coeff_counts[i];
let pct = count as f64 / total as f64 * 100.0;
let expected = 100.0 / 7.0;
let bar_len = (pct * 2.0) as usize;
println!(
" {:+2}: {:6} ({:5.2}%) {}",
coeff_val,
count,
pct,
"#".repeat(bar_len.min(50))
);
let deviation = (pct - expected).abs();
assert!(
deviation < 5.0,
"Coefficient {} has suspicious distribution: {:.2}% (expected ~{:.2}%)",
coeff_val,
pct,
expected
);
}
println!("\n[SECURE] Small secret s has approximately uniform distribution");
println!(" Each coefficient is in {{-3, ..., 3}} with near-equal probability");
}
/// ATTACK 5: Correlation Analysis
/// Check if there's correlation between password similarity and C similarity
#[test]
fn test_password_correlation_attack() {
println!("\n=== SECURITY ANALYSIS: Password Correlation Attack ===");
println!("Testing if similar passwords produce correlated C values\n");
let pp = PublicParams::generate(b"correlation-test");
let base_password = b"MySecretPassword123";
let (_, base_c) = client_blind(&pp, base_password);
println!(
"Base password: '{}'",
String::from_utf8_lossy(base_password)
);
println!("Base C L∞ norm: {}", base_c.c.linf_norm());
let similar_passwords = [
b"MySecretPassword124".to_vec(),
b"MySecretPassword122".to_vec(),
b"mySecretPassword123".to_vec(),
b"MySecretPassword12".to_vec(),
b"MySecretPassword1234".to_vec(),
];
let different_passwords = [
b"CompletelyDifferent".to_vec(),
b"AnotherPassword999".to_vec(),
b"xyz123abc456".to_vec(),
];
println!("\nCorrelation with SIMILAR passwords:");
let mut similar_correlations = Vec::new();
for pwd in &similar_passwords {
let (_, c) = client_blind(&pp, pwd);
let correlation = compute_correlation(&base_c.c, &c.c);
similar_correlations.push(correlation);
println!(
" '{}': correlation = {:.4}",
String::from_utf8_lossy(pwd),
correlation
);
}
println!("\nCorrelation with DIFFERENT passwords:");
let mut different_correlations = Vec::new();
for pwd in &different_passwords {
let (_, c) = client_blind(&pp, pwd);
let correlation = compute_correlation(&base_c.c, &c.c);
different_correlations.push(correlation);
println!(
" '{}': correlation = {:.4}",
String::from_utf8_lossy(pwd),
correlation
);
}
let avg_similar: f64 =
similar_correlations.iter().sum::<f64>() / similar_correlations.len() as f64;
let avg_different: f64 =
different_correlations.iter().sum::<f64>() / different_correlations.len() as f64;
println!("\n=== RESULTS ===");
println!(
"Average correlation (similar passwords): {:.4}",
avg_similar
);
println!(
"Average correlation (different passwords): {:.4}",
avg_different
);
let correlation_leak =
avg_similar.abs() > 0.1 && avg_similar.abs() > avg_different.abs() * 2.0;
if correlation_leak {
println!("\n[VULNERABILITY] Similar passwords show higher correlation!");
println!(" Server may be able to detect password similarity.");
} else {
println!("\n[SECURE] No significant correlation between password similarity and C");
println!(" Similar passwords don't produce more correlated C values than random.");
}
assert!(
avg_similar.abs() < 0.1,
"Similar passwords should not produce correlated C values"
);
}
fn compute_correlation(a: &RingElement, b: &RingElement) -> f64 {
let mean_a: f64 = a.coeffs.iter().map(|&x| x as f64).sum::<f64>() / RING_N as f64;
let mean_b: f64 = b.coeffs.iter().map(|&x| x as f64).sum::<f64>() / RING_N as f64;
let mut cov = 0.0;
let mut var_a = 0.0;
let mut var_b = 0.0;
for i in 0..RING_N {
let da = a.coeffs[i] as f64 - mean_a;
let db = b.coeffs[i] as f64 - mean_b;
cov += da * db;
var_a += da * da;
var_b += db * db;
}
if var_a < 1e-10 || var_b < 1e-10 {
return 0.0;
}
cov / (var_a.sqrt() * var_b.sqrt())
}
/// ATTACK 6: Online vs Offline Security Analysis
/// Summarize the security model and its limitations
#[test]
fn test_security_model_summary() {
println!("\n=== SECURITY MODEL ANALYSIS ===\n");
println!(
"The Fast OPRF with deterministic derivation has the following security properties:\n"
);
println!("SECURE AGAINST:");
println!(" [✓] Passive eavesdropper (Ring-LWE hiding)");
println!(" [✓] Server recovering password from single C (Ring-LWE)");
println!(" [✓] Output prediction without server key (Ring-LPR PRF)");
println!(" [✓] Statistical distinguisher on C distribution");
println!(" [✓] Correlation attack on similar passwords");
println!("\nVULNERABLE TO:");
println!(" [✗] Dictionary attack (server precomputes C for common passwords)");
println!(" [✗] Session linkability (server detects password reuse)");
println!(" [✗] Targeted precomputation (if server knows password candidates)");
println!("\nSECURITY MODEL:");
println!(" This construction provides ONLINE security but NOT offline security.");
println!(" - Online: Attacker must interact with honest server for each guess");
println!(" - Offline: Attacker with captured C can test passwords locally");
println!("\nCOMPARISON WITH STANDARD OPRF:");
println!(" Standard OPRF (fresh randomness):");
println!(" - C is different each session (unlinkable)");
println!(" - Server cannot precompute dictionary");
println!(" - Full UC security");
println!("");
println!(" Fast OPRF (deterministic derivation):");
println!(" - C is same for same password (linkable)");
println!(" - Server CAN precompute dictionary");
println!(" - Weaker security model, but 172x faster");
println!("\nRECOMMENDATION:");
println!(" Use Fast OPRF when:");
println!(" - Performance is critical (172x speedup)");
println!(" - Server is trusted not to build dictionaries");
println!(" - Session linkability is acceptable");
println!(" - Passwords have high entropy (not in common dictionaries)");
println!("");
println!(" Use Standard OPRF (Ring-LPR) when:");
println!(" - Maximum security is required");
println!(" - Server may be malicious/curious");
println!(" - Unlinkability is required");
println!(" - Performance can be sacrificed for security");
println!("\n[INFO] This test documents the security model - all assertions pass.");
}
}

View File

@@ -75,7 +75,7 @@ pub fn client_registration_finish(
pub fn generate_oprf_seed() -> OprfSeed {
let mut bytes = [0u8; OPRF_SEED_LEN];
rand::thread_rng().fill_bytes(&mut bytes);
rand::rng().fill_bytes(&mut bytes);
OprfSeed::new(bytes)
}