Skip to content

Commit 6aedc3c

Browse files
committed
Helpers used by the AVID/AVSS PR: ecies, nodes, RS
Splits the support-module changes out of the batch_avss PR (#954). No protocol changes here: - Cargo.toml: drop serde-big-array (no longer used), add reed-solomon-erasure for the RS code. - ecies_v1.rs: small additions used by the AVSS complaint flow, with a new test. - nodes.rs: prune unused methods; nodes_tests.rs follows. - reed_solomon.rs: extend the RS wrapper used by AVID dispersal, with doc comments on encode/decode.
1 parent b7c6bbe commit 6aedc3c

6 files changed

Lines changed: 305 additions & 272 deletions

File tree

fastcrypto-tbls/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ zeroize.workspace = true
2323
itertools = "0.10.5"
2424
hex = "0.4.3"
2525
tap = { version = "1.0.1", features = [] }
26-
serde-big-array = "0.5.1"
26+
reed-solomon-erasure = "6.0.0"
2727

2828
[dev-dependencies]
2929
criterion = "0.5.1"

fastcrypto-tbls/src/ecies_v1.rs

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use fastcrypto::error::{FastCryptoError, FastCryptoResult};
88
use fastcrypto::groups::{FiatShamirChallenge, GroupElement, HashToGroupElement, Scalar};
99
use fastcrypto::traits::{AllowedRng, ToFromBytes};
1010
use serde::{de::DeserializeOwned, Deserialize, Serialize};
11+
use std::marker::PhantomData;
1112
use typenum::consts::{U16, U32};
1213
use typenum::Unsigned;
1314
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -45,10 +46,23 @@ pub const AES_KEY_LENGTH: usize = 32;
4546
pub struct MultiRecipientEncryption<G: GroupElement> {
4647
c: G,
4748
c_hat: G,
48-
encs: Vec<Vec<u8>>,
49+
pub(crate) encs: Vec<Vec<u8>>,
4950
proof: DdhTupleNizk<G>,
5051
}
5152

53+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54+
pub struct SharedComponents<G: GroupElement> {
55+
c: G,
56+
c_hat: G,
57+
proof: DdhTupleNizk<G>,
58+
}
59+
60+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61+
pub struct EncryptedPart<G> {
62+
enc: Vec<u8>,
63+
g: PhantomData<G>,
64+
}
65+
5266
impl<G: GroupElement + Serialize> MultiRecipientEncryption<G>
5367
where
5468
<G as GroupElement>::ScalarType: FiatShamirChallenge + Zeroize,
@@ -194,6 +208,24 @@ where
194208
&self.proof
195209
}
196210

211+
pub fn into_parts(self) -> (SharedComponents<G>, Vec<Vec<u8>>) {
212+
let MultiRecipientEncryption {
213+
c,
214+
c_hat,
215+
encs,
216+
proof,
217+
} = self;
218+
(SharedComponents { c, c_hat, proof }, encs)
219+
}
220+
221+
pub fn shared(&self) -> SharedComponents<G> {
222+
SharedComponents {
223+
c: self.c,
224+
c_hat: self.c_hat,
225+
proof: self.proof.clone(),
226+
}
227+
}
228+
197229
fn encs_random_oracle(encryption_random_oracle: &RandomOracle) -> RandomOracle {
198230
encryption_random_oracle.extend("encs")
199231
}
@@ -229,6 +261,92 @@ fn sym_cipher(k: &[u8; 64]) -> Aes256Ctr {
229261
)
230262
}
231263

