use crate::lox_context::LoxServerContext; use curve25519_dalek::ristretto::RistrettoBasepointTable; use hyper::{body, header::HeaderValue, Body, Method, Request, Response, StatusCode}; use std::{collections::HashMap, convert::Infallible}; // Handle for each Troll Patrol request/protocol pub async fn handle( cloned_context: LoxServerContext, Htables: &mut HashMap, req: Request, ) -> Result, Infallible> { match req.method() { &Method::OPTIONS => Ok(Response::builder() .header("Access-Control-Allow-Origin", HeaderValue::from_static("*")) .header("Access-Control-Allow-Headers", "accept, content-type") .header("Access-Control-Allow-Methods", "POST") .status(200) .body(Body::from("Allow POST")) .unwrap()), _ => match (req.method(), req.uri().path()) { (&Method::POST, "/verifynegative") => Ok::<_, Infallible>({ let bytes = body::to_bytes(req.into_body()).await.unwrap(); cloned_context.verify_negative_reports(bytes) }), (&Method::POST, "/verifypositive") => Ok::<_, Infallible>({ let bytes = body::to_bytes(req.into_body()).await.unwrap(); cloned_context.verify_positive_reports(bytes, Htables) }), _ => { // Return 404 not found response. Ok(Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::from("Not found")) .unwrap()) } }, } } #[cfg(test)] mod tests { use crate::lox_context; use crate::metrics::Metrics; use base64::{engine::general_purpose, Engine as _}; use curve25519_dalek::Scalar; use lox_library::{ bridge_table::{self, BridgeLine, BridgeTable}, cred::Lox, proto::*, scalar_u32, BridgeAuth, BridgeDb, }; use rand::RngCore; use sha1::{Digest, Sha1}; use std::{ collections::{BTreeMap, HashSet}, sync::{Arc, Mutex}, }; use troll_patrol::{ bridge_info::BridgeInfo, negative_report::{NegativeReport, SerializableNegativeReport}, positive_report::{PositiveReport, SerializablePositiveReport}, BridgeDistributor, }; use super::*; trait TpClient { fn verifynegative(&self, reports: BTreeMap) -> Request; fn verifypositive(&self, reports: Vec) -> Request; } struct TpClientMock {} impl TpClient for TpClientMock { fn verifynegative(&self, reports: BTreeMap) -> Request { let req = serde_json::to_string(&reports).unwrap(); Request::builder() .method("POST") .uri("http://localhost/verifynegative") .body(Body::from(req)) .unwrap() } fn verifypositive(&self, reports: Vec) -> Request { let req = serde_json::to_string(&reports).unwrap(); Request::builder() .method("POST") .uri("http://localhost/verifypositive") .body(Body::from(req)) .unwrap() } } struct TestHarness { context: LoxServerContext, } impl TestHarness { fn new() -> Self { let mut bridgedb = BridgeDb::new(); let mut lox_auth = BridgeAuth::new(bridgedb.pubkey); // Make 3 x num_buckets open invitation bridges, in sets of 3 for _ in 0..5 { let bucket = [random(), random(), random()]; let _ = lox_auth.add_openinv_bridges(bucket, &mut bridgedb); } // Add hot_spare more hot spare buckets for _ in 0..5 { let bucket = [random(), random(), random()]; let _ = lox_auth.add_spare_bucket(bucket, &mut bridgedb); } // Create the encrypted bridge table lox_auth.enc_bridge_table(); let context = lox_context::LoxServerContext { db: Arc::new(Mutex::new(bridgedb)), ba: Arc::new(Mutex::new(lox_auth)), extra_bridges: Arc::new(Mutex::new(Vec::new())), to_be_replaced_bridges: Arc::new(Mutex::new(Vec::new())), tp_bridge_infos: Arc::new(Mutex::new(HashMap::<[u8; 20], BridgeInfo>::new())), metrics: Metrics::default(), }; Self { context } } pub fn generate_bridge_infos(&self) { // We want to ignore empty bridgelines let mut hasher = Sha1::new(); hasher.update([0; 20]); let empty_bridgeline_fingerprint: [u8; 20] = hasher.finalize().into(); let mut lox_auth = self.context.ba.lock().unwrap(); // Recompute table let mut tp_bridge_infos = self.context.tp_bridge_infos.lock().unwrap(); tp_bridge_infos.clear(); // Go through all buckets and all bridges in buckets, map bridge to // buckets containing it. Note that a bridge may be contained within // multiple buckets (open invitaion buckets and invite-only buckets). let buckets = &lox_auth.bridge_table.buckets; for id in buckets.keys() { let bridges = buckets.get(id).unwrap(); let key = lox_auth.bridge_table.keys.get(id).unwrap(); let bucket = bridge_table::to_scalar(*id, key); for bridge in bridges { // Get hashed fingerprint let mut hasher = Sha1::new(); hasher.update(&bridge.fingerprint); let fingerprint: [u8; 20] = hasher.finalize().into(); if fingerprint != empty_bridgeline_fingerprint { // Add new entry or add bucket to existing entry if tp_bridge_infos.contains_key(&fingerprint) { tp_bridge_infos .get_mut(&fingerprint) .unwrap() .buckets .insert(bucket); } else { let mut buckets = HashSet::::new(); buckets.insert(bucket); tp_bridge_infos.insert( fingerprint, BridgeInfo { bridge_line: *bridge, buckets: buckets, pubkey: None, // TODO: add pubkey for signed bridge tokens }, ); } } } } } } 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 } async fn body_to_string(res: Response) -> String { let body_bytes = hyper::body::to_bytes(res.into_body()).await.unwrap(); String::from_utf8(body_bytes.to_vec()).unwrap() } async fn get_new_credential(th: &mut TestHarness) -> Lox { let inv = th.context.db.lock().unwrap().invite().unwrap(); let (req, state) = open_invite::request(&inv); let resp = th .context .ba .lock() .unwrap() .handle_open_invite(req) .unwrap(); let (cred, _bridgeline) = open_invite::handle_response(state, resp, &th.context.ba.lock().unwrap().lox_pub) .unwrap(); cred } async fn level_up(th: &mut TestHarness, cred: &Lox) -> Lox { let current_level = scalar_u32(&cred.trust_level).unwrap(); if current_level == 0 { th.context .advance_days_test(trust_promotion::UNTRUSTED_INTERVAL.try_into().unwrap()); let mut ba = th.context.ba.lock().unwrap(); let (promreq, promstate) = trust_promotion::request(cred, &ba.lox_pub, ba.today()).unwrap(); let promresp = ba.handle_trust_promotion(promreq).unwrap(); let migcred = trust_promotion::handle_response(promstate, promresp).unwrap(); let (migreq, migstate) = migration::request(cred, &migcred, &ba.lox_pub, &ba.migration_pub).unwrap(); let migresp = ba.handle_migration(migreq).unwrap(); let new_cred = migration::handle_response(migstate, migresp, &ba.lox_pub).unwrap(); new_cred } else { th.context.advance_days_test( level_up::LEVEL_INTERVAL[usize::try_from(current_level).unwrap()] .try_into() .unwrap(), ); let mut ba = th.context.ba.lock().unwrap(); let (id, key) = bridge_table::from_scalar(cred.bucket).unwrap(); let encbuckets = 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, &ba.lox_pub, &ba.reachability_pub, ba.today(), ) .unwrap(); let lvresp = ba.handle_level_up(lvreq).unwrap(); let new_cred = level_up::handle_response(lvstate, lvresp, &ba.lox_pub).unwrap(); new_cred } } #[tokio::test] async fn test_negative_reports() { let mut th = TestHarness::new(); th.generate_bridge_infos(); let tpc = TpClientMock {}; let mut Htables = HashMap::::new(); // Get new level 1 credential let cred = get_new_credential(&mut th).await; let cred = level_up(&mut th, &cred).await; th.generate_bridge_infos(); let mut ba = th.context.ba.lock().unwrap(); // Get bucket let (id, key) = bridge_table::from_scalar(cred.bucket).unwrap(); let encbuckets = ba.enc_bridge_table(); let bucket = bridge_table::BridgeTable::decrypt_bucket(id, &key, encbuckets.get(&id).unwrap()) .unwrap(); let bridges = bucket.0; // Create random number of negative reports for each bridge in bucket let mut rng = rand::thread_rng(); let num_report_1 = rng.next_u32() % 4 + 1; let num_report_2 = rng.next_u32() % 4 + 1; let num_report_3 = rng.next_u32() % 4 + 1; let mut reports = BTreeMap::::new(); let report_1 = NegativeReport::from_bridgeline(bridges[0], "ru".to_string(), BridgeDistributor::Lox); println!( "report_1: {}, count: {}", array_bytes::bytes2hex("", report_1.fingerprint), num_report_1 ); reports.insert(report_1.to_json(), num_report_1); let report_2 = NegativeReport::from_lox_bucket(bridges[1].fingerprint, cred.bucket, "ru".to_string()); println!( "report_2: {}, count: {}", array_bytes::bytes2hex("", report_2.fingerprint), num_report_2 ); reports.insert(report_2.to_json(), num_report_2); let report_3 = NegativeReport::from_lox_credential(bridges[2].fingerprint, cred, "ru".to_string()); println!( "report_3: {}, count: {}", array_bytes::bytes2hex("", report_3.fingerprint), num_report_3 ); reports.insert(report_3.to_json(), num_report_3); // TODO: Check reports with invalid fields // TODO: Check well-formed reports with incorrect bridge data let request = tpc.verifynegative(reports); let response = handle(th.context.clone(), &mut Htables, request) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let count: u32 = body_to_string(response).await.parse().unwrap(); assert_eq!(num_report_1 + num_report_2 + num_report_3, count); } }