//! # rust-cascade //! //! A library for creating and querying the cascading bloom filters described by //! Larisch, Choffnes, Levin, Maggs, Mislove, and Wilson in //! "CRLite: A Scalable System for Pushing All TLS Revocations to All Browsers" //! <https://www.ieee-security.org/TC/SP2017/papers/567.pdf>
use byteorder::{ByteOrder, LittleEndian, ReadBytesExt}; use murmurhash3::murmurhash3_x86_32; #[cfg(feature = "builder")] use rand::rngs::OsRng; #[cfg(feature = "builder")] use rand::RngCore; use sha2::{Digest, Sha256}; use std::convert::{TryFrom, TryInto}; use std::fmt; use std::io::{ErrorKind, Read}; use std::mem::size_of;
impl fmt::Display for CascadeError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self {
CascadeError::LongSalt => {
write!(f, "Cannot serialize a filter with a salt of length >= 256.")
}
CascadeError::TooManyLayers => {
write!(f, "Cannot serialize a filter with >= 255 layers.")
}
CascadeError::Collision => {
write!(f, "Collision between included and excluded sets.")
}
CascadeError::UnknownHashFunction => {
write!(f, "Unknown hash function.")
}
CascadeError::CapacityViolation(function) => {
write!(f, "Unexpected call to {}", function)
}
CascadeError::Parse(reason) => {
write!(f, "Cannot parse cascade: {}", reason)
}
}
}
}
/// A Bloom filter representing a specific layer in a multi-layer cascading Bloom filter. /// The same hash function is used for all layers, so it is not encoded here. struct Bloom { /// How many hash functions this filter uses
n_hash_funcs: u32, /// The bit length of the filter
size: u32, /// The data of the filter
data: Vec<u8>,
}
impl TryFrom<u8> for HashAlgorithm { type Error = CascadeError; fn try_from(value: u8) -> Result<HashAlgorithm, CascadeError> { match value { // Naturally, these need to match the enum declaration 1 => Ok(Self::MurmurHash3), 2 => Ok(Self::Sha256l32), 3 => Ok(Self::Sha256),
_ => Err(CascadeError::UnknownHashFunction),
}
}
}
/// A CascadeIndexGenerator provides one-time access to a table of pseudorandom functions H_ij /// in which each function is of the form /// H(s: &[u8], r: u32) -> usize /// and for which 0 <= H(s,r) < r for all s, r. /// The pseudorandom functions share a common key, represented as a octet string, and the table can /// be constructed from this key alone. The functions are pseudorandom with respect to s, but not /// r. For a uniformly random key/table, fixed r, and arbitrary strings m0 and m1, /// H_ij(m0, r) is computationally indistinguishable from H_ij(m1,r) /// for all i,j. /// /// A call to next_layer() increments i and resets j. /// A call to next_index(s, r) increments j, and outputs some value H_ij(s) with 0 <= H_ij(s) < r.
Self::Sha256Ctr {
key, refmut counter, refmut state, refmut state_available,
} => { // |bytes_needed| is the minimum number of bytes needed to represent a value in [0, range). let bytes_needed = ((range.next_power_of_two().trailing_zeros() + 7) / 8) as usize; letmut index_arr = [0u8; 4]; for byte in index_arr.iter_mut().take(bytes_needed) { if *state_available == 0 { letmut hasher = Sha256::new();
hasher.update(counter.to_le_bytes());
hasher.update(salt);
hasher.update(&key);
hasher.finalize_into(state.into());
*state_available = state.len() as u8;
*counter += 1;
}
*byte = state[state.len() - *state_available as usize];
*state_available -= 1;
}
LittleEndian::read_u32(&index_arr)
}
};
(index % range) as usize
}
}
impl Bloom { /// `new_crlite_bloom` creates an empty bloom filter for a layer of a cascade with the /// parameters specified in [LCL+17, Section III.C]. /// /// # Arguments /// * `include_capacity` - the number of elements that will be encoded at the new layer. /// * `exclude_capacity` - the number of elements in the complement of the encoded set. /// * `top_layer` - whether this is the top layer of the filter. #[cfg(feature = "builder")] pubfn new_crlite_bloom(
include_capacity: usize,
exclude_capacity: usize,
top_layer: bool,
) -> Self {
assert!(include_capacity != 0 && exclude_capacity != 0);
let r = include_capacity as f64; let s = exclude_capacity as f64;
// The desired false positive rate for the top layer is // p = r/(sqrt(2)*s). // With this setting, the number of false positives (which will need to be // encoded at the second layer) is expected to be a factor of sqrt(2) // smaller than the number of elements encoded at the top layer. // // At layer i > 1 we try to ensure that the number of elements to be // encoded at layer i+1 is half the number of elements encoded at // layer i. So we take p = 1/2. let log2_fp_rate = match top_layer { true => (r / s).log2() - 0.5f64, false => -1f64,
};
// the number of hash functions (k) and the size of the bloom filter (m) are given in // [LCL+17] as k = log2(1/p) and m = r log2(1/p) / ln(2). // // If this formula gives a value of m < 256, we take m=256 instead. This results in very // slightly sub-optimal size, but gives us the added benefit of doing less hashing. let n_hash_funcs = (-log2_fp_rate).round() as u32; let size = match (r * (-log2_fp_rate) / (f64::ln(2f64))).round() as u32 {
size if size >= 256 => size,
_ => 256,
};
/// `read` attempts to decode the Bloom filter represented by the bytes in the given reader. /// /// # Arguments /// * `reader` - The encoded representation of this Bloom filter. May be empty. May include /// additional data describing further Bloom filters. /// The format of an encoded Bloom filter is: /// [1 byte] - the hash algorithm to use in the filter /// [4 little endian bytes] - the length in bits of the filter /// [4 little endian bytes] - the number of hash functions to use in the filter /// [1 byte] - which layer in the cascade this filter is /// [variable length bytes] - the filter itself (must be of minimal length) pubfn read<R: Read>(
reader: &mut R,
) -> Result<Option<(Bloom, usize, HashAlgorithm)>, CascadeError> { let hash_algorithm_val = match reader.read_u8() {
Ok(val) => val, // If reader is at EOF, there is no bloom filter.
Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(None),
Err(_) => return Err(CascadeError::Parse("read error")),
}; let hash_algorithm = HashAlgorithm::try_from(hash_algorithm_val)?;
let size = reader
.read_u32::<byteorder::LittleEndian>()
.or(Err(CascadeError::Parse("truncated at layer size")))?; let n_hash_funcs = reader
.read_u32::<byteorder::LittleEndian>()
.or(Err(CascadeError::Parse("truncated at layer hash count")))?; let layer = reader
.read_u8()
.or(Err(CascadeError::Parse("truncated at layer number")))?;
let byte_count = ((size + 7) / 8) as usize; letmut data = vec![0; byte_count];
reader
.read_exact(&mut data)
.or(Err(CascadeError::Parse("truncated at layer data")))?; let bloom = Bloom {
n_hash_funcs,
size,
data,
};
Ok(Some((bloom, layer as usize, hash_algorithm)))
}
fn has(&self, generator: &mut CascadeIndexGenerator, salt: &[u8]) -> bool { for _ in0..self.n_hash_funcs { let bit_index = generator.next_index(salt, self.size);
assert!(bit_index < self.size as usize); let byte_index = bit_index / 8; let mask = 1 << (bit_index % 8); ifself.data[byte_index] & mask == 0 { returnfalse;
}
} true
}
#[cfg(feature = "builder")] fn insert(&mutself, generator: &mut CascadeIndexGenerator, salt: &[u8]) { for _ in0..self.n_hash_funcs { let bit_index = generator.next_index(salt, self.size); let byte_index = bit_index / 8; let mask = 1 << (bit_index % 8); self.data[byte_index] |= mask;
}
}
/// A multi-layer cascading Bloom filter. pubstruct Cascade { /// The Bloom filter for this layer in the cascade
filters: Vec<Bloom>, /// The salt in use, if any
salt: Vec<u8>, /// The hash algorithm / index generating function to use
hash_algorithm: HashAlgorithm, /// Whether the logic should be inverted
inverted: bool,
}
impl Cascade { /// from_bytes attempts to decode and return a multi-layer cascading Bloom filter. /// /// # Arguments /// `bytes` - The encoded representation of the Bloom filters in this cascade. Starts with 2 /// little endian bytes indicating the version. The current version is 2. The Python /// filter-cascade project defines the formats, see /// <https://github.com/mozilla/filter-cascade/blob/v0.3.0/filtercascade/fileformats.py> /// /// May be of length 0, in which case `None` is returned. pubfn from_bytes(bytes: Vec<u8>) -> Result<Option<Self>, CascadeError> { if bytes.is_empty() { return Ok(None);
} letmut reader = bytes.as_slice(); let version = reader
.read_u16::<byteorder::LittleEndian>()
.or(Err(CascadeError::Parse("truncated at version")))?;
if version > 2 { return Err(CascadeError::Parse("unknown version"));
}
if version == 2 { let inverted_val = reader
.read_u8()
.or(Err(CascadeError::Parse("truncated at inverted")))?; if inverted_val > 1 { return Err(CascadeError::Parse("invalid value for inverted"));
}
inverted = 0 != inverted_val; let salt_len: usize = reader
.read_u8()
.or(Err(CascadeError::Parse("truncated at salt length")))?
.into(); if salt_len >= 256 { return Err(CascadeError::Parse("salt too long"));
} if salt_len > 0 { letmut salt_bytes = vec![0; salt_len];
reader
.read_exact(&mut salt_bytes)
.or(Err(CascadeError::Parse("truncated at salt")))?;
salt = salt_bytes;
}
}
/// to_bytes encodes a cascade in the version 2 format. pubfn to_bytes(&self) -> Result<Vec<u8>, CascadeError> { ifself.salt.len() >= 256 { return Err(CascadeError::LongSalt);
} ifself.filters.len() >= 255 { return Err(CascadeError::TooManyLayers);
} letmut out = vec![]; let version: u16 = 2; let inverted: u8 = self.inverted.into(); let salt_len: u8 = self.salt.len() as u8; let hash_alg: u8 = self.hash_algorithm as u8;
out.extend_from_slice(&version.to_le_bytes());
out.push(inverted);
out.push(salt_len);
out.extend_from_slice(&self.salt); for (layer, bloom) inself.filters.iter().enumerate() {
out.push(hash_alg);
out.extend_from_slice(&bloom.size.to_le_bytes());
out.extend_from_slice(&bloom.n_hash_funcs.to_le_bytes());
out.push((1 + layer) as u8); // 1-indexed
out.extend_from_slice(&bloom.data);
}
Ok(out)
}
/// has determines if the given sequence of bytes is in the cascade. /// /// # Arguments /// `entry` - The bytes to query pubfn has(&self, entry: Vec<u8>) -> bool { // Query filters 0..self.filters.len() until we get a non-membership result. // If this occurs at an even index filter, the element *is not* included. // ... at an odd-index filter, the element *is* included. letmut generator = CascadeIndexGenerator::new(self.hash_algorithm, entry); letmut rv = false; for filter in &self.filters { if filter.has(&mut generator, &self.salt) {
rv = !rv;
generator.next_layer();
} else { break;
}
} ifself.inverted {
rv = !rv;
}
rv
}
/// Determine the approximate amount of memory in bytes used by this /// Cascade. Because this implementation does not integrate with the /// allocator, it can't get an accurate measurement of how much memory it /// uses. However, it can make a reasonable guess, assuming the sizes of /// the bloom filters are large enough to dominate the overall allocated /// size. pubfn approximate_size_of(&self) -> usize {
size_of::<Cascade>()
+ self
.filters
.iter()
.map(|x| x.approximate_size_of())
.sum::<usize>()
+ self.salt.len()
}
}
/// A CascadeBuilder creates a Cascade with layers given by `Bloom::new_crlite_bloom`. /// /// A builder is initialized using [`CascadeBuilder::default`] or [`CascadeBuilder::new`]. Prefer `default`. The `new` constructor /// allows the user to specify sensitive internal details such as the hash function and the domain /// separation parameter. /// /// Both constructors take `include_capacity` and an `exclude_capacity` parameters. The /// `include_capacity` is the number of elements that will be encoded in the Cascade. The /// `exclude_capacity` is size of the complement of the encoded set. /// /// The encoded set is specified through calls to [`CascadeBuilder::include`]. Its complement is specified through /// calls to [`CascadeBuilder::exclude`]. The cascade is built with a call to [`CascadeBuilder::finalize`]. /// /// The builder will track of the number of calls to `include` and `exclude`. /// The caller is responsible for making *exactly* `include_capacity` calls to `include` /// followed by *exactly* `exclude_capacity` calls to `exclude`. /// Calling `exclude` before all `include` calls have been made will result in a panic!(). /// Calling `finalize` before all `exclude` calls have been made will result in a panic!(). /// #[cfg(feature = "builder")] pubstruct CascadeBuilder {
filters: Vec<Bloom>,
salt: Vec<u8>,
hash_algorithm: HashAlgorithm,
to_include: Vec<CascadeIndexGenerator>,
to_exclude: Vec<CascadeIndexGenerator>,
status: BuildStatus,
}
/// `exclude_threaded` is like `exclude` but it stores false positives in a caller-owned /// `ExcludeSet`. This allows the caller to exclude items in parallel. pubfn exclude_threaded(&self, exclude_set: &mut ExcludeSet, item: Vec<u8>) {
exclude_set.size += 1; letmut generator = CascadeIndexGenerator::new(self.hash_algorithm, item); ifself.filters[0].has(&mut generator, &>self.salt) {
exclude_set.set.push(generator);
}
}
/// `collect_exclude_set` merges an `ExcludeSet` into the internal storage of the CascadeBuilder. pubfn collect_exclude_set(
&mutself,
exclude_set: &mut ExcludeSet,
) -> Result<(), CascadeError> { matchself.status {
BuildStatus(0, refmut cap) if *cap >= exclude_set.size => *cap -= exclude_set.size,
_ => return Err(CascadeError::CapacityViolation("exclude")),
} self.to_exclude.append(&mut exclude_set.set);
Ok(())
}
fn push_layer(&mutself) -> Result<(), CascadeError> { // At even layers we encode elements of to_include. At odd layers we encode elements of // to_exclude. In both cases, we track false positives by filtering the complement of the // encoded set through the newly produced bloom filter. let at_even_layer = self.filters.len() % 2 == 0; let (to_encode, to_filter) = match at_even_layer { true => (&mutself.to_include, &mutself.to_exclude), false => (&mutself.to_exclude, &mutself.to_include),
};
// split ownership of `salt` away from `to_encode` and `to_filter` // We need an immutable reference to salt during `to_encode.iter_mut()` letmut bloom = Bloom::new_crlite_bloom(to_encode.len(), to_filter.len(), false);
if delta == 0 { // Check for collisions between the |to_encode| and |to_filter| sets. // The implementation of PartialEq for CascadeIndexGenerator will successfully // identify cases where the user called |include(item)| and |exclude(item)| for the // same item. It will not identify collisions in the underlying hash function. for x in to_encode.iter_mut() { if to_filter.contains(x) { return Err(CascadeError::Collision);
}
}
}
/// BuildStatus is used to ensure that the `include`, `exclude`, and `finalize` calls to /// CascadeBuilder are made in the right order. The (a,b) state indicates that the /// CascadeBuilder is waiting for `a` calls to `include` and `b` calls to `exclude`. #[cfg(feature = "builder")] struct BuildStatus(usize, usize);
/// CascadeBuilder::exclude takes `&mut self` so that it can count exclusions and push items to /// self.to_exclude. The bulk of the work it does, however, can be done with an immutable reference /// to the top level bloom filter. An `ExcludeSet` is used by `CascadeBuilder::exclude_threaded` to /// track the changes to a `CascadeBuilder` that would be made with a call to /// `CascadeBuilder::exclude`. #[cfg(feature = "builder")] #[derive(Default)] pubstruct ExcludeSet {
size: usize,
set: Vec<CascadeIndexGenerator>,
}
#[cfg(test)] mod tests { use Bloom; use Cascade; #[cfg(feature = "builder")] use CascadeBuilder; #[cfg(feature = "builder")] use CascadeError; use CascadeIndexGenerator; #[cfg(feature = "builder")] use ExcludeSet; use HashAlgorithm;
let v = include_bytes!("../test_data/test_v1_murmur_short_mlbf").to_vec();
assert!(Cascade::from_bytes(v).is_err());
}
#[test] fn cascade_v2_sha256l32_from_file_bytes_test() { let v = include_bytes!("../test_data/test_v2_sha256l32_mlbf").to_vec(); let cascade = Cascade::from_bytes(v)
.expect("parsing Cascade should succeed")
.expect("Cascade should be Some");
#[test] fn cascade_v2_sha256l32_with_salt_from_file_bytes_test() { let v = include_bytes!("../test_data/test_v2_sha256l32_salt_mlbf").to_vec(); let cascade = Cascade::from_bytes(v)
.expect("parsing Cascade should succeed")
.expect("Cascade should be Some");
#[test] fn cascade_v2_murmur_from_file_bytes_test() { let v = include_bytes!("../test_data/test_v2_murmur_mlbf").to_vec(); let cascade = Cascade::from_bytes(v)
.expect("parsing Cascade should succeed")
.expect("Cascade should be Some");
#[test] fn cascade_v2_murmur_inverted_from_file_bytes_test() { let v = include_bytes!("../test_data/test_v2_murmur_inverted_mlbf").to_vec(); let cascade = Cascade::from_bytes(v)
.expect("parsing Cascade should succeed")
.expect("Cascade should be Some");
#[test] fn cascade_v2_sha256l32_inverted_from_file_bytes_test() { let v = include_bytes!("../test_data/test_v2_sha256l32_inverted_mlbf").to_vec(); let cascade = Cascade::from_bytes(v)
.expect("parsing Cascade should succeed")
.expect("Cascade should be Some");
#[test] fn cascade_v2_sha256ctr_from_file_bytes_test() { let v = include_bytes!("../test_data/test_v2_sha256ctr_salt_mlbf").to_vec(); let cascade = Cascade::from_bytes(v)
.expect("parsing Cascade should succeed")
.expect("Cascade should be Some");
#[cfg(feature = "builder")] fn cascade_builder_test_generate(hash_alg: HashAlgorithm, inverted: bool) { let total = 10_000_usize; let included = 100_usize;
let salt = vec![0u8; 16]; letmut builder =
CascadeBuilder::new(hash_alg, salt, included, (total - included) as usize); for i in0..included {
builder.include(i.to_le_bytes().to_vec()).ok();
} for i in included..total {
builder.exclude(i.to_le_bytes().to_vec()).ok();
} letmut cascade = builder.finalize().unwrap();
if inverted {
cascade.invert()
}
// Ensure we can serialize / deserialize let cascade_bytes = cascade.to_bytes().expect("failed to serialize cascade");
let cascade = Cascade::from_bytes(cascade_bytes)
.expect("failed to deserialize cascade")
.expect("cascade should not be None here");
// Ensure each query gives the correct result for i in0..included {
assert!(cascade.has(i.to_le_bytes().to_vec()) == true ^ inverted)
} for i in included..total {
assert!(cascade.has(i.to_le_bytes().to_vec()) == false ^ inverted)
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.