Fixed reconciliation bug - Peikert-style reconciliation now achieves 100% accuracy (was 50% with broken XOR)
This commit is contained in:
219
src/ct_utils.rs
Normal file
219
src/ct_utils.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user