diff --git a/src/tests.rs b/src/tests.rs index eacf575..1adcd07 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,1804 +1,10 @@ -#![allow(non_snake_case)] - -use crate::{ - analysis::{blocked_in, Analyzer}, - bridge_verification_info::BridgeVerificationInfo, - simulation::extra_infos_server, - *, -}; -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}, - time::Duration, -}; -use tokio::{spawn, time::sleep}; -use x25519_dalek::{PublicKey, StaticSecret}; - -struct TestHarness { - bdb: BridgeDb, - pub ba: BridgeAuth, +mod analysis { + mod stage_one; + mod stage_three; + mod stage_two; } - -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 -} - -#[tokio::test] -async fn test_download_extra_infos() { - let bridge_to_test = - array_bytes::hex2array("72E12B89136B45BBC81D1EF0AC7DDDBB91B148DB").unwrap(); - - // Open test database - let db: Db = sled::open("test_db_dei").unwrap(); - - // Delete all data in test DB - db.clear().unwrap(); - assert!(!db.contains_key("bridges").unwrap()); - assert!(!db.contains_key(bridge_to_test).unwrap()); - - // Download and process recent extra-infos files - update_extra_infos( - &db, - "https://collector.torproject.org/recent/bridge-descriptors/extra-infos/", - ) - .await - .unwrap(); - - // Check that DB contains information on a bridge with high uptime - assert!(db.contains_key("bridges").unwrap()); - let bridges: HashSet<[u8; 20]> = - bincode::deserialize(&db.get("bridges").unwrap().unwrap()).unwrap(); - assert!(bridges.contains(&bridge_to_test)); - assert!(db.contains_key(bridge_to_test).unwrap()); - let _bridge_info: BridgeInfo = - bincode::deserialize(&db.get(bridge_to_test).unwrap().unwrap()).unwrap(); -} - -#[tokio::test] -async fn test_simulate_extra_infos() { - let extra_info_str = r#"@type bridge-extra-info 1.3 -extra-info ElephantBridgeDE2 72E12B89136B45BBC81D1EF0AC7DDDBB91B148DB -master-key-ed25519 eWxjRwAWW7n8BGG9fNa6rApmBFbe3f0xcD7dqwOICW8 -published 2024-04-06 03:51:04 -transport obfs4 -write-history 2024-04-05 04:55:22 (86400 s) 31665735680,14918491136,15423603712,36168353792,40396827648 -read-history 2024-04-05 04:55:22 (86400 s) 31799622656,15229917184,15479115776,36317251584,40444155904 -ipv6-write-history 2024-04-05 04:55:22 (86400 s) 5972127744,610078720,516897792,327949312,640708608 -ipv6-read-history 2024-04-05 04:55:22 (86400 s) 4156158976,4040448000,2935756800,4263080960,6513532928 -dirreq-write-history 2024-04-05 04:55:22 (86400 s) 625217536,646188032,618014720,584386560,600778752 -dirreq-read-history 2024-04-05 04:55:22 (86400 s) 18816000,19000320,18484224,17364992,18443264 -geoip-db-digest 44073997E1ED63E183B79DE2A1757E9997A834E3 -geoip6-db-digest C0BF46880C6C132D746683279CC90DD4B2D31786 -dirreq-stats-end 2024-04-05 06:51:23 (86400 s) -dirreq-v3-ips ru=16,au=8,by=8,cn=8,gb=8,ir=8,mt=8,nl=8,pl=8,tn=8,tr=8,us=8 -dirreq-v3-reqs ru=72,gb=64,pl=32,cn=16,ir=16,us=16,au=8,by=8,mt=8,nl=8,tn=8,tr=8 -dirreq-v3-resp ok=216,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=328,busy=0 -dirreq-v3-direct-dl complete=0,timeout=0,running=0 -dirreq-v3-tunneled-dl complete=212,timeout=4,running=0,min=21595,d1=293347,d2=1624137,q1=1911800,d3=2066929,d4=2415000,md=2888500,d6=3264000,d7=3851333,q3=41> -hidserv-stats-end 2024-04-05 06:51:23 (86400 s) -hidserv-rend-relayed-cells 7924 delta_f=2048 epsilon=0.30 bin_size=1024 -hidserv-dir-onions-seen -12 delta_f=8 epsilon=0.30 bin_size=8 -hidserv-v3-stats-end 2024-04-05 12:00:00 (86400 s) -hidserv-rend-v3-relayed-cells -4785 delta_f=2048 epsilon=0.30 bin_size=1024 -hidserv-dir-v3-onions-seen 5 delta_f=8 epsilon=0.30 bin_size=8 -padding-counts 2024-04-05 06:51:42 (86400 s) bin-size=10000 write-drop=0 write-pad=80000 write-total=79980000 read-drop=0 read-pad=1110000 read-total=7989000> -bridge-stats-end 2024-04-05 06:51:44 (86400 s) -bridge-ips ru=40,us=32,??=8,au=8,br=8,by=8,cn=8,de=8,eg=8,eu=8,gb=8,ge=8,hr=8,ie=8,ir=8,kp=8,lt=8,mt=8,nl=8,pl=8,ro=8,sg=8,tn=8,tr=8,vn=8 -bridge-ip-versions v4=104,v6=8 -bridge-ip-transports =56,obfs4=56 -router-digest-sha256 zK0VMl3i0B2eaeQTR03e2hZ0i8ytkuhK/psgD2J1/lQ -router-digest F30B38390C375E1EE74BFED844177804442569E0"#; - - let extra_info_set = ExtraInfo::parse_file(&extra_info_str); - assert_eq!(extra_info_set.len(), 1); - - let extra_info = extra_info_set.iter().next().unwrap().clone(); - - let extra_info_str = extra_info.to_string(); - - let extra_info_2 = ExtraInfo::parse_file(&extra_info_str) - .into_iter() - .next() - .unwrap() - .clone(); - assert_eq!(extra_info, extra_info_2); - - let bridge_to_test: [u8; 20] = - array_bytes::hex2array("72E12B89136B45BBC81D1EF0AC7DDDBB91B148DB").unwrap(); - - // Open test database - let db: Db = sled::open("test_db_sei").unwrap(); - - // Delete all data in test DB - db.clear().unwrap(); - assert!(!db.contains_key("bridges").unwrap()); - assert!(!db.contains_key(bridge_to_test).unwrap()); - - // Start web server - spawn(async move { - extra_infos_server::server().await; - }); - - // Give server time to start - sleep(Duration::new(1, 0)).await; - - // Update extra-infos (no new data to add) - update_extra_infos(&db, "http://localhost:8004/") - .await - .unwrap(); - - // Check that database is still empty - assert!(!db.contains_key("bridges").unwrap()); - assert!(!db.contains_key(bridge_to_test).unwrap()); - - // Add our extra-info to the server's records - { - use hyper::{Body, Client, Method, Request}; - let client = Client::new(); - let req = Request::builder() - .method(Method::POST) - .uri("http://localhost:8004/add".parse::().unwrap()) - .body(Body::from(serde_json::to_string(&extra_info_set).unwrap())) - .unwrap(); - client.request(req).await.unwrap(); - } - - // Update extra-infos (add new record) - update_extra_infos(&db, "http://localhost:8004/") - .await - .unwrap(); - - // Check that DB now contains information on this bridge - assert!(db.contains_key("bridges").unwrap()); - let bridges: HashSet<[u8; 20]> = - bincode::deserialize(&db.get("bridges").unwrap().unwrap()).unwrap(); - assert!(bridges.contains(&bridge_to_test)); - assert!(db.contains_key(bridge_to_test).unwrap()); - let _bridge_info: BridgeInfo = - bincode::deserialize(&db.get(bridge_to_test).unwrap().unwrap()).unwrap(); -} - -#[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()); - - // Backdated reports - let date = get_date(); - let mut rng = rand::thread_rng(); - - let mut nonce = [0; 32]; - rng.fill_bytes(&mut nonce); - let report_4 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( - &bridges[0], - date - 1, - nonce, - )), - "ru".to_string(), - date - 1, - nonce, - BridgeDistributor::Lox, - ); - - let mut nonce = [0; 32]; - rng.fill_bytes(&mut nonce); - let report_5 = NegativeReport::new( - bridges[1].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( - &bridges[1], - date - 2, - nonce, - )), - "ru".to_string(), - date - 2, - nonce, - BridgeDistributor::Lox, - ); - - let mut nonce = [0; 32]; - rng.fill_bytes(&mut nonce); - let report_6 = NegativeReport::new( - bridges[2].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( - &bridges[2], - date - 3, - nonce, - )), - "ru".to_string(), - date - 3, - nonce, - BridgeDistributor::Lox, - ); - - // Verify reports - assert!(report_1.verify(&bridge_info_1)); - assert!(report_2.verify(&bridge_info_2)); - assert!(report_3.verify(&bridge_info_3)); - assert!(report_4.verify(&bridge_info_1)); - assert!(report_5.verify(&bridge_info_2)); - assert!(report_6.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; - - // Date too far in past - let mut invalid_report_2 = - NegativeReport::from_bridgeline(bridges[1], "ru".to_string(), BridgeDistributor::Lox) - .to_serializable_report(); - invalid_report_2.date = invalid_report_2.date - MAX_BACKDATE - 1; - - // Invalid country code - let invalid_report_3 = - NegativeReport::from_bridgeline(bridges[2], "xx".to_string(), BridgeDistributor::Lox) - .to_serializable_report(); - - assert!(invalid_report_1.to_report().is_err()); - assert!(invalid_report_2.to_report().is_err()); - assert!(invalid_report_3.to_report().is_err()); - - // Check that verification fails with incorrect data - - let date = get_date(); - let mut rng = rand::thread_rng(); - - // Incorrect BridgeLine hash - let mut nonce = [0; 32]; - rng.fill_bytes(&mut nonce); - let invalid_report_4 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( - &BridgeLine::default(), - date, - nonce, - )), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - // Incorrect bucket hash - let mut nonce = [0; 32]; - rng.fill_bytes(&mut nonce); - let invalid_report_5 = NegativeReport::new( - bridges[1].fingerprint, - ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&Scalar::ZERO, date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - assert!(!invalid_report_4.verify(&bridge_info_1)); - assert!(!invalid_report_5.verify(&bridge_info_2)); - - // Test that reports with duplicate nonces are rejected - // (Also test encryption and decryption.) - - // Open test database - let db: Db = sled::open("test_db_nr").unwrap(); - - // Delete all data in test DB - db.clear().unwrap(); - assert!(!db.contains_key("nrs-to-process").unwrap()); - - let mut nonce = [0; 32]; - rng.fill_bytes(&mut nonce); - - // A valid report - let valid_report_1 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - let valid_report_1_copy_1 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - let valid_report_1_copy_2 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - // Report which reuses this nonce - let invalid_report_1 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - // This is the same report - assert_eq!(valid_report_1, invalid_report_1); - - // Report which reuses this nonce for a different bridge - let invalid_report_2 = NegativeReport::new( - bridges[1].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[1], date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - // Report which uses this nonce but on a different day - let valid_report_2 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( - &bridges[0], - date - 1, - nonce, - )), - "ru".to_string(), - date - 1, - nonce, - BridgeDistributor::Lox, - ); - - // Report with different nonce - let mut nonce = [0; 32]; - rng.fill_bytes(&mut nonce); - - let valid_report_3 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - let map_key_1 = format!( - "{}_{}_{}", - array_bytes::bytes2hex("", valid_report_1.fingerprint), - "ru".to_string(), - date - ); - - // Generate key for today - let secret = StaticSecret::random_from_rng(&mut rng); - let public = PublicKey::from(&secret); - let secret_yesterday = StaticSecret::random_from_rng(&mut rng); - let public_yesterday = PublicKey::from(&secret_yesterday); - assert!(!db.contains_key("nr-keys").unwrap()); - - // Fail to add to database because we can't decrypt - handle_encrypted_negative_report(&db, valid_report_1_copy_1.encrypt(&public)); - assert!(!db.contains_key("nrs-to-process").unwrap()); - - // Store yesterday's key but not today's - let mut nr_keys = BTreeMap::::new(); - nr_keys.insert(date - 1, secret_yesterday); - db.insert("nr-keys", bincode::serialize(&nr_keys).unwrap()) - .unwrap(); - - // Fail to add to database because we still can't decrypt - handle_encrypted_negative_report(&db, valid_report_1_copy_2.encrypt(&public)); - assert!(!db.contains_key("nrs-to-process").unwrap()); - - // Store today's key - nr_keys.insert(date, secret); - db.insert("nr-keys", bincode::serialize(&nr_keys).unwrap()) - .unwrap(); - - handle_encrypted_negative_report(&db, valid_report_1.encrypt(&public)); - let nrs_to_process: BTreeMap> = - bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); - let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); - assert_eq!(negative_reports.len(), 1); - - handle_encrypted_negative_report(&db, invalid_report_1.encrypt(&public)); // no change - let nrs_to_process: BTreeMap> = - bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); - let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); - assert_eq!(negative_reports.len(), 1); - - let map_key_2 = format!( - "{}_{}_{}", - array_bytes::bytes2hex("", invalid_report_2.fingerprint), - "ru".to_string(), - date - ); - handle_encrypted_negative_report(&db, invalid_report_2.encrypt(&public)); // no change - let nrs_to_process: BTreeMap> = - bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); - assert!(!nrs_to_process.contains_key(&map_key_2)); - - let map_key_3 = format!( - "{}_{}_{}", - array_bytes::bytes2hex("", valid_report_2.fingerprint), - "ru".to_string(), - date - 1 - ); - handle_encrypted_negative_report(&db, valid_report_2.encrypt(&public_yesterday)); - let nrs_to_process: BTreeMap> = - bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); - let negative_reports = nrs_to_process.get(&map_key_3).unwrap(); - assert_eq!(negative_reports.len(), 1); - - handle_encrypted_negative_report(&db, valid_report_3.encrypt(&public)); - let nrs_to_process: BTreeMap> = - bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); - let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); - assert_eq!(negative_reports.len(), 2); - - // Same tests, but use hash of bucket - - // Delete all data in test DB - db.clear().unwrap(); - assert!(!db.contains_key("nrs-to-process").unwrap()); - - // Re-generate keys and save in database - let public = new_negative_report_key(&db, date).unwrap(); - let public_yesterday = new_negative_report_key(&db, date - 1).unwrap(); - - let mut nonce = [0; 32]; - rng.fill_bytes(&mut nonce); - - // A valid report - let valid_report_1 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - // Report which reuses this nonce - let invalid_report_1 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - // This is the same report - assert_eq!(valid_report_1, invalid_report_1); - - // Report which reuses this nonce for a different bridge - let invalid_report_2 = NegativeReport::new( - bridges[1].fingerprint, - ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - // Report which uses this nonce but on a different day - let valid_report_2 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date - 1, nonce)), - "ru".to_string(), - date - 1, - nonce, - BridgeDistributor::Lox, - ); - - // Report with different nonce - let mut nonce = [0; 32]; - rng.fill_bytes(&mut nonce); - - let valid_report_3 = NegativeReport::new( - bridges[0].fingerprint, - ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), - "ru".to_string(), - date, - nonce, - BridgeDistributor::Lox, - ); - - let map_key_1 = format!( - "{}_{}_{}", - array_bytes::bytes2hex("", valid_report_1.fingerprint), - "ru".to_string(), - date - ); - handle_encrypted_negative_report(&db, valid_report_1.encrypt(&public)); - let nrs_to_process: BTreeMap> = - bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); - let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); - assert_eq!(negative_reports.len(), 1); - - handle_encrypted_negative_report(&db, invalid_report_1.encrypt(&public)); // no change - let nrs_to_process: BTreeMap> = - bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); - let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); - assert_eq!(negative_reports.len(), 1); - - let map_key_2 = format!( - "{}_{}_{}", - array_bytes::bytes2hex("", invalid_report_2.fingerprint), - "ru".to_string(), - date - ); - handle_encrypted_negative_report(&db, invalid_report_2.encrypt(&public)); // no change - let nrs_to_process: BTreeMap> = - bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); - assert!(!nrs_to_process.contains_key(&map_key_2)); - - let map_key_3 = format!( - "{}_{}_{}", - array_bytes::bytes2hex("", valid_report_2.fingerprint), - "ru".to_string(), - date - 1 - ); - handle_encrypted_negative_report(&db, valid_report_2.encrypt(&public_yesterday)); - let nrs_to_process: BTreeMap> = - bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); - let negative_reports = nrs_to_process.get(&map_key_3).unwrap(); - assert_eq!(negative_reports.len(), 1); - - handle_encrypted_negative_report(&db, valid_report_3.encrypt(&public)); - let nrs_to_process: BTreeMap> = - bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); - let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); - assert_eq!(negative_reports.len(), 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()); - - // Test storing to-be-processed positive reports to database - - // 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[0].fingerprint, - None, - &cred, - &th.ba.lox_pub, - "ru".to_string(), - ) - .unwrap(); - let report_3 = PositiveReport::from_lox_credential( - bridges[1].fingerprint, - None, - &cred, - &th.ba.lox_pub, - "ru".to_string(), - ) - .unwrap(); - - // Open test database - let db: Db = sled::open("test_db_pr").unwrap(); - - // Delete all data in test DB - db.clear().unwrap(); - assert!(!db.contains_key("prs-to-process").unwrap()); - - let map_key_1 = format!( - "{}_{}_{}", - array_bytes::bytes2hex("", report_1.fingerprint), - &report_1.country, - &report_1.date - ); - let map_key_2 = format!( - "{}_{}_{}", - array_bytes::bytes2hex("", report_3.fingerprint), - &report_3.country, - &report_3.date - ); - - save_positive_report_to_process(&db, report_1); - let prs_to_process: BTreeMap> = - bincode::deserialize(&db.get("prs-to-process").unwrap().unwrap()).unwrap(); - let positive_reports = prs_to_process.get(&map_key_1).unwrap(); - assert_eq!(positive_reports.len(), 1); - assert!(!prs_to_process.contains_key(&map_key_2)); - - save_positive_report_to_process(&db, report_2); - let prs_to_process: BTreeMap> = - bincode::deserialize(&db.get("prs-to-process").unwrap().unwrap()).unwrap(); - let positive_reports = prs_to_process.get(&map_key_1).unwrap(); - assert_eq!(positive_reports.len(), 2); - assert!(!prs_to_process.contains_key(&map_key_2)); - - save_positive_report_to_process(&db, report_3); - let prs_to_process: BTreeMap> = - bincode::deserialize(&db.get("prs-to-process").unwrap().unwrap()).unwrap(); - // Check that this has not changed - let positive_reports = prs_to_process.get(&map_key_1).unwrap(); - assert_eq!(positive_reports.len(), 2); - // New report added to its own collection - let positive_reports = prs_to_process.get(&map_key_2).unwrap(); - assert_eq!(positive_reports.len(), 1); -} - -#[test] -fn test_analysis() { - // Test stage 1 analysis - { - let mut date = get_date(); - - // New bridge info - let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); - - bridge_info - .info_by_country - .insert("ru".to_string(), BridgeCountryInfo::new(date)); - let analyzer = analysis::NormalAnalyzer::new(5, 0.25); - let confidence = 0.95; - - let mut blocking_countries = HashSet::::new(); - - // No data today - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // 1 connection, 0 negative reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 8, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // 0 connections, 0 negative reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 0, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // 0 connections, 1 negative report - // (exceeds scaled threshold) - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 1, - ); - blocking_countries.insert("ru".to_string()); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - { - let mut date = get_date(); - - // New bridge info - let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); - - bridge_info - .info_by_country - .insert("ru".to_string(), BridgeCountryInfo::new(date)); - let analyzer = analysis::NormalAnalyzer::new(5, 0.25); - let confidence = 0.95; - - let mut blocking_countries = HashSet::::new(); - - // No data today - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // 1 connection, 1 negative report - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 8, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 1, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // 8 connections, 2 negative reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 8, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 2, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // 8 connections, 3 negative reports - // (exceeds scaled threshold) - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 8, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 3, - ); - blocking_countries.insert("ru".to_string()); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - { - let mut date = get_date(); - - // New bridge info - let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); - - bridge_info - .info_by_country - .insert("ru".to_string(), BridgeCountryInfo::new(date)); - let analyzer = analysis::NormalAnalyzer::new(5, 0.25); - let confidence = 0.95; - - let mut blocking_countries = HashSet::::new(); - - // 24 connections, 5 negative reports - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 24, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 5, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // 24 connections, 6 negative reports - // (exceeds max threshold) - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 24, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 6, - ); - blocking_countries.insert("ru".to_string()); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - // Test stage 2 analysis - - { - let mut date = get_date(); - - // New bridge info - let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); - - bridge_info - .info_by_country - .insert("ru".to_string(), BridgeCountryInfo::new(date)); - let analyzer = analysis::NormalAnalyzer::new(5, 0.25); - let confidence = 0.95; - - let mut blocking_countries = HashSet::::new(); - - // No data today - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - for i in 1..30 { - // 9-32 connections, 0-3 negative reports each day - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 8 * (i % 3 + 2), - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - i % 4, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - // Data similar to previous days: - // 24 connections, 2 negative reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 24, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 2, - ); - - // Should not be blocked because we have similar data. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 104 connections, 1 negative report - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 104, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 1, - ); - - // This should not be blocked even though it's very different because - // it's different in the good direction. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 40 connections, 12 negative reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 40, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 12, - ); - blocking_countries.insert("ru".to_string()); - - // This should be blocked because it's different in the bad direction. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - { - let mut date = get_date(); - - // New bridge info - let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); - - bridge_info - .info_by_country - .insert("ru".to_string(), BridgeCountryInfo::new(date)); - let analyzer = analysis::NormalAnalyzer::new(5, 0.25); - let confidence = 0.95; - - let mut blocking_countries = HashSet::::new(); - - // No data today - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - for i in 1..30 { - // 9-32 connections, 0-3 negative reports each day - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 8 * (i % 3 + 2), - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - i % 4, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - // Data similar to previous days: - // 24 connections, 2 negative reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 24, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 2, - ); - - // Should not be blocked because we have similar data. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 104 connections, 1 negative report - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 104, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 1, - ); - - // This should not be blocked even though it's very different because - // it's different in the good direction. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 800 connections, 12 negative reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 800, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 12, - ); - blocking_countries.insert("ru".to_string()); - - // The censor artificially inflated bridge stats to prevent detection. - // Ensure we still detect the censorship from negative reports. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - { - let mut date = get_date(); - - // New bridge info - let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); - - bridge_info - .info_by_country - .insert("ru".to_string(), BridgeCountryInfo::new(date)); - let analyzer = analysis::NormalAnalyzer::new(5, 0.25); - let confidence = 0.95; - - let mut blocking_countries = HashSet::::new(); - - // No data today - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - for i in 1..30 { - // 9-32 connections, 0-3 negative reports each day - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 8 * (i % 3 + 2), - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - i % 4, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - // Data similar to previous days: - // 24 connections, 2 negative reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 24, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 2, - ); - - // Should not be blocked because we have similar data. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 104 connections, 1 negative report - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 104, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 1, - ); - - // This should not be blocked even though it's very different because - // it's different in the good direction. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 0 connections, 0 negative reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 0, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 0, - ); - blocking_countries.insert("ru".to_string()); - - // This should be blocked because it's different in the bad direction. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - // Test stage 3 analysis - - { - let mut date = get_date(); - - // New bridge info - let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); - - bridge_info - .info_by_country - .insert("ru".to_string(), BridgeCountryInfo::new(date)); - let analyzer = analysis::NormalAnalyzer::new(5, 0.25); - let confidence = 0.95; - - let mut blocking_countries = HashSet::::new(); - - // No data today - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - for i in 1..30 { - // 9-32 connections, 0-3 negative reports, 16-20 positive reports each day - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 8 * (i % 3 + 2), - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - i % 4, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 16 + i % 5, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - // Data similar to previous days: - // 24 connections, 2 negative reports, 17 positive reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 24, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 2, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 17, - ); - - // Should not be blocked because we have similar data. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 104 connections, 1 negative report, 100 positive reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 104, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 1, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 100, - ); - - // This should not be blocked even though it's very different because - // it's different in the good direction. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 40 connections, 12 negative reports, 40 positive reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 40, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 12, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 40, - ); - blocking_countries.insert("ru".to_string()); - - // This should be blocked because it's different in the bad direction. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - { - let mut date = get_date(); - - // New bridge info - let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); - - bridge_info - .info_by_country - .insert("ru".to_string(), BridgeCountryInfo::new(date)); - let analyzer = analysis::NormalAnalyzer::new(5, 0.25); - let confidence = 0.95; - - let mut blocking_countries = HashSet::::new(); - - // No data today - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - for i in 1..30 { - // 9-32 connections, 0-3 negative reports, 16-20 positive reports each day - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 8 * (i % 3 + 2), - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - i % 4, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 16 + i % 5, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - // Data similar to previous days: - // 24 connections, 2 negative reports, 17 positive reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 24, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 2, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 17, - ); - - // Should not be blocked because we have similar data. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 104 connections, 1 negative report, 85 positive reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 104, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 1, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 85, - ); - - // This should not be blocked even though it's very different because - // it's different in the good direction. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 800 connections, 12 negative reports, 750 positive reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 800, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 12, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 750, - ); - - blocking_countries.insert("ru".to_string()); - - // The censor artificially inflated bridge stats to prevent detection. - // Ensure we still detect the censorship from negative reports. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - { - let mut date = get_date(); - - // New bridge info - let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); - - bridge_info - .info_by_country - .insert("ru".to_string(), BridgeCountryInfo::new(date)); - let analyzer = analysis::NormalAnalyzer::new(5, 0.25); - let confidence = 0.95; - - let mut blocking_countries = HashSet::::new(); - - // No data today - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - for i in 1..30 { - // 9-32 connections, 0-3 negative reports, 16-20 positive reports each day - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 8 * (i % 3 + 2), - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - i % 4, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 16 + i % 5, - ); - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } - - // Data similar to previous days: - // 24 connections, 2 negative reports, 17 positive reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 24, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 2, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 17, - ); - - // Should not be blocked because we have similar data. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 104 connections, 1 negative report, 100 positive reports - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 104, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 1, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 100, - ); - - // This should not be blocked even though it's very different because - // it's different in the good direction. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - - // Data different from previous days: - // 24 connections, 1 negative report, 1 positive report - date += 1; - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::BridgeIps, - date, - 24, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::NegativeReports, - date, - 1, - ); - bridge_info.info_by_country.get_mut("ru").unwrap().add_info( - BridgeInfoType::PositiveReports, - date, - 1, - ); - - blocking_countries.insert("ru".to_string()); - - // This should be blocked because it's different in the bad direction. - assert_eq!( - blocked_in(&analyzer, &bridge_info, confidence, date), - blocking_countries - ); - } +mod extra_infos; +mod reports { + mod negative_reports; + mod positive_reports; } diff --git a/src/tests/analysis/stage_one.rs b/src/tests/analysis/stage_one.rs new file mode 100644 index 0000000..62bfd5a --- /dev/null +++ b/src/tests/analysis/stage_one.rs @@ -0,0 +1,189 @@ +use crate::{analysis::blocked_in, *}; +use std::collections::HashSet; + +#[test] +fn test_stage_1_analysis() { + // Test stage 1 analysis + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // No data today + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // 1 connection, 0 negative reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // 0 connections, 0 negative reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 0, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // 0 connections, 1 negative report + // (exceeds scaled threshold) + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + blocking_countries.insert("ru".to_string()); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // No data today + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // 1 connection, 1 negative report + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // 8 connections, 2 negative reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 2, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // 8 connections, 3 negative reports + // (exceeds scaled threshold) + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 3, + ); + blocking_countries.insert("ru".to_string()); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // 24 connections, 5 negative reports + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 24, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 5, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // 24 connections, 6 negative reports + // (exceeds max threshold) + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 24, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 6, + ); + blocking_countries.insert("ru".to_string()); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } +} diff --git a/src/tests/analysis/stage_three.rs b/src/tests/analysis/stage_three.rs new file mode 100644 index 0000000..192a9c1 --- /dev/null +++ b/src/tests/analysis/stage_three.rs @@ -0,0 +1,497 @@ +use crate::{analysis::blocked_in, *}; +use std::collections::HashSet; + +#[tokio::test] +async fn test_stage_3_analysis() { + // Test workaround when computed covariance matrix is not positive definite + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // No data today + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + for _ in 1..30 { + // 9-16 connections, 1 negative report, 13 positive reports each day + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 16, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 13, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + // Data similar to previous days: + // 16 connections, 1 negative report, 12 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 16, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 12, + ); + + // Should not be blocked because we have similar data. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 104 connections, 1 negative report, 100 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 104, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 100, + ); + + // This should not be blocked even though it's very different because + // it's different in the good direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 40 connections, 12 negative reports, 40 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 40, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 12, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 40, + ); + blocking_countries.insert("ru".to_string()); + + // This should be blocked because it's different in the bad direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // No data today + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + for i in 1..30 { + // 9-32 connections, 0-3 negative reports, 16-20 positive reports each day + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8 * (i % 3 + 2), + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + i % 4, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 16 + i % 5, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + // Data similar to previous days: + // 24 connections, 2 negative reports, 17 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 24, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 2, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 17, + ); + + // Should not be blocked because we have similar data. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 104 connections, 1 negative report, 100 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 104, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 100, + ); + + // This should not be blocked even though it's very different because + // it's different in the good direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 40 connections, 12 negative reports, 40 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 40, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 12, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 40, + ); + blocking_countries.insert("ru".to_string()); + + // This should be blocked because it's different in the bad direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // No data today + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + for i in 1..30 { + // 9-32 connections, 0-3 negative reports, 16-20 positive reports each day + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8 * (i % 3 + 2), + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + i % 4, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 16 + i % 5, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + // Data similar to previous days: + // 24 connections, 2 negative reports, 17 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 24, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 2, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 17, + ); + + // Should not be blocked because we have similar data. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 104 connections, 1 negative report, 85 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 104, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 85, + ); + + // This should not be blocked even though it's very different because + // it's different in the good direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 800 connections, 12 negative reports, 750 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 800, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 12, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 750, + ); + + blocking_countries.insert("ru".to_string()); + + // The censor artificially inflated bridge stats to prevent detection. + // Ensure we still detect the censorship from negative reports. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // No data today + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + for i in 1..30 { + // 9-32 connections, 0-3 negative reports, 16-20 positive reports each day + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8 * (i % 3 + 2), + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + i % 4, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 16 + i % 5, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + // Data similar to previous days: + // 24 connections, 2 negative reports, 17 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 24, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 2, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 17, + ); + + // Should not be blocked because we have similar data. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 104 connections, 1 negative report, 100 positive reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 104, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 100, + ); + + // This should not be blocked even though it's very different because + // it's different in the good direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 24 connections, 1 negative report, 1 positive report + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 24, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::PositiveReports, + date, + 1, + ); + + blocking_countries.insert("ru".to_string()); + + // This should be blocked because it's different in the bad direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } +} diff --git a/src/tests/analysis/stage_two.rs b/src/tests/analysis/stage_two.rs new file mode 100644 index 0000000..98f2b50 --- /dev/null +++ b/src/tests/analysis/stage_two.rs @@ -0,0 +1,374 @@ +use crate::{analysis::blocked_in, *}; +use std::collections::HashSet; + +#[tokio::test] +async fn test_stage_2_analysis() { + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // No data today + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + for i in 1..30 { + // 9-32 connections, 0-3 negative reports each day + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8 * (i % 3 + 2), + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + i % 4, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + // Data similar to previous days: + // 24 connections, 2 negative reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 24, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 2, + ); + + // Should not be blocked because we have similar data. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 104 connections, 1 negative report + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 104, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + + // This should not be blocked even though it's very different because + // it's different in the good direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 40 connections, 12 negative reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 40, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 12, + ); + blocking_countries.insert("ru".to_string()); + + // This should be blocked because it's different in the bad direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + // Test workaround when computed covariance matrix is not positive definite + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // No data today + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + for _ in 1..30 { + // 1-8 connections, 1 negative report each day + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + // Data similar to previous days: + // 8 connections, 1 negative report + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + + // Should not be blocked because we have similar data. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 104 connections, 1 negative report + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 104, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + + // This should not be blocked even though it's very different because + // it's different in the good direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 800 connections, 12 negative reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 800, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 12, + ); + blocking_countries.insert("ru".to_string()); + + // The censor artificially inflated bridge stats to prevent detection. + // Ensure we still detect the censorship from negative reports. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // No data today + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + for i in 1..30 { + // 9-32 connections, 0-3 negative reports each day + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8 * (i % 3 + 2), + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + i % 4, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + // Data similar to previous days: + // 24 connections, 2 negative reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 24, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 2, + ); + + // Should not be blocked because we have similar data. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 104 connections, 1 negative report + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 104, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 1, + ); + + // This should not be blocked even though it's very different because + // it's different in the good direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + // Data different from previous days: + // 0 connections, 0 negative reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 0, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 0, + ); + //blocking_countries.insert("ru".to_string()); + + // This should be blocked because it's different in the bad direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + { + let mut date = get_date(); + + // New bridge info + let mut bridge_info = BridgeInfo::new([0; 20], &String::default()); + + bridge_info + .info_by_country + .insert("ru".to_string(), BridgeCountryInfo::new(date)); + let analyzer = analysis::NormalAnalyzer::new(5, 0.25); + let confidence = 0.95; + + let mut blocking_countries = HashSet::::new(); + + // No data today + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + + for i in 1..30 { + // ~96 connections, 0-3 negative reports each day + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 8 * (i % 3 + 11), + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + i % 4, + ); + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } + + // Data different from previous days: + // 0 connections, 0 negative reports + date += 1; + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::BridgeIps, + date, + 0, + ); + bridge_info.info_by_country.get_mut("ru").unwrap().add_info( + BridgeInfoType::NegativeReports, + date, + 2, + ); + blocking_countries.insert("ru".to_string()); + + // This should be blocked because it's different in the bad direction. + assert_eq!( + blocked_in(&analyzer, &bridge_info, confidence, date), + blocking_countries + ); + } +} diff --git a/src/tests/extra_infos.rs b/src/tests/extra_infos.rs new file mode 100644 index 0000000..c62f65a --- /dev/null +++ b/src/tests/extra_infos.rs @@ -0,0 +1,141 @@ +#![allow(non_snake_case)] + +use crate::{simulation::extra_infos_server, *}; + +use std::{collections::HashSet, time::Duration}; +use tokio::{spawn, time::sleep}; + +#[tokio::test] +async fn test_download_extra_infos() { + let bridge_to_test = + array_bytes::hex2array("72E12B89136B45BBC81D1EF0AC7DDDBB91B148DB").unwrap(); + + // Open test database + let db: Db = sled::open("test_db_dei").unwrap(); + + // Delete all data in test DB + db.clear().unwrap(); + assert!(!db.contains_key("bridges").unwrap()); + assert!(!db.contains_key(bridge_to_test).unwrap()); + + // Download and process recent extra-infos files + update_extra_infos( + &db, + "https://collector.torproject.org/recent/bridge-descriptors/extra-infos/", + ) + .await + .unwrap(); + + // Check that DB contains information on a bridge with high uptime + assert!(db.contains_key("bridges").unwrap()); + let bridges: HashSet<[u8; 20]> = + bincode::deserialize(&db.get("bridges").unwrap().unwrap()).unwrap(); + assert!(bridges.contains(&bridge_to_test)); + assert!(db.contains_key(bridge_to_test).unwrap()); + let _bridge_info: BridgeInfo = + bincode::deserialize(&db.get(bridge_to_test).unwrap().unwrap()).unwrap(); +} + +#[tokio::test] +async fn test_simulate_extra_infos() { + let extra_info_str = r#"@type bridge-extra-info 1.3 +extra-info ElephantBridgeDE2 72E12B89136B45BBC81D1EF0AC7DDDBB91B148DB +master-key-ed25519 eWxjRwAWW7n8BGG9fNa6rApmBFbe3f0xcD7dqwOICW8 +published 2024-04-06 03:51:04 +transport obfs4 +write-history 2024-04-05 04:55:22 (86400 s) 31665735680,14918491136,15423603712,36168353792,40396827648 +read-history 2024-04-05 04:55:22 (86400 s) 31799622656,15229917184,15479115776,36317251584,40444155904 +ipv6-write-history 2024-04-05 04:55:22 (86400 s) 5972127744,610078720,516897792,327949312,640708608 +ipv6-read-history 2024-04-05 04:55:22 (86400 s) 4156158976,4040448000,2935756800,4263080960,6513532928 +dirreq-write-history 2024-04-05 04:55:22 (86400 s) 625217536,646188032,618014720,584386560,600778752 +dirreq-read-history 2024-04-05 04:55:22 (86400 s) 18816000,19000320,18484224,17364992,18443264 +geoip-db-digest 44073997E1ED63E183B79DE2A1757E9997A834E3 +geoip6-db-digest C0BF46880C6C132D746683279CC90DD4B2D31786 +dirreq-stats-end 2024-04-05 06:51:23 (86400 s) +dirreq-v3-ips ru=16,au=8,by=8,cn=8,gb=8,ir=8,mt=8,nl=8,pl=8,tn=8,tr=8,us=8 +dirreq-v3-reqs ru=72,gb=64,pl=32,cn=16,ir=16,us=16,au=8,by=8,mt=8,nl=8,tn=8,tr=8 +dirreq-v3-resp ok=216,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=328,busy=0 +dirreq-v3-direct-dl complete=0,timeout=0,running=0 +dirreq-v3-tunneled-dl complete=212,timeout=4,running=0,min=21595,d1=293347,d2=1624137,q1=1911800,d3=2066929,d4=2415000,md=2888500,d6=3264000,d7=3851333,q3=41> +hidserv-stats-end 2024-04-05 06:51:23 (86400 s) +hidserv-rend-relayed-cells 7924 delta_f=2048 epsilon=0.30 bin_size=1024 +hidserv-dir-onions-seen -12 delta_f=8 epsilon=0.30 bin_size=8 +hidserv-v3-stats-end 2024-04-05 12:00:00 (86400 s) +hidserv-rend-v3-relayed-cells -4785 delta_f=2048 epsilon=0.30 bin_size=1024 +hidserv-dir-v3-onions-seen 5 delta_f=8 epsilon=0.30 bin_size=8 +padding-counts 2024-04-05 06:51:42 (86400 s) bin-size=10000 write-drop=0 write-pad=80000 write-total=79980000 read-drop=0 read-pad=1110000 read-total=7989000> +bridge-stats-end 2024-04-05 06:51:44 (86400 s) +bridge-ips ru=40,us=32,??=8,au=8,br=8,by=8,cn=8,de=8,eg=8,eu=8,gb=8,ge=8,hr=8,ie=8,ir=8,kp=8,lt=8,mt=8,nl=8,pl=8,ro=8,sg=8,tn=8,tr=8,vn=8 +bridge-ip-versions v4=104,v6=8 +bridge-ip-transports =56,obfs4=56 +router-digest-sha256 zK0VMl3i0B2eaeQTR03e2hZ0i8ytkuhK/psgD2J1/lQ +router-digest F30B38390C375E1EE74BFED844177804442569E0"#; + + let extra_info_set = ExtraInfo::parse_file(&extra_info_str); + assert_eq!(extra_info_set.len(), 1); + + let extra_info = extra_info_set.iter().next().unwrap().clone(); + + let extra_info_str = extra_info.to_string(); + + let extra_info_2 = ExtraInfo::parse_file(&extra_info_str) + .into_iter() + .next() + .unwrap() + .clone(); + assert_eq!(extra_info, extra_info_2); + + let bridge_to_test: [u8; 20] = + array_bytes::hex2array("72E12B89136B45BBC81D1EF0AC7DDDBB91B148DB").unwrap(); + + // Open test database + let db: Db = sled::open("test_db_sei").unwrap(); + + // Delete all data in test DB + db.clear().unwrap(); + assert!(!db.contains_key("bridges").unwrap()); + assert!(!db.contains_key(bridge_to_test).unwrap()); + + // Start web server + spawn(async move { + extra_infos_server::server().await; + }); + + // Give server time to start + sleep(Duration::new(1, 0)).await; + + // Update extra-infos (no new data to add) + update_extra_infos(&db, "http://localhost:8004/") + .await + .unwrap(); + + // Check that database is still empty + assert!(!db.contains_key("bridges").unwrap()); + assert!(!db.contains_key(bridge_to_test).unwrap()); + + // Add our extra-info to the server's records + { + use hyper::{Body, Client, Method, Request}; + let client = Client::new(); + let req = Request::builder() + .method(Method::POST) + .uri("http://localhost:8004/add".parse::().unwrap()) + .body(Body::from(serde_json::to_string(&extra_info_set).unwrap())) + .unwrap(); + client.request(req).await.unwrap(); + } + + // Update extra-infos (add new record) + update_extra_infos(&db, "http://localhost:8004/") + .await + .unwrap(); + + // Check that DB now contains information on this bridge + assert!(db.contains_key("bridges").unwrap()); + let bridges: HashSet<[u8; 20]> = + bincode::deserialize(&db.get("bridges").unwrap().unwrap()).unwrap(); + assert!(bridges.contains(&bridge_to_test)); + assert!(db.contains_key(bridge_to_test).unwrap()); + let _bridge_info: BridgeInfo = + bincode::deserialize(&db.get(bridge_to_test).unwrap().unwrap()).unwrap(); +} diff --git a/src/tests/reports/negative_reports.rs b/src/tests/reports/negative_reports.rs new file mode 100644 index 0000000..da5a659 --- /dev/null +++ b/src/tests/reports/negative_reports.rs @@ -0,0 +1,577 @@ +#![allow(non_snake_case)] + +use crate::{bridge_verification_info::BridgeVerificationInfo, *}; +use lox_library::{ + bridge_table::{self, BridgeLine}, + cred::Lox, + proto::*, + scalar_u32, BridgeAuth, BridgeDb, +}; + +use base64::{engine::general_purpose, Engine as _}; +use curve25519_dalek::Scalar; +use rand::RngCore; +use std::collections::{BTreeMap, HashSet}; +use x25519_dalek::{PublicKey, StaticSecret}; + +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()); + + // Backdated reports + let date = get_date(); + let mut rng = rand::thread_rng(); + + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + let report_4 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( + &bridges[0], + date - 1, + nonce, + )), + "ru".to_string(), + date - 1, + nonce, + BridgeDistributor::Lox, + ); + + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + let report_5 = NegativeReport::new( + bridges[1].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( + &bridges[1], + date - 2, + nonce, + )), + "ru".to_string(), + date - 2, + nonce, + BridgeDistributor::Lox, + ); + + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + let report_6 = NegativeReport::new( + bridges[2].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( + &bridges[2], + date - 3, + nonce, + )), + "ru".to_string(), + date - 3, + nonce, + BridgeDistributor::Lox, + ); + + // Verify reports + assert!(report_1.verify(&bridge_info_1)); + assert!(report_2.verify(&bridge_info_2)); + assert!(report_3.verify(&bridge_info_3)); + assert!(report_4.verify(&bridge_info_1)); + assert!(report_5.verify(&bridge_info_2)); + assert!(report_6.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; + + // Date too far in past + let mut invalid_report_2 = + NegativeReport::from_bridgeline(bridges[1], "ru".to_string(), BridgeDistributor::Lox) + .to_serializable_report(); + invalid_report_2.date = invalid_report_2.date - MAX_BACKDATE - 1; + + // Invalid country code + let invalid_report_3 = + NegativeReport::from_bridgeline(bridges[2], "xx".to_string(), BridgeDistributor::Lox) + .to_serializable_report(); + + assert!(invalid_report_1.to_report().is_err()); + assert!(invalid_report_2.to_report().is_err()); + assert!(invalid_report_3.to_report().is_err()); + + // Check that verification fails with incorrect data + + let date = get_date(); + let mut rng = rand::thread_rng(); + + // Incorrect BridgeLine hash + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + let invalid_report_4 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( + &BridgeLine::default(), + date, + nonce, + )), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + // Incorrect bucket hash + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + let invalid_report_5 = NegativeReport::new( + bridges[1].fingerprint, + ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&Scalar::ZERO, date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + assert!(!invalid_report_4.verify(&bridge_info_1)); + assert!(!invalid_report_5.verify(&bridge_info_2)); + + // Test that reports with duplicate nonces are rejected + // (Also test encryption and decryption.) + + // Open test database + let db: Db = sled::open("test_db_nr").unwrap(); + + // Delete all data in test DB + db.clear().unwrap(); + assert!(!db.contains_key("nrs-to-process").unwrap()); + + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + + // A valid report + let valid_report_1 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + let valid_report_1_copy_1 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + let valid_report_1_copy_2 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + // Report which reuses this nonce + let invalid_report_1 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + // This is the same report + assert_eq!(valid_report_1, invalid_report_1); + + // Report which reuses this nonce for a different bridge + let invalid_report_2 = NegativeReport::new( + bridges[1].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[1], date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + // Report which uses this nonce but on a different day + let valid_report_2 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( + &bridges[0], + date - 1, + nonce, + )), + "ru".to_string(), + date - 1, + nonce, + BridgeDistributor::Lox, + ); + + // Report with different nonce + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + + let valid_report_3 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + let map_key_1 = format!( + "{}_{}_{}", + array_bytes::bytes2hex("", valid_report_1.fingerprint), + "ru".to_string(), + date + ); + + // Generate key for today + let secret = StaticSecret::random_from_rng(&mut rng); + let public = PublicKey::from(&secret); + let secret_yesterday = StaticSecret::random_from_rng(&mut rng); + let public_yesterday = PublicKey::from(&secret_yesterday); + assert!(!db.contains_key("nr-keys").unwrap()); + + // Fail to add to database because we can't decrypt + handle_encrypted_negative_report(&db, valid_report_1_copy_1.encrypt(&public)); + assert!(!db.contains_key("nrs-to-process").unwrap()); + + // Store yesterday's key but not today's + let mut nr_keys = BTreeMap::::new(); + nr_keys.insert(date - 1, secret_yesterday); + db.insert("nr-keys", bincode::serialize(&nr_keys).unwrap()) + .unwrap(); + + // Fail to add to database because we still can't decrypt + handle_encrypted_negative_report(&db, valid_report_1_copy_2.encrypt(&public)); + assert!(!db.contains_key("nrs-to-process").unwrap()); + + // Store today's key + nr_keys.insert(date, secret); + db.insert("nr-keys", bincode::serialize(&nr_keys).unwrap()) + .unwrap(); + + handle_encrypted_negative_report(&db, valid_report_1.encrypt(&public)); + let nrs_to_process: BTreeMap> = + bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); + let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); + assert_eq!(negative_reports.len(), 1); + + handle_encrypted_negative_report(&db, invalid_report_1.encrypt(&public)); // no change + let nrs_to_process: BTreeMap> = + bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); + let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); + assert_eq!(negative_reports.len(), 1); + + let map_key_2 = format!( + "{}_{}_{}", + array_bytes::bytes2hex("", invalid_report_2.fingerprint), + "ru".to_string(), + date + ); + handle_encrypted_negative_report(&db, invalid_report_2.encrypt(&public)); // no change + let nrs_to_process: BTreeMap> = + bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); + assert!(!nrs_to_process.contains_key(&map_key_2)); + + let map_key_3 = format!( + "{}_{}_{}", + array_bytes::bytes2hex("", valid_report_2.fingerprint), + "ru".to_string(), + date - 1 + ); + handle_encrypted_negative_report(&db, valid_report_2.encrypt(&public_yesterday)); + let nrs_to_process: BTreeMap> = + bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); + let negative_reports = nrs_to_process.get(&map_key_3).unwrap(); + assert_eq!(negative_reports.len(), 1); + + handle_encrypted_negative_report(&db, valid_report_3.encrypt(&public)); + let nrs_to_process: BTreeMap> = + bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); + let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); + assert_eq!(negative_reports.len(), 2); + + // Same tests, but use hash of bucket + + // Delete all data in test DB + db.clear().unwrap(); + assert!(!db.contains_key("nrs-to-process").unwrap()); + + // Re-generate keys and save in database + let public = new_negative_report_key(&db, date).unwrap(); + let public_yesterday = new_negative_report_key(&db, date - 1).unwrap(); + + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + + // A valid report + let valid_report_1 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + // Report which reuses this nonce + let invalid_report_1 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + // This is the same report + assert_eq!(valid_report_1, invalid_report_1); + + // Report which reuses this nonce for a different bridge + let invalid_report_2 = NegativeReport::new( + bridges[1].fingerprint, + ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + // Report which uses this nonce but on a different day + let valid_report_2 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date - 1, nonce)), + "ru".to_string(), + date - 1, + nonce, + BridgeDistributor::Lox, + ); + + // Report with different nonce + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + + let valid_report_3 = NegativeReport::new( + bridges[0].fingerprint, + ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&cred.bucket, date, nonce)), + "ru".to_string(), + date, + nonce, + BridgeDistributor::Lox, + ); + + let map_key_1 = format!( + "{}_{}_{}", + array_bytes::bytes2hex("", valid_report_1.fingerprint), + "ru".to_string(), + date + ); + handle_encrypted_negative_report(&db, valid_report_1.encrypt(&public)); + let nrs_to_process: BTreeMap> = + bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); + let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); + assert_eq!(negative_reports.len(), 1); + + handle_encrypted_negative_report(&db, invalid_report_1.encrypt(&public)); // no change + let nrs_to_process: BTreeMap> = + bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); + let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); + assert_eq!(negative_reports.len(), 1); + + let map_key_2 = format!( + "{}_{}_{}", + array_bytes::bytes2hex("", invalid_report_2.fingerprint), + "ru".to_string(), + date + ); + handle_encrypted_negative_report(&db, invalid_report_2.encrypt(&public)); // no change + let nrs_to_process: BTreeMap> = + bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); + assert!(!nrs_to_process.contains_key(&map_key_2)); + + let map_key_3 = format!( + "{}_{}_{}", + array_bytes::bytes2hex("", valid_report_2.fingerprint), + "ru".to_string(), + date - 1 + ); + handle_encrypted_negative_report(&db, valid_report_2.encrypt(&public_yesterday)); + let nrs_to_process: BTreeMap> = + bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); + let negative_reports = nrs_to_process.get(&map_key_3).unwrap(); + assert_eq!(negative_reports.len(), 1); + + handle_encrypted_negative_report(&db, valid_report_3.encrypt(&public)); + let nrs_to_process: BTreeMap> = + bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap(); + let negative_reports = nrs_to_process.get(&map_key_1).unwrap(); + assert_eq!(negative_reports.len(), 2); +} diff --git a/src/tests/reports/positive_reports.rs b/src/tests/reports/positive_reports.rs new file mode 100644 index 0000000..53a5fb5 --- /dev/null +++ b/src/tests/reports/positive_reports.rs @@ -0,0 +1,355 @@ +#![allow(non_snake_case)] + +use crate::{bridge_verification_info::BridgeVerificationInfo, *}; +use lox_library::{ + bridge_table::{self, BridgeLine}, + cred::Lox, + proto::*, + scalar_u32, BridgeAuth, BridgeDb, +}; + +use base64::{engine::general_purpose, Engine as _}; +use curve25519_dalek::{ristretto::RistrettoBasepointTable, Scalar}; +use rand::RngCore; +use std::collections::HashSet; + +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_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()); + + // Test storing to-be-processed positive reports to database + + // 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[0].fingerprint, + None, + &cred, + &th.ba.lox_pub, + "ru".to_string(), + ) + .unwrap(); + let report_3 = PositiveReport::from_lox_credential( + bridges[1].fingerprint, + None, + &cred, + &th.ba.lox_pub, + "ru".to_string(), + ) + .unwrap(); + + // Open test database + let db: Db = sled::open("test_db_pr").unwrap(); + + // Delete all data in test DB + db.clear().unwrap(); + assert!(!db.contains_key("prs-to-process").unwrap()); + + let map_key_1 = format!( + "{}_{}_{}", + array_bytes::bytes2hex("", report_1.fingerprint), + &report_1.country, + &report_1.date + ); + let map_key_2 = format!( + "{}_{}_{}", + array_bytes::bytes2hex("", report_3.fingerprint), + &report_3.country, + &report_3.date + ); + + save_positive_report_to_process(&db, report_1); + let prs_to_process: BTreeMap> = + bincode::deserialize(&db.get("prs-to-process").unwrap().unwrap()).unwrap(); + let positive_reports = prs_to_process.get(&map_key_1).unwrap(); + assert_eq!(positive_reports.len(), 1); + assert!(!prs_to_process.contains_key(&map_key_2)); + + save_positive_report_to_process(&db, report_2); + let prs_to_process: BTreeMap> = + bincode::deserialize(&db.get("prs-to-process").unwrap().unwrap()).unwrap(); + let positive_reports = prs_to_process.get(&map_key_1).unwrap(); + assert_eq!(positive_reports.len(), 2); + assert!(!prs_to_process.contains_key(&map_key_2)); + + save_positive_report_to_process(&db, report_3); + let prs_to_process: BTreeMap> = + bincode::deserialize(&db.get("prs-to-process").unwrap().unwrap()).unwrap(); + // Check that this has not changed + let positive_reports = prs_to_process.get(&map_key_1).unwrap(); + assert_eq!(positive_reports.len(), 2); + // New report added to its own collection + let positive_reports = prs_to_process.get(&map_key_2).unwrap(); + assert_eq!(positive_reports.len(), 1); +}