Skip to content

Commit

Permalink
Add tests for HD derivation/keygen/signing
Browse files Browse the repository at this point in the history
  • Loading branch information
survived committed Jan 12, 2024
1 parent c86fc41 commit df05378
Show file tree
Hide file tree
Showing 12 changed files with 85,011 additions and 19,018 deletions.
49 changes: 49 additions & 0 deletions cggmp21/src/key_share.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,44 @@ impl<E: Curve> DirtyIncompleteKeyShare<E> {
}
Ok(())
}

/// Checks whether HD wallet support was enabled for this key
///
/// In order to generate an HD wallet, [`.hw_wallet(true)`](crate::keygen::GenericKeygenBuilder::hd_wallet)
/// needs to be set at key generation.
#[cfg(feature = "hd-wallets")]
pub fn is_hd_wallet(&self) -> bool {
self.chain_code.is_some()
}

/// Returns extended public key, if HD support was enabled
#[cfg(feature = "hd-wallets")]
pub fn extended_public_key(&self) -> Option<slip_10::ExtendedPublicKey<E>> {
Some(slip_10::ExtendedPublicKey {
public_key: self.shared_public_key,
chain_code: self.chain_code?,
})
}

/// Derives child public key, if it's HD key
#[cfg(feature = "hd-wallets")]
pub fn derive_child_public_key<ChildIndex>(
&self,
derivation_path: impl IntoIterator<Item = ChildIndex>,
) -> Result<
slip_10::ExtendedPublicKey<E>,
HdError<<ChildIndex as TryInto<slip_10::NonHardenedIndex>>::Error>,
>
where
slip_10::NonHardenedIndex: TryFrom<ChildIndex>,
{
let epub = self.extended_public_key().ok_or(HdError::DisabledHd)?;
slip_10::try_derive_child_public_key_with_path(
&epub,
derivation_path.into_iter().map(|index| index.try_into()),
)
.map_err(HdError::InvalidPath)
}
}

