diff --git a/synedrion/Cargo.toml b/synedrion/Cargo.toml index a6bc0a03..c451ed31 100644 --- a/synedrion/Cargo.toml +++ b/synedrion/Cargo.toml @@ -21,6 +21,7 @@ base64 = { version = "0.21", default-features = false, features = ["alloc"] } hashing-serializer = { version = "0.1", default-features = false } secrecy = { version = "0.8", default-features = false, features = ["alloc", "serde"] } zeroize = { version = "1.8", default-features = false, features = ["alloc", "zeroize_derive"] } +bip32 = { version = "0.5.2", default-features = false, features = ["alloc", "secp256k1"] } # Note: `alloc` is needed for `crytpto-bigint`'s dependency `serdect` to be able # to serialize Uints in human-readable formats. diff --git a/synedrion/src/curve/arithmetic.rs b/synedrion/src/curve/arithmetic.rs index 0986795f..cd18eb3a 100644 --- a/synedrion/src/curve/arithmetic.rs +++ b/synedrion/src/curve/arithmetic.rs @@ -19,7 +19,10 @@ use k256::elliptic_curve::{ FieldBytesSize, NonZeroScalar, }; -use k256::{ecdsa::VerifyingKey, Secp256k1}; +use k256::{ + ecdsa::{SigningKey, VerifyingKey}, + Secp256k1, +}; use rand_core::CryptoRngCore; use secrecy::{CloneableSecret, DebugSecret, SerializableSecret}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -94,6 +97,15 @@ impl Scalar { self.0 } + pub fn to_signing_key(self) -> Option { + let scalar: Option> = NonZeroScalar::new(self.0).into(); + Some(SigningKey::from(scalar?)) + } + + pub fn from_signing_key(sk: &SigningKey) -> Self { + Self(*sk.as_nonzero_scalar().as_ref()) + } + pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result { let arr = GenericArray::>::from_exact_iter(bytes.iter().cloned()) diff --git a/synedrion/src/lib.rs b/synedrion/src/lib.rs index 1715670d..a0a8447d 100644 --- a/synedrion/src/lib.rs +++ b/synedrion/src/lib.rs @@ -31,6 +31,7 @@ mod uint; mod www02; // Some re-exports to avoid the need for version-matching +pub use bip32; pub use k256; pub use k256::ecdsa; pub use signature; diff --git a/synedrion/src/tools/hashing.rs b/synedrion/src/tools/hashing.rs index ce6b30eb..98ed4146 100644 --- a/synedrion/src/tools/hashing.rs +++ b/synedrion/src/tools/hashing.rs @@ -71,7 +71,7 @@ impl Chain for FofHasher { #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) struct HashOutput( // Length of the BackendDigest output. Unfortunately we can't get it in compile-time. - #[serde(with = "serde_bytes::as_hex")] [u8; 32], + #[serde(with = "serde_bytes::as_hex")] pub(crate) [u8; 32], ); impl AsRef<[u8]> for HashOutput { diff --git a/synedrion/src/www02/entities.rs b/synedrion/src/www02/entities.rs index e33b639d..f567297f 100644 --- a/synedrion/src/www02/entities.rs +++ b/synedrion/src/www02/entities.rs @@ -1,8 +1,10 @@ use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::vec::Vec; use core::fmt::Debug; use core::marker::PhantomData; -use k256::ecdsa::VerifyingKey; +use bip32::{DerivationPath, PrivateKey, PrivateKeyBytes, PublicKey}; +use k256::ecdsa::{SigningKey, VerifyingKey}; use rand_core::CryptoRngCore; use secrecy::{ExposeSecret, Secret}; use serde::{Deserialize, Serialize}; @@ -43,7 +45,7 @@ impl ThresholdKeyShare, threshold: usize, - signing_key: Option<&k256::ecdsa::SigningKey>, + signing_key: Option<&SigningKey>, ) -> BTreeMap { debug_assert!(threshold <= ids.len()); // TODO (#68): make the method fallible @@ -170,41 +172,94 @@ impl ThresholdKeyShare Scalar { - FofHasher::new_with_dst(b"key-derivation") - .chain_bytes(seed) - .finalize_to_scalar() + fn derive_tweaks( + public_key: VerifyingKey, + derivation_path: &DerivationPath, + ) -> Result, bip32::Error> { + let mut public_key = public_key; + + // Note: deriving the initial chain code from public information. Is this okay? + let mut chain_code = FofHasher::new_with_dst(b"chain-code-derivation") + .chain_bytes(&Point::from_verifying_key(&public_key).to_compressed_array()) + .finalize() + .0; + + let mut tweaks = Vec::new(); + for child_number in derivation_path.iter() { + let (tweak, new_chain_code) = public_key.derive_tweak(&chain_code, child_number)?; + public_key = public_key.derive_child(tweak)?; + tweaks.push(tweak); + chain_code = new_chain_code; + } + + Ok(tweaks) + } + + fn apply_tweaks_public( + public_key: VerifyingKey, + tweaks: &[PrivateKeyBytes], + ) -> Result { + let mut public_key = public_key; + for tweak in tweaks { + public_key = public_key.derive_child(*tweak)?; + } + Ok(public_key) + } + + fn apply_tweaks_private( + private_key: SigningKey, + tweaks: &[PrivateKeyBytes], + ) -> Result { + let mut private_key = private_key; + for tweak in tweaks { + private_key = private_key.derive_child(*tweak)?; + } + Ok(private_key) } /// Return the verifying key to which the derive set of shares will correspond. - pub fn derived_verifying_key(&self, seed: &[u8]) -> VerifyingKey { - let parent = self.verifying_key_as_point(); - let tweak = Self::derive_tweak(seed); - (parent + tweak.mul_by_generator()) - .to_verifying_key() - .unwrap() + pub fn derived_verifying_key_bip32( + public_key: &VerifyingKey, + derivation_path: &DerivationPath, + ) -> Result { + let tweaks = Self::derive_tweaks(*public_key, derivation_path)?; + Self::apply_tweaks_public(*public_key, &tweaks) } - /// Deterministically derives a child share. - pub fn derive(&self, seed: &[u8]) -> Self { - let tweak = Self::derive_tweak(seed); - let tweak_point = tweak.mul_by_generator(); - let secret_share = Secret::new(self.secret_share.expose_secret() + &tweak); + /// Deterministically derives a child share using BIP-32 standard. + pub fn derive_bip32(&self, derivation_path: &DerivationPath) -> Result { + let tweaks = Self::derive_tweaks(self.verifying_key(), derivation_path)?; + + // Will fail here if secret share is zero + let secret_share = self + .secret_share + .expose_secret() + .to_signing_key() + .ok_or(bip32::Error::Crypto)?; + let secret_share = Secret::new(Scalar::from_signing_key(&Self::apply_tweaks_private( + secret_share, + &tweaks, + )?)); + let public_shares = self .public_shares .clone() .into_iter() - .map(|(id, point)| (id, point + tweak_point)) - .collect(); - - Self { + .map(|(id, point)| + // Will fail here if the final or one of the intermediate points is an identity + point.to_verifying_key().ok_or(bip32::Error::Crypto) + .and_then(|vkey| Self::apply_tweaks_public(vkey, &tweaks)) + .map(|vkey| (id, Point::from_verifying_key(&vkey)))) + .collect::>()?; + + Ok(Self { owner: self.owner.clone(), threshold: self.threshold, share_ids: self.share_ids.clone(), secret_share, public_shares, phantom: PhantomData, - } + }) } } diff --git a/synedrion/tests/threshold.rs b/synedrion/tests/threshold.rs index f2899412..65ed7e3c 100644 --- a/synedrion/tests/threshold.rs +++ b/synedrion/tests/threshold.rs @@ -236,14 +236,18 @@ async fn full_sequence() { .collect::>(); // Derive child shares - let child_key_seed = b"some derivation path"; + let path = "m/0/2/1/4/2".parse().unwrap(); let child_key_shares = t_key_shares .iter() - .map(|key_share| key_share.derive(child_key_seed)) + .map(|key_share| key_share.derive_bip32(&path).unwrap()) .collect::>(); // The full verifying key can be obtained both from the original key shares and child key shares - let child_vkey = t_key_shares[0].derived_verifying_key(child_key_seed); + let child_vkey = ThresholdKeyShare::::derived_verifying_key_bip32( + &t_key_shares[0].verifying_key(), + &path, + ) + .unwrap(); assert_eq!(child_vkey, child_key_shares[0].verifying_key()); // Reshare to `n` nodes @@ -313,7 +317,12 @@ async fn full_sequence() { ); // Check that resharing did not change the derived child key - let child_vkey_after_resharing = new_t_key_shares[0].derived_verifying_key(child_key_seed); + let child_vkey_after_resharing = + ThresholdKeyShare::::derived_verifying_key_bip32( + &new_t_key_shares[0].verifying_key(), + &path, + ) + .unwrap(); assert_eq!(child_vkey, child_vkey_after_resharing); // Generate auxiliary data @@ -341,13 +350,16 @@ async fn full_sequence() { let selected_parties = BTreeSet::from([verifiers[0], verifiers[2], verifiers[4]]); let selected_key_shares = vec![ new_t_key_shares[0] - .derive(child_key_seed) + .derive_bip32(&path) + .unwrap() .to_key_share(&selected_parties), new_t_key_shares[2] - .derive(child_key_seed) + .derive_bip32(&path) + .unwrap() .to_key_share(&selected_parties), new_t_key_shares[4] - .derive(child_key_seed) + .derive_bip32(&path) + .unwrap() .to_key_share(&selected_parties), ]; let selected_aux_infos = vec![