264+
impl<G: GroupElement + Serialize> SharedComponents<G>
265+
where
266+
<G as GroupElement>::ScalarType: FiatShamirChallenge + Zeroize,
267+
G: HashToGroupElement,
268+
{
269+
pub fn decrypt(
270+
&self,
271+
enc: &[u8],
272+
sk: &PrivateKey<G>,
273+
encryption_random_oracle: &RandomOracle,
274+
receiver_index: usize,
275+
) -> Vec<u8> {
276+
let enc_ro = MultiRecipientEncryption::<G>::encs_random_oracle(encryption_random_oracle);
277+
let ephemeral_key = self.c * sk.0;
278+
let k = enc_ro.evaluate(&(receiver_index, ephemeral_key));
279+
let cipher = sym_cipher(&k);
280+
cipher
281+
.decrypt(&fixed_zero_nonce(), enc)
282+
.expect("Decrypt should never fail for CTR mode")
283+
}
284+
285+
pub fn ephemeral_key(&self) -> &G {
286+
&self.c
287+
}
288+
289+
pub fn verify(&self, encryption_random_oracle: &RandomOracle) -> FastCryptoResult<()> {
290+
let g_hat = G::hash_to_group_element(
291+
&MultiRecipientEncryption::<G>::g_hat_random_oracle(encryption_random_oracle)
292+
.evaluate(&self.c),
293+
);
294+
self.proof.verify(
295+
&g_hat,
296+
&self.c,
297+
&self.c_hat,
298+
&MultiRecipientEncryption::<G>::zk_random_oracle(encryption_random_oracle),
299+
)
300+
}
301+
302+
pub fn create_recovery_package<R: AllowedRng>(
303+
&self,
304+
sk: &PrivateKey<G>,
305+
recovery_random_oracle: &RandomOracle,
306+
rng: &mut R,
307+
) -> RecoveryPackage<G> {
308+
let pk = G::generator() * sk.0;
309+
let ephemeral_key = self.c * sk.0;
310+
311+
let proof = DdhTupleNizk::<G>::create(
312+
&sk.0,
313+
&self.c,
314+
&pk,
315+
&ephemeral_key,
316+
recovery_random_oracle,
317+
rng,
318+
);
319+
320+
RecoveryPackage {
321+
ephemeral_key,
322+
proof,
323+
}
324+
}
325+
326+
pub fn decrypt_with_recovery_package(
327+
&self,
328+
enc: &[u8],
329+
pkg: &RecoveryPackage<G>,
330+
recovery_random_oracle: &RandomOracle,
331+
encryption_random_oracle: &RandomOracle,
332+
receiver_pk: &PublicKey<G>,
333+
receiver_index: usize,
334+
) -> FastCryptoResult<Vec<u8>> {
335+
pkg.proof.verify(
336+
&self.c,
337+
&receiver_pk.0,
338+
&pkg.ephemeral_key,
339+
recovery_random_oracle,
340+
)?;
341+
let encs_ro = MultiRecipientEncryption::<G>::encs_random_oracle(encryption_random_oracle);
342+
let k = encs_ro.evaluate(&(receiver_index, pkg.ephemeral_key));
343+
let cipher = sym_cipher(&k);
344+
Ok(cipher
345+
.decrypt(&fixed_zero_nonce(), enc)
346+
.expect("Decrypt should never fail for CTR mode"))
347+
}
348+
}
349+
232350
impl<G> PrivateKey<G>
233351
where
234352
G: GroupElement + Serialize,

fastcrypto-tbls/src/nodes.rs

Lines changed: 23 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::types::ShareIndex;
66
use fastcrypto::error::{FastCryptoError, FastCryptoResult};
77
use fastcrypto::groups::GroupElement;
88
use fastcrypto::hash::{Blake2b256, Digest, HashFunction};
9+
use itertools::Itertools;
910
use serde::{Deserialize, Serialize};
1011
use tracing::debug;
1112

@@ -157,6 +158,28 @@ impl<G: GroupElement + Serialize> Nodes<G> {
157158
hash.finalize()
158159
}
159160

