Skip to content

Commit

Permalink
Merge pull request #5897 from oasisprotocol/peternose/internal/verify…
Browse files Browse the repository at this point in the history
…-bp-leading-coefficients

secret-sharing/src/churp/switch: Verify combined bivariate polynomial
  • Loading branch information
kostko authored Oct 14, 2024
2 parents f67c39b + 30bb325 commit 3f241f7
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 176 deletions.
7 changes: 7 additions & 0 deletions .changelog/5897.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
secret-sharing/src/churp/switch: Verify combined bivariate polynomial

After all bivariate shares are collected and the switch either
creates a new shareholder or proactivates the share of an existing
one, the new share should be verified to ensure that the verification
matrix of the combined bivariate polynomial satisfies the non-zero
leading term requirements.
7 changes: 5 additions & 2 deletions keymanager/src/churp/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ impl<S: Suite> Instance<S> {
/// be removed, its bivariate polynomial overwritten, and permanently
/// lost.
///
/// Note that since the host controls the local storage, he can restart
/// Warning: Since the host controls the local storage, he can restart
/// the enclave to create multiple dealers for the same epoch and then
/// replace the last backup with a bivariate polynomial from a dealer
/// of his choice. Therefore, it is essential to verify the bivariate
Expand All @@ -866,7 +866,10 @@ impl<S: Suite> Instance<S> {
dealing_phase: bool,
) -> Result<Arc<Dealer<S::Group>>> {
// Create a new dealer.
let dealer = Dealer::create(threshold, dealing_phase, &mut OsRng)?;
let dealer = match dealing_phase {
true => Dealer::new(threshold, &mut OsRng),
false => Dealer::new_proactive(threshold, &mut OsRng),
}?;
let dealer = Arc::new(dealer);

// Encrypt and store the polynomial in case of a restart.
Expand Down
254 changes: 191 additions & 63 deletions secret-sharing/src/churp/dealer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! CHURP dealer.

use anyhow::Result;
use group::{Group, GroupEncoding};
use group::{ff::Field, Group, GroupEncoding};
use rand_core::RngCore;

use crate::{poly::BivariatePolynomial, vss::VerificationMatrix};
Expand All @@ -27,35 +27,65 @@ impl<G> Dealer<G>
where
G: Group + GroupEncoding,
{
/// Creates a new dealer based on the provided parameters.
/// Creates a new dealer of secret bivariate shares, which can be used
/// to recover a randomly selected shared secret.
///
/// A dealer for the dealing phase uses a random bivariate polynomial,
/// otherwise, it uses a zero-holed bivariate polynomial.
pub fn create(threshold: u8, dealing_phase: bool, rng: &mut impl RngCore) -> Result<Self> {
match dealing_phase {
true => Self::random(threshold, rng),
false => Self::zero_hole(threshold, rng),
}
}

/// Creates a new dealer with a predefined shared secret.
pub fn new(threshold: u8, secret: G::Scalar, rng: &mut impl RngCore) -> Result<Self> {
let mut bp = Self::random_bivariate_polynomial(threshold, rng)?;
let updated = bp.set_coefficient(0, 0, secret);
debug_assert!(updated);
/// The dealer uses a random bivariate polynomial `B(x, y)` to generate
/// full and reduced bivariate shares, i.e., `B(ID, y)` and `B(x, ID)`,
/// where `ID` represents the identity of a participant, respectively.
///
/// To ensure that the full and reduced bivariate shares form
/// a (t, n)-sharing and a (2t, n)-sharing of the secret `B(0, 0)`,
/// respectively, the bivariate polynomial is selected such that
/// the polynomials `B(x, y)`, `B(x, 0)`, and `B(0, y)` have non-zero
/// leading terms. This ensures that more than the threshold number
/// of full shares, and more than twice the threshold number of reduced
/// shares, are required to reconstruct the secret.
///
/// Warning: If multiple dealers are used to generate the shared secret,
/// it is essential to verify that the combined bivariate polynomial
/// also satisfies the aforementioned non-zero leading term requirements.
///
/// This function is not constant time because it uses rejection sampling.
pub fn new(threshold: u8, rng: &mut impl RngCore) -> Result<Self> {
let bp = Self::generate_bivariate_polynomial(threshold, rng)?;
Ok(bp.into())
}

/// Creates a new dealer with a random bivariate polynomial.
pub fn random(threshold: u8, rng: &mut impl RngCore) -> Result<Self> {
let bp = Self::random_bivariate_polynomial(threshold, rng)?;
/// Creates a new dealer of secret proactive bivariate shares, which can
/// be used to randomize a shared secret.
///
/// The dealer uses a random zero-hole bivariate polynomial `B(x, y)`
/// to generate full and reduced proactive bivariate shares, i.e.,
/// `B(ID, y)` and `B(x, ID)`, where `ID` represents the identity
/// of a participant. Since `B(0, 0) = 0`, adding a proactive share
/// to an existing share does not change the shared secret.
///
/// Warning: If one or more proactive dealers are used to randomize
/// the shared secret, it is essential to verify that the combined
/// bivariate polynomial still satisfies the non-zero leading term
/// requirements.
///
/// This function is not constant time because it uses rejection sampling.
pub fn new_proactive(threshold: u8, rng: &mut impl RngCore) -> Result<Self> {
let mut bp = Self::generate_bivariate_polynomial(threshold, rng)?;
bp.to_zero_hole();
Ok(bp.into())
}

/// Creates a new dealer with a random zero-hole bivariate polynomial.
pub fn zero_hole(threshold: u8, rng: &mut impl RngCore) -> Result<Self> {
let mut bp = Self::random_bivariate_polynomial(threshold, rng)?;
bp.to_zero_hole();
/// Creates a new dealer of secret bivariate shares, which can be used
/// to recover a predefined shared secret.
///
/// This function is not constant time because it uses rejection sampling.
#[cfg(test)]
pub fn new_with_secret(
threshold: u8,
secret: G::Scalar,
rng: &mut impl RngCore,
) -> Result<Self> {
let mut bp = Self::generate_bivariate_polynomial(threshold, rng)?;
let updated = bp.set_coefficient(0, 0, secret);
debug_assert!(updated);
Ok(bp.into())
}

Expand Down Expand Up @@ -89,15 +119,41 @@ where
SecretShare::new(x, p)
}

/// Returns a random bivariate polynomial.
fn random_bivariate_polynomial(
/// Generates a random bivariate polynomial `B(x, y)` such that
/// the polynomials `B(x, y)`, `B(x, 0)`, and `B(0, y)` have non-zero
/// leading term, and the secret `B(0, 0)` is non-zero.
///
/// This function is not constant time because it uses rejection
/// sampling to ensure that the polynomials have the maximum degree.
/// Additionally, the underlying prime field implementation may also
/// use rejection sampling to generate uniformly random elements.
fn generate_bivariate_polynomial(
threshold: u8,
rng: &mut impl RngCore,
) -> Result<BivariatePolynomial<G::Scalar>> {
let deg_x = threshold;
let deg_y = threshold.checked_mul(2).ok_or(Error::ThresholdTooLarge)?;
let bp = BivariatePolynomial::random(deg_x, deg_y, rng);
Ok(bp)

// When using a random RNG and a large prime field, this loop
// should execute once with an extremely high probability,
// so there is no need to optimize it by randomly selecting
// only the problematic coefficients.
loop {
let bp = BivariatePolynomial::<G::Scalar>::random(deg_x, deg_y, rng);

let i = deg_x as usize;
let j = deg_y as usize;
let is_zero_00 = bp.coefficient(0, 0).unwrap().is_zero();
let is_zero_xy = bp.coefficient(i, j).unwrap().is_zero();
let is_zero_x0 = bp.coefficient(i, 0).unwrap().is_zero();
let is_zero_0y = bp.coefficient(0, j).unwrap().is_zero();

if (is_zero_00 | is_zero_xy | is_zero_x0 | is_zero_0y).into() {
continue;
}

return Ok(bp);
}
}
}

Expand All @@ -114,7 +170,7 @@ where

#[cfg(test)]
mod tests {
use rand::{rngs::StdRng, SeedableRng};
use rand::{rngs::StdRng, Error, RngCore, SeedableRng};

use super::{BivariatePolynomial, HandoffKind};

Expand All @@ -123,74 +179,77 @@ mod tests {
type Dealer = super::Dealer<Group>;

#[test]
fn test_create() {
fn test_new() {
let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]);

let test_cases = vec![
// Zero threshold.
(0, 0, 0, 1, 1),
// Non-zero threshold.
(2, 2, 4, 3, 5),
(0, 0, 0, 1, 1), // Zero threshold.
(2, 2, 4, 3, 5), // Non-zero threshold.
];

for (threshold, deg_x, deg_y, rows, cols) in test_cases {
for dealing_phase in vec![true, false] {
let dealer = Dealer::create(threshold, dealing_phase, &mut rng).unwrap();
assert_eq!(dealer.bivariate_polynomial().deg_x, deg_x);
assert_eq!(dealer.bivariate_polynomial().deg_y, deg_y);
assert_eq!(dealer.verification_matrix().rows, rows);
assert_eq!(dealer.verification_matrix().cols, cols);
assert_eq!(dealer.verification_matrix().is_zero_hole(), !dealing_phase);
}
let dealer = Dealer::new(threshold, &mut rng).unwrap();
assert_eq!(dealer.bivariate_polynomial().deg_x, deg_x);
assert_eq!(dealer.bivariate_polynomial().deg_y, deg_y);
assert_eq!(dealer.verification_matrix().rows, rows);
assert_eq!(dealer.verification_matrix().cols, cols);
assert_ne!(
dealer.bivariate_polynomial().coefficient(0, 0), // Zero with negligible probability.
Some(&PrimeField::ZERO)
);
}
}

#[test]
fn test_new() {
fn test_new_proactive() {
let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]);

let tcs = vec![
// Zero threshold.
(0, 0, 0, 0, 1, 1),
// Non-zero threshold.
(100, 2, 2, 4, 3, 5),
let test_cases = vec![
(0, 0, 0, 1, 1), // Zero threshold.
(2, 2, 4, 3, 5), // Non-zero threshold.
];

for (secret, threshold, deg_x, deg_y, rows, cols) in tcs {
let secret = PrimeField::from_u64(secret);
let dealer = Dealer::new(threshold, secret, &mut rng).unwrap();
for (threshold, deg_x, deg_y, rows, cols) in test_cases {
let dealer = Dealer::new_proactive(threshold, &mut rng).unwrap();
assert_eq!(dealer.bivariate_polynomial().deg_x, deg_x);
assert_eq!(dealer.bivariate_polynomial().deg_y, deg_y);
assert_eq!(dealer.verification_matrix().rows, rows);
assert_eq!(dealer.verification_matrix().cols, cols);
assert_eq!(
dealer.bivariate_polynomial().coefficient(0, 0),
Some(&secret)
Some(&PrimeField::ZERO)
);
}
}

#[test]
fn test_random() {
let threshold = 2;
fn test_new_with_secret() {
let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]);
let dealer = Dealer::random(threshold, &mut rng).unwrap();
assert!(!dealer.verification_matrix().is_zero_hole()); // Zero-hole with negligible probability.
}

#[test]
fn test_zero_hole() {
let threshold = 2;
let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]);
let dealer = Dealer::zero_hole(threshold, &mut rng).unwrap();
assert!(dealer.verification_matrix().is_zero_hole());
let test_cases = vec![
(0, 0, 0, 1, 1, 0), // Zero threshold.
(2, 2, 4, 3, 5, 100), // Non-zero threshold.
];

for (threshold, deg_x, deg_y, rows, cols, secret) in test_cases {
let secret = PrimeField::from_u64(secret);
let dealer = Dealer::new_with_secret(threshold, secret, &mut rng).unwrap();
assert_eq!(dealer.bivariate_polynomial().deg_x, deg_x);
assert_eq!(dealer.bivariate_polynomial().deg_y, deg_y);
assert_eq!(dealer.verification_matrix().rows, rows);
assert_eq!(dealer.verification_matrix().cols, cols);
assert_eq!(
dealer.bivariate_polynomial().coefficient(0, 0),
Some(&secret)
);
}
}

#[test]
fn test_make_share() {
let threshold = 2;
let mut rng: StdRng = SeedableRng::from_seed([1u8; 32]);
let dealer = Dealer::random(threshold, &mut rng).unwrap();
let dealer = Dealer::new(threshold, &mut rng).unwrap();
let x = PrimeField::from_u64(2);

let test_cases = vec![
Expand All @@ -205,6 +264,75 @@ mod tests {
}
}

#[test]
fn test_generate_bivariate_polynomial() {
/// A custom RNG that fills the first few slices with zeros,
/// and subsequent slices with ones.
struct ZeroOneRng {
/// Tracks how many times the RNG has been called.
counter: usize,
/// The number of slices that should be filled with zeros.
limit: usize,
}

impl ZeroOneRng {
/// Creates a new generator with the given limit.
fn new(limit: usize) -> Self {
Self { limit, counter: 0 }
}

// Returns the total number of times the generator has been invoked.
fn total(&self) -> usize {
self.counter
}
}

impl RngCore for ZeroOneRng {
fn next_u32(&mut self) -> u32 {
panic!("not implemented")
}

fn next_u64(&mut self) -> u64 {
panic!("not implemented")
}

fn try_fill_bytes(&mut self, _dest: &mut [u8]) -> Result<(), Error> {
panic!("not implemented")
}

fn fill_bytes(&mut self, dest: &mut [u8]) {
match self.counter < self.limit {
true => dest.fill(0),
false => dest.fill(1),
}
self.counter += 1;
}
}

let test_cases = vec![0, 2, 4];

for threshold in test_cases {
// Prepare RNG that will generate the first two bivariate polynomials
// with all coefficients set to zero.
let num_terms = (threshold + 1) * (2 * threshold + 1);
let mut rng = ZeroOneRng::new(2 * num_terms);

// Generate a random bivariate polynomial and verify leading coefficients.
let bp = Dealer::generate_bivariate_polynomial(threshold as u8, &mut rng).unwrap();
let f = bp.eval_y(&PrimeField::ZERO);
let g = bp.eval_x(&PrimeField::ZERO);
let i = threshold;
let j = 2 * threshold;

assert!(!Into::<bool>::into(bp.coefficient(i, j).unwrap().is_zero()));
assert!(!Into::<bool>::into(f.coefficient(i).unwrap().is_zero()));
assert!(!Into::<bool>::into(g.coefficient(j).unwrap().is_zero()));

// Verify that the RNG generated coefficients for three polynomials.
assert_eq!(3 * num_terms, rng.total());
}
}

#[test]
fn test_from() {
let bp = BivariatePolynomial::zero(2, 3);
Expand Down
4 changes: 4 additions & 0 deletions secret-sharing/src/churp/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub enum Error {
InvalidKind,
#[error("invalid polynomial")]
InvalidPolynomial,
#[error("insecure bivariate polynomial")]
InsecureBivariatePolynomial,
#[error("invalid switch point")]
InvalidSwitchPoint,
#[error("invalid state")]
Expand All @@ -22,6 +24,8 @@ pub enum Error {
PolynomialDegreeMismatch,
#[error("shareholder encoding failed")]
ShareholderEncodingFailed,
#[error("shareholder proactivization already completed")]
ShareholderProactivizationCompleted,
#[error("shareholder identity mismatch")]
ShareholderIdentityMismatch,
#[error("shareholder identity required")]
Expand Down
Loading

0 comments on commit 3f241f7

Please sign in to comment.