Fixed reconciliation bug - Peikert-style reconciliation now achieves 100% accuracy (was 50% with broken XOR)
This commit is contained in:
12
Cargo.toml
12
Cargo.toml
@@ -16,8 +16,7 @@ hkdf = "0.12"
|
|||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
|
|
||||||
rand = "0.8"
|
rand = "0.9.2"
|
||||||
getrandom = "0.2"
|
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
@@ -30,13 +29,18 @@ subtle = "2.5"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1", features = ["full", "test-util"] }
|
tokio = { version = "1", features = ["full", "test-util"] }
|
||||||
rand_chacha = "0.3"
|
rand_chacha = "0.9.0"
|
||||||
criterion = "0.5"
|
criterion = "0.8.1"
|
||||||
|
dudect-bencher = "0.6"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "oprf_benchmark"
|
name = "oprf_benchmark"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "timing_verification"
|
||||||
|
harness = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
server = ["dep:axum", "dep:tokio", "dep:tower-http"]
|
server = ["dep:axum", "dep:tokio", "dep:tower-http"]
|
||||||
|
|||||||
278
benches/timing_verification.rs
Normal file
278
benches/timing_verification.rs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
//! DudeCT-based timing verification for constant-time operations.
|
||||||
|
//!
|
||||||
|
//! A |t-value| > 5 indicates a timing leak with high confidence.
|
||||||
|
//! Functions should show |t-value| < 5 after sufficient samples.
|
||||||
|
|
||||||
|
use dudect_bencher::{BenchRng, Class, CtRunner, ctbench_main};
|
||||||
|
use opaque_lattice::oprf::fast_oprf::{
|
||||||
|
PublicParams, Q, RING_N, ReconciliationHelper, RingElement, ServerKey, client_blind,
|
||||||
|
client_finalize, server_evaluate,
|
||||||
|
};
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
fn coin_flip(rng: &mut BenchRng) -> bool {
|
||||||
|
rng.gen_range(0u8..2) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const NUM_SAMPLES: usize = 10_000;
|
||||||
|
|
||||||
|
fn timing_ring_mul(runner: &mut CtRunner, rng: &mut BenchRng) {
|
||||||
|
let mut inputs: Vec<(RingElement, RingElement)> = Vec::new();
|
||||||
|
let mut classes = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..NUM_SAMPLES {
|
||||||
|
if coin_flip(rng) {
|
||||||
|
let mut coeffs_a = [0i32; RING_N];
|
||||||
|
let mut coeffs_b = [0i32; RING_N];
|
||||||
|
for i in 0..RING_N {
|
||||||
|
coeffs_a[i] = 0;
|
||||||
|
coeffs_b[i] = 0;
|
||||||
|
}
|
||||||
|
inputs.push((
|
||||||
|
RingElement { coeffs: coeffs_a },
|
||||||
|
RingElement { coeffs: coeffs_b },
|
||||||
|
));
|
||||||
|
classes.push(Class::Left);
|
||||||
|
} else {
|
||||||
|
let mut coeffs_a = [0i32; RING_N];
|
||||||
|
let mut coeffs_b = [0i32; RING_N];
|
||||||
|
for i in 0..RING_N {
|
||||||
|
coeffs_a[i] = rng.gen_range(0..Q);
|
||||||
|
coeffs_b[i] = rng.gen_range(0..Q);
|
||||||
|
}
|
||||||
|
inputs.push((
|
||||||
|
RingElement { coeffs: coeffs_a },
|
||||||
|
RingElement { coeffs: coeffs_b },
|
||||||
|
));
|
||||||
|
classes.push(Class::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (class, (a, b)) in classes.into_iter().zip(inputs.into_iter()) {
|
||||||
|
runner.run_one(class, || a.mul(&b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timing_linf_norm(runner: &mut CtRunner, rng: &mut BenchRng) {
|
||||||
|
let mut inputs: Vec<RingElement> = Vec::new();
|
||||||
|
let mut classes = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..NUM_SAMPLES {
|
||||||
|
if coin_flip(rng) {
|
||||||
|
let coeffs = [0i32; RING_N];
|
||||||
|
inputs.push(RingElement { coeffs });
|
||||||
|
classes.push(Class::Left);
|
||||||
|
} else {
|
||||||
|
let mut coeffs = [0i32; RING_N];
|
||||||
|
for i in 0..RING_N {
|
||||||
|
coeffs[i] = rng.gen_range(0..Q);
|
||||||
|
}
|
||||||
|
inputs.push(RingElement { coeffs });
|
||||||
|
classes.push(Class::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (class, elem) in classes.into_iter().zip(inputs.into_iter()) {
|
||||||
|
runner.run_one(class, || elem.linf_norm());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timing_round_to_binary(runner: &mut CtRunner, rng: &mut BenchRng) {
|
||||||
|
let mut inputs: Vec<RingElement> = Vec::new();
|
||||||
|
let mut classes = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..NUM_SAMPLES {
|
||||||
|
if coin_flip(rng) {
|
||||||
|
let mut coeffs = [0i32; RING_N];
|
||||||
|
for i in 0..RING_N {
|
||||||
|
coeffs[i] = Q / 4;
|
||||||
|
}
|
||||||
|
inputs.push(RingElement { coeffs });
|
||||||
|
classes.push(Class::Left);
|
||||||
|
} else {
|
||||||
|
let mut coeffs = [0i32; RING_N];
|
||||||
|
for i in 0..RING_N {
|
||||||
|
coeffs[i] = (3 * Q) / 4;
|
||||||
|
}
|
||||||
|
inputs.push(RingElement { coeffs });
|
||||||
|
classes.push(Class::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (class, elem) in classes.into_iter().zip(inputs.into_iter()) {
|
||||||
|
runner.run_one(class, || elem.round_to_binary());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timing_ring_eq(runner: &mut CtRunner, rng: &mut BenchRng) {
|
||||||
|
let mut inputs: Vec<(RingElement, RingElement)> = Vec::new();
|
||||||
|
let mut classes = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..NUM_SAMPLES {
|
||||||
|
if coin_flip(rng) {
|
||||||
|
let coeffs = [0i32; RING_N];
|
||||||
|
inputs.push((RingElement { coeffs }, RingElement { coeffs }));
|
||||||
|
classes.push(Class::Left);
|
||||||
|
} else {
|
||||||
|
let mut coeffs_a = [0i32; RING_N];
|
||||||
|
let mut coeffs_b = [0i32; RING_N];
|
||||||
|
for i in 0..RING_N {
|
||||||
|
coeffs_a[i] = rng.gen_range(0..Q);
|
||||||
|
coeffs_b[i] = rng.gen_range(0..Q);
|
||||||
|
}
|
||||||
|
inputs.push((
|
||||||
|
RingElement { coeffs: coeffs_a },
|
||||||
|
RingElement { coeffs: coeffs_b },
|
||||||
|
));
|
||||||
|
classes.push(Class::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (class, (a, b)) in classes.into_iter().zip(inputs.into_iter()) {
|
||||||
|
runner.run_one(class, || a.eq(&b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timing_extract_bits(runner: &mut CtRunner, rng: &mut BenchRng) {
|
||||||
|
let mut inputs: Vec<(ReconciliationHelper, RingElement)> = Vec::new();
|
||||||
|
let mut classes = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..NUM_SAMPLES {
|
||||||
|
if coin_flip(rng) {
|
||||||
|
let mut coeffs = [0i32; RING_N];
|
||||||
|
for i in 0..RING_N {
|
||||||
|
coeffs[i] = Q / 8;
|
||||||
|
}
|
||||||
|
let elem = RingElement { coeffs };
|
||||||
|
let helper = ReconciliationHelper::from_ring(&elem);
|
||||||
|
inputs.push((helper, elem));
|
||||||
|
classes.push(Class::Left);
|
||||||
|
} else {
|
||||||
|
let mut coeffs = [0i32; RING_N];
|
||||||
|
for i in 0..RING_N {
|
||||||
|
coeffs[i] = (7 * Q) / 8;
|
||||||
|
}
|
||||||
|
let elem = RingElement { coeffs };
|
||||||
|
let helper = ReconciliationHelper::from_ring(&elem);
|
||||||
|
inputs.push((helper, elem));
|
||||||
|
classes.push(Class::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (class, (helper, elem)) in classes.into_iter().zip(inputs.into_iter()) {
|
||||||
|
runner.run_one(class, || helper.extract_bits(&elem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timing_server_bits(runner: &mut CtRunner, rng: &mut BenchRng) {
|
||||||
|
let mut inputs: Vec<RingElement> = Vec::new();
|
||||||
|
let mut classes = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..NUM_SAMPLES {
|
||||||
|
if coin_flip(rng) {
|
||||||
|
let mut coeffs = [0i32; RING_N];
|
||||||
|
for i in 0..RING_N {
|
||||||
|
coeffs[i] = Q / 4;
|
||||||
|
}
|
||||||
|
inputs.push(RingElement { coeffs });
|
||||||
|
classes.push(Class::Left);
|
||||||
|
} else {
|
||||||
|
let mut coeffs = [0i32; RING_N];
|
||||||
|
for i in 0..RING_N {
|
||||||
|
coeffs[i] = (3 * Q) / 4;
|
||||||
|
}
|
||||||
|
inputs.push(RingElement { coeffs });
|
||||||
|
classes.push(Class::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (class, elem) in classes.into_iter().zip(inputs.into_iter()) {
|
||||||
|
runner.run_one(class, || ReconciliationHelper::server_bits(&elem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timing_client_blind(runner: &mut CtRunner, rng: &mut BenchRng) {
|
||||||
|
let pp = PublicParams::generate(b"timing-test-seed");
|
||||||
|
let mut inputs: Vec<Vec<u8>> = Vec::new();
|
||||||
|
let mut classes = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..NUM_SAMPLES {
|
||||||
|
if coin_flip(rng) {
|
||||||
|
inputs.push(vec![0u8; 16]);
|
||||||
|
classes.push(Class::Left);
|
||||||
|
} else {
|
||||||
|
let mut pwd = vec![0u8; 16];
|
||||||
|
rng.fill(&mut pwd[..]);
|
||||||
|
inputs.push(pwd);
|
||||||
|
classes.push(Class::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (class, password) in classes.into_iter().zip(inputs.into_iter()) {
|
||||||
|
runner.run_one(class, || client_blind(&pp, &password));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timing_server_evaluate(runner: &mut CtRunner, rng: &mut BenchRng) {
|
||||||
|
let pp = PublicParams::generate(b"timing-test-seed");
|
||||||
|
let key = ServerKey::generate(&pp, b"timing-test-key");
|
||||||
|
let mut inputs = Vec::new();
|
||||||
|
let mut classes = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..NUM_SAMPLES {
|
||||||
|
if coin_flip(rng) {
|
||||||
|
let (_, blinded) = client_blind(&pp, &[0u8; 16]);
|
||||||
|
inputs.push(blinded);
|
||||||
|
classes.push(Class::Left);
|
||||||
|
} else {
|
||||||
|
let mut pwd = [0u8; 16];
|
||||||
|
rng.fill(&mut pwd[..]);
|
||||||
|
let (_, blinded) = client_blind(&pp, &pwd);
|
||||||
|
inputs.push(blinded);
|
||||||
|
classes.push(Class::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (class, blinded) in classes.into_iter().zip(inputs.into_iter()) {
|
||||||
|
runner.run_one(class, || server_evaluate(&key, &blinded));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timing_full_protocol(runner: &mut CtRunner, rng: &mut BenchRng) {
|
||||||
|
let pp = PublicParams::generate(b"timing-test-seed");
|
||||||
|
let key = ServerKey::generate(&pp, b"timing-test-key");
|
||||||
|
let mut inputs: Vec<Vec<u8>> = Vec::new();
|
||||||
|
let mut classes = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..NUM_SAMPLES {
|
||||||
|
if coin_flip(rng) {
|
||||||
|
inputs.push(vec![0u8; 16]);
|
||||||
|
classes.push(Class::Left);
|
||||||
|
} else {
|
||||||
|
let mut pwd = vec![0u8; 16];
|
||||||
|
rng.fill(&mut pwd[..]);
|
||||||
|
inputs.push(pwd);
|
||||||
|
classes.push(Class::Right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (class, password) in classes.into_iter().zip(inputs.into_iter()) {
|
||||||
|
runner.run_one(class, || {
|
||||||
|
let (state, blinded) = client_blind(&pp, &password);
|
||||||
|
let response = server_evaluate(&key, &blinded);
|
||||||
|
client_finalize(&state, &key.b, &response)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctbench_main!(
|
||||||
|
timing_ring_mul,
|
||||||
|
timing_linf_norm,
|
||||||
|
timing_round_to_binary,
|
||||||
|
timing_ring_eq,
|
||||||
|
timing_extract_bits,
|
||||||
|
timing_server_bits,
|
||||||
|
timing_client_blind,
|
||||||
|
timing_server_evaluate,
|
||||||
|
timing_full_protocol
|
||||||
|
);
|
||||||
346
idk2.txt
Normal file
346
idk2.txt
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
warning: unused variable: `state`
|
||||||
|
--> benches/oprf_benchmark.rs:77:14
|
||||||
|
|
|
||||||
|
77 | let (state, _) = lpr_client_blind(&mut rng, password).unwrap();
|
||||||
|
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_state`
|
||||||
|
|
|
||||||
|
= note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default
|
||||||
|
|
||||||
|
warning: unused variable: `state`
|
||||||
|
--> benches/oprf_benchmark.rs:68:10
|
||||||
|
|
|
||||||
|
68 | let (state, blinded) = lpr_client_blind(&mut rng2, password).unwrap();
|
||||||
|
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_state`
|
||||||
|
|
||||||
|
warning: unused variable: `c`
|
||||||
|
--> benches/oprf_benchmark.rs:142:24
|
||||||
|
|
|
||||||
|
142 | fn bench_message_sizes(c: &mut Criterion) {
|
||||||
|
| ^ help: if this is intentional, prefix it with an underscore: `_c`
|
||||||
|
|
||||||
|
warning: unused variable: `response`
|
||||||
|
--> benches/oprf_benchmark.rs:149:9
|
||||||
|
|
|
||||||
|
149 | let response = fast_server_evaluate(&fast_key, &blinded);
|
||||||
|
| ^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_response`
|
||||||
|
|
||||||
|
warning: function `bench_message_sizes` is never used
|
||||||
|
--> benches/oprf_benchmark.rs:142:4
|
||||||
|
|
|
||||||
|
142 | fn bench_message_sizes(c: &mut Criterion) {
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
|
||||||
|
|
||||||
|
warning: `opaque-lattice` (bench "oprf_benchmark") generated 5 warnings (run `cargo fix --bench "oprf_benchmark" -p opaque-lattice` to apply 4 suggestions)
|
||||||
|
Finished `bench` profile [optimized] target(s) in 0.07s
|
||||||
|
Running unittests src/lib.rs (target/release/deps/opaque_lattice-e85c89d8ee7a70ad)
|
||||||
|
|
||||||
|
running 143 tests
|
||||||
|
test ake::dilithium::tests::test_invalid_key_length ... ignored
|
||||||
|
test ake::dilithium::tests::test_keypair_generation ... ignored
|
||||||
|
test ake::dilithium::tests::test_public_key_serialization ... ignored
|
||||||
|
test ake::dilithium::tests::test_sign_verify ... ignored
|
||||||
|
test ake::dilithium::tests::test_signature_serialization ... ignored
|
||||||
|
test ake::dilithium::tests::test_verify_wrong_key ... ignored
|
||||||
|
test ake::dilithium::tests::test_verify_wrong_message ... ignored
|
||||||
|
test ake::kyber::tests::test_ciphertext_serialization ... ignored
|
||||||
|
test ake::kyber::tests::test_encapsulate_decapsulate ... ignored
|
||||||
|
test ake::kyber::tests::test_invalid_key_length ... ignored
|
||||||
|
test ake::kyber::tests::test_keypair_generation ... ignored
|
||||||
|
test ake::kyber::tests::test_public_key_serialization ... ignored
|
||||||
|
test ake::kyber::tests::test_secret_key_serialization ... ignored
|
||||||
|
test ct_utils::tests::test_ct_abs_mod ... ignored
|
||||||
|
test ct_utils::tests::test_ct_adjacent_quadrants ... ignored
|
||||||
|
test ct_utils::tests::test_ct_eq_u8 ... ignored
|
||||||
|
test ct_utils::tests::test_ct_gt_i32 ... ignored
|
||||||
|
test ct_utils::tests::test_ct_gte_i32 ... ignored
|
||||||
|
test ct_utils::tests::test_ct_max_i32 ... ignored
|
||||||
|
test ct_utils::tests::test_ct_select_i32 ... ignored
|
||||||
|
test ct_utils::tests::test_ct_select_u8 ... ignored
|
||||||
|
test ct_utils::tests::test_ct_slice_eq_i32 ... ignored
|
||||||
|
test envelope::tests::test_different_identities_different_envelopes ... ignored
|
||||||
|
test envelope::tests::test_envelope_store_recover ... ignored
|
||||||
|
test envelope::tests::test_envelope_wrong_password ... ignored
|
||||||
|
test envelope::tests::test_masking ... ignored
|
||||||
|
test kdf::tests::test_hkdf_expand ... ignored
|
||||||
|
test kdf::tests::test_hkdf_expand_fixed ... ignored
|
||||||
|
test kdf::tests::test_hkdf_extract ... ignored
|
||||||
|
test kdf::tests::test_labeled_expand ... ignored
|
||||||
|
test kdf::tests::test_labeled_extract ... ignored
|
||||||
|
test kdf::tests::test_one_shot_functions ... ignored
|
||||||
|
test login::tests::test_full_login_flow ... ignored
|
||||||
|
test login::tests::test_tampered_signature_fails ... ignored
|
||||||
|
test login::tests::test_wrong_password_fails ... ignored
|
||||||
|
test mac::tests::test_compute_and_verify ... ignored
|
||||||
|
test mac::tests::test_compute_multi ... ignored
|
||||||
|
test mac::tests::test_different_keys_different_tags ... ignored
|
||||||
|
test mac::tests::test_hmac_context ... ignored
|
||||||
|
test mac::tests::test_hmac_context_verify ... ignored
|
||||||
|
test mac::tests::test_verify_wrong_length ... ignored
|
||||||
|
test mac::tests::test_verify_wrong_tag ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_comparison_with_direct_prf ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_determinism ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_different_keys ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_different_passwords ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_empty_password ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_error_bounds ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_full_protocol_multiple_runs ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_long_password ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_obliviousness_statistical ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_output_determinism_and_distribution ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_protocol_correctness ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_ring_arithmetic ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_run_all_experiments ... ignored
|
||||||
|
test oprf::fast_oprf::tests::test_small_element_determinism ... ignored
|
||||||
|
test oprf::hybrid::tests::test_blinded_element_serialization ... ignored
|
||||||
|
test oprf::hybrid::tests::test_evaluate_with_credential_id ... ignored
|
||||||
|
test oprf::hybrid::tests::test_evaluated_element_serialization ... ignored
|
||||||
|
test oprf::hybrid::tests::test_oprf_deterministic ... ignored
|
||||||
|
test oprf::hybrid::tests::test_oprf_different_passwords ... ignored
|
||||||
|
test oprf::hybrid::tests::test_oprf_different_seeds ... ignored
|
||||||
|
test oprf::hybrid::tests::test_oprf_roundtrip ... ignored
|
||||||
|
test oprf::ot::tests::test_ot_multiple_choices ... ignored
|
||||||
|
test oprf::ot::tests::test_ot_receiver_cannot_get_both ... ignored
|
||||||
|
test oprf::ot::tests::test_ot_single_choice_0 ... ignored
|
||||||
|
test oprf::ot::tests::test_ot_single_choice_1 ... ignored
|
||||||
|
test oprf::ring::tests::test_deterministic_round ... ignored
|
||||||
|
test oprf::ring::tests::test_expand_seed_to_ring ... ignored
|
||||||
|
test oprf::ring::tests::test_hash_from_ring ... ignored
|
||||||
|
test oprf::ring::tests::test_hash_to_ring_deterministic ... ignored
|
||||||
|
test oprf::ring::tests::test_hash_to_ring_different_inputs ... ignored
|
||||||
|
test oprf::ring::tests::test_ring_add ... ignored
|
||||||
|
test oprf::ring::tests::test_ring_element_creation ... ignored
|
||||||
|
test oprf::ring::tests::test_ring_element_reduction ... ignored
|
||||||
|
test oprf::ring::tests::test_ring_multiply_commutativity ... ignored
|
||||||
|
test oprf::ring::tests::test_ring_multiply_identity ... ignored
|
||||||
|
test oprf::ring::tests::test_ring_sub ... ignored
|
||||||
|
test oprf::ring_lpr::tests::test_key_from_seed_deterministic ... ignored
|
||||||
|
test oprf::ring_lpr::tests::test_key_serialization ... ignored
|
||||||
|
test oprf::ring_lpr::tests::test_oprf_deterministic_same_key ... ignored
|
||||||
|
test oprf::ring_lpr::tests::test_oprf_different_keys ... ignored
|
||||||
|
test oprf::ring_lpr::tests::test_oprf_different_passwords ... ignored
|
||||||
|
test oprf::ring_lpr::tests::test_oprf_roundtrip ... ignored
|
||||||
|
test oprf::ring_lpr::tests::test_oprf_with_credential_id ... ignored
|
||||||
|
test oprf::security_proofs::correctness_bounds::test_determinism_independent_of_accuracy ... ignored
|
||||||
|
test oprf::security_proofs::correctness_bounds::test_error_distribution_analysis ... ignored
|
||||||
|
test oprf::security_proofs::correctness_bounds::test_gaussian_error_model ... ignored
|
||||||
|
test oprf::security_proofs::correctness_bounds::test_reconciliation_accuracy ... ignored
|
||||||
|
test oprf::security_proofs::deterministic_derivation_security::test_dictionary_attack_vulnerability ... ignored
|
||||||
|
test oprf::security_proofs::deterministic_derivation_security::test_password_correlation_attack ... ignored
|
||||||
|
test oprf::security_proofs::deterministic_derivation_security::test_security_model_summary ... ignored
|
||||||
|
test oprf::security_proofs::deterministic_derivation_security::test_session_linkability ... ignored
|
||||||
|
test oprf::security_proofs::deterministic_derivation_security::test_small_secret_entropy ... ignored
|
||||||
|
test oprf::security_proofs::deterministic_derivation_security::test_statistical_distinguisher ... ignored
|
||||||
|
test oprf::security_proofs::edge_case_tests::test_binary_passwords ... ignored
|
||||||
|
test oprf::security_proofs::edge_case_tests::test_empty_password ... ignored
|
||||||
|
test oprf::security_proofs::edge_case_tests::test_maximum_length_password ... ignored
|
||||||
|
test oprf::security_proofs::edge_case_tests::test_password_length_boundaries ... ignored
|
||||||
|
test oprf::security_proofs::edge_case_tests::test_repeated_patterns ... ignored
|
||||||
|
test oprf::security_proofs::edge_case_tests::test_similar_passwords ... ignored
|
||||||
|
test oprf::security_proofs::edge_case_tests::test_single_byte_passwords ... ignored
|
||||||
|
test oprf::security_proofs::edge_case_tests::test_unicode_passwords ... ignored
|
||||||
|
test oprf::security_proofs::edge_case_tests::test_whitespace_sensitivity ... ignored
|
||||||
|
test oprf::security_proofs::formal_reductions::test_ring_lpr_reduction ... ignored
|
||||||
|
test oprf::security_proofs::formal_reductions::test_ring_lwe_reduction ... ignored
|
||||||
|
test oprf::security_proofs::malicious_adversary::test_chosen_input_attack ... ignored
|
||||||
|
test oprf::security_proofs::malicious_adversary::test_malicious_client_key_extraction ... ignored
|
||||||
|
test oprf::security_proofs::malicious_adversary::test_malicious_server_password_extraction ... ignored
|
||||||
|
test oprf::security_proofs::malicious_adversary::test_replay_attack_resistance ... ignored
|
||||||
|
test oprf::security_proofs::malicious_adversary::test_timing_attack_resistance ... ignored
|
||||||
|
test oprf::security_proofs::ring_lwe_security::test_key_recovery_resistance ... ignored
|
||||||
|
test oprf::security_proofs::ring_lwe_security::test_lwe_sample_statistical_properties ... ignored
|
||||||
|
test oprf::security_proofs::ring_lwe_security::test_multiple_query_security ... ignored
|
||||||
|
test oprf::security_proofs::ring_lwe_security::test_password_hiding_indistinguishability ... ignored
|
||||||
|
test oprf::security_proofs::ring_lwe_security::test_small_secret_bounds ... ignored
|
||||||
|
test oprf::security_proofs::uc_security::test_no_information_leakage ... ignored
|
||||||
|
test oprf::security_proofs::uc_security::test_real_ideal_indistinguishability ... ignored
|
||||||
|
test oprf::security_proofs::uc_security::test_simulator_construction ... ignored
|
||||||
|
test oprf::security_proofs::voprf_security::test_completeness ... ignored
|
||||||
|
test oprf::security_proofs::voprf_security::test_rejection_sampling_security ... ignored
|
||||||
|
test oprf::security_proofs::voprf_security::test_soundness_tampered_proof ... ignored
|
||||||
|
test oprf::security_proofs::voprf_security::test_soundness_wrong_input ... ignored
|
||||||
|
test oprf::security_proofs::voprf_security::test_soundness_wrong_key ... ignored
|
||||||
|
test oprf::security_proofs::voprf_security::test_zero_knowledge_response_distribution ... ignored
|
||||||
|
test oprf::security_proofs::voprf_security::test_zero_knowledge_simulatability ... ignored
|
||||||
|
test oprf::voprf::tests::test_commitment_deterministic_from_seed ... ignored
|
||||||
|
test oprf::voprf::tests::test_commitment_generation ... ignored
|
||||||
|
test oprf::voprf::tests::test_consistent_outputs_same_key ... ignored
|
||||||
|
test oprf::voprf::tests::test_different_outputs_different_keys ... ignored
|
||||||
|
test oprf::voprf::tests::test_proof_fails_with_wrong_commitment ... ignored
|
||||||
|
test oprf::voprf::tests::test_proof_fails_with_wrong_input ... ignored
|
||||||
|
test oprf::voprf::tests::test_proof_generation_and_verification ... ignored
|
||||||
|
test oprf::voprf::tests::test_proof_serialization ... ignored
|
||||||
|
test oprf::voprf::tests::test_response_bounds ... ignored
|
||||||
|
test oprf::voprf::tests::test_signed_ring_operations ... ignored
|
||||||
|
test registration::tests::test_different_passwords_different_records ... ignored
|
||||||
|
test registration::tests::test_full_registration_flow ... ignored
|
||||||
|
test registration::tests::test_registration_with_identities ... ignored
|
||||||
|
test types::tests::test_envelope_creation ... ignored
|
||||||
|
test types::tests::test_oprf_seed_zeroize ... ignored
|
||||||
|
test types::tests::test_server_public_key ... ignored
|
||||||
|
test types::tests::test_session_key_zeroize ... ignored
|
||||||
|
|
||||||
|
test result: ok. 0 passed; 0 failed; 143 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||||
|
|
||||||
|
Running benches/oprf_benchmark.rs (target/release/deps/oprf_benchmark-88b0e08ed7f0aa66)
|
||||||
|
Gnuplot not found, using plotters backend
|
||||||
|
Benchmarking fast_oprf/client_blind
|
||||||
|
Benchmarking fast_oprf/client_blind: Warming up for 3.0000 s
|
||||||
|
Benchmarking fast_oprf/client_blind: Collecting 100 samples in estimated 5.0268 s (242k iterations)
|
||||||
|
Benchmarking fast_oprf/client_blind: Analyzing
|
||||||
|
fast_oprf/client_blind time: [20.647 µs 20.708 µs 20.785 µs]
|
||||||
|
change: [-0.0463% +0.5159% +1.1220%] (p = 0.07 > 0.05)
|
||||||
|
No change in performance detected.
|
||||||
|
Found 10 outliers among 100 measurements (10.00%)
|
||||||
|
2 (2.00%) high mild
|
||||||
|
8 (8.00%) high severe
|
||||||
|
Benchmarking fast_oprf/server_evaluate
|
||||||
|
Benchmarking fast_oprf/server_evaluate: Warming up for 3.0000 s
|
||||||
|
Benchmarking fast_oprf/server_evaluate: Collecting 100 samples in estimated 5.0673 s (263k iterations)
|
||||||
|
Benchmarking fast_oprf/server_evaluate: Analyzing
|
||||||
|
fast_oprf/server_evaluate
|
||||||
|
time: [18.951 µs 18.955 µs 18.960 µs]
|
||||||
|
change: [+1.9579% +1.9986% +2.0423%] (p = 0.00 < 0.05)
|
||||||
|
Performance has regressed.
|
||||||
|
Found 5 outliers among 100 measurements (5.00%)
|
||||||
|
1 (1.00%) low severe
|
||||||
|
1 (1.00%) low mild
|
||||||
|
2 (2.00%) high mild
|
||||||
|
1 (1.00%) high severe
|
||||||
|
Benchmarking fast_oprf/client_finalize
|
||||||
|
Benchmarking fast_oprf/client_finalize: Warming up for 3.0000 s
|
||||||
|
Benchmarking fast_oprf/client_finalize: Collecting 100 samples in estimated 5.0093 s (258k iterations)
|
||||||
|
Benchmarking fast_oprf/client_finalize: Analyzing
|
||||||
|
fast_oprf/client_finalize
|
||||||
|
time: [19.297 µs 19.332 µs 19.380 µs]
|
||||||
|
change: [+0.7140% +1.4212% +2.0918%] (p = 0.00 < 0.05)
|
||||||
|
Change within noise threshold.
|
||||||
|
Found 13 outliers among 100 measurements (13.00%)
|
||||||
|
2 (2.00%) low mild
|
||||||
|
2 (2.00%) high mild
|
||||||
|
9 (9.00%) high severe
|
||||||
|
Benchmarking fast_oprf/full_protocol
|
||||||
|
Benchmarking fast_oprf/full_protocol: Warming up for 3.0000 s
|
||||||
|
Benchmarking fast_oprf/full_protocol: Collecting 100 samples in estimated 5.0863 s (86k iterations)
|
||||||
|
Benchmarking fast_oprf/full_protocol: Analyzing
|
||||||
|
fast_oprf/full_protocol time: [58.338 µs 58.356 µs 58.375 µs]
|
||||||
|
change: [-2.1791% -1.7316% -1.3554%] (p = 0.00 < 0.05)
|
||||||
|
Performance has improved.
|
||||||
|
Found 9 outliers among 100 measurements (9.00%)
|
||||||
|
7 (7.00%) high mild
|
||||||
|
2 (2.00%) high severe
|
||||||
|
|
||||||
|
Benchmarking ring_lpr_oprf/client_blind
|
||||||
|
Benchmarking ring_lpr_oprf/client_blind: Warming up for 3.0000 s
|
||||||
|
Benchmarking ring_lpr_oprf/client_blind: Collecting 100 samples in estimated 5.2282 s (1600 iterations)
|
||||||
|
Benchmarking ring_lpr_oprf/client_blind: Analyzing
|
||||||
|
ring_lpr_oprf/client_blind
|
||||||
|
time: [3.2041 ms 3.2203 ms 3.2413 ms]
|
||||||
|
change: [-0.0758% +0.5688% +1.2853%] (p = 0.10 > 0.05)
|
||||||
|
No change in performance detected.
|
||||||
|
Found 13 outliers among 100 measurements (13.00%)
|
||||||
|
3 (3.00%) high mild
|
||||||
|
10 (10.00%) high severe
|
||||||
|
Benchmarking ring_lpr_oprf/server_evaluate
|
||||||
|
Benchmarking ring_lpr_oprf/server_evaluate: Warming up for 3.0000 s
|
||||||
|
Benchmarking ring_lpr_oprf/server_evaluate: Collecting 100 samples in estimated 5.3322 s (1000 iterations)
|
||||||
|
Benchmarking ring_lpr_oprf/server_evaluate: Analyzing
|
||||||
|
ring_lpr_oprf/server_evaluate
|
||||||
|
time: [5.2297 ms 5.2483 ms 5.2711 ms]
|
||||||
|
change: [-4.1265% -3.4361% -2.7931%] (p = 0.00 < 0.05)
|
||||||
|
Performance has improved.
|
||||||
|
Found 19 outliers among 100 measurements (19.00%)
|
||||||
|
3 (3.00%) high mild
|
||||||
|
16 (16.00%) high severe
|
||||||
|
Benchmarking ring_lpr_oprf/client_finalize
|
||||||
|
Benchmarking ring_lpr_oprf/client_finalize: Warming up for 3.0000 s
|
||||||
|
Benchmarking ring_lpr_oprf/client_finalize: Collecting 100 samples in estimated 5.2498 s (1000 iterations)
|
||||||
|
Benchmarking ring_lpr_oprf/client_finalize: Analyzing
|
||||||
|
ring_lpr_oprf/client_finalize
|
||||||
|
time: [5.2369 ms 5.2556 ms 5.2781 ms]
|
||||||
|
change: [-1.1174% -0.3571% +0.3041%] (p = 0.35 > 0.05)
|
||||||
|
No change in performance detected.
|
||||||
|
Found 14 outliers among 100 measurements (14.00%)
|
||||||
|
4 (4.00%) high mild
|
||||||
|
10 (10.00%) high severe
|
||||||
|
Benchmarking ring_lpr_oprf/full_protocol
|
||||||
|
Benchmarking ring_lpr_oprf/full_protocol: Warming up for 3.0000 s
|
||||||
|
Benchmarking ring_lpr_oprf/full_protocol: Collecting 100 samples in estimated 5.3095 s (500 iterations)
|
||||||
|
Benchmarking ring_lpr_oprf/full_protocol: Analyzing
|
||||||
|
ring_lpr_oprf/full_protocol
|
||||||
|
time: [10.531 ms 10.563 ms 10.601 ms]
|
||||||
|
change: [-1.7281% -1.1647% -0.6455%] (p = 0.00 < 0.05)
|
||||||
|
Change within noise threshold.
|
||||||
|
Found 15 outliers among 100 measurements (15.00%)
|
||||||
|
8 (8.00%) high mild
|
||||||
|
7 (7.00%) high severe
|
||||||
|
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/5
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/5: Warming up for 3.0000 s
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/5: Collecting 100 samples in estimated 5.1493 s (86k iterations)
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/5: Analyzing
|
||||||
|
oprf_comparison/fast_oprf/5
|
||||||
|
time: [58.845 µs 58.867 µs 58.894 µs]
|
||||||
|
change: [-0.5227% +0.1308% +0.6516%] (p = 0.70 > 0.05)
|
||||||
|
No change in performance detected.
|
||||||
|
Found 6 outliers among 100 measurements (6.00%)
|
||||||
|
1 (1.00%) low mild
|
||||||
|
4 (4.00%) high mild
|
||||||
|
1 (1.00%) high severe
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/5
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/5: Warming up for 3.0000 s
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/5: Collecting 100 samples in estimated 5.4629 s (500 iterations)
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/5: Analyzing
|
||||||
|
oprf_comparison/ring_lpr_oprf/5
|
||||||
|
time: [10.887 ms 10.891 ms 10.895 ms]
|
||||||
|
change: [+4.0777% +4.7919% +5.3741%] (p = 0.00 < 0.05)
|
||||||
|
Performance has regressed.
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/19
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/19: Warming up for 3.0000 s
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/19: Collecting 100 samples in estimated 5.2519 s (86k iterations)
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/19: Analyzing
|
||||||
|
oprf_comparison/fast_oprf/19
|
||||||
|
time: [60.587 µs 60.761 µs 60.971 µs]
|
||||||
|
change: [+0.5082% +1.0838% +1.5274%] (p = 0.00 < 0.05)
|
||||||
|
Change within noise threshold.
|
||||||
|
Found 13 outliers among 100 measurements (13.00%)
|
||||||
|
2 (2.00%) low mild
|
||||||
|
6 (6.00%) high mild
|
||||||
|
5 (5.00%) high severe
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/19
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/19: Warming up for 3.0000 s
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/19: Collecting 100 samples in estimated 5.2839 s (500 iterations)
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/19: Analyzing
|
||||||
|
oprf_comparison/ring_lpr_oprf/19
|
||||||
|
time: [10.388 ms 10.391 ms 10.394 ms]
|
||||||
|
change: [-2.3108% -1.7852% -1.3619%] (p = 0.00 < 0.05)
|
||||||
|
Performance has improved.
|
||||||
|
Found 2 outliers among 100 measurements (2.00%)
|
||||||
|
1 (1.00%) high mild
|
||||||
|
1 (1.00%) high severe
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/53
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/53: Warming up for 3.0000 s
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/53: Collecting 100 samples in estimated 5.1486 s (86k iterations)
|
||||||
|
Benchmarking oprf_comparison/fast_oprf/53: Analyzing
|
||||||
|
oprf_comparison/fast_oprf/53
|
||||||
|
time: [59.666 µs 59.705 µs 59.746 µs]
|
||||||
|
change: [-0.5597% -0.1197% +0.2256%] (p = 0.59 > 0.05)
|
||||||
|
No change in performance detected.
|
||||||
|
Found 9 outliers among 100 measurements (9.00%)
|
||||||
|
4 (4.00%) low severe
|
||||||
|
3 (3.00%) low mild
|
||||||
|
1 (1.00%) high mild
|
||||||
|
1 (1.00%) high severe
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/53
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/53: Warming up for 3.0000 s
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/53: Collecting 100 samples in estimated 5.1871 s (500 iterations)
|
||||||
|
Benchmarking oprf_comparison/ring_lpr_oprf/53: Analyzing
|
||||||
|
oprf_comparison/ring_lpr_oprf/53
|
||||||
|
time: [10.352 ms 10.385 ms 10.425 ms]
|
||||||
|
change: [-3.6849% -3.0378% -2.4316%] (p = 0.00 < 0.05)
|
||||||
|
Performance has improved.
|
||||||
|
Found 11 outliers among 100 measurements (11.00%)
|
||||||
|
1 (1.00%) high mild
|
||||||
|
10 (10.00%) high severe
|
||||||
|
|
||||||
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]>,
|
client_identity: Option<&[u8]>,
|
||||||
) -> Result<(Envelope, ClientPublicKey, [u8; HASH_LEN], [u8; HASH_LEN])> {
|
) -> Result<(Envelope, ClientPublicKey, [u8; HASH_LEN], [u8; HASH_LEN])> {
|
||||||
let mut nonce = [0u8; ENVELOPE_NONCE_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)?;
|
let keys = derive_keys(randomized_pwd, &nonce)?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
pub mod ake;
|
pub mod ake;
|
||||||
|
pub(crate) mod ct_utils;
|
||||||
pub(crate) mod debug;
|
pub(crate) mod debug;
|
||||||
pub mod envelope;
|
pub mod envelope;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub fn client_login_start(password: &[u8]) -> (ClientLoginState, KE1) {
|
|||||||
let (oprf_client, blinded) = OprfClient::blind(password);
|
let (oprf_client, blinded) = OprfClient::blind(password);
|
||||||
|
|
||||||
let mut client_nonce = [0u8; NONCE_LEN];
|
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();
|
let (client_kem_pk, client_kem_sk) = generate_kem_keypair();
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ pub fn server_login_respond(
|
|||||||
eprintln!(" OPRF evaluation complete");
|
eprintln!(" OPRF evaluation complete");
|
||||||
|
|
||||||
let mut masking_nonce = [0u8; NONCE_LEN];
|
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 envelope_bytes = serialize_envelope(&record.envelope);
|
||||||
let to_mask = [
|
let to_mask = [
|
||||||
@@ -98,7 +98,7 @@ pub fn server_login_respond(
|
|||||||
eprintln!(" masked_response len: {}", masked_response.len());
|
eprintln!(" masked_response len: {}", masked_response.len());
|
||||||
|
|
||||||
let mut server_nonce = [0u8; NONCE_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();
|
let (server_kem_pk, _server_kem_sk) = generate_kem_keypair();
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
use sha3::{Digest, Sha3_256, Sha3_512};
|
use sha3::{Digest, Sha3_256, Sha3_512};
|
||||||
use std::fmt;
|
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;
|
use crate::debug::trace;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -182,59 +183,57 @@ impl RingElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Multiply ring elements in R_q = Z_q[x]/(x^n + 1)
|
/// 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 {
|
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 i in 0..RING_N {
|
||||||
for j 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);
|
let prod = (self.coeffs[i] as i64) * (other.coeffs[j] as i64);
|
||||||
|
result[i + j] += prod;
|
||||||
if idx < RING_N {
|
|
||||||
result[idx] += prod;
|
|
||||||
} else {
|
|
||||||
// x^n = -1 in this ring
|
|
||||||
result[idx - RING_N] -= prod;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut out = Self::zero();
|
let mut out = Self::zero();
|
||||||
for i in 0..RING_N {
|
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
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute L∞ norm (max absolute coefficient, treating values > Q/2 as negative)
|
/// 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 {
|
pub fn linf_norm(&self) -> i32 {
|
||||||
self.coeffs
|
let mut max_val = 0i32;
|
||||||
.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];
|
|
||||||
for i in 0..RING_N {
|
for i in 0..RING_N {
|
||||||
let c = self.coeffs[i].rem_euclid(Q);
|
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
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if two elements are equal
|
/// Check if two elements are equal
|
||||||
|
/// Constant-time: always iterates all elements, no early exit
|
||||||
pub fn eq(&self, other: &Self) -> bool {
|
pub fn eq(&self, other: &Self) -> bool {
|
||||||
self.coeffs
|
let mut diff = 0i32;
|
||||||
.iter()
|
for i in 0..RING_N {
|
||||||
.zip(other.coeffs.iter())
|
diff |= self.coeffs[i].rem_euclid(Q) ^ other.coeffs[i].rem_euclid(Q);
|
||||||
.all(|(a, b)| a.rem_euclid(Q) == b.rem_euclid(Q))
|
}
|
||||||
|
diff == 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,14 +267,7 @@ impl ReconciliationHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extract agreed-upon bits using client's value W and server's hint
|
/// Extract agreed-upon bits using client's value W and server's hint
|
||||||
///
|
/// Constant-time: uses arithmetic selection instead of branches
|
||||||
/// 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
|
|
||||||
pub fn extract_bits(&self, client_value: &RingElement) -> [u8; RING_N] {
|
pub fn extract_bits(&self, client_value: &RingElement) -> [u8; RING_N] {
|
||||||
let mut bits = [0u8; RING_N];
|
let mut bits = [0u8; RING_N];
|
||||||
let q2 = Q / 2;
|
let q2 = Q / 2;
|
||||||
@@ -287,24 +279,23 @@ impl ReconciliationHelper {
|
|||||||
let client_quadrant = ((w / q4) % 4) as u8;
|
let client_quadrant = ((w / q4) % 4) as u8;
|
||||||
|
|
||||||
let server_bit = server_quadrant / 2;
|
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
|
let is_adjacent = ct_adjacent_quadrants(client_quadrant, server_quadrant);
|
||||||
|| (client_quadrant + 1) % 4 == server_quadrant
|
|
||||||
|| (server_quadrant + 1) % 4 == client_quadrant;
|
|
||||||
|
|
||||||
bits[i] = if is_adjacent { server_bit } else { client_bit };
|
bits[i] = ct_select_u8(is_adjacent, server_bit, client_bit);
|
||||||
}
|
}
|
||||||
|
|
||||||
bits
|
bits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Constant-time: uses arithmetic comparison instead of branch
|
||||||
pub fn server_bits(elem: &RingElement) -> [u8; RING_N] {
|
pub fn server_bits(elem: &RingElement) -> [u8; RING_N] {
|
||||||
let mut bits = [0u8; RING_N];
|
let mut bits = [0u8; RING_N];
|
||||||
let q2 = Q / 2;
|
let q2 = Q / 2;
|
||||||
for i in 0..RING_N {
|
for i in 0..RING_N {
|
||||||
let v = elem.coeffs[i].rem_euclid(Q);
|
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
|
bits
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ mod tests {
|
|||||||
use crate::types::OPRF_SEED_LEN;
|
use crate::types::OPRF_SEED_LEN;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
let mut bytes = [0u8; OPRF_SEED_LEN];
|
let mut bytes = [0u8; OPRF_SEED_LEN];
|
||||||
rand::thread_rng().fill_bytes(&mut bytes);
|
rand::rng().fill_bytes(&mut bytes);
|
||||||
OprfSeed::new(bytes)
|
OprfSeed::new(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2037,3 +2037,498 @@ mod edge_case_tests {
|
|||||||
println!("[PASS] Whitespace handled correctly (each variant is unique)");
|
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 {
|
pub fn generate_oprf_seed() -> OprfSeed {
|
||||||
let mut bytes = [0u8; OPRF_SEED_LEN];
|
let mut bytes = [0u8; OPRF_SEED_LEN];
|
||||||
rand::thread_rng().fill_bytes(&mut bytes);
|
rand::rng().fill_bytes(&mut bytes);
|
||||||
OprfSeed::new(bytes)
|
OprfSeed::new(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user