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

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

View File

@@ -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"]

View 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
View 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
View File

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

View File

@@ -57,7 +57,7 @@ pub fn store(
client_identity: Option<&[u8]>, 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)?;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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
} }

View File

@@ -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)
} }

View File

@@ -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.");
}
}

View File

@@ -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)
} }