From c82e604e3df7ffd801f58fd71d4ed22462e9f9d7 Mon Sep 17 00:00:00 2001 From: Vecna Date: Fri, 12 Apr 2024 02:38:35 -0400 Subject: [PATCH] Include nonce in negative reports --- Cargo.toml | 2 +- src/lib.rs | 42 +++++++++------------ src/negative_report.rs | 58 +++++++++++++++++++++++------ src/positive_report.rs | 10 ++++- src/tests.rs | 83 ++++++++++++++++++++++++++++++++++++++---- 5 files changed, 148 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index badb170..f348a3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ hyper-util = { version = "0.1", features = ["full"] } julianday = "1.2.0" lazy_static = "1" lox-library = { git = "https://gitlab.torproject.org/vecna/lox.git", version = "0.1.0" } +rand = { version = "0.8" } #select = "0.6.0" serde = "1.0.197" serde_json = "1.0" @@ -34,4 +35,3 @@ tokio-cron = "0.1.2" [dev-dependencies] base64 = "0.21.7" -rand = "0.8.5" diff --git a/src/lib.rs b/src/lib.rs index 699eae5..33ec69f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,9 @@ lazy_static! { pub static ref COUNTRY_CODES: HashSet<&'static str> = HashSet::from(["??","ad","ae","af","ag","ai","al","am","ao","ap","aq","ar","as","at","au","aw","ax","az","ba","bb","bd","be","bf","bg","bh","bi","bj","bl","bm","bn","bo","bq","br","bs","bt","bv","bw","by","bz","ca","cc","cd","cf","cg","ch","ci","ck","cl","cm","cn","co","cr","cs","cu","cv","cw","cx","cy","cz","de","dj","dk","dm","do","dz","ec","ee","eg","eh","er","es","et","eu","fi","fj","fk","fm","fo","fr","ga","gb","gd","ge","gf","gg","gh","gi","gl","gm","gn","gp","gq","gr","gs","gt","gu","gw","gy","hk","hm","hn","hr","ht","hu","id","ie","il","im","in","io","iq","ir","is","it","je","jm","jo","jp","ke","kg","kh","ki","km","kn","kp","kr","kw","ky","kz","la","lb","lc","li","lk","lr","ls","lt","lu","lv","ly","ma","mc","md","me","mf","mg","mh","mk","ml","mm","mn","mo","mp","mq","mr","ms","mt","mu","mv","mw","mx","my","mz","na","nc","ne","nf","ng","ni","nl","no","np","nr","nu","nz","om","pa","pe","pf","pg","ph","pk","pl","pm","pn","pr","ps","pt","pw","py","qa","re","ro","rs","ru","rw","sa","sb","sc","sd","se","sg","sh","si","sj","sk","sl","sm","sn","so","sr","ss","st","sv","sx","sy","sz","tc","td","tf","tg","th","tj","tk","tl","tm","tn","to","tr","tt","tv","tw","tz","ua","ug","um","us","uy","uz","va","vc","ve","vg","vi","vn","vu","wf","ws","ye","yt","za","zm","zw"]); } +/// We will accept reports up to this many days old. +pub const MAX_BACKDATE: u32 = 3; + /// Get Julian date pub fn get_date() -> u32 { time::OffsetDateTime::now_utc() @@ -315,14 +318,12 @@ pub async fn update_extra_infos( // Process negative reports -/// Negative reports can be deduplicated, so we store to-be-processed -/// negative reports as a map of [report] to [count of report]. Add this -/// NR to that map (or create a new map if necessary). +/// We store to-be-processed negative reports as a vector. Add this NR +/// to that vector (or create a new vector if necessary) pub fn save_negative_report_to_process(db: &Db, nr: NegativeReport) { - // We serialize the negative reports as strings to use them as map keys. let mut reports = match db.get("nrs-to-process").unwrap() { Some(v) => bincode::deserialize(&v).unwrap(), - None => BTreeMap::>::new(), + None => BTreeMap::>::new(), }; // Store to-be-processed reports with key [fingerprint]_[country]_[date] let map_key = format!( @@ -331,19 +332,15 @@ pub fn save_negative_report_to_process(db: &Db, nr: NegativeReport) { &nr.country, &nr.date, ); - let serialized_nr = nr.to_json(); if reports.contains_key(&map_key) { - let nr_map = reports.get_mut(&map_key).unwrap(); - if nr_map.contains_key(&serialized_nr) { - let prev_count = nr_map.get(&serialized_nr).unwrap(); - nr_map.insert(serialized_nr, prev_count + 1); - } else { - nr_map.insert(serialized_nr, 1); - } + reports + .get_mut(&map_key) + .unwrap() + .push(nr.to_serializable_report()); } else { - let mut nr_map = BTreeMap::::new(); - nr_map.insert(serialized_nr, 1); - reports.insert(map_key, nr_map); + let mut nrs = Vec::::new(); + nrs.push(nr.to_serializable_report()); + reports.insert(map_key, nrs); } // Commit changes to database db.insert("nrs-to-process", bincode::serialize(&reports).unwrap()) @@ -356,15 +353,14 @@ pub fn save_negative_report_to_process(db: &Db, nr: NegativeReport) { /// distributor. pub async fn verify_negative_reports( distributors: &BTreeMap, - reports: &BTreeMap, + reports: &Vec, ) -> u32 { // Don't make a network call if we don't have any reports anyway if reports.is_empty() { return 0; } // Get one report, assume the rest have the same distributor - let first_report: SerializableNegativeReport = - serde_json::from_str(reports.first_key_value().unwrap().0).unwrap(); + let first_report = &reports[0]; let distributor = first_report.distributor; let client = Client::new(); let uri: String = (distributors.get(&distributor).unwrap().to_owned() + "/verifynegative") @@ -385,17 +381,16 @@ pub async fn verify_negative_reports( pub async fn update_negative_reports(db: &Db, distributors: &BTreeMap) { let mut all_negative_reports = match db.get("nrs-to-process").unwrap() { Some(v) => bincode::deserialize(&v).unwrap(), - None => BTreeMap::>::new(), + None => BTreeMap::>::new(), }; // Key is [fingerprint]_[country]_[date] for bridge_country_date in all_negative_reports.keys() { let reports = all_negative_reports.get(bridge_country_date).unwrap(); if !reports.is_empty() { - let first_report: SerializableNegativeReport = - serde_json::from_str(reports.first_key_value().unwrap().0).unwrap(); + let first_report = &reports[0]; let fingerprint = first_report.fingerprint; let date = first_report.date; - let country = first_report.country; + let country = first_report.country.clone(); let count_valid = verify_negative_reports(&distributors, reports).await; // Get bridge info or make new one @@ -421,7 +416,6 @@ pub async fn update_negative_reports(db: &Db, distributors: &BTreeMap Self { let mut hasher = Sha1::new(); @@ -52,6 +58,7 @@ impl NegativeReport { bridge_pok, country, date, + nonce, distributor, } } @@ -62,24 +69,42 @@ impl NegativeReport { distributor: BridgeDistributor, ) -> Self { let date = get_date(); - let bridge_pok = - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridgeline, date)); - NegativeReport::new( + let mut rng = rand::thread_rng(); + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + let bridge_pok = ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new( + &bridgeline, + date, + nonce, + )); + Self::new( bridgeline.fingerprint, bridge_pok, country, date, + nonce, distributor, ) } pub fn from_lox_bucket(bridge_id: [u8; 20], bucket: Scalar, country: String) -> Self { let date = get_date(); - let bridge_pok = ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&bucket, date)); - NegativeReport::new(bridge_id, bridge_pok, country, date, BridgeDistributor::Lox) + let mut rng = rand::thread_rng(); + let mut nonce = [0; 32]; + rng.fill_bytes(&mut nonce); + let bridge_pok = + ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&bucket, date, nonce)); + Self::new( + bridge_id, + bridge_pok, + country, + date, + nonce, + BridgeDistributor::Lox, + ) } - pub fn from_lox_credential(bridge_id: [u8; 20], cred: Lox, country: String) -> Self { + pub fn from_lox_credential(bridge_id: [u8; 20], cred: &Lox, country: String) -> Self { NegativeReport::from_lox_bucket(bridge_id, cred.bucket, country) } @@ -90,6 +115,7 @@ impl NegativeReport { bridge_pok: self.bridge_pok, country: self.country, date: self.date, + nonce: self.nonce, distributor: self.distributor, } } @@ -119,12 +145,12 @@ impl NegativeReport { pub fn verify(self, bridge_info: &BridgeVerificationInfo) -> bool { match self.bridge_pok { ProofOfBridgeKnowledge::HashOfBridgeLine(pok) => { - let hash = HashOfBridgeLine::new(&bridge_info.bridge_line, self.date); + let hash = HashOfBridgeLine::new(&bridge_info.bridge_line, self.date, self.nonce); hash == pok } ProofOfBridgeKnowledge::HashOfBucket(pok) => { for b in &bridge_info.buckets { - let hash = HashOfBucket::new(&b, self.date); + let hash = HashOfBucket::new(&b, self.date, self.nonce); if hash == pok { return true; } @@ -143,6 +169,7 @@ pub struct SerializableNegativeReport { bridge_pok: ProofOfBridgeKnowledge, pub country: String, pub date: u32, + pub nonce: [u8; 32], pub distributor: BridgeDistributor, } @@ -154,14 +181,19 @@ impl SerializableNegativeReport { if !COUNTRY_CODES.contains(self.country.as_str()) { return Err(NegativeReportError::InvalidCountryCode); } - if self.date > get_date().into() { + let date = get_date(); + if self.date > date { return Err(NegativeReportError::DateInFuture); } + if self.date < date - MAX_BACKDATE { + return Err(NegativeReportError::DateInPast); + } Ok(NegativeReport { fingerprint: self.fingerprint, bridge_pok: self.bridge_pok, country: self.country.to_string(), date: self.date.try_into().unwrap(), + nonce: self.nonce, distributor: self.distributor, }) } @@ -184,9 +216,10 @@ pub struct HashOfBridgeLine { } impl HashOfBridgeLine { - pub fn new(bl: &BridgeLine, date: u32) -> Self { + pub fn new(bl: &BridgeLine, date: u32, nonce: [u8; 32]) -> Self { let mut hasher = Sha3_256::new(); hasher.update(date.to_le_bytes()); + hasher.update(nonce); hasher.update(bincode::serialize(&bl).unwrap()); let hash: [u8; 32] = hasher.finalize().into(); Self { hash } @@ -200,9 +233,10 @@ pub struct HashOfBucket { } impl HashOfBucket { - pub fn new(bucket: &Scalar, date: u32) -> Self { + pub fn new(bucket: &Scalar, date: u32, nonce: [u8; 32]) -> Self { let mut hasher = Sha3_256::new(); hasher.update(date.to_le_bytes()); + hasher.update(nonce); hasher.update(bucket.to_bytes()); let hash: [u8; 32] = hasher.finalize().into(); Self { hash } diff --git a/src/positive_report.rs b/src/positive_report.rs index 9d78dd8..a7c4c3a 100644 --- a/src/positive_report.rs +++ b/src/positive_report.rs @@ -1,7 +1,9 @@ // For Lox-related code where points are uppercase and scalars are lowercase #![allow(non_snake_case)] -use crate::{bridge_verification_info::BridgeVerificationInfo, get_date, COUNTRY_CODES}; +use crate::{ + bridge_verification_info::BridgeVerificationInfo, get_date, COUNTRY_CODES, MAX_BACKDATE, +}; use curve25519_dalek::ristretto::RistrettoBasepointTable; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier}; @@ -15,6 +17,7 @@ pub const REQUIRE_BRIDGE_TOKEN: bool = false; #[derive(Debug, Serialize)] pub enum PositiveReportError { DateInFuture, + DateInPast, // report is more than MAX_BACKDATE days old FailedToDeserialize, // couldn't deserialize to SerializablePositiveReport InvalidBridgeToken, InvalidCountryCode, @@ -180,10 +183,13 @@ impl SerializablePositiveReport { if !COUNTRY_CODES.contains(self.country.as_str()) { return Err(PositiveReportError::InvalidCountryCode); } - let date: u32 = get_date().into(); + let date: u32 = get_date(); if self.date > date { return Err(PositiveReportError::DateInFuture); } + if self.date < date - MAX_BACKDATE { + return Err(PositiveReportError::DateInPast); + } if self.lox_proof.date != date { return Err(PositiveReportError::InvalidLoxProof); } diff --git a/src/tests.rs b/src/tests.rs index 59d77ed..8787b14 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -210,12 +210,64 @@ fn test_negative_reports() { 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()); + 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 @@ -225,41 +277,56 @@ fn test_negative_reports() { .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_2 = - NegativeReport::from_bridgeline(bridges[1], "xx".to_string(), BridgeDistributor::Lox) + 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 invalid_report_3 = NegativeReport::new( + 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 invalid_report_4 = NegativeReport::new( + 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)), + ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket::new(&Scalar::ZERO, date, nonce)), "ru".to_string(), date, + nonce, BridgeDistributor::Lox, ); - assert!(!invalid_report_3.verify(&bridge_info_1)); - assert!(!invalid_report_4.verify(&bridge_info_2)); + assert!(!invalid_report_4.verify(&bridge_info_1)); + assert!(!invalid_report_5.verify(&bridge_info_2)); } #[test]