Fuzzytags for Groups with Tweaked Hashes (Prototype)

This commit is contained in:
Sarah Jamie Lewis 2021-10-02 15:59:39 -07:00
parent 0687943a5c
commit 45f349a92d
4 changed files with 163 additions and 56 deletions

View File

@ -18,7 +18,6 @@ curve25519-dalek = { package = "curve25519-dalek", version="3.2", features=["se
sha3 = "0.9.1"
serde = {version="1.0.123", features=["derive"]}
bit-vec = {version="0.6.3"}
brute-force = {version="0.1.1", features=["curve25519"], optional=true}
rayon = {version="1.5.0", optional=true}
[dev-dependencies]
@ -35,6 +34,6 @@ name = "entangled"
harness = false
[features]
entangled = ["brute-force"]
entangled = []
bulk_verify = ["rayon"]
simd = ["curve25519-dalek/simd_backend"]

View File

@ -3,7 +3,7 @@ use fuzzytags::{RootSecret, TaggingKey};
use rand::rngs::OsRng;
use std::time::Duration;
fn benchmark_entangled(c: &mut Criterion) {
fn benchmark_entangled5(c: &mut Criterion) {
let mut group = c.benchmark_group("entangling");
group.measurement_time(Duration::new(10, 0));
group.sample_size(10);
@ -11,12 +11,25 @@ fn benchmark_entangled(c: &mut Criterion) {
for p in [24].iter() {
let secret_key_1 = RootSecret::<24>::generate(&mut rng);
let secret_key_2 = RootSecret::<24>::generate(&mut rng);
let secret_key_3 = RootSecret::<24>::generate(&mut rng);
let secret_key_4 = RootSecret::<24>::generate(&mut rng);
let secret_key_5 = RootSecret::<24>::generate(&mut rng);
let public_key_1 = secret_key_1.tagging_key();
let public_key_2 = secret_key_2.tagging_key();
let public_key_3 = secret_key_3.tagging_key();
let public_key_4 = secret_key_4.tagging_key();
let public_key_5 = secret_key_5.tagging_key();
group.bench_with_input(BenchmarkId::from_parameter(p), p, |b, _gamma| {
b.iter(|| {
TaggingKey::generate_entangled_tag(
vec![public_key_1.clone(), public_key_2.clone()],
vec![
public_key_1.clone(),
public_key_2.clone(),
public_key_3.clone(),
public_key_4.clone(),
public_key_5.clone(),
],
&mut rng,
*p,
)
@ -25,5 +38,5 @@ fn benchmark_entangled(c: &mut Criterion) {
}
}
criterion_group!(benches, benchmark_entangled);
criterion_group!(benches, benchmark_entangled5);
criterion_main!(benches);

View File

@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use fuzzytags::{RootSecret};
use fuzzytags::RootSecret;
use rand::rngs::OsRng;
use std::time::Duration;

View File

@ -15,11 +15,7 @@ use std::fmt;
use std::fmt::{Display, Formatter};
use std::ops::{Mul, Sub};
#[cfg(feature = "entangled")]
use brute_force::adaptors;
#[cfg(feature = "entangled")]
use brute_force::brute_force;
use rand::Rng;
use rand_core::{CryptoRng, RngCore};
#[cfg(feature = "bulk_verify")]
use rayon::iter::IndexedParallelIterator;
@ -27,6 +23,7 @@ use rayon::iter::IndexedParallelIterator;
use rayon::iter::IntoParallelRefIterator;
#[cfg(feature = "bulk_verify")]
use rayon::iter::ParallelIterator;
use std::borrow::BorrowMut;
#[cfg(feature = "bulk_verify")]
use std::sync::mpsc::channel;
@ -41,6 +38,7 @@ use std::sync::mpsc::channel;
pub struct Tag<const GAMMA: u8> {
u: RistrettoPoint,
y: Scalar,
z: Vec<u8>,
ciphertexts: BitVec,
}
@ -70,7 +68,7 @@ impl<'de, const GAMMA: u8> Deserialize<'de> for Tag<{ GAMMA }> {
type Value = Tag<{ GAMMA }>;
fn expecting(&self, formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
formatter.write_str("64 bytes + GAMMA+bits of data")
formatter.write_str("64 bytes + GAMMA Bytes + GAMMA + bits of data")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Tag<{ GAMMA }>, A::Error>
@ -90,12 +88,13 @@ impl<'de, const GAMMA: u8> Deserialize<'de> for Tag<{ GAMMA }> {
_ => break,
}
}
Tag::<GAMMA>::decompress(&bytes).ok_or(serde::de::Error::custom("invalid fuzzytag"))
}
}
// support up to GAMMA = 64
deserializer.deserialize_tuple(72, FuzzyTagVisitor::<GAMMA>)
deserializer.deserialize_tuple((72 + GAMMA) as usize, FuzzyTagVisitor::<GAMMA>)
}
}
@ -122,6 +121,7 @@ impl<const GAMMA: u8> Tag<{ GAMMA }> {
let mut bytes = vec![];
bytes.extend_from_slice(self.u.compress().as_bytes());
bytes.extend_from_slice(self.y.as_bytes());
bytes.extend_from_slice(self.z.as_slice());
bytes.extend_from_slice(self.ciphertexts.to_bytes().as_slice());
bytes
}
@ -145,9 +145,10 @@ impl<const GAMMA: u8> Tag<{ GAMMA }> {
/// assert_eq!(tag, decompressed_tag);
/// ```
pub fn decompress(bytes: &[u8]) -> Option<Tag<{ GAMMA }>> {
if bytes.len() > 64 {
if bytes.len() > (64 + GAMMA as usize) {
let (u_bytes, rest) = bytes.split_at(32);
let (y_bytes, ciphertext) = rest.split_at(32);
let (y_bytes, rest) = rest.split_at(32);
let (z_bytes, ciphertext) = rest.split_at({ GAMMA } as usize);
// if the ciphertext is too short, then this is an invalid tag
let min_bytes = GAMMA / 8;
@ -160,13 +161,20 @@ impl<const GAMMA: u8> Tag<{ GAMMA }> {
Ok(fixed_size) => fixed_size,
_ => return None,
};
let mut ciphertexts = BitVec::from_bytes(ciphertext);
ciphertexts.truncate(GAMMA as usize);
return match (
CompressedRistretto::from_slice(u_bytes).decompress(),
Scalar::from_canonical_bytes(y_bytes_fixed),
z_bytes.to_vec(),
) {
(Some(u), Some(y)) => Some(Tag { u, y, ciphertexts }),
(Some(u), Some(y), z) => Some(Tag {
u,
y,
z: z,
ciphertexts,
}),
_ => None,
};
}
@ -178,9 +186,10 @@ impl<const GAMMA: u8> Display for Tag<{ GAMMA }> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{} {} {}",
"{} {} {} {}",
hex::encode(self.u.compress().as_bytes()),
hex::encode(self.y.as_bytes()),
hex::encode(self.z.as_slice()),
hex::encode(self.ciphertexts.to_bytes())
)
}
@ -257,6 +266,25 @@ impl<const GAMMA: u8> RootSecret<{ GAMMA }> {
TaggingKey::<GAMMA> { 0: tagging_key }
}
/// derive the tagging key for this secret
/// Example:
/// ```
/// use fuzzytags::RootSecret;
/// use rand::rngs::OsRng;
/// let mut rng = OsRng;
/// let secret = RootSecret::<24>::generate(&mut rng);
/// let anti_key = secret.anti_key();
/// ```
pub fn anti_key(&self) -> TaggingKey<{ GAMMA }> {
let g = RISTRETTO_BASEPOINT_POINT;
let mut tagging_key = vec![];
for sk_i in self.secret.iter() {
let pk_i = g.mul(sk_i.invert());
tagging_key.push(pk_i);
}
TaggingKey::<GAMMA> { 0: tagging_key }
}
/// precompute the first part of h
fn pre_h(u: RistrettoPoint, w: RistrettoPoint) -> PrecomputeH {
let mut hash = sha3::Sha3_256::new();
@ -266,6 +294,11 @@ impl<const GAMMA: u8> RootSecret<{ GAMMA }> {
return PrecomputeH(hash);
}
fn nonce_h(mut hash: PrecomputeH, nonce: u8) -> PrecomputeH {
hash.0.update(vec![nonce]);
hash
}
/// compute the rest of h from a precomputed hash
fn post_h(mut hash: PrecomputeH, h: RistrettoPoint) -> u8 {
hash.0.update(h.compress().as_bytes());
@ -353,7 +386,9 @@ impl<const GAMMA: u8> DetectionKey<{ GAMMA }> {
return false;
}
let m = RootSecret::<GAMMA>::g(tag.u, &tag.ciphertexts);
let mut bitvec = BitVec::from_bytes(&*tag.z);
bitvec.append(tag.ciphertexts.clone().borrow_mut());
let m = RootSecret::<GAMMA>::g(tag.u, &bitvec);
let g = RISTRETTO_BASEPOINT_POINT;
// Re-derive w = g^z from the public tag.
@ -370,9 +405,10 @@ impl<const GAMMA: u8> DetectionKey<{ GAMMA }> {
// for each secret part...
let mut result = 0;
for (x_i, c_i) in self.0.iter().zip(&tag.ciphertexts) {
for (nonce_i, (x_i, c_i)) in self.0.iter().zip(&tag.ciphertexts).enumerate() {
// re-derive the key from the tag
let k_i = RootSecret::<GAMMA>::post_h(pre_h.clone(), tag.u.mul(x_i));
let nonce_h = RootSecret::<GAMMA>::nonce_h(pre_h.clone(), tag.z[nonce_i]);
let k_i = RootSecret::<GAMMA>::post_h(nonce_h, tag.u.mul(x_i));
// calculate the "original" plaintext
let b_i = k_i ^ (c_i as u8);
@ -418,7 +454,10 @@ impl<const GAMMA: u8> DetectionKey<{ GAMMA }> {
return vec![];
}
let m = RootSecret::<GAMMA>::g(tag.u, &tag.ciphertexts);
let mut bitvec = BitVec::from_bytes(&*tag.z);
bitvec.append(tag.ciphertexts.clone().borrow_mut());
let m = RootSecret::<GAMMA>::g(tag.u, &bitvec);
let g = RISTRETTO_BASEPOINT_POINT;
// Re-derive w = g^z from the public tag.
@ -440,9 +479,10 @@ impl<const GAMMA: u8> DetectionKey<{ GAMMA }> {
.enumerate()
.for_each_with(tx.clone(), |tx, (index, detection_key)| {
let mut result = 0;
for (x_i, c_i) in detection_key.0.iter().zip(&tag.ciphertexts) {
for (nonce_i, (x_i, c_i)) in detection_key.0.iter().zip(&tag.ciphertexts).enumerate() {
// re-derive the key from the tag
let k_i = RootSecret::<GAMMA>::post_h(pre_h.clone(), tag.u.mul(x_i));
let nonce_h = RootSecret::<GAMMA>::nonce_h(pre_h.clone(), tag.z[nonce_i]);
let k_i = RootSecret::<GAMMA>::post_h(nonce_h, tag.u.mul(x_i));
// calculate the "original" plaintext
let b_i = k_i ^ (c_i as u8);
@ -516,8 +556,14 @@ impl<const GAMMA: u8> TaggingKey<{ GAMMA }> {
// construct the ciphertext portion of the tag
let mut ciphertexts = BitVec::with_capacity(GAMMA.into());
for h_i in self.0.iter() {
let k_i = RootSecret::<GAMMA>::post_h(pre_h.clone(), h_i.mul(r));
let mut nonces = vec![];
for _i in 0..GAMMA {
nonces.push(rng.gen())
}
for (nonce_i, h_i) in self.0.iter().enumerate() {
let nonce_h = RootSecret::<GAMMA>::nonce_h(pre_h.clone(), nonces[nonce_i]);
let k_i = RootSecret::<GAMMA>::post_h(nonce_h, h_i.mul(r));
// encrypt a plaintext of all 1's
let c_i = k_i ^ 0x01;
ciphertexts.push(c_i == 0x01);
@ -538,10 +584,17 @@ impl<const GAMMA: u8> TaggingKey<{ GAMMA }> {
// used to derive the key.
// finally calculate a `y` = 1/r * (z-m) which will be used to re-derive `w`
let m = RootSecret::<GAMMA>::g(u, &ciphertexts);
let mut bitvec = BitVec::from_bytes(&*nonces);
bitvec.append(ciphertexts.clone().borrow_mut());
let m = RootSecret::<GAMMA>::g(u, &bitvec);
let y = r.invert().mul(z.sub(m));
return Tag { u, y, ciphertexts };
return Tag {
u,
y,
z: nonces,
ciphertexts,
};
}
#[cfg(feature = "entangled")]
@ -578,23 +631,41 @@ impl<const GAMMA: u8> TaggingKey<{ GAMMA }> {
tagging_key_precomputes.push(precompute);
}
let config = brute_force::Config::default();
// generate some random points...
let f = |z: &Scalar| {
loop {
let z = Scalar::random(rng);
let w = g.mul(z);
let pre_h = RootSecret::<GAMMA>::pre_h(u, w);
let mut key = vec![];
let mut nonces = vec![];
for (i, precompute) in tagging_key_precomputes[0].iter().enumerate() {
let k_i = RootSecret::<GAMMA>::post_h(pre_h.clone(), *precompute);
if i < length {
for precompute in tagging_key_precomputes.iter().skip(1) {
let n_k_i = RootSecret::<GAMMA>::post_h(pre_h.clone(), precompute[i]);
if k_i != n_k_i {
return None;
for _nonce_attmepts in 0..=255 {
let nonce: u8 = rng.gen();
let mut success = true;
let nonce_h = RootSecret::<GAMMA>::nonce_h(pre_h.clone(), nonce);
let k_i = RootSecret::<GAMMA>::post_h(nonce_h.clone(), *precompute);
if i < length {
for precompute in tagging_key_precomputes.iter().skip(1) {
let n_k_i = RootSecret::<GAMMA>::post_h(nonce_h.clone(), precompute[i]);
if k_i != n_k_i {
success = false;
break;
}
}
}
key.push(k_i)
if success {
nonces.push(nonce);
key.push(k_i);
break;
}
}
continue;
}
// generate the tag
@ -606,11 +677,17 @@ impl<const GAMMA: u8> TaggingKey<{ GAMMA }> {
}
// This is the same as generate_tag, kept separate to avoid over-decomposition
let m = RootSecret::<GAMMA>::g(u, &ciphertexts);
let mut bitvec = BitVec::from_bytes(&*nonces);
bitvec.append(ciphertexts.clone().borrow_mut());
let m = RootSecret::<GAMMA>::g(u, &bitvec);
let y = r.invert().mul(z.sub(m));
return Some(Tag { u, y, ciphertexts });
};
brute_force(config, adaptors::auto_advance(f))
return Tag {
u,
y,
z: nonces,
ciphertexts,
};
}
}
}
@ -661,7 +738,7 @@ mod tests {
// Test some bincode...
let bincode_tag = bincode::serialize(&tag);
// println!("Serialized: {:?}", bincode_tag);
// println!("Serialized: {:?}", bincode_tag);
let deserialized_tag: Tag<24> = bincode::deserialize(&bincode_tag.unwrap()).unwrap();
//println!("Deserialized: {}", deserialized_tag);
//assert_eq!(tag.compress(), deserialized_tag.compress());
@ -697,7 +774,7 @@ mod tests {
// it takes ~2 minutes on a standard desktop to find a length=24 match for 2 parties, so for testing let's keep things light
let len = 16;
let entangled_tag = TaggingKey::generate_entangled_tag(tagging_keys, &mut rng, len);
println!("{}", entangled_tag);
println!("entangled tag: {}", entangled_tag);
for secret in secrets.iter() {
let detection_key = secret.extract_detection_key(len);
assert!(detection_key.test_tag(&entangled_tag));
@ -710,26 +787,26 @@ mod tests {
fn test_check_multiple() {
use crate::TaggingKey;
let mut rng = OsRng;
let secrets: Vec<RootSecret<24>> = (0..2)
let secrets: Vec<RootSecret<24>> = (0..5)
.map(|_x| RootSecret::<24>::generate(&mut rng))
.collect();
let tagging_keys: Vec<TaggingKey<24>> = secrets.iter().map(|x| x.tagging_key()).collect();
// it takes ~2 minutes on a standard desktop to find a length=24 match for 2 parties, so for testing let's keep things light
let entangled_tag = TaggingKey::generate_entangled_tag(tagging_keys, &mut rng, 16);
let entangled_tag = TaggingKey::generate_entangled_tag(tagging_keys, &mut rng, 24);
let detection_keys = secrets
.iter()
.map(|x| x.extract_detection_key(16))
.collect();
let results = DetectionKey::test_tag_bulk(&detection_keys, &entangled_tag);
assert_eq!(results.len(), 2);
assert_eq!(results.len(), 5);
}
#[test]
fn correctness() {
let number_of_messages = 100;
let mut rng = OsRng;
let secret = RootSecret::<16>::generate(&mut rng);
let secret = RootSecret::<24>::generate(&mut rng);
for i in 0..number_of_messages {
let tag = secret.tagging_key().generate_tag(&mut rng);
println!("{}: {}", i, tag);
@ -741,6 +818,7 @@ mod tests {
let tag = Tag {
u: RistrettoPoint::default(),
y: Scalar::default(),
z: vec![],
ciphertexts: BitVec::from_elem(24, false),
};
tag
@ -750,6 +828,7 @@ mod tests {
let mut tag = Tag {
u: RistrettoPoint::default(),
y: Scalar::default(),
z: vec![],
ciphertexts: BitVec::from_elem(24, false),
};
tag.ciphertexts.set_all();
@ -798,21 +877,37 @@ mod tests {
#[test]
fn false_positives() {
let mut rng = OsRng;
let number_of_messages = 1000;
let secret = RootSecret::<24>::generate(&mut rng);
let number_of_reference_checks = 100;
let number_of_messages = 100;
let limit = 1;
let mut false_positives = 0;
for _i in 0..number_of_messages {
let secret2 = RootSecret::<24>::generate(&mut rng);
let tag = secret2.tagging_key().generate_tag(&mut rng);
assert!(secret2.extract_detection_key(3).test_tag(&tag));
if secret.extract_detection_key(3).test_tag(&tag) == true {
false_positives += 1;
let total_attempts = number_of_reference_checks * number_of_messages;
let secret = RootSecret::<24>::generate(&mut rng);
for _i in 0..number_of_reference_checks {
let secret = RootSecret::<24>::generate(&mut rng);
for _i in 0..number_of_messages {
let secret2 = RootSecret::<24>::generate(&mut rng);
let tag = secret2.tagging_key().generate_tag(&mut rng);
assert!(secret2.extract_detection_key(limit).test_tag(&tag));
if secret.extract_detection_key(limit).test_tag(&tag) == true {
false_positives += 1;
}
}
}
println!(
"Expected False Positive Rate: {}\nActual False Positive Rate: {}",
secret.extract_detection_key(3).false_positive_probability(),
(false_positives as f64 / number_of_messages as f64)
secret
.extract_detection_key(limit)
.false_positive_probability(),
(false_positives as f64 / (total_attempts) as f64)
);
assert!(
secret
.extract_detection_key(limit)
.false_positive_probability()
- (false_positives as f64 / (total_attempts) as f64)
< 0.01
);
}
}