From 0664dd7b45da28a93bdf6dd5b899b0c714734f3f Mon Sep 17 00:00:00 2001 From: Ian Goldberg Date: Wed, 5 May 2021 13:58:43 -0400 Subject: [PATCH] API for adding bridges and marking them as unreachable --- crates/lox-library/src/bridge_table.rs | 35 ++-- crates/lox-library/src/lib.rs | 163 ++++++++++++++++-- crates/lox-library/src/migration_table.rs | 31 ++++ .../lox-library/src/proto/trust_promotion.rs | 2 +- crates/lox-library/src/tests.rs | 87 +++++++--- crates/lox-library/tests/tests.rs | 5 +- 6 files changed, 277 insertions(+), 46 deletions(-) diff --git a/crates/lox-library/src/bridge_table.rs b/crates/lox-library/src/bridge_table.rs index c341d72..929ee64 100644 --- a/crates/lox-library/src/bridge_table.rs +++ b/crates/lox-library/src/bridge_table.rs @@ -18,7 +18,7 @@ use curve25519_dalek::ristretto::CompressedRistretto; use curve25519_dalek::ristretto::RistrettoBasepointTable; use curve25519_dalek::scalar::Scalar; use rand::RngCore; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::convert::TryInto; use subtle::ConstantTimeEq; @@ -92,7 +92,7 @@ impl BridgeLine { /// credential if appropriate pub fn bucket_encode( bucket: &[BridgeLine; MAX_BRIDGES_PER_BUCKET], - reachable: &HashSet, + reachable: &HashMap>, today: u32, bucket_attr: &Scalar, reachability_priv: &IssuerPrivKey, @@ -102,7 +102,7 @@ impl BridgeLine { let mut num_reachable: usize = 0; for bridge in bucket { res[pos..pos + BRIDGE_BYTES].copy_from_slice(&bridge.encode()); - if reachable.contains(bridge) { + if reachable.contains_key(bridge) { num_reachable += 1; } pos += BRIDGE_BYTES; @@ -208,7 +208,13 @@ pub struct BridgeTable { pub keys: Vec<[u8; 16]>, pub buckets: Vec<[BridgeLine; MAX_BRIDGES_PER_BUCKET]>, pub encbuckets: Vec<[u8; ENC_BUCKET_BYTES]>, - pub reachable: HashSet, + /// Individual bridges that are reachable + pub reachable: HashMap>, + /// bucket ids of "hot spare" buckets. These buckets are not handed + /// to users, nor do they have any Migration credentials pointing to + /// them. When a new Migration credential is needed, a bucket is + /// removed from this set and used for that purpose. + pub spares: HashSet, /// The date the buckets were last encrypted to make the encbucket. /// /// The encbucket must be rebuilt each day so that the Bucket @@ -225,20 +231,27 @@ impl BridgeTable { self.buckets.len() } - /// Append a new bucket to the bridge table - pub fn new_bucket(&mut self, bucket: [BridgeLine; MAX_BRIDGES_PER_BUCKET]) { + /// Append a new bucket to the bridge table, returning its index + pub fn new_bucket(&mut self, bucket: &[BridgeLine; MAX_BRIDGES_PER_BUCKET]) -> u32 { // Pick a random key to encrypt this bucket let mut rng = rand::thread_rng(); let mut key: [u8; 16] = [0; 16]; rng.fill_bytes(&mut key); self.keys.push(key); - self.buckets.push(bucket); + self.buckets.push(*bucket); + let bucketnum: u32 = (self.buckets.len() - 1).try_into().unwrap(); // Mark the new bridges as available - for b in bucket.iter() { + for (i, b) in bucket.iter().enumerate() { if b.port > 0 { - self.reachable.insert(*b); + if let Some(v) = self.reachable.get_mut(b) { + v.push((bucketnum, i)); + } else { + let v = vec![(bucketnum, i)]; + self.reachable.insert(*b, v); + } } } + bucketnum } /// Create the vector of encrypted buckets from the keys and buckets @@ -321,7 +334,7 @@ mod tests { for _ in 0..20 { let bucket: [BridgeLine; 3] = [BridgeLine::random(), Default::default(), Default::default()]; - btable.new_bucket(bucket); + btable.new_bucket(&bucket); } // And 20 more with three random bridges each for _ in 0..20 { @@ -330,7 +343,7 @@ mod tests { BridgeLine::random(), BridgeLine::random(), ]; - btable.new_bucket(bucket); + btable.new_bucket(&bucket); } let today: u32 = time::OffsetDateTime::now_utc() .date() diff --git a/crates/lox-library/src/lib.rs b/crates/lox-library/src/lib.rs index 51961d4..f9a9f1c 100644 --- a/crates/lox-library/src/lib.rs +++ b/crates/lox-library/src/lib.rs @@ -25,7 +25,7 @@ pub mod migration_table; use sha2::Sha512; use rand::rngs::OsRng; -use rand::RngCore; +use rand::Rng; use std::convert::{TryFrom, TryInto}; use curve25519_dalek::constants as dalek_constants; @@ -38,6 +38,13 @@ use curve25519_dalek::traits::IsIdentity; use ed25519_dalek::{Keypair, PublicKey, Signature, SignatureError, Signer, Verifier}; use subtle::ConstantTimeEq; +use std::collections::HashSet; + +use bridge_table::{ + BridgeLine, BridgeTable, ENC_BUCKET_BYTES, MAX_BRIDGES_PER_BUCKET, MIN_BUCKET_REACHABILITY, +}; +use migration_table::{MigrationTable, MigrationType}; + use lazy_static::lazy_static; lazy_static! { @@ -104,8 +111,8 @@ pub struct BridgeDb { keypair: Keypair, /// The public key for verifying open invitations pub pubkey: PublicKey, - /// The number of open-invitation buckets - num_openinv_buckets: u32, + /// The set of open-invitation buckets + openinv_buckets: HashSet, } /// An open invitation is a [u8; OPENINV_LENGTH] where the first 32 @@ -119,17 +126,27 @@ pub const OPENINV_LENGTH: usize = 32 // the length of the random impl BridgeDb { /// Create the BridgeDb. - pub fn new(num_openinv_buckets: u32) -> Self { + pub fn new() -> Self { let mut csprng = OsRng {}; let keypair = Keypair::generate(&mut csprng); let pubkey = keypair.public; Self { keypair, pubkey, - num_openinv_buckets, + openinv_buckets: Default::default(), } } + /// Insert an open-invitation bucket into the set + pub fn insert_openinv(&mut self, bucket: u32) { + self.openinv_buckets.insert(bucket); + } + + /// Remove an open-invitation bucket from the set + pub fn remove_openinv(&mut self, bucket: u32) { + self.openinv_buckets.remove(&bucket); + } + /// Produce an open invitation. In this example code, we just /// choose a random open-invitation bucket. pub fn invite(&self) -> [u8; OPENINV_LENGTH] { @@ -138,9 +155,10 @@ impl BridgeDb { // Choose a random invitation id (a Scalar) and serialize it let id = Scalar::random(&mut rng); res[0..32].copy_from_slice(&id.to_bytes()); - // Choose a random bucket number (mod num_openinv_buckets) and - // serialize it - let bucket_num = rng.next_u32() % self.num_openinv_buckets; + // Choose a random bucket number (from the set of open + // invitation buckets) and serialize it + let openinv_vec: Vec<&u32> = self.openinv_buckets.iter().collect(); + let bucket_num = *openinv_vec[rng.gen_range(0, openinv_vec.len())]; res[32..(32 + 4)].copy_from_slice(&bucket_num.to_le_bytes()); // Sign the first 36 bytes and serialize it let sig = self.keypair.sign(&res[0..(32 + 4)]); @@ -171,6 +189,12 @@ impl BridgeDb { } } +impl Default for BridgeDb { + fn default() -> Self { + Self::new() + } +} + /// The bridge authority. This will typically be a singleton object. #[derive(Debug)] pub struct BridgeAuth { @@ -199,10 +223,11 @@ pub struct BridgeAuth { pub bridgedb_pub: PublicKey, /// The bridge table - bridge_table: bridge_table::BridgeTable, + bridge_table: BridgeTable, - /// The migration table - migration_table: migration_table::MigrationTable, + /// The migration tables + trustup_migration_table: MigrationTable, + blockage_migration_table: MigrationTable, /// Duplicate filter for open invitations openinv_filter: dup_filter::DupFilter, @@ -245,7 +270,8 @@ impl BridgeAuth { invitation_pub, bridgedb_pub, bridge_table: Default::default(), - migration_table: Default::default(), + trustup_migration_table: MigrationTable::new(MigrationType::TrustUpgrade), + blockage_migration_table: MigrationTable::new(MigrationType::Blockage), openinv_filter: Default::default(), id_filter: Default::default(), inv_id_filter: Default::default(), @@ -254,6 +280,117 @@ impl BridgeAuth { } } + /// Insert a set of open invitation bridges. + /// + /// Each of the bridges will be given its own open invitation + /// bucket, and the BridgeDb will be informed. A single bucket + /// containing all of the bridges will also be created, with a trust + /// upgrade migration from each of the single-bridge buckets. + pub fn add_openinv_bridges( + &mut self, + bridges: [BridgeLine; MAX_BRIDGES_PER_BUCKET], + bdb: &mut BridgeDb, + ) { + let bnum = self.bridge_table.new_bucket(&bridges); + let mut single = [BridgeLine::default(); MAX_BRIDGES_PER_BUCKET]; + for b in bridges.iter() { + single[0] = *b; + let snum = self.bridge_table.new_bucket(&single); + bdb.insert_openinv(snum); + println!("Adding {} -> {}", snum, bnum); + self.trustup_migration_table.table.insert(snum, bnum); + } + } + + /// Insert a hot spare bucket of bridges + pub fn add_spare_bucket(&mut self, bucket: [BridgeLine; MAX_BRIDGES_PER_BUCKET]) { + let bnum = self.bridge_table.new_bucket(&bucket); + self.bridge_table.spares.insert(bnum); + } + + /// Mark a bridge as unreachable + /// + /// This bridge will be removed from each of the buckets that + /// contains it. If any of those are open-invitation buckets, the + /// trust upgrade migration for that bucket will be removed and the + /// BridgeDb will be informed to stop handing out that bridge. If + /// any of those are trusted buckets where the number of reachable + /// bridges has fallen below the threshold, a blockage migration + /// from that bucket to a spare bucket will be added, and the spare + /// bucket will be removed from the list of hot spares. In + /// addition, if the blocked bucket was the _target_ of a blockage + /// migration, change the target to the new (formerly spare) bucket. + /// Returns true if sucessful, or false if it needed a hot spare but + /// there was none available. + pub fn bridge_unreachable(&mut self, bridge: &BridgeLine, bdb: &mut BridgeDb) -> bool { + let mut res: bool = true; + let positions = self.bridge_table.reachable.get(bridge); + if let Some(v) = positions { + for (bucketnum, offset) in v.iter() { + // Count how many bridges in this bucket are reachable + let numreachable = self.bridge_table.buckets[*bucketnum as usize] + .iter() + .filter(|br| self.bridge_table.reachable.get(br).is_some()) + .count(); + + // Remove the bridge from the bucket + assert!(self.bridge_table.buckets[*bucketnum as usize][*offset] == *bridge); + self.bridge_table.buckets[*bucketnum as usize][*offset] = BridgeLine::default(); + + // Is this bucket an open-invitation bucket? + if bdb.openinv_buckets.contains(bucketnum) { + bdb.openinv_buckets.remove(bucketnum); + self.trustup_migration_table.table.remove(bucketnum); + continue; + } + + // Does this removal cause the bucket to go below the + // threshold? + if numreachable != MIN_BUCKET_REACHABILITY { + // No + continue; + } + + // This bucket is now unreachable. Get a spare bucket + if self.bridge_table.spares.is_empty() { + // Uh, oh. No spares available. Just delete any + // migrations leading to this bucket. + res = false; + self.trustup_migration_table + .table + .retain(|_, &mut v| v != *bucketnum); + self.blockage_migration_table + .table + .retain(|_, &mut v| v != *bucketnum); + } else { + // Get the first spare and remove it from the spares + // set. + let spare = *self.bridge_table.spares.iter().next().unwrap(); + self.bridge_table.spares.remove(&spare); + // Add a blockage migration from this bucket to the spare + self.blockage_migration_table + .table + .insert(*bucketnum, spare); + // Remove any trust upgrade migrations to this + // bucket + self.trustup_migration_table + .table + .retain(|_, &mut v| v != *bucketnum); + // Change any blockage migrations with this bucket + // as the destination to the spare + for (_, v) in self.blockage_migration_table.table.iter_mut() { + if *v == *bucketnum { + *v = spare; + } + } + } + } + } + self.bridge_table.reachable.remove(bridge); + + res + } + #[cfg(test)] /// For testing only: manually advance the day by 1 day pub fn advance_day(&mut self) { @@ -282,7 +419,7 @@ impl BridgeAuth { /// Be sure to call this function when you want the latest version /// of the table, since it will put fresh Bucket Reachability /// credentials in the buckets each day. - pub fn enc_bridge_table(&mut self) -> &Vec<[u8; bridge_table::ENC_BUCKET_BYTES]> { + pub fn enc_bridge_table(&mut self) -> &Vec<[u8; ENC_BUCKET_BYTES]> { let today = self.today(); if self.bridge_table.date_last_enc != today { self.bridge_table diff --git a/crates/lox-library/src/migration_table.rs b/crates/lox-library/src/migration_table.rs index 7bf5357..8411568 100644 --- a/crates/lox-library/src/migration_table.rs +++ b/crates/lox-library/src/migration_table.rs @@ -34,10 +34,33 @@ pub const MIGRATION_BYTES: usize = 96; /// The size of an encrypted entry in the returned migration table pub const ENC_MIGRATION_BYTES: usize = MIGRATION_BYTES + 12 + 16; +/// The type of migration table: TrustUpgrade is for migrations from +/// untrusted (level 0) 1-bridge buckets to trusted (level 1) 3-bridge +/// buckets. Blockage is for migrations that drop you down two levels +/// (level 3 to 1, level 4 to 2) because the bridges in your current +/// bucket were blocked. +pub enum MigrationType { + TrustUpgrade, + Blockage, +} + +impl From for Scalar { + /// Convert a MigrationType into the Scalar value that represents + /// it in the Migration credential + fn from(m: MigrationType) -> Self { + match m { + MigrationType::TrustUpgrade => 0u32, + MigrationType::Blockage => 1u32, + } + .into() + } +} + /// The migration table #[derive(Default, Debug)] pub struct MigrationTable { pub table: HashMap, + pub migration_type: Scalar, } /// Create an encrypted Migration credential for returning to the user @@ -143,6 +166,14 @@ pub fn encrypt_cred_ids( } impl MigrationTable { + /// Create a MigrationTable of the given MigrationType + pub fn new(table_type: MigrationType) -> Self { + Self { + table: Default::default(), + migration_type: table_type.into(), + } + } + /// For each entry in the MigrationTable, use encrypt_cred_ids to /// produce an entry in an output HashMap (from labels to encrypted /// Migration credentials). diff --git a/crates/lox-library/src/proto/trust_promotion.rs b/crates/lox-library/src/proto/trust_promotion.rs index 81ebab8..f0e41c4 100644 --- a/crates/lox-library/src/proto/trust_promotion.rs +++ b/crates/lox-library/src/proto/trust_promotion.rs @@ -517,7 +517,7 @@ impl BridgeAuth { Ok(Response { Pk, EncQk, - enc_migration_table: self.migration_table.encrypt_table( + enc_migration_table: self.trustup_migration_table.encrypt_table( &req.id, &self.bridge_table, &Pktable, diff --git a/crates/lox-library/src/tests.rs b/crates/lox-library/src/tests.rs index a960fd9..00ca397 100644 --- a/crates/lox-library/src/tests.rs +++ b/crates/lox-library/src/tests.rs @@ -13,30 +13,27 @@ struct TestHarness { impl TestHarness { fn new() -> Self { // Create a BridegDb - let bdb = BridgeDb::new(15); + let mut bdb = BridgeDb::new(); // Create a BridgeAuth let mut ba = BridgeAuth::new(bdb.pubkey); - // Make 15 buckets with one random bridge each - for _ in 0..15 { - let bucket: [BridgeLine; 3] = - [BridgeLine::random(), Default::default(), Default::default()]; - ba.bridge_table.new_bucket(bucket); - } - // Make 5 more buckets, each containing 3 of the previously - // created bridges - for i in 0u32..5 { - let iusize = i as usize; - let bucket: [BridgeLine; 3] = [ - ba.bridge_table.buckets[3 * iusize][0], - ba.bridge_table.buckets[3 * iusize + 1][0], - ba.bridge_table.buckets[3 * iusize + 2][0], + // Make 15 open invitation bridges, in 5 sets of 3 + for _ in 0..5 { + let bucket = [ + BridgeLine::random(), + BridgeLine::random(), + BridgeLine::random(), ]; - ba.bridge_table.new_bucket(bucket); - // Add the allowed migrations to the migration table - ba.migration_table.table.insert(3 * i, 15 + i); - ba.migration_table.table.insert(3 * i + 1, 15 + i); - ba.migration_table.table.insert(3 * i + 2, 15 + i); + ba.add_openinv_bridges(bucket, &mut bdb); + } + // Add 5 more hot spare buckets + for _ in 0..5 { + let bucket = [ + BridgeLine::random(), + BridgeLine::random(), + BridgeLine::random(), + ]; + ba.add_spare_bucket(bucket); } // Create the encrypted bridge table ba.enc_bridge_table(); @@ -303,3 +300,53 @@ fn test_redeem_invite() { assert!(th.ba.verify_lox(&bob_cred)); println!("bob_cred = {:?}", bob_cred); } + +#[test] +fn test_mark_unreachable() { + let mut th = TestHarness::new(); + + println!("spares = {:?}", th.ba.bridge_table.spares); + println!("tmig = {:?}", th.ba.trustup_migration_table.table); + println!("bmig = {:?}", th.ba.blockage_migration_table.table); + println!("openinv = {:?}\n", th.bdb.openinv_buckets); + + // Mark a bridge in an untrusted bucket as unreachable + let b6 = th.ba.bridge_table.buckets[6][0]; + th.ba.bridge_unreachable(&b6, &mut th.bdb); + + println!("spares = {:?}", th.ba.bridge_table.spares); + println!("tmig = {:?}", th.ba.trustup_migration_table.table); + println!("bmig = {:?}", th.ba.blockage_migration_table.table); + println!("openinv = {:?}\n", th.bdb.openinv_buckets); + + // Mark another bridge grouped to the same trusted bucket as + // unreachable + let b7 = th.ba.bridge_table.buckets[7][0]; + th.ba.bridge_unreachable(&b7, &mut th.bdb); + + println!("spares = {:?}", th.ba.bridge_table.spares); + println!("tmig = {:?}", th.ba.trustup_migration_table.table); + println!("bmig = {:?}", th.ba.blockage_migration_table.table); + println!("openinv = {:?}\n", th.bdb.openinv_buckets); + + // That will have introduced a blockage migration. Get the target + let target: u32 = *th + .ba + .blockage_migration_table + .table + .iter() + .next() + .unwrap() + .1; + + // Block two of the bridges in that target bucket + let bt1 = th.ba.bridge_table.buckets[target as usize][1]; + let bt2 = th.ba.bridge_table.buckets[target as usize][2]; + th.ba.bridge_unreachable(&bt1, &mut th.bdb); + th.ba.bridge_unreachable(&bt2, &mut th.bdb); + + println!("spares = {:?}", th.ba.bridge_table.spares); + println!("tmig = {:?}", th.ba.trustup_migration_table.table); + println!("bmig = {:?}", th.ba.blockage_migration_table.table); + println!("openinv = {:?}\n", th.bdb.openinv_buckets); +} diff --git a/crates/lox-library/tests/tests.rs b/crates/lox-library/tests/tests.rs index cdd1abd..0416c5b 100644 --- a/crates/lox-library/tests/tests.rs +++ b/crates/lox-library/tests/tests.rs @@ -6,7 +6,10 @@ use curve25519_dalek::scalar::Scalar; #[test] fn test_bridgedb() { - let bdb = BridgeDb::new(20); + let mut bdb = BridgeDb::new(); + for i in &[1u32, 5, 7, 12, 19, 20, 22] { + bdb.insert_openinv(*i); + } let inv = bdb.invite(); println!("{:?}", inv); let res = BridgeDb::verify(inv, bdb.pubkey);