impl<L: SecurityLevel> AuxInfo<L> {
Expand Down Expand Up @@ -795,3 +833,14 @@ enum ReconstructErrorReason {
#[error("interpolation failed (seems like a bug)")]
Interpolation,
}

/// Error related to HD key derivation
#[derive(Debug, Error)]
pub enum HdError<E> {
/// HD derivation is disabled for the key
#[error("HD derivation is disabled for the key")]
DisabledHd,
/// Derivation path is not valid
#[error("derivation path is not valid")]
InvalidPath(#[source] E),
}
2 changes: 2 additions & 0 deletions cggmp21/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@
#![cfg_attr(not(test), forbid(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

#[cfg(feature = "hd-wallets")]
pub use slip_10;
pub use {
generic_ec, paillier_zk,
paillier_zk::{fast_paillier, rug},
Expand Down
29 changes: 9 additions & 20 deletions cggmp21/src/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,23 +296,21 @@ where
pub fn set_derivation_path<Index>(
mut self,
path: impl IntoIterator<Item = Index>,
) -> Result<Self, SetDerivationPathError<<Index as TryInto<slip_10::NonHardenedIndex>>::Error>>
) -> Result<Self, crate::key_share::HdError<<Index as TryInto<slip_10::NonHardenedIndex>>::Error>>
where
slip_10::NonHardenedIndex: TryFrom<Index>,
{
let mut public_key = slip_10::ExtendedPublicKey {
public_key: self.key_share.shared_public_key,
chain_code: self
.key_share
.chain_code
.ok_or(SetDerivationPathError::DisabledHd)?,
};
use crate::key_share::HdError;

let mut public_key = self
.key_share
.extended_public_key()
.ok_or(HdError::DisabledHd)?;
let mut additive_shift = Scalar::<E>::zero();

for child_index in path {
let child_index: slip_10::NonHardenedIndex = child_index
.try_into()
.map_err(SetDerivationPathError::InvalidPath)?;
let child_index: slip_10::NonHardenedIndex =
child_index.try_into().map_err(HdError::InvalidPath)?;
let shift = slip_10::derive_public_shift(&public_key, child_index);

additive_shift += shift.shift;
Expand Down Expand Up @@ -1395,15 +1393,6 @@ enum BugSource {
#[error("signature is not valid")]
pub struct InvalidSignature;

/// Error indicating why [`set_derivation_path`](SigningBuilder::set_derivation_path)
/// failed
pub enum SetDerivationPathError<E> {
/// HD derivation is disabled for the key
DisabledHd,
/// Derivation path is not valid
InvalidPath(E),
}

#[cfg(test)]
mod test {
fn read_write_signature<E: generic_ec::Curve>() {
Expand Down
8 changes: 1 addition & 7 deletions cggmp21/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,14 +322,8 @@ pub mod serde {
));
}

let bytes = hex::decode(v).map_err(E::custom)?;
hex::decode_to_slice(v, self.out.as_mut()).map_err(E::custom)?;

let out_len = self.out.as_mut().len();
if out_len != v.len() {
return Err(E::invalid_length(v.len(), &ExpectedLen(out_len)));
}

self.out.as_mut().copy_from_slice(&bytes);
Ok(self.out)
}
}
Expand Down
103,768 changes: 84,830 additions & 18,938 deletions test-data/precomputed_shares.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
cggmp21 = { path = "../cggmp21", features = ["all-curves", "spof"] }
cggmp21 = { path = "../cggmp21", features = ["all-curves", "spof", "hd-wallets"] }

anyhow = "1"
bpaf = "0.7"
Expand Down
36 changes: 22 additions & 14 deletions tests/src/bin/precompute_shares.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,28 @@ fn precompute_shares_for_curve<E: Curve, R: RngCore + CryptoRng>(
.into_iter()
.filter(|t| t.map(|t| t <= n).unwrap_or(true))
{
eprintln!("t={t:?},n={n},curve={}", E::CURVE_NAME);
let primes = std::iter::repeat_with(|| {
let p = generate_blum_prime(rng, SecurityLevel128::SECURITY_BITS * 4);
let q = generate_blum_prime(rng, SecurityLevel128::SECURITY_BITS * 4);
(p, q)
})
.take(n.into())
.collect();
let shares = trusted_dealer::builder::<E, SecurityLevel128>(n)
.set_threshold(t)
.set_pregenerated_primes(primes)
.generate_shares(rng)
.context("generate shares")?;
cache.add_shares(t, n, &shares).context("add shares")?;
for hd_enabled in [false, true] {
eprintln!(
"t={t:?},n={n},curve={},hd_enabled={hd_enabled}",
E::CURVE_NAME
);
let primes = std::iter::repeat_with(|| {
let p = generate_blum_prime(rng, SecurityLevel128::SECURITY_BITS * 4);
let q = generate_blum_prime(rng, SecurityLevel128::SECURITY_BITS * 4);
(p, q)
})
.take(n.into())
.collect();
let shares = trusted_dealer::builder::<E, SecurityLevel128>(n)
.set_threshold(t)
.set_pregenerated_primes(primes)
.hd_wallet(hd_enabled)
.generate_shares(rng)
.context("generate shares")?;
cache
.add_shares(t, n, hd_enabled, &shares)
.context("add shares")?;
}
}
}
Ok(())
Expand Down
13 changes: 11 additions & 2 deletions tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ impl PrecomputedKeyShares {
&self,
t: Option<u16>,
n: u16,
hd_enabled: bool,
) -> Result<Vec<KeyShare<E, L>>> {
let key_shares = self
.shares
.get(&format!("t={t:?},n={n},curve={}", E::CURVE_NAME))
.get(&Self::key::<E>(t, n, hd_enabled))
.context("shares not found")?;
serde_json::from_value(key_shares.clone()).context("parse key shares")
}
Expand All @@ -53,16 +54,24 @@ impl PrecomputedKeyShares {
&mut self,
t: Option<u16>,
n: u16,
hd_enabled: bool,
shares: &[KeyShare<E, L>],
) -> Result<()> {
if usize::from(n) != shares.len() {
bail!("expected {n} key shares, only {} provided", shares.len());
}
let key_shares = serde_json::to_value(shares).context("serialize shares")?;
self.shares
.insert(format!("t={t:?},n={n},curve={}", E::CURVE_NAME), key_shares);
.insert(Self::key::<E>(t, n, hd_enabled), key_shares);
Ok(())
}

fn key<E: Curve>(t: Option<u16>, n: u16, hd_enabled: bool) -> String {
format!(
"t={t:?},n={n},curve={},hd_wallet={hd_enabled}",
E::CURVE_NAME
)
}
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
Expand Down
8 changes: 6 additions & 2 deletions tests/tests/key_refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ mod generic {
let mut rng = rand_dev::DevRng::new();

let shares = cggmp21_tests::CACHED_SHARES
.get_shares::<E, SecurityLevel128>(None, n)
.get_shares::<E, SecurityLevel128>(None, n, true)
.expect("retrieve cached shares");
assert!(shares[0].chain_code.is_some());
let mut primes = cggmp21_tests::CACHED_PRIMES.iter();

// Perform refresh
Expand Down Expand Up @@ -73,6 +74,9 @@ mod generic {
shares[0].core.shared_public_key
);
}
for key_share in &key_shares {
assert_eq!(key_share.chain_code, shares[0].chain_code);
}

// attempt to sign with new shares and verify the signature

Expand Down Expand Up @@ -115,7 +119,7 @@ mod generic {
let mut rng = rand_dev::DevRng::new();

let shares = cggmp21_tests::CACHED_SHARES
.get_shares::<E, SecurityLevel128>(Some(t), n)
.get_shares::<E, SecurityLevel128>(Some(t), n, false)
.expect("retrieve cached shares");
let mut primes = cggmp21_tests::CACHED_PRIMES.iter();

Expand Down
48 changes: 35 additions & 13 deletions tests/tests/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,21 @@ mod generic {
type Share<E> = KeyShare<E>;
type Incomplete<E> = IncompleteKeyShare<E>;

#[test_case::case(2, 3; "t2n3")]
#[test_case::case(4, 7; "t4n7")]
#[test_case::case(2, 3, false; "t2n3")]
#[test_case::case(3, 5, false; "t3n5")]
#[test_case::case(3, 5, true; "t3n5-hd")]
#[tokio::test]
async fn full_pipeline_works<E: Curve>(t: u16, n: u16)
async fn full_pipeline_works<E: Curve>(t: u16, n: u16, hd_enabled: bool)
where
Point<E>: generic_ec::coords::HasAffineX<E>,
{
let mut rng = DevRng::new();
let incomplete_shares = run_keygen(t, n, &mut rng).await;
let shares = run_refresh(incomplete_shares, &mut rng).await;
run_signing(&shares, &mut rng).await;
let incomplete_shares = run_keygen(t, n, hd_enabled, &mut rng).await;
let shares = run_aux_gen(incomplete_shares, &mut rng).await;
run_signing(&shares, hd_enabled, &mut rng).await;
}

async fn run_keygen<E>(t: u16, n: u16, rng: &mut DevRng) -> Vec<Incomplete<E>>
async fn run_keygen<E>(t: u16, n: u16, hd_enabled: bool, rng: &mut DevRng) -> Vec<Incomplete<E>>
where
E: Curve,
{
Expand All @@ -46,6 +47,7 @@ mod generic {
outputs.push(async move {
cggmp21::keygen(eid, i, n)
.set_threshold(t)
.hd_wallet(hd_enabled)
.start(&mut party_rng, party)
.await
})
Expand All @@ -56,7 +58,7 @@ mod generic {
.expect("keygen failed")
}

async fn run_refresh<E>(shares: Vec<Incomplete<E>>, rng: &mut DevRng) -> Vec<Share<E>>
async fn run_aux_gen<E>(shares: Vec<Incomplete<E>>, rng: &mut DevRng) -> Vec<Share<E>>
where
E: Curve,
{
Expand Down Expand Up @@ -91,7 +93,7 @@ mod generic {
.collect()
}

async fn run_signing<E>(shares: &[Share<E>], rng: &mut DevRng)
async fn run_signing<E>(shares: &[Share<E>], random_derivation_path: bool, rng: &mut DevRng)
where
E: Curve,
Point<E>: generic_ec::coords::HasAffineX<E>,
Expand All @@ -101,6 +103,20 @@ mod generic {
let t = shares[0].min_signers();
let n = shares.len().try_into().unwrap();

let (derivation_path, public_key) = if random_derivation_path {
let len = rng.gen_range(1..=3);
let path = std::iter::repeat_with(|| rng.gen_range(0..=cggmp21::slip_10::H))
.take(len)
.collect::<Vec<u32>>();
let public_key = shares[0]
.derive_child_public_key(path.iter().copied())
.unwrap()
.public_key;
(Some(path), public_key)
} else {
(None, shares[0].shared_public_key)
};

let mut simulation = Simulation::<cggmp21::signing::msg::Msg<E, Sha256>>::new();

let eid: [u8; 32] = rng.gen();
Expand All @@ -122,11 +138,17 @@ mod generic {
for (i, share) in (0..).zip(participants_shares) {
let party = simulation.add_party();
let mut party_rng = rng.fork();
let derivation_path = derivation_path.clone();

outputs.push(async move {
cggmp21::signing(eid, i, participants, share)
.sign(&mut party_rng, party, message_to_sign)
.await
let signing = cggmp21::signing(eid, i, participants, share);
let signing = if let Some(derivation_path) = derivation_path {
signing.set_derivation_path(derivation_path).unwrap()
} else {
signing
};

signing.sign(&mut party_rng, party, message_to_sign).await
});
}

Expand All @@ -135,7 +157,7 @@ mod generic {
.expect("signing failed");

signatures[0]
.verify(&shares[0].core.shared_public_key, &message_to_sign)
.verify(&public_key, &message_to_sign)
.expect("signature is not valid");

assert!(signatures.iter().all(|s_i| signatures[0] == *s_i));
Expand Down
Loading

0 comments on commit df05378

Please sign in to comment.