From bc351630840155e2eceff05797978c29cfc50a86 Mon Sep 17 00:00:00 2001 From: Vecna Date: Fri, 29 Mar 2024 16:12:48 -0400 Subject: [PATCH] Add tests for reports --- src/lib.rs | 4 + src/tests.rs | 367 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 src/tests.rs diff --git a/src/lib.rs b/src/lib.rs index 11a17bc..59cd6ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -561,3 +561,7 @@ pub async fn report_blockages( let resp_str: String = serde_json::from_slice(&buf).unwrap(); assert_eq!("OK", resp_str); } + +// Unit tests +#[cfg(test)] +mod tests; diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..ea8d13f --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,367 @@ +#![allow(non_snake_case)] + +use crate::{bridge_verification_info::BridgeVerificationInfo, *}; +use lox_library::{ + bridge_table::{self, BridgeLine, BridgeTable}, + cred::Lox, + proto::*, + scalar_u32, BridgeAuth, BridgeDb, +}; + +use base64::{engine::general_purpose, Engine as _}; +use curve25519_dalek::{ristretto::RistrettoBasepointTable, Scalar}; +use rand::RngCore; +use sha1::{Digest, Sha1}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + sync::{Arc, Mutex}, +}; + +struct TestHarness { + bdb: BridgeDb, + pub ba: BridgeAuth, +} + +impl TestHarness { + fn new() -> Self { + TestHarness::new_buckets(5, 5) + } + + fn new_buckets(num_buckets: u16, hot_spare: u16) -> Self { + // Create a BridegDb + let mut bdb = BridgeDb::new(); + // Create a BridgeAuth + let mut ba = BridgeAuth::new(bdb.pubkey); + + // Make 3 x num_buckets open invitation bridges, in sets of 3 + for _ in 0..num_buckets { + let bucket = [random(), random(), random()]; + let _ = ba.add_openinv_bridges(bucket, &mut bdb); + } + // Add hot_spare more hot spare buckets + for _ in 0..hot_spare { + let bucket = [random(), random(), random()]; + let _ = ba.add_spare_bucket(bucket, &mut bdb); + } + // Create the encrypted bridge table + ba.enc_bridge_table(); + + Self { bdb, ba } + } + + fn advance_days(&mut self, days: u16) { + self.ba.advance_days(days); + } + + fn get_new_credential(&mut self) -> Lox { + let inv = self.bdb.invite().unwrap(); + let (req, state) = open_invite::request(&inv); + let resp = self.ba.handle_open_invite(req).unwrap(); + let (cred, _bridgeline) = + open_invite::handle_response(state, resp, &self.ba.lox_pub).unwrap(); + cred + } + + fn level_up(&mut self, cred: &Lox) -> Lox { + let current_level = scalar_u32(&cred.trust_level).unwrap(); + if current_level == 0 { + self.advance_days(trust_promotion::UNTRUSTED_INTERVAL.try_into().unwrap()); + let (promreq, promstate) = + trust_promotion::request(cred, &self.ba.lox_pub, self.ba.today()).unwrap(); + let promresp = self.ba.handle_trust_promotion(promreq).unwrap(); + let migcred = trust_promotion::handle_response(promstate, promresp).unwrap(); + let (migreq, migstate) = + migration::request(cred, &migcred, &self.ba.lox_pub, &self.ba.migration_pub) + .unwrap(); + let migresp = self.ba.handle_migration(migreq).unwrap(); + let new_cred = migration::handle_response(migstate, migresp, &self.ba.lox_pub).unwrap(); + new_cred + } else { + self.advance_days( + level_up::LEVEL_INTERVAL[usize::try_from(current_level).unwrap()] + .try_into() + .unwrap(), + ); + let (id, key) = bridge_table::from_scalar(cred.bucket).unwrap(); + let encbuckets = self.ba.enc_bridge_table(); + let bucket = + bridge_table::BridgeTable::decrypt_bucket(id, &key, encbuckets.get(&id).unwrap()) + .unwrap(); + let reachcred = bucket.1.unwrap(); + let (lvreq, lvstate) = level_up::request( + cred, + &reachcred, + &self.ba.lox_pub, + &self.ba.reachability_pub, + self.ba.today(), + ) + .unwrap(); + let lvresp = self.ba.handle_level_up(lvreq).unwrap(); + let new_cred = level_up::handle_response(lvstate, lvresp, &self.ba.lox_pub).unwrap(); + new_cred + } + } + + fn get_bucket(&mut self, cred: &Lox) -> [BridgeLine; bridge_table::MAX_BRIDGES_PER_BUCKET] { + let (id, key) = bridge_table::from_scalar(cred.bucket).unwrap(); + let encbuckets = self.ba.enc_bridge_table(); + let bucket = + bridge_table::BridgeTable::decrypt_bucket(id, &key, encbuckets.get(&id).unwrap()) + .unwrap(); + bucket.0 + } +} + +pub fn random() -> BridgeLine { + let mut rng = rand::thread_rng(); + let mut res: BridgeLine = BridgeLine::default(); + // Pick a random 4-byte address + let mut addr: [u8; 4] = [0; 4]; + rng.fill_bytes(&mut addr); + // If the leading byte is 224 or more, that's not a valid IPv4 + // address. Choose an IPv6 address instead (but don't worry too + // much about it being well formed). + if addr[0] >= 224 { + rng.fill_bytes(&mut res.addr); + } else { + // Store an IPv4 address as a v4-mapped IPv6 address + res.addr[10] = 255; + res.addr[11] = 255; + res.addr[12..16].copy_from_slice(&addr); + }; + let ports: [u16; 4] = [443, 4433, 8080, 43079]; + let portidx = (rng.next_u32() % 4) as usize; + res.port = ports[portidx]; + res.uid_fingerprint = rng.next_u64(); + rng.fill_bytes(&mut res.fingerprint); + let mut cert: [u8; 52] = [0; 52]; + rng.fill_bytes(&mut cert); + let infostr: String = format!( + "obfs4 cert={}, iat-mode=0", + general_purpose::STANDARD_NO_PAD.encode(cert) + ); + res.info[..infostr.len()].copy_from_slice(infostr.as_bytes()); + res +} + +#[test] +fn test_negative_reports() { + let mut th = TestHarness::new(); + + // Get new level 1 credential + let cred = th.get_new_credential(); + let cred = th.level_up(&cred); + + let bridges = th.get_bucket(&cred); + + // Create BridgeVerificationInfo for each bridge + let mut buckets = HashSet::::new(); + buckets.insert(cred.bucket); + let bridge_info_1 = BridgeVerificationInfo { + bridge_line: bridges[0], + buckets: buckets.clone(), + pubkey: None, + }; + let bridge_info_2 = BridgeVerificationInfo { + bridge_line: bridges[1], + buckets: buckets.clone(), + pubkey: None, + }; + let bridge_info_3 = BridgeVerificationInfo { + bridge_line: bridges[2], + buckets: buckets.clone(), + pubkey: None, + }; + + // Create reports + let report_1 = + NegativeReport::from_bridgeline(bridges[0], "ru".to_string(), BridgeDistributor::Lox); + let report_2 = + NegativeReport::from_lox_bucket(bridges[1].fingerprint, cred.bucket, "ru".to_string()); + let report_3 = + NegativeReport::from_lox_credential(bridges[2].fingerprint, cred, "ru".to_string()); + + // Verify reports + assert!(report_1.verify(&bridge_info_1)); + assert!(report_2.verify(&bridge_info_2)); + assert!(report_3.verify(&bridge_info_3)); + + // Check that deserialization fails under invalid conditions + + // Date in the future + let mut invalid_report_1 = + NegativeReport::from_bridgeline(bridges[0], "ru".to_string(), BridgeDistributor::Lox) + .to_serializable_report(); + invalid_report_1.date = invalid_report_1.date + 2; + + // Invalid country code + let invalid_report_2 = + NegativeReport::from_bridgeline(bridges[1], "xx".to_string(), BridgeDistributor::Lox) + .to_serializable_report(); + + assert!(invalid_report_1.to_report().is_err()); + assert!(invalid_report_2.to_report().is_err()); + + // Check that verification fails with incorrect data + + // Incorrect BridgeLine hash + let invalid_report_3 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&BridgeLine::default())), + "ru".to_string(), + BridgeDistributor::Lox, + ); + + // Incorrect bucket hash + let invalid_report_4 = NegativeReport::new( + bridges[1].fingerprint, + ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&Scalar::ZERO)), + "ru".to_string(), + BridgeDistributor::Lox, + ); + + assert!(!invalid_report_3.verify(&bridge_info_1)); + assert!(!invalid_report_4.verify(&bridge_info_2)); +} + +#[test] +fn test_positive_reports() { + let mut th = TestHarness::new(); + + // Get new level 3 credential + let cred = th.get_new_credential(); + let cred = th.level_up(&cred); + let cred = th.level_up(&cred); + let cred = th.level_up(&cred); + + let bridges = th.get_bucket(&cred); + + // Create BridgeVerificationInfo for each bridge + let mut buckets = HashSet::::new(); + buckets.insert(cred.bucket); + let bridge_info_1 = BridgeVerificationInfo { + bridge_line: bridges[0], + buckets: buckets.clone(), + pubkey: None, + }; + let bridge_info_2 = BridgeVerificationInfo { + bridge_line: bridges[1], + buckets: buckets.clone(), + pubkey: None, + }; + let bridge_info_3 = BridgeVerificationInfo { + bridge_line: bridges[2], + buckets: buckets.clone(), + pubkey: None, + }; + + // Create reports + let report_1 = PositiveReport::from_lox_credential( + bridges[0].fingerprint, + None, + &cred, + &th.ba.lox_pub, + "ru".to_string(), + ) + .unwrap(); + let report_2 = PositiveReport::from_lox_credential( + bridges[1].fingerprint, + None, + &cred, + &th.ba.lox_pub, + "ru".to_string(), + ) + .unwrap(); + let report_3 = PositiveReport::from_lox_credential( + bridges[2].fingerprint, + None, + &cred, + &th.ba.lox_pub, + "ru".to_string(), + ) + .unwrap(); + + // Compute Htable + let H = lox_library::proto::positive_report::compute_H(report_1.date); + let Htable = RistrettoBasepointTable::create(&H); + + assert!(report_1.verify(&mut th.ba, &bridge_info_1, &Htable)); + assert!(report_2.verify(&mut th.ba, &bridge_info_2, &Htable)); + assert!(report_3.verify(&mut th.ba, &bridge_info_3, &Htable)); + + // Check that user cannot use credential for other bridge + + // Get new credential + let cred_2 = th.get_new_credential(); + let bridges_2 = th.get_bucket(&cred_2); + + let mut buckets_2 = HashSet::::new(); + buckets_2.insert(cred_2.bucket); + let bridge_info_4 = BridgeVerificationInfo { + bridge_line: bridges_2[0], + buckets: buckets_2.clone(), + pubkey: None, + }; + + // Use new credential to create positive report even we don't trust it + let invalid_report_1 = PositiveReport::from_lox_credential( + bridges_2[0].fingerprint, + None, + &cred_2, + &th.ba.lox_pub, + "ru".to_string(), + ); + + // Use first credential for bridge from second bucket + let invalid_report_2 = PositiveReport::from_lox_credential( + bridges_2[0].fingerprint, + None, + &cred, + &th.ba.lox_pub, + "ru".to_string(), + ); + + // Use second credential for bridge from first bucket + let invalid_report_3 = PositiveReport::from_lox_credential( + bridges[0].fingerprint, + None, + &cred_2, + &th.ba.lox_pub, + "ru".to_string(), + ); + + // Check that all of these fail + assert!(invalid_report_1.is_err()); + assert!(!invalid_report_2 + .unwrap() + .verify(&mut th.ba, &bridge_info_4, &Htable)); + assert!(invalid_report_3.is_err()); + + // Check that deserialization fails under invalid conditions + + // Date in the future + let mut invalid_report_4 = PositiveReport::from_lox_credential( + bridges[0].fingerprint, + None, + &cred, + &th.ba.lox_pub, + "ru".to_string(), + ) + .unwrap() + .to_serializable_report(); + invalid_report_4.date = invalid_report_4.date + 2; + + // Invalid country code + let invalid_report_5 = PositiveReport::from_lox_credential( + bridges[0].fingerprint, + None, + &cred, + &th.ba.lox_pub, + "xx".to_string(), + ) + .unwrap() + .to_serializable_report(); + + assert!(invalid_report_4.to_report().is_err()); + assert!(invalid_report_5.to_report().is_err()); +}