Code for processing extra-infos files
This commit is contained in:
parent
4a56229c5d
commit
08cfacbf85
11
Cargo.toml
11
Cargo.toml
|
@ -8,16 +8,27 @@ edition = "2021"
|
|||
[dependencies]
|
||||
array-bytes = "6.2.0"
|
||||
bincode = "1"
|
||||
chrono = "0.4"
|
||||
clap = { version = "4.4.14", features = ["derive"] }
|
||||
curve25519-dalek = { version = "4", default-features = false, features = ["serde", "rand_core", "digest"] }
|
||||
ed25519-dalek = { version = "2", features = ["serde", "rand_core"] }
|
||||
http-body-util = "0.1"
|
||||
hyper = { version = "1", features = ["full"] }
|
||||
hyper-rustls = "0.26.0"
|
||||
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" }
|
||||
#scraper = "0.18"
|
||||
select = "0.6.0"
|
||||
serde = "1.0.195"
|
||||
serde_json = "1.0"
|
||||
serde_with = {version = "3.4.0", features = ["json"]}
|
||||
sha1 = "0.10"
|
||||
sha3 = "0.10"
|
||||
sled = "0.34.7"
|
||||
time = "0.3.30"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# probably not needed once I can query an API
|
||||
rand = { version = "0.8", features = ["std_rng"]}
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
/*! Fields we need from the extra-info documents for bridges...
|
||||
Note, this is NOT a complete implementation of the document format.
|
||||
(https://spec.torproject.org/dir-spec/extra-info-document-format.html) */
|
||||
|
||||
use chrono::DateTime;
|
||||
use http_body_util::{BodyExt, Empty};
|
||||
use hyper::body::Bytes;
|
||||
use hyper_util::{client::legacy::Client, rt::TokioExecutor};
|
||||
use julianday::JulianDay;
|
||||
use select::{document::Document, predicate::Name};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
fs::File,
|
||||
io::{prelude::*, BufReader, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// Directory where we store these files
|
||||
pub const DIRECTORY: &str = "extra_infos";
|
||||
|
||||
/// Fields we need from extra-info document
|
||||
#[derive(Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct ExtraInfo {
|
||||
/// Bridge nickname, probably unused
|
||||
pub nickname: String,
|
||||
/// Bridge fingerprint, a SHA-1 hash of the bridge ID
|
||||
pub fingerprint: [u8; 20],
|
||||
/// Date (in UTC) that this document was published, stored as a Julian
|
||||
/// date because we don't need to know more precisely than the day.
|
||||
pub published: u32,
|
||||
/// Map of country codes and how many users (rounded up to a multiple of
|
||||
/// 8) have connected to that bridge during the day.
|
||||
/// Uses BTreeMap instead of HashMap so ExtraInfo can implement Hash.
|
||||
pub bridge_ips: BTreeMap<String, u32>, // TODO: What size for count?
|
||||
}
|
||||
|
||||
fn get_extra_info_or_error(entry: &HashMap<String, String>) -> Result<ExtraInfo, String> {
|
||||
if !entry.contains_key("nickname") || !entry.contains_key("fingerprint") {
|
||||
// How did we get here??
|
||||
return Err("Cannot parse extra-info: Missing nickname or fingerprint".to_string());
|
||||
}
|
||||
if !entry.contains_key("published") || !entry.contains_key("bridge-ips") {
|
||||
// Some extra-infos are missing data on connecting IPs...
|
||||
// But we can't do anything in that case.
|
||||
return Err(format!(
|
||||
"Failed to parse extra-info for {} {}",
|
||||
entry.get("nickname").unwrap(),
|
||||
entry.get("fingerprint").unwrap()
|
||||
));
|
||||
}
|
||||
let nickname = entry.get("nickname").unwrap().to_string();
|
||||
let fingerprint_str = entry.get("fingerprint").unwrap();
|
||||
if fingerprint_str.len() != 40 {
|
||||
return Err("Fingerprint must be 20 bytes".to_string());
|
||||
}
|
||||
let fingerprint = array_bytes::hex2array(fingerprint_str).unwrap();
|
||||
let published: u32 = JulianDay::from(
|
||||
DateTime::parse_from_str(
|
||||
&(entry.get("published").unwrap().to_owned() + " +0000"),
|
||||
"%F %T %z",
|
||||
)
|
||||
.unwrap()
|
||||
.date_naive(),
|
||||
)
|
||||
.inner()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let bridge_ips_str = entry.get("bridge-ips").unwrap();
|
||||
let mut bridge_ips: BTreeMap<String, u32> = BTreeMap::new();
|
||||
let countries: Vec<&str> = bridge_ips_str.split(',').collect();
|
||||
for country in countries {
|
||||
if country != "" {
|
||||
// bridge-ips may be empty
|
||||
let (cc, count) = country.split_once('=').unwrap();
|
||||
bridge_ips.insert(cc.to_string(), count.parse::<u32>().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExtraInfo {
|
||||
nickname,
|
||||
fingerprint,
|
||||
published,
|
||||
bridge_ips,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_extra_infos<'a>(filename: &str, set: &mut HashSet<ExtraInfo>) {
|
||||
let infile = File::open(format!("{}/{}", DIRECTORY, filename)).unwrap();
|
||||
let reader = BufReader::new(infile);
|
||||
|
||||
let mut entry = HashMap::<String, String>::new();
|
||||
for line in reader.lines() {
|
||||
let line = line.unwrap();
|
||||
if line.starts_with("@type bridge-extra-info ") {
|
||||
if !entry.is_empty() {
|
||||
let extra_info = get_extra_info_or_error(&entry);
|
||||
if extra_info.is_ok() {
|
||||
set.insert(extra_info.unwrap());
|
||||
} else {
|
||||
// Just print the error and continue.
|
||||
println!("{}", extra_info.err().unwrap());
|
||||
}
|
||||
entry = HashMap::<String, String>::new();
|
||||
}
|
||||
} else {
|
||||
if line.starts_with("extra-info ") {
|
||||
// extra-info line has format:
|
||||
// extra-info <nickname> <fingerprint>
|
||||
let line_split: Vec<&str> = line.split(' ').collect();
|
||||
if line_split.len() != 3 {
|
||||
println!("Misformed extra-info line");
|
||||
} else {
|
||||
entry.insert("nickname".to_string(), line_split[1].to_string());
|
||||
entry.insert("fingerprint".to_string(), line_split[2].to_string());
|
||||
}
|
||||
} else {
|
||||
let (key, value) = match line.split_once(' ') {
|
||||
Some((k, v)) => (k, v),
|
||||
None => (line.as_str(), ""),
|
||||
};
|
||||
entry.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Do for the last one
|
||||
let extra_info = get_extra_info_or_error(&entry);
|
||||
if extra_info.is_ok() {
|
||||
set.insert(extra_info.unwrap());
|
||||
} else {
|
||||
println!("{}", extra_info.err().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
/// Download new extra-infos files and save them in DIRECTORY. This function
|
||||
/// returns the set of newly downloaded filenames.
|
||||
pub async fn download_extra_infos(
|
||||
) -> Result<HashSet<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Download directory of recent extra-infos
|
||||
let base_url = "https://collector.torproject.org/recent/bridge-descriptors/extra-infos/";
|
||||
let url = base_url.parse().unwrap();
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
.with_native_roots() // TODO: Pin certificate? Is this data signed/verifiable?
|
||||
.expect("no native root CA certificates found")
|
||||
.https_only()
|
||||
.enable_http1()
|
||||
.build();
|
||||
|
||||
let client: Client<_, Empty<Bytes>> = Client::builder(TokioExecutor::new()).build(https);
|
||||
|
||||
println!("Downloading {}", base_url);
|
||||
let mut res = client.get(url).await?;
|
||||
|
||||
assert_eq!(res.status(), hyper::StatusCode::OK);
|
||||
let mut body_str = String::from("");
|
||||
while let Some(next) = res.frame().await {
|
||||
let frame = next?;
|
||||
if let Some(chunk) = frame.data_ref() {
|
||||
body_str.push_str(&String::from_utf8(chunk.to_vec())?);
|
||||
}
|
||||
}
|
||||
|
||||
let doc = Document::from(body_str.as_str());
|
||||
|
||||
// Create extra-infos directory if it doesn't exist
|
||||
std::fs::create_dir_all(&DIRECTORY)?;
|
||||
|
||||
let mut new_files = HashSet::<String>::new();
|
||||
|
||||
// Go through all the links in the page and download new files
|
||||
let links = doc.find(Name("a")).filter_map(|n| n.attr("href"));
|
||||
for link in links {
|
||||
if link.ends_with("-extra-infos") {
|
||||
let filename = format!("{}/{}", DIRECTORY, link);
|
||||
|
||||
// Download file if it's not already downloaded
|
||||
if !Path::new(&filename).exists() {
|
||||
let extra_infos_url = format!("{}{}", base_url, link);
|
||||
println!("Downloading {}", extra_infos_url);
|
||||
let mut res = client.get(extra_infos_url.parse().unwrap()).await?;
|
||||
assert_eq!(res.status(), hyper::StatusCode::OK);
|
||||
let mut file = std::fs::File::create(filename).unwrap();
|
||||
while let Some(next) = res.frame().await {
|
||||
let frame = next?;
|
||||
if let Some(chunk) = frame.data_ref() {
|
||||
file.write_all(&chunk)?;
|
||||
}
|
||||
}
|
||||
new_files.insert(link.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(new_files)
|
||||
}
|
762
src/lib.rs
762
src/lib.rs
|
@ -1,32 +1,53 @@
|
|||
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::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;
|
||||
use sled::Db;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
fmt,
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
};
|
||||
|
||||
// for generating ed25519 keys during initial development
|
||||
use rand::rngs::OsRng;
|
||||
pub mod extra_info;
|
||||
pub mod negative_report;
|
||||
pub mod positive_report;
|
||||
|
||||
// TODO: These should be loaded from config file
|
||||
pub const REQUIRE_BRIDGE_TOKEN: bool = false;
|
||||
use extra_info::*;
|
||||
use negative_report::*;
|
||||
use positive_report::*;
|
||||
|
||||
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"]);
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub db: DbConfig,
|
||||
require_bridge_token: bool,
|
||||
}
|
||||
|
||||
/// 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;
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DbConfig {
|
||||
// The path for the server database, default is "server_db"
|
||||
pub db_path: String,
|
||||
}
|
||||
|
||||
impl Default for DbConfig {
|
||||
fn default() -> DbConfig {
|
||||
DbConfig {
|
||||
db_path: "server_db".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
// known country codes based on Tor geoIP database
|
||||
// Produced with `cat /usr/share/tor/geoip{,6} | grep -v ^# | grep -o ..$ | sort | uniq | tr '[:upper:]' '[:lower:]' | tr '\n' ',' | sed 's/,/","/g'`
|
||||
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"]);
|
||||
|
||||
// read config data at run time
|
||||
pub static ref CONFIG: Config = serde_json::from_reader(
|
||||
BufReader::new(
|
||||
File::open("config.json").expect("Could not read config file") // TODO: Make config filename configurable
|
||||
)
|
||||
).expect("Reading config file from JSON failed");
|
||||
}
|
||||
|
||||
/// Get Julian date
|
||||
pub fn get_date() -> u32 {
|
||||
|
@ -37,606 +58,147 @@ pub fn get_date() -> u32 {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
/// Get bridge line for a bridge, requires some oracle
|
||||
pub fn get_bridge_line(fingerprint: &[u8; 20]) -> BridgeLine {
|
||||
// TODO
|
||||
// for now just return empty bridgeline
|
||||
BridgeLine::default()
|
||||
}
|
||||
|
||||
/// Get verifying key for a bridge, requires some oracle
|
||||
pub fn get_bridge_signing_pubkey(fingerprint: &[u8; 20]) -> VerifyingKey {
|
||||
// TODO
|
||||
// for now just return new pubkey
|
||||
let mut csprng = OsRng {};
|
||||
let keypair = SigningKey::generate(&mut csprng);
|
||||
keypair.verifying_key()
|
||||
}
|
||||
|
||||
/// Get bucket from hash of bucket ID, requires some oracle
|
||||
pub fn get_bucket(beta_hash: &[u8; 32]) -> [BridgeLine; MAX_BRIDGES_PER_BUCKET] {
|
||||
// TODO
|
||||
// for now just return bucket of empty bridgelines
|
||||
[
|
||||
BridgeLine::default(),
|
||||
BridgeLine::default(),
|
||||
BridgeLine::default(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Proof that the user knows (and should be able to access) a given bridge
|
||||
/// All the info for a bridge, to be stored in the database
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum ProofOfBridgeKnowledge {
|
||||
/// Hash of bridge line as proof of knowledge of bridge line
|
||||
HashOfBridgeLine(HashOfBridgeLine),
|
||||
/// Hash of bucket ID for Lox user
|
||||
HashOfBucket(HashOfBucket),
|
||||
}
|
||||
|
||||
impl ProofOfBridgeKnowledge {
|
||||
pub fn verify(&self, bridge_fingerprint: [u8; 20]) -> bool {
|
||||
// TODO: It seems like there ought to be a cleaner way to do this?
|
||||
match self {
|
||||
ProofOfBridgeKnowledge::HashOfBridgeLine(bl_hash) => bl_hash.verify(bridge_fingerprint),
|
||||
ProofOfBridgeKnowledge::HashOfBucket(bucket_hash) => {
|
||||
bucket_hash.verify(bridge_fingerprint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash of bridge line to prove knowledge of that bridge
|
||||
#[derive(PartialEq, Serialize, Deserialize)]
|
||||
pub struct HashOfBridgeLine {
|
||||
hash: [u8; 32],
|
||||
}
|
||||
|
||||
impl HashOfBridgeLine {
|
||||
pub fn new(bl: BridgeLine) -> Self {
|
||||
let mut hasher = Sha3_256::new();
|
||||
hasher.update(bincode::serialize(&bl).unwrap());
|
||||
let hash: [u8; 32] = hasher.finalize().into();
|
||||
Self { hash }
|
||||
}
|
||||
|
||||
pub fn verify(&self, bridge_fingerprint: [u8; 20]) -> bool {
|
||||
let bl = get_bridge_line(&bridge_fingerprint);
|
||||
self == &HashOfBridgeLine::new(bl)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash of bucket ID to prove knowledge of bridges in that bucket
|
||||
#[derive(PartialEq, Serialize, Deserialize)]
|
||||
pub struct HashOfBucket {
|
||||
hash: [u8; 32],
|
||||
}
|
||||
|
||||
impl HashOfBucket {
|
||||
pub fn new(bucket: Scalar) -> Self {
|
||||
let mut hasher = Sha3_256::new();
|
||||
hasher.update(bucket.to_bytes());
|
||||
let hash: [u8; 32] = hasher.finalize().into();
|
||||
Self { hash }
|
||||
}
|
||||
|
||||
pub fn verify(&self, bridge_fingerprint: [u8; 20]) -> bool {
|
||||
let bucket = get_bucket(&self.hash);
|
||||
for bl in bucket {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(bl.uid_fingerprint.to_le_bytes());
|
||||
if bridge_fingerprint == <[u8; 20]>::from(hasher.finalize()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports from users about whether or not their bridges are blocked
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Report {
|
||||
/// Negative report indicating user was unable to connect
|
||||
NegativeReport(NegativeReport),
|
||||
/// Positive report indicating user was able to connect
|
||||
PositiveReport(PositiveReport),
|
||||
}
|
||||
|
||||
impl Report {
|
||||
fn verify(&self) -> bool {
|
||||
match self {
|
||||
Report::NegativeReport(report) => report.verify(),
|
||||
Report::PositiveReport(report) => report.verify(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A report that the user was unable to connect to the bridge
|
||||
#[derive(Serialize)]
|
||||
pub struct NegativeReport {
|
||||
pub struct BridgeInfo {
|
||||
/// hashed fingerprint (SHA-1 hash of 20-byte bridge ID)
|
||||
pub fingerprint: [u8; 20],
|
||||
/// some way to prove knowledge of bridge
|
||||
bridge_pok: ProofOfBridgeKnowledge,
|
||||
/// user's country code, may be an empty string
|
||||
pub country: String,
|
||||
/// today's Julian date
|
||||
pub date: u32,
|
||||
/// nickname of bridge (probably not necessary)
|
||||
pub nickname: String,
|
||||
/// flag indicating whether the bridge is believed to be blocked
|
||||
pub is_blocked: bool,
|
||||
/// map of dates to data for that day
|
||||
pub info_by_day: HashMap<u32, DailyBridgeInfo>,
|
||||
}
|
||||
|
||||
// Ensure public fields are legal while deserializing
|
||||
// Based on https://serde.rs/deserialize-struct.html
|
||||
impl<'de> Deserialize<'de> for NegativeReport {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
enum Field {
|
||||
Fingerprint,
|
||||
BridgePOK,
|
||||
Country,
|
||||
Date,
|
||||
impl BridgeInfo {
|
||||
pub fn new(fingerprint: [u8; 20], nickname: String) -> Self {
|
||||
Self {
|
||||
fingerprint: fingerprint,
|
||||
nickname: nickname,
|
||||
is_blocked: false,
|
||||
info_by_day: HashMap::<u32, DailyBridgeInfo>::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Field {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
|
||||
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<E>(self, value: &str) -> Result<Field, E>
|
||||
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)
|
||||
impl fmt::Display for BridgeInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut str = format!(
|
||||
"fingerprint:{}\n",
|
||||
array_bytes::bytes2hex("", self.fingerprint).as_str()
|
||||
);
|
||||
str.push_str(format!("nickname: {}\n", self.nickname).as_str());
|
||||
str.push_str(format!("is_blocked: {}\n", self.is_blocked).as_str());
|
||||
str.push_str("info_by_day:");
|
||||
for day in self.info_by_day.keys() {
|
||||
str.push_str(format!("\n day: {}", day).as_str());
|
||||
let daily_info = self.info_by_day.get(day).unwrap();
|
||||
for line in daily_info.to_string().lines() {
|
||||
str.push_str(format!("\n {}", line).as_str());
|
||||
}
|
||||
}
|
||||
write!(f, "{}", str)
|
||||
}
|
||||
}
|
||||
|
||||
struct NegativeReportVisitor;
|
||||
// TODO: Should this be an enum to make it easier to implement different
|
||||
// versions for plugins?
|
||||
|
||||
impl<'de> de::Visitor<'de> for NegativeReportVisitor {
|
||||
type Value = NegativeReport;
|
||||
/// Information about bridge reachability, gathered daily
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DailyBridgeInfo {
|
||||
/// Map of country codes and how many users (rounded up to a multiple of
|
||||
/// 8) have connected to that bridge during the day.
|
||||
pub bridge_ips: BTreeMap<String, u32>,
|
||||
/// Set of negative reports received during this day
|
||||
pub negative_reports: Vec<SerializableNegativeReport>,
|
||||
/// Set of positive reports received during this day
|
||||
pub positive_reports: Vec<SerializablePositiveReport>,
|
||||
// We don't care about ordering of the reports, but I'm using vectors for
|
||||
// reports because we don't want a set to deduplicate our reports, and
|
||||
// I don't want to implement Hash or Ord. Another possibility might be a
|
||||
// map of the report to the number of that exact report we received.
|
||||
// Positive reports include a Lox proof and should be unique, but negative
|
||||
// reports could be deduplicated.
|
||||
}
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str(
|
||||
"negative report with valid country code and date no later than today",
|
||||
)
|
||||
}
|
||||
impl DailyBridgeInfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
bridge_ips: BTreeMap::<String, u32>::new(),
|
||||
negative_reports: Vec::<SerializableNegativeReport>::new(),
|
||||
positive_reports: Vec::<SerializablePositiveReport>::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_seq<V>(self, mut seq: V) -> Result<NegativeReport, V::Error>
|
||||
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,
|
||||
impl fmt::Display for DailyBridgeInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut str = String::from("bridge_ips:");
|
||||
for country in self.bridge_ips.keys() {
|
||||
str.push_str(
|
||||
format!(
|
||||
"\n cc: {}, connections: {}",
|
||||
country,
|
||||
date,
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<NegativeReport, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut fingerprint = None;
|
||||
let mut bridge_pok = None;
|
||||
let mut country: Option<String> = 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(),
|
||||
})
|
||||
}
|
||||
self.bridge_ips.get(country).unwrap()
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
const FIELDS: &'static [&'static str] = &["fingerprint", "bridge_pok", "country", "date"];
|
||||
deserializer.deserialize_struct("NegativeReport", FIELDS, NegativeReportVisitor)
|
||||
write!(f, "{}", str)
|
||||
}
|
||||
}
|
||||
|
||||
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 date = get_date();
|
||||
Self {
|
||||
fingerprint,
|
||||
bridge_pok,
|
||||
country,
|
||||
date,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bridgeline(bridge_id: [u8; 20], bridgeline: BridgeLine, country: String) -> Self {
|
||||
let bridge_pok =
|
||||
ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(bridgeline));
|
||||
NegativeReport::new(bridge_id, bridge_pok, country)
|
||||
}
|
||||
|
||||
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 });
|
||||
NegativeReport::new(bridge_id, bridge_pok, 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 {
|
||||
self.bridge_pok.verify(self.fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
/// A report that the user was able to connect to the bridge
|
||||
#[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
|
||||
bridge_token: Option<BridgeToken>,
|
||||
// proof of Lox cred with level >= 3 and this bridge
|
||||
lox_proof: positive_report::Request,
|
||||
/// user's country code, may be an empty string
|
||||
pub country: String,
|
||||
/// today's Julian date
|
||||
pub date: u32,
|
||||
}
|
||||
|
||||
// Ensure public fields are legal while deserializing
|
||||
// Based on https://serde.rs/deserialize-struct.html
|
||||
impl<'de> Deserialize<'de> for PositiveReport {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
enum Field {
|
||||
Fingerprint,
|
||||
BridgeToken,
|
||||
LoxProof,
|
||||
Country,
|
||||
Date,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Field {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
|
||||
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<E>(self, value: &str) -> Result<Field, E>
|
||||
where
|
||||
E: de::Error,
|
||||
/// Adds the extra-info data for a single bridge to the database. If the
|
||||
/// database already contains an extra-info for this bridge for thid date,
|
||||
/// but this extra-info contains different data for some reason, use the
|
||||
/// greater count of connections from each country.
|
||||
pub fn add_extra_info_to_db(db: &Db, extra_info: ExtraInfo) {
|
||||
let fingerprint = extra_info.fingerprint;
|
||||
let mut bridge_info = match db.get(&fingerprint).unwrap() {
|
||||
Some(v) => bincode::deserialize(&v).unwrap(),
|
||||
None => BridgeInfo::new(fingerprint, extra_info.nickname),
|
||||
};
|
||||
// If we already have an entry, compare it with the new one. For each
|
||||
// country:count mapping, use the greater of the two counts.
|
||||
if bridge_info.info_by_day.contains_key(&extra_info.published) {
|
||||
let daily_bridge_info = bridge_info
|
||||
.info_by_day
|
||||
.get_mut(&extra_info.published)
|
||||
.unwrap();
|
||||
if extra_info.bridge_ips != daily_bridge_info.bridge_ips {
|
||||
for country in extra_info.bridge_ips.keys() {
|
||||
if daily_bridge_info.bridge_ips.contains_key(country) {
|
||||
// Use greatest value we've seen today
|
||||
if daily_bridge_info.bridge_ips.get(country).unwrap()
|
||||
< extra_info.bridge_ips.get(country).unwrap()
|
||||
{
|
||||
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)),
|
||||
}
|
||||
daily_bridge_info.bridge_ips.insert(
|
||||
country.to_string(),
|
||||
*extra_info.bridge_ips.get(country).unwrap(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
daily_bridge_info.bridge_ips.insert(
|
||||
country.to_string(),
|
||||
*extra_info.bridge_ips.get(country).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
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<V>(self, mut seq: V) -> Result<PositiveReport, V::Error>
|
||||
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<V>(self, mut map: V) -> Result<PositiveReport, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut fingerprint = None;
|
||||
let mut bridge_token: Option<Option<BridgeToken>> = None;
|
||||
let mut lox_proof = None;
|
||||
let mut country: Option<String> = 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<BridgeToken>,
|
||||
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 date = get_date();
|
||||
Self {
|
||||
fingerprint,
|
||||
bridge_token,
|
||||
lox_proof,
|
||||
country,
|
||||
date,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_lox_credential(
|
||||
bridge_id: [u8; 20],
|
||||
bridge_token: Option<BridgeToken>,
|
||||
lox_cred: &Lox,
|
||||
lox_pub: &IssuerPubKey,
|
||||
country: String,
|
||||
) -> Self {
|
||||
let lox_proof = positive_report::request(lox_cred, lox_pub).unwrap();
|
||||
PositiveReport::new(bridge_id, bridge_token, lox_proof, country)
|
||||
}
|
||||
|
||||
fn verify(&self) -> bool {
|
||||
!REQUIRE_BRIDGE_TOKEN || {
|
||||
if self.bridge_token.is_none() {
|
||||
false
|
||||
} else {
|
||||
let bt = self.bridge_token.as_ref().unwrap();
|
||||
bt.verify()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An unsigned token which indicates that the bridge was reached
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct UnsignedBridgeToken {
|
||||
/// hashed fingerprint (SHA-1 hash of 20-byte bridge ID)
|
||||
pub fingerprint: [u8; 20],
|
||||
/// client's country code
|
||||
pub country: String,
|
||||
/// today's Julian date
|
||||
pub date: u32,
|
||||
}
|
||||
|
||||
impl UnsignedBridgeToken {
|
||||
pub fn new(bridge_id: [u8; 20], country: String) -> Self {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(bridge_id);
|
||||
let fingerprint: [u8; 20] = hasher.finalize().into();
|
||||
let date = get_date();
|
||||
Self {
|
||||
fingerprint,
|
||||
country,
|
||||
date,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A signed token which indicates that the bridge was reached
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BridgeToken {
|
||||
/// the unsigned version of this token
|
||||
pub unsigned_bridge_token: UnsignedBridgeToken,
|
||||
/// signature from bridge's ed25519 key
|
||||
pub sig: Signature,
|
||||
}
|
||||
|
||||
impl BridgeToken {
|
||||
pub fn new(unsigned_bridge_token: UnsignedBridgeToken, keypair: SigningKey) -> Self {
|
||||
let sig = keypair.sign(&bincode::serialize(&unsigned_bridge_token).unwrap());
|
||||
Self {
|
||||
unsigned_bridge_token,
|
||||
sig,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> bool {
|
||||
let pubkey = get_bridge_signing_pubkey(&self.unsigned_bridge_token.fingerprint);
|
||||
self.unsigned_bridge_token.date <= get_date()
|
||||
&& pubkey
|
||||
.verify(
|
||||
&bincode::serialize(&self.unsigned_bridge_token).unwrap(),
|
||||
&self.sig,
|
||||
)
|
||||
.is_ok()
|
||||
} else {
|
||||
// No existing entry; make a new one.
|
||||
let daily_bridge_info = DailyBridgeInfo {
|
||||
bridge_ips: extra_info.bridge_ips,
|
||||
negative_reports: Vec::<SerializableNegativeReport>::new(),
|
||||
positive_reports: Vec::<SerializablePositiveReport>::new(),
|
||||
};
|
||||
bridge_info
|
||||
.info_by_day
|
||||
.insert(extra_info.published, daily_bridge_info);
|
||||
}
|
||||
// Commit changes to database
|
||||
db.insert(fingerprint, bincode::serialize(&bridge_info).unwrap())
|
||||
.unwrap();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue