diff --git a/Cargo.toml b/Cargo.toml index 3f1d797..12e7742 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,10 @@ array-bytes = "6.2.0" bincode = "1" curve25519-dalek = { version = "4", default-features = false, features = ["serde", "rand_core", "digest"] } ed25519-dalek = { version = "2", features = ["serde", "rand_core"] } +lazy_static = "1" lox-library = { git = "https://gitlab.torproject.org/vecna/lox.git", version = "0.1.0" } serde = "1.0.195" +serde_json = "1.0" serde_with = {version = "3.4.0", features = ["json"]} sha1 = "0.10" sha3 = "0.10" diff --git a/src/lib.rs b/src/lib.rs index 8d04f27..0c14a5d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,25 +1,35 @@ use curve25519_dalek::scalar::Scalar; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use lazy_static::lazy_static; use lox_library::bridge_table::{BridgeLine, MAX_BRIDGES_PER_BUCKET}; use lox_library::cred::Lox; -use lox_library::IssuerPubKey; use lox_library::proto::positive_report; +use lox_library::IssuerPubKey; +use serde::de::{self, Deserializer, MapAccess, SeqAccess, Unexpected, Visitor}; use serde::{Deserialize, Serialize}; use sha1::{Digest, Sha1}; use sha3::Sha3_256; +use std::collections::HashSet; +use std::fmt; +use std::option::Option; // for generating ed25519 keys during initial development use rand::rngs::OsRng; // TODO: These should be loaded from config file -pub const REQUIRE_BRIDGE_TOKEN: bool = true; +pub const REQUIRE_BRIDGE_TOKEN: bool = false; + +lazy_static! { + // known country codes, TODO: Verify these are correct + pub static ref COUNTRY_CODES: HashSet<&'static str> = HashSet::from(["ac","af","ax","al","dz","ad","ao","ai","aq","ag","ar","am","aw","au","at","az","bs","bh","bd","bb","by","be","bz","bj","bm","bt","bo","ba","bw","bv","br","io","vg","bn","bg","bf","bi","kh","cm","ca","cv","ky","cf","td","cl","cn","cx","cc","co","km","cg","cd","ck","cr","ci","hr","cu","cy","cz","dk","dj","dm","do","tp","ec","eg","sv","gq","ee","et","fk","fo","fj","fi","fr","fx","gf","pf","tf","ga","gm","ge","de","gh","gi","gr","gl","gd","gp","gu","gt","gn","gw","gy","ht","hm","hn","hk","hu","is","in","id","ir","iq","ie","im","il","it","jm","jp","jo","kz","ke","ki","kp","kr","kw","kg","la","lv","lb","ls","lr","ly","li","lt","lu","mo","mk","mg","mw","my","mv","ml","mt","mh","mq","mr","mu","yt","mx","fm","md","mc","mn","me","ms","ma","mz","mm","na","nr","np","an","nl","nc","nz","ni","ne","ng","nu","nf","mp","no","om","pk","pw","ps","pa","pg","py","pe","ph","pn","pl","pt","pr","qa","re","ro","ru","rw","ws","sm","st","sa","uk","sn","rs","sc","sl","sg","sk","si","sb","so","as","za","gs","su","es","lk","sh","kn","lc","pm","vc","sd","sr","sj","sz","se","ch","sy","tw","tj","tz","th","tg","tk","to","tt","tn","tr","tm","tc","tv","ug","ua","ae","gb","uk","us","um","uy","uz","vu","va","ve","vn","vi","wf","eh","ye","zm","zw"]); +} /// The minimum trust level a Lox credential must have to be allowed to /// submit a positive report pub const PR_MIN_TRUST_LEVEL: u32 = 3; /// Get Julian date -pub fn today() -> u32 { +pub fn get_date() -> u32 { time::OffsetDateTime::now_utc() .date() .to_julian_day() @@ -126,23 +136,23 @@ impl HashOfBucket { #[derive(Serialize, Deserialize)] pub enum Report { /// Negative report indicating user was unable to connect - NegativeUserReport(NegativeUserReport), + NegativeReport(NegativeReport), /// Positive report indicating user was able to connect - PositiveUserReport(PositiveUserReport), + PositiveReport(PositiveReport), } impl Report { fn verify(&self) -> bool { match self { - Report::NegativeUserReport(report) => report.verify(), - Report::PositiveUserReport(report) => report.verify(), + Report::NegativeReport(report) => report.verify(), + Report::PositiveReport(report) => report.verify(), } } } /// A report that the user was unable to connect to the bridge -#[derive(Serialize, Deserialize)] -pub struct NegativeUserReport { +#[derive(Serialize)] +pub struct NegativeReport { /// hashed fingerprint (SHA-1 hash of 20-byte bridge ID) pub fingerprint: [u8; 20], /// some way to prove knowledge of bridge @@ -150,55 +160,197 @@ pub struct NegativeUserReport { /// user's country code, may be an empty string pub country: String, /// today's Julian date - pub today: u32, + pub date: u32, } -impl NegativeUserReport { +// Ensure public fields are legal while deserializing +// Based on https://serde.rs/deserialize-struct.html +impl<'de> Deserialize<'de> for NegativeReport { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + enum Field { + Fingerprint, + BridgePOK, + Country, + Date, + } + + impl<'de> Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct FieldVisitor; + + impl<'de> Visitor<'de> for FieldVisitor { + type Value = Field; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("`fingerprint`, `bridge_pok`, `country`, or `date`") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "fingerprint" => Ok(Field::Fingerprint), + "bridge_pok" => Ok(Field::BridgePOK), + "country" => Ok(Field::Country), + "date" => Ok(Field::Date), + _ => Err(de::Error::unknown_field(value, FIELDS)), + } + } + } + + deserializer.deserialize_identifier(FieldVisitor) + } + } + + struct NegativeReportVisitor; + + impl<'de> de::Visitor<'de> for NegativeReportVisitor { + type Value = NegativeReport; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "negative report with valid country code and date no later than today", + ) + } + + fn visit_seq(self, mut seq: V) -> Result + where + V: SeqAccess<'de>, + { + let fingerprint = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + let bridge_pok = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &self))?; + let country = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(2, &self))?; + let date = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(3, &self))?; + Ok(NegativeReport { + fingerprint, + bridge_pok, + country, + date, + }) + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut fingerprint = None; + let mut bridge_pok = None; + let mut country: Option = None; + let mut date = None; + while let Some(key) = map.next_key()? { + match key { + Field::Fingerprint => { + if fingerprint.is_some() { + return Err(de::Error::duplicate_field("fingerprint")); + } + fingerprint = Some(map.next_value()?); + } + Field::BridgePOK => { + if bridge_pok.is_some() { + return Err(de::Error::duplicate_field("bridge_pok")); + } + bridge_pok = Some(map.next_value()?); + } + Field::Country => { + if country.is_some() { + return Err(de::Error::duplicate_field("country")); + } + country = Some(map.next_value()?); + } + Field::Date => { + if date.is_some() { + return Err(de::Error::duplicate_field("date")); + } + date = Some(map.next_value()?); + } + } + } + let fingerprint = + fingerprint.ok_or_else(|| de::Error::missing_field("fingerprint"))?; + let bridge_pok = + bridge_pok.ok_or_else(|| de::Error::missing_field("bridge_pok"))?; + let country = country.ok_or_else(|| de::Error::missing_field("country"))?; + if country != "" && !COUNTRY_CODES.contains(country.as_str()) { + return Err(de::Error::invalid_value( + Unexpected::Str(&country), + &"a country code or empty string", + )); + } + let date = date.ok_or_else(|| de::Error::missing_field("date"))?; + if date > get_date().into() { + return Err(de::Error::invalid_value( + Unexpected::Unsigned(date), + &"report date no later than today", + )); + } + Ok(NegativeReport { + fingerprint: fingerprint, + bridge_pok: bridge_pok, + country: country.to_string(), + date: date.try_into().unwrap(), + }) + } + } + const FIELDS: &'static [&'static str] = &["fingerprint", "bridge_pok", "country", "date"]; + deserializer.deserialize_struct("NegativeReport", FIELDS, NegativeReportVisitor) + } +} + +impl NegativeReport { pub fn new(bridge_id: [u8; 20], bridge_pok: ProofOfBridgeKnowledge, country: String) -> Self { let mut hasher = Sha1::new(); hasher.update(bridge_id); let fingerprint: [u8; 20] = hasher.finalize().into(); - let today = today(); + let date = get_date(); Self { fingerprint, bridge_pok, country, - today, + date, } } - pub fn from_bridgeline( - &self, - bridge_id: [u8; 20], - bridgeline: BridgeLine, - country: String, - ) -> Self { + pub fn from_bridgeline(bridge_id: [u8; 20], bridgeline: BridgeLine, country: String) -> Self { let bridge_pok = ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(bridgeline)); - NegativeUserReport::new(bridge_id, bridge_pok, country) + NegativeReport::new(bridge_id, bridge_pok, country) } - pub fn from_bucket(&self, bridge_id: [u8; 20], bucket: Scalar, country: String) -> Self { + pub fn from_bucket(bridge_id: [u8; 20], bucket: Scalar, country: String) -> Self { let mut hasher = Sha3_256::new(); hasher.update(bucket.to_bytes()); let bucket_hash: [u8; 32] = hasher.finalize().into(); let bridge_pok = ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket { hash: bucket_hash }); - NegativeUserReport::new(bridge_id, bridge_pok, country) + NegativeReport::new(bridge_id, bridge_pok, country) } - pub fn from_lox_credential(&self, bridge_id: [u8; 20], cred: Lox, country: String) -> Self { - self.from_bucket(bridge_id, cred.bucket, country) + pub fn from_lox_credential(bridge_id: [u8; 20], cred: Lox, country: String) -> Self { + NegativeReport::from_bucket(bridge_id, cred.bucket, country) } fn verify(&self) -> bool { - // possibly include check that self.today is recent as well - self.today <= today() && self.bridge_pok.verify(self.fingerprint) + self.bridge_pok.verify(self.fingerprint) } } /// A report that the user was able to connect to the bridge -#[derive(Serialize, Deserialize)] -pub struct PositiveUserReport { +#[derive(Serialize)] +pub struct PositiveReport { /// hashed fingerprint (SHA-1 hash of 20-byte bridge ID) pub fingerprint: [u8; 20], /// token from the bridge indicating it was reached @@ -208,40 +360,229 @@ pub struct PositiveUserReport { /// user's country code, may be an empty string pub country: String, /// today's Julian date - pub today: u32, + pub date: u32, } -impl PositiveUserReport { - pub fn new(bridge_id: [u8; 20], bridge_token: Option, lox_proof: positive_report::Request, country: String) -> Self { +// Ensure public fields are legal while deserializing +// Based on https://serde.rs/deserialize-struct.html +impl<'de> Deserialize<'de> for PositiveReport { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + enum Field { + Fingerprint, + BridgeToken, + LoxProof, + Country, + Date, + } + + impl<'de> Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct FieldVisitor; + + impl<'de> Visitor<'de> for FieldVisitor { + type Value = Field; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "`fingerprint`, `bridge_token`, `lox_proof`, `country`, or `date`", + ) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "fingerprint" => Ok(Field::Fingerprint), + "bridge_token" => Ok(Field::BridgeToken), + "lox_proof" => Ok(Field::LoxProof), + "country" => Ok(Field::Country), + "date" => Ok(Field::Date), + _ => Err(de::Error::unknown_field(value, FIELDS)), + } + } + } + + deserializer.deserialize_identifier(FieldVisitor) + } + } + + struct PositiveReportVisitor; + + impl<'de> de::Visitor<'de> for PositiveReportVisitor { + type Value = PositiveReport; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "positive report with valid country code and date no later than today", + ) + } + + fn visit_seq(self, mut seq: V) -> Result + where + V: SeqAccess<'de>, + { + let fingerprint = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + let bridge_token = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &self))?; + let lox_proof = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(2, &self))?; + let country = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(3, &self))?; + let date = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(4, &self))?; + Ok(PositiveReport { + fingerprint, + bridge_token, + lox_proof, + country, + date, + }) + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut fingerprint = None; + let mut bridge_token: Option> = None; + let mut lox_proof = None; + let mut country: Option = None; + let mut date = None; + while let Some(key) = map.next_key()? { + match key { + Field::Fingerprint => { + if fingerprint.is_some() { + return Err(de::Error::duplicate_field("fingerprint")); + } + fingerprint = Some(map.next_value()?); + } + Field::BridgeToken => { + if bridge_token.is_some() { + return Err(de::Error::duplicate_field("bridge_token")); + } + bridge_token = Some(map.next_value()?); + } + Field::LoxProof => { + if lox_proof.is_some() { + return Err(de::Error::duplicate_field("lox_proof")); + } + lox_proof = Some(map.next_value()?); + } + Field::Country => { + if country.is_some() { + return Err(de::Error::duplicate_field("country")); + } + country = Some(map.next_value()?); + } + Field::Date => { + if date.is_some() { + return Err(de::Error::duplicate_field("date")); + } + date = Some(map.next_value()?); + } + } + } + let fingerprint = + fingerprint.ok_or_else(|| de::Error::missing_field("fingerprint"))?; + let bridge_token = + bridge_token.ok_or_else(|| de::Error::missing_field("bridge_token"))?; + if REQUIRE_BRIDGE_TOKEN && bridge_token.is_none() { + return Err(de::Error::invalid_value( + Unexpected::Option, + &"a bridge token (mandatory, per system configuration)", + )); + } + let lox_proof = lox_proof.ok_or_else(|| de::Error::missing_field("lox_proof"))?; + let country = country.ok_or_else(|| de::Error::missing_field("country"))?; + if country != "" && !COUNTRY_CODES.contains(country.as_str()) { + return Err(de::Error::invalid_value( + Unexpected::Str(&country), + &"a country code or empty string", + )); + } + let date = date.ok_or_else(|| de::Error::missing_field("date"))?; + if date > get_date().into() { + return Err(de::Error::invalid_value( + Unexpected::Unsigned(date), + &"report date no later than today", + )); + } + Ok(PositiveReport { + fingerprint: fingerprint, + bridge_token: bridge_token, + lox_proof: lox_proof, + country: country.to_string(), + date: date.try_into().unwrap(), + }) + } + } + const FIELDS: &'static [&'static str] = &[ + "fingerprint", + "bridge_token", + "lox_proof", + "country", + "date", + ]; + deserializer.deserialize_struct("PositiveReport", FIELDS, PositiveReportVisitor) + } +} + +impl PositiveReport { + pub fn new( + bridge_id: [u8; 20], + bridge_token: Option, + lox_proof: positive_report::Request, + country: String, + ) -> Self { + if REQUIRE_BRIDGE_TOKEN && bridge_token.is_none() { + panic!("Bridge tokens are required for positive reports."); + } let mut hasher = Sha1::new(); hasher.update(bridge_id); let fingerprint: [u8; 20] = hasher.finalize().into(); - let today = today(); + let date = get_date(); Self { fingerprint, bridge_token, lox_proof, country, - today, + date, } } - pub fn from_lox_credential(bridge_id: [u8; 20], bridge_token: Option, lox_cred: &Lox, lox_pub: &IssuerPubKey, country: String) -> Self { + pub fn from_lox_credential( + bridge_id: [u8; 20], + bridge_token: Option, + lox_cred: &Lox, + lox_pub: &IssuerPubKey, + country: String, + ) -> Self { let lox_proof = positive_report::request(lox_cred, lox_pub).unwrap(); - PositiveUserReport::new(bridge_id, bridge_token, lox_proof, country) + PositiveReport::new(bridge_id, bridge_token, lox_proof, country) } fn verify(&self) -> bool { - // possibly include check that self.today is recent as well - self.today <= today() - && (!REQUIRE_BRIDGE_TOKEN || { - if self.bridge_token.is_none() { - false - } else { - let bt = self.bridge_token.as_ref().unwrap(); - self.today == bt.unsigned_bridge_token.today && bt.verify() - } - }) + !REQUIRE_BRIDGE_TOKEN || { + if self.bridge_token.is_none() { + false + } else { + let bt = self.bridge_token.as_ref().unwrap(); + bt.verify() + } + } } } @@ -253,7 +594,7 @@ pub struct UnsignedBridgeToken { /// client's country code pub country: String, /// today's Julian date - pub today: u32, + pub date: u32, } impl UnsignedBridgeToken { @@ -261,11 +602,11 @@ impl UnsignedBridgeToken { let mut hasher = Sha1::new(); hasher.update(bridge_id); let fingerprint: [u8; 20] = hasher.finalize().into(); - let today = today(); + let date = get_date(); Self { fingerprint, country, - today, + date, } } } @@ -290,7 +631,7 @@ impl BridgeToken { pub fn verify(&self) -> bool { let pubkey = get_bridge_signing_pubkey(&self.unsigned_bridge_token.fingerprint); - self.unsigned_bridge_token.today <= today() + self.unsigned_bridge_token.date <= get_date() && pubkey .verify( &bincode::serialize(&self.unsigned_bridge_token).unwrap(),