161+
/// Given an iterator over a set of items, one per share index, this function groups them into
162+
/// a vector of vectors, one per node, according to the share ids of the nodes.
163+
/// Returns error if the number of items does not match the total weight.
164+
pub fn collect_to_nodes<T>(
165+
&self,
166+
items: impl ExactSizeIterator<Item = T>,
167+
) -> FastCryptoResult<Vec<Vec<T>>> {
168+
if items.len() != self.total_weight as usize {
169+
return Err(FastCryptoError::InvalidInput);
170+
}
171+
let mut items = items;
172+
Ok(self
173+
.node_ids_iter()
174+
.map(|id| {
175+
items
176+
.by_ref()
177+
.take(self.weight_of(id).unwrap() as usize)
178+
.collect_vec()
179+
})
180+
.collect_vec())
181+
}
182+
160183
/// Create a new set of nodes. Nodes must have consecutive ids starting from 0.
161184
/// Reduces weights up to an allowed delta in the original total weight.
162185
/// Finds the largest d such that:
@@ -292,132 +315,9 @@ impl<G: GroupElement + Serialize> Nodes<G> {
292315
new_f,
293316
))
294317
}
295-
296-
/// Create a new set of nodes. Nodes must have consecutive ids starting from 0.
297-
/// Like [`Nodes::new_reduced_with_f`], but in addition to the integer divisor
298-
/// sweep, fine-sweeps the unit interval above the first feasible integer at
299-
/// granularity 0.01 to find a (possibly strictly larger) fractional divisor d.
300-
/// Finds the largest d such that:
301-
/// - The new threshold is ceil(t / d)
302-
/// - The new threshold for Byzantine parties is ceil(f / d)
303-
/// - The new weights are all divided by d (floor division)
304-
/// - The precision loss, counted as the sum of remainders Σ_i (w_i mod d) plus the
305-
/// ceiling overheads (-t) mod d and (-f) mod d, is at most the allowed delta
306-
///
307-
/// Operates on a 0.01-multiple grid for d (represented internally in u64
308-
/// arithmetic via d_x100 = 100*d). The Stage-1 criterion is the natural
309-
/// fractional-d extension of the one in `new_reduced_with_f`, with
310-
/// (w mod d) := w - floor(w/d) * d ∈ [0, d) for any real d > 0; the
311-
/// Safety, Liveness, and Byzantine-removal proofs go through verbatim.
312-
/// Since prop_reduce considers a strict superset of `new_reduced_with_f`'s
313-
/// candidates, its reduced total weight (and ceilings t', f') are always
314-
/// ≤ those of `new_reduced_with_f`.
315-
///
316-
/// In practice, allowed delta will be the extra liveness we would assume above 2f+1.
317-
///
318-
/// total_weight_lower_bound allows limiting the level of reduction (e.g., in benchmarks). To
319-
/// get the best results, set it to 1.
320-
pub fn prop_reduce(
321-
nodes_vec: Vec<Node<G>>,
322-
t: u16,
323-
f: u16,
324-
allowed_delta: u16,
325-
total_weight_lower_bound: u16,
326-
) -> FastCryptoResult<(Self, u16, u16)> {
327-
let n = Self::new(nodes_vec)?; // checks the input, etc
328-
assert!(total_weight_lower_bound <= n.total_weight && total_weight_lower_bound > 0);
329-
let allowed_delta_x100 = (allowed_delta as u64) * 100;
330-
let mut max_d_x100: u32 = 100; // d = 1, no reduction
331-
// Sweep d downward from 40 to 2. Going down, the first feasible integer
332-
// is the largest feasible integer divisor (the criterion is non-monotone
333-
// in d, so an upward sweep cannot break early). After locking onto the
334-
// first feasible integer d, fine-sweep (d, d+1) at 0.01 in decreasing
335-
// order to find the largest feasible fractional divisor in that interval.
336-
'outer: for d in (2u16..=40).rev() {
337-
// Continue if we are below the lower bound. (Going down, W' grows as
338-
// d shrinks, so once W' clears the lower bound it stays cleared.)
339-
// U16 is safe here since total_weight is u16.
340-
let new_total_weight = n.nodes.iter().map(|n| n.weight / d).sum::<u16>();
341-
if new_total_weight < total_weight_lower_bound {
342-
continue;
343-
}
344-
// Compute the precision loss at integer d.
345-
// U16 is safe here since total_weight is u16.
346-
let delta =
347-
n.nodes.iter().map(|n| n.weight % d).sum::<u16>() + neg_mod(t, d) + neg_mod(f, d);
348-
if delta > allowed_delta {
349-
continue;
350-
}
351-
// Integer d is feasible; lock it as the fallback.
352-
max_d_x100 = (d as u32) * 100;
353-
// Fine-sweep (d, d+1) at 0.01, largest-first. The first hit is the
354-
// largest feasible 0.01-multiple in that interval.
355-
for k in (1..100u32).rev() {
356-
let d_x100 = (d as u32) * 100 + k;
357-
let new_w_total = n
358-
.nodes
359-
.iter()
360-
.map(|n| ((n.weight as u32) * 100) / d_x100)
361-
.sum::<u32>();
362-
if (new_w_total as u16) < total_weight_lower_bound {
363-
continue;
364-
}
365-
// Σ_i (w_i mod d) * 100 = W*100 − W' * d_x100 (telescopes by floor).
366-
let sum_mod_x100 =
367-
(n.total_weight as u64) * 100 - (new_w_total as u64) * (d_x100 as u64);
368-
let delta_x100 = sum_mod_x100 + neg_mod_x100(t, d_x100) + neg_mod_x100(f, d_x100);
369-
if delta_x100 <= allowed_delta_x100 {
370-
max_d_x100 = d_x100;
371-
break;
372-
}
373-
}
374-
break 'outer;
375-
}
376-
debug!(
377-
"Nodes::prop_reduce reducing from {} with max_d_x100 {}, allowed_delta {}, total_weight_lower_bound {}",
378-
n.total_weight, max_d_x100, allowed_delta, total_weight_lower_bound
379-
);
380-
381-
let nodes = n
382-
.nodes
383-
.iter()
384-
.map(|n| Node {
385-
id: n.id,
386-
pk: n.pk.clone(),
387-
weight: (((n.weight as u32) * 100) / max_d_x100) as u16,
388-
})
389-
.collect::<Vec<_>>();
390-
let accumulated_weights = Self::get_accumulated_weights(&nodes);
391-
let nodes_with_nonzero_weight = Self::filter_nonzero_weights(&nodes);
392-
// U16 is safe here since the original total_weight is u16.
393-
let total_weight = nodes.iter().map(|n| n.weight).sum::<u16>();
394-
let new_t = ((t as u64) * 100).div_ceil(max_d_x100 as u64) as u16;
395-
let new_f = ((f as u64) * 100).div_ceil(max_d_x100 as u64) as u16;
396-
Ok((
397-
Self {
398-
nodes,
399-
total_weight,
400-
accumulated_weights,
401-
nodes_with_nonzero_weight,
402-
},
403-
new_t,
404-
new_f,
405-
))
406-
}
407318
}
408319

