344 lines
13 KiB
Rust
344 lines
13 KiB
Rust
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<u32, RistrettoBasepointTable>,
|
|
req: Request<Body>,
|
|
) -> Result<Response<Body>, 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<String, u32>) -> Request<Body>;
|
|
fn verifypositive(&self, reports: Vec<SerializablePositiveReport>) -> Request<Body>;
|
|
}
|
|
|
|
struct TpClientMock {}
|
|
|
|
impl TpClient for TpClientMock {
|
|
fn verifynegative(&self, reports: BTreeMap<String, u32>) -> Request<Body> {
|
|
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<SerializablePositiveReport>) -> Request<Body> {
|
|
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::<Scalar>::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<Body>) -> 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::<u32, RistrettoBasepointTable>::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::<String, u32>::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);
|
|
}
|
|
}
|