409320
/// Compute (-x) mod d = d * ceil(x/d) - x
410321
fn neg_mod(x: u16, d: u16) -> u16 {
411322
(-(x as i32)).rem_euclid(d as i32) as u16
412323
}
413-
414-
/// Compute ((-w) mod d) * 100 for the possibly-fractional divisor d = d_x100 / 100.
415-
/// Equals (ceil(w/d) * d - w) * 100, a non-negative integer in [0, d_x100).
416-
fn neg_mod_x100(w: u16, d_x100: u32) -> u64 {
417-
let r = ((w as u64) * 100) % (d_x100 as u64);
418-
if r == 0 {
419-
0
420-
} else {
421-
(d_x100 as u64) - r
422-
}
423-
}

fastcrypto-tbls/src/tests/ecies_v1_tests.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ mod point_tests {
5959
assert_eq!(msg.as_bytes(), &decrypted);
6060
}
6161

62+
let (common, parts) = mr_enc.clone().into_parts();
63+
assert!(common.verify(&ro).is_ok());
64+
for (i, (part, (sk, _, msg))) in parts.iter().zip(keys_and_msg.iter()).enumerate() {
65+
// Using parts should work as well
66+
assert_eq!(msg.as_bytes(), common.decrypt(part, sk, &ro, i));
67+
}
68+
6269
// test empty messages
6370
let mr_enc2 = MultiRecipientEncryption::encrypt(
6471
&keys_and_msg

0 commit comments

Comments
 (0)