diff --git a/Cargo.lock b/Cargo.lock index 00c7bc7..f742113 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "arrayvec" version = "0.7.4" @@ -636,17 +642,20 @@ dependencies = [ name = "givre-tests" version = "0.1.0" dependencies = [ + "anyhow", "bitcoin", "ed25519-dalek", "futures", "generic-tests", "givre", + "hex", "hex-literal", "rand", "rand_core", "rand_dev", "round-based", "secp256k1", + "slip-10", "test-case", "tokio", ] diff --git a/givre/src/signing/aggregate.rs b/givre/src/signing/aggregate.rs index 197a484..4a4edb7 100644 --- a/givre/src/signing/aggregate.rs +++ b/givre/src/signing/aggregate.rs @@ -97,11 +97,12 @@ pub struct AggregateOptions<'a, C: Ciphersuite> { /// Additive shift derived from HD path hd_additive_shift: Option>, /// Possible values: - /// * `None` if script tree is empty - /// * `Some(root)` if script tree is not empty + /// * `None` if it wasn't specified + /// * `Some(None)` if script tree is empty + /// * `Some(Some(root))` if script tree is not empty /// - /// It only matters when `C::IS_TAPROOT` is `true` - taproot_merkle_root: Option<[u8; 32]>, + /// It must be `None` when `C::IS_TAPROOT` is `true`, and it must be `Some(_)` otherwise + taproot_merkle_root: Option>, } impl<'a, C: Ciphersuite> AggregateOptions<'a, C> { @@ -133,7 +134,7 @@ impl<'a, C: Ciphersuite> AggregateOptions<'a, C> { /// Returns error if the key doesn't support HD derivation, or if the path is invalid #[cfg(feature = "hd-wallets")] pub fn set_derivation_path( - mut self, + self, path: impl IntoIterator, ) -> Result>::Error>> where @@ -145,10 +146,22 @@ impl<'a, C: Ciphersuite> AggregateOptions<'a, C> { .key_info .extended_public_key() .ok_or(HdError::DisabledHd)?; - self.hd_additive_shift = - Some(utils::derive_additive_shift(public_key, path).map_err(HdError::InvalidPath)?); + let additive_shift = + utils::derive_additive_shift(public_key, path).map_err(HdError::InvalidPath)?; - Ok(self) + Ok(self.dangerous_set_hd_additive_shift(additive_shift)) + } + + /// Specifies HD derivation additive shift + /// + /// CAUTION: additive shift MUST BE derived from the extended public key obtained from + /// the key share which is used for signing by calling [`utils::derive_additive_shift`]. + pub(crate) fn dangerous_set_hd_additive_shift( + mut self, + hd_additive_shift: Scalar, + ) -> Self { + self.hd_additive_shift = Some(hd_additive_shift); + self } /// Tweaks the key with specified merkle root following [BIP-341] @@ -170,7 +183,7 @@ impl<'a, C: Ciphersuite> AggregateOptions<'a, C> { return Err(Reason::NonTaprootCiphersuite.into()); } - self.taproot_merkle_root = merkle_root; + self.taproot_merkle_root = Some(merkle_root); Ok(self) } @@ -222,7 +235,7 @@ fn aggregate_inner( hd_additive_shift: Option>, #[rustfmt::skip] #[cfg_attr(not(feature = "taproot"), allow(unused_variables))] - taproot_merkle_root: Option<[u8; 32]>, + taproot_merkle_root: Option>, signers: &[(SignerIndex, PublicCommitments, SigShare)], msg: &[u8], ) -> Result, AggregateError> { @@ -253,7 +266,8 @@ fn aggregate_inner( #[cfg(feature = "taproot")] let pk = if C::IS_TAPROOT { // Taproot: tweak the key share - let t = crate::signing::taproot::tweak::(pk, taproot_merkle_root) + let merkle_root = taproot_merkle_root.ok_or(Reason::MissingTaprootMerkleRoot)?; + let t = crate::signing::taproot::tweak::(pk, merkle_root) .ok_or(Reason::TaprootTweakUndefined)?; let pk = *pk + Point::generator() * t; let pk = NonZero::from_point(pk).ok_or(Reason::TaprootChildPkZero)?; @@ -315,6 +329,8 @@ enum Reason { InvalidSig, HdChildPkZero, #[cfg(feature = "taproot")] + MissingTaprootMerkleRoot, + #[cfg(feature = "taproot")] NonTaprootCiphersuite, #[cfg(feature = "taproot")] TaprootTweakUndefined, @@ -338,6 +354,11 @@ impl fmt::Display for AggregateError { Reason::InvalidSig => f.write_str("invalid signature"), Reason::HdChildPkZero => f.write_str("HD derivation error: child pk is zero"), #[cfg(feature = "taproot")] + Reason::MissingTaprootMerkleRoot => f.write_str( + "taproot merkle tree is missing: it must be specified \ + for taproot ciphersuite via `SigningOptions::set_taproot_tweak`", + ), + #[cfg(feature = "taproot")] Reason::NonTaprootCiphersuite => { f.write_str("ciphersuite doesn't support taproot tweaks") } @@ -358,7 +379,8 @@ impl std::error::Error for AggregateError { | Reason::InvalidSig | Reason::HdChildPkZero => None, #[cfg(feature = "taproot")] - Reason::NonTaprootCiphersuite + Reason::MissingTaprootMerkleRoot + | Reason::NonTaprootCiphersuite | Reason::TaprootTweakUndefined | Reason::TaprootChildPkZero => None, } diff --git a/givre/src/signing/full_signing.rs b/givre/src/signing/full_signing.rs index ef79940..26b3392 100644 --- a/givre/src/signing/full_signing.rs +++ b/givre/src/signing/full_signing.rs @@ -15,7 +15,7 @@ use crate::{ Ciphersuite, SignerIndex, }; -use super::{aggregate::Signature, round1::PublicCommitments, round2::SigShare}; +use super::{aggregate::Signature, round1::PublicCommitments, round2::SigShare, utils}; /// Message of FROST Signing Protocol #[derive(Debug, Clone, Copy, round_based::ProtocolMessage)] @@ -37,6 +37,9 @@ pub struct SigningBuilder<'a, C: Ciphersuite> { key_share: &'a KeyShare, signers: &'a [SignerIndex], msg: &'a [u8], + + hd_additive_shift: Option>, + taproot_merkle_root: Option>, } impl<'a, C: Ciphersuite> SigningBuilder<'a, C> { @@ -54,13 +57,63 @@ impl<'a, C: Ciphersuite> SigningBuilder<'a, C> { key_share, signers, msg, + hd_additive_shift: None, + taproot_merkle_root: None, + } + } + + /// Specifies HD derivation path + /// + /// If called twice, the second call overwrites the first. + /// + /// Returns error if the key doesn't support HD derivation, or if the path is invalid + #[cfg(feature = "hd-wallets")] + pub fn set_derivation_path( + mut self, + path: impl IntoIterator, + ) -> Result>::Error>> + where + slip_10::NonHardenedIndex: TryFrom, + { + use crate::key_share::HdError; + + let public_key = self + .key_share + .extended_public_key() + .ok_or(HdError::DisabledHd)?; + let additive_shift = + utils::derive_additive_shift(public_key, path).map_err(HdError::InvalidPath)?; + self.hd_additive_shift = Some(additive_shift); + Ok(self) + } + + /// Tweaks the key with specified merkle root following [BIP-341] + /// + /// Note that the taproot spec requires that any key must be tweaked. By default, if this + /// method is not called for taproot-enabled ciphersuite, then an empty merkle root + /// is assumed. + /// + /// The method returns an error if the ciphersuite doesn't support taproot, i.e. if + /// [`Ciphersuite::IS_TAPROOT`] is `false` + /// + /// [BIP-341]: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki + #[cfg(feature = "taproot")] + pub fn set_taproot_tweak( + mut self, + merkle_root: Option<[u8; 32]>, + ) -> Result { + if !C::IS_TAPROOT { + return Err(Reason::NonTaprootCiphersuite.into()); } + + self.taproot_merkle_root = Some(merkle_root); + Ok(self) } /// Issues signature share /// /// Signer will output a signature share. It'll be more efficient than [generating a full signature](Self::sign), - /// but it requires you to collect all sig shares in one place and [aggreate](crate::signing::aggregate::aggregate) + /// but it requires you to collect all sig shares in one place and [aggregate](crate::signing::aggregate::aggregate) /// them. pub async fn issue_sig_share( self, @@ -78,6 +131,8 @@ impl<'a, C: Ciphersuite> SigningBuilder<'a, C> { self.key_share, self.signers, self.msg, + self.hd_additive_shift, + self.taproot_merkle_root, true, ) .await? @@ -100,6 +155,8 @@ impl<'a, C: Ciphersuite> SigningBuilder<'a, C> { self.key_share, self.signers, self.msg, + self.hd_additive_shift, + self.taproot_merkle_root, false, ) .await? @@ -117,6 +174,8 @@ async fn signing( key_share: &KeyShare, signers: &[SignerIndex], msg: &[u8], + hd_additive_shift: Option>, + taproot_merkle_root: Option>, output_sig_share: bool, ) -> Result, FullSigningError> where @@ -159,8 +218,25 @@ where .map(|(&j, &comm)| (j, comm)) .collect::>(); - let sig_share = crate::signing::round2::sign::(key_share, nonces, msg, &signers_list) - .map_err(Reason::Sign)?; + let mut options = + crate::signing::round2::SigningOptions::::new(key_share, nonces, msg, &signers_list); + #[cfg(feature = "hd-wallets")] + if let Some(additive_shift) = hd_additive_shift { + options = options.dangerous_set_hd_additive_shift(additive_shift); + } + if cfg!(not(feature = "hd-wallets")) && hd_additive_shift.is_some() { + return Err(Bug::AdditiveShiftWithoutHdFeature.into()); + } + #[cfg(feature = "taproot")] + if let Some(root) = taproot_merkle_root { + options = options + .set_taproot_tweak(root) + .map_err(Bug::SetTaprootTweakSign)?; + } + if cfg!(not(feature = "taproot")) && taproot_merkle_root.is_some() { + return Err(Bug::TaprootSpecifiedButDisabled.into()); + } + let sig_share = options.sign().map_err(Reason::Sign)?; if output_sig_share { return Ok(SigningOutput::SigShare(sig_share)); @@ -171,7 +247,7 @@ where .await .map_err(IoError::send)?; - // Aggregate sigature + // Aggregate signature let sig_shares = rounds.complete(round2).await.map_err(IoError::recv)?; let signers_list = signers_list @@ -181,8 +257,19 @@ where .collect::>(); let key_info: &KeyInfo<_> = key_share.as_ref(); - let sig = crate::signing::aggregate::aggregate::(key_info, &signers_list, msg) - .map_err(Reason::Aggregate)?; + let mut options = + crate::signing::aggregate::AggregateOptions::new(key_info, &signers_list, msg); + #[cfg(feature = "hd-wallets")] + if let Some(additive_shift) = hd_additive_shift { + options = options.dangerous_set_hd_additive_shift(additive_shift); + } + #[cfg(feature = "taproot")] + if let Some(root) = taproot_merkle_root { + options = options + .set_taproot_tweak(root) + .map_err(Bug::SetTaprootTweakAggregate)?; + } + let sig = options.aggregate().map_err(Reason::Aggregate)?; Ok(SigningOutput::Signature(sig)) } @@ -198,6 +285,7 @@ pub struct FullSigningError(Reason); #[derive(Debug)] enum Reason { + NonTaprootCiphersuite, NOverflowsU16, INotInRange, UnexpectedNumberOfSigners, @@ -225,11 +313,16 @@ impl IoError { #[derive(Debug)] enum Bug { UnexpectedOutput, + AdditiveShiftWithoutHdFeature, + SetTaprootTweakSign(crate::signing::round2::SigningError), + SetTaprootTweakAggregate(crate::signing::aggregate::AggregateError), + TaprootSpecifiedButDisabled, } impl fmt::Display for FullSigningError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { + Reason::NonTaprootCiphersuite => f.write_str("ciphersuite doesn't support taproot"), Reason::NOverflowsU16 => f.write_str("number of signers overflows u16"), Reason::INotInRange => f.write_str("signer index not in range (it must be 0 <= i < n)"), Reason::UnexpectedNumberOfSigners => f.write_str( @@ -250,6 +343,14 @@ impl fmt::Display for Bug { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Bug::UnexpectedOutput => f.write_str("unexpected output"), + Bug::AdditiveShiftWithoutHdFeature => { + f.write_str("additive shift is specified, but hd wallets are disabled") + } + Bug::SetTaprootTweakSign(_) => f.write_str("set taproot tweak failed (sign)"), + Bug::SetTaprootTweakAggregate(_) => f.write_str("set taproot tweak failed (aggregate)"), + Bug::TaprootSpecifiedButDisabled => { + f.write_str("taproot merkle root is specified, but taproot feature is not enabled") + } } } } @@ -258,7 +359,10 @@ impl fmt::Display for Bug { impl std::error::Error for FullSigningError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match &self.0 { - Reason::NOverflowsU16 | Reason::INotInRange | Reason::UnexpectedNumberOfSigners => None, + Reason::NonTaprootCiphersuite + | Reason::NOverflowsU16 + | Reason::INotInRange + | Reason::UnexpectedNumberOfSigners => None, Reason::IoError(IoError::Send(err)) | Reason::IoError(IoError::Recv(err)) => { Some(&**err) } @@ -274,6 +378,10 @@ impl std::error::Error for Bug { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Bug::UnexpectedOutput => None, + Bug::AdditiveShiftWithoutHdFeature => None, + Bug::SetTaprootTweakSign(err) => Some(err), + Bug::SetTaprootTweakAggregate(err) => Some(err), + Bug::TaprootSpecifiedButDisabled => None, } } } diff --git a/givre/src/signing/round2.rs b/givre/src/signing/round2.rs index ba90849..75c367a 100644 --- a/givre/src/signing/round2.rs +++ b/givre/src/signing/round2.rs @@ -43,11 +43,12 @@ pub struct SigningOptions<'a, C: Ciphersuite> { /// Additive shift derived from HD path hd_additive_shift: Option>, /// Possible values: - /// * `None` if script tree is empty - /// * `Some(root)` if script tree is not empty + /// * `None` if it wasn't specified + /// * `Some(None)` if script tree is empty + /// * `Some(Some(root))` if script tree is not empty /// - /// It only takes effect when `C::IS_TAPROOT` is `true` - taproot_merkle_root: Option<[u8; 32]>, + /// It must be `None` when `C::IS_TAPROOT` is `true`, and it must be `Some(_)` otherwise + taproot_merkle_root: Option>, } impl<'a, C: Ciphersuite> SigningOptions<'a, C> { @@ -86,7 +87,7 @@ impl<'a, C: Ciphersuite> SigningOptions<'a, C> { /// Returns error if the key doesn't support HD derivation, or if the path is invalid #[cfg(feature = "hd-wallets")] pub fn set_derivation_path( - mut self, + self, path: impl IntoIterator, ) -> Result>::Error>> where @@ -98,9 +99,21 @@ impl<'a, C: Ciphersuite> SigningOptions<'a, C> { .key_share .extended_public_key() .ok_or(HdError::DisabledHd)?; - self.hd_additive_shift = - Some(utils::derive_additive_shift(public_key, path).map_err(HdError::InvalidPath)?); - Ok(self) + let additive_shift = + utils::derive_additive_shift(public_key, path).map_err(HdError::InvalidPath)?; + Ok(self.dangerous_set_hd_additive_shift(additive_shift)) + } + + /// Specifies HD derivation additive shift + /// + /// CAUTION: additive shift MUST BE derived from the extended public key obtained from + /// the key share which is used for signing by calling [`utils::derive_additive_shift`]. + pub(crate) fn dangerous_set_hd_additive_shift( + mut self, + hd_additive_shift: Scalar, + ) -> Self { + self.hd_additive_shift = Some(hd_additive_shift); + self } /// Tweaks the key with specified merkle root following [BIP-341] @@ -122,7 +135,7 @@ impl<'a, C: Ciphersuite> SigningOptions<'a, C> { return Err(Reason::NonTaprootCiphersuite.into()); } - self.taproot_merkle_root = merkle_root; + self.taproot_merkle_root = Some(merkle_root); Ok(self) } @@ -184,7 +197,7 @@ fn sign_inner( hd_additive_shift: Option>, #[rustfmt::skip] #[cfg_attr(not(feature = "taproot"), allow(unused_variables))] - taproot_merkle_root: Option<[u8; 32]>, + taproot_merkle_root: Option>, nonce: SecretNonces, msg: &[u8], signers: &[(SignerIndex, PublicCommitments)], @@ -222,7 +235,8 @@ fn sign_inner( #[cfg(feature = "taproot")] let (x, pk) = if C::IS_TAPROOT { // Taproot: tweak the key share - let t = crate::signing::taproot::tweak::(pk, taproot_merkle_root) + let merkle_root = taproot_merkle_root.ok_or(Reason::MissingTaprootMerkleRoot)?; + let t = crate::signing::taproot::tweak::(pk, merkle_root) .ok_or(Reason::TaprootTweakUndefined)?; let (x, pk) = apply_additive_shift(*i, vss_setup, x, *pk, t).map_err(Reason::TaprootShift)?; @@ -410,6 +424,8 @@ pub struct SigningError(Reason); #[derive(Debug)] enum Reason { + #[cfg(feature = "taproot")] + MissingTaprootMerkleRoot, #[cfg(feature = "taproot")] NonTaprootCiphersuite, TooFewSigners { @@ -445,7 +461,12 @@ impl fmt::Display for SigningError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { #[cfg(feature = "taproot")] - Reason::NonTaprootCiphersuite => write!(f, "ciphersuite doesn't support taproot"), + Reason::MissingTaprootMerkleRoot => f.write_str( + "taproot merkle tree is missing: it must be specified \ + for taproot ciphersuite via `SigningOptions::set_taproot_tweak`", + ), + #[cfg(feature = "taproot")] + Reason::NonTaprootCiphersuite => f.write_str("ciphersuite doesn't support taproot"), Reason::TooFewSigners { min_signers, n } => write!( f, "signers list contains {n} signers, although at \ @@ -502,7 +523,9 @@ impl std::error::Error for SigningError { | Reason::SameSignerTwice | Reason::SignerNotInList => None, #[cfg(feature = "taproot")] - Reason::NonTaprootCiphersuite | Reason::TaprootTweakUndefined => None, + Reason::MissingTaprootMerkleRoot + | Reason::NonTaprootCiphersuite + | Reason::TaprootTweakUndefined => None, #[cfg(feature = "taproot")] Reason::TaprootShift(err) => Some(err), Reason::Bug(bug) => Some(bug), diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 155e54e..6495a04 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -16,6 +16,7 @@ rand_dev = "0.1" rand = "0.8" rand_core = "0.6" +hex = "0.4" hex-literal = "0.4" tokio = { version = "1", features = ["macros", "rt"]} @@ -25,3 +26,6 @@ round-based = { version = "0.3", features = ["dev"] } ed25519 = { package = "ed25519-dalek", version = "2.1" } secp256k1 = { version = "0.29", features = ["global-context"] } bitcoin = "0.32" +slip-10 = { version = "0.4", default-features = false } + +anyhow = "1" diff --git a/tests/src/lib.rs b/tests/src/lib.rs index df413de..9f51e95 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -1,3 +1,4 @@ +use anyhow::{Context, Result}; use givre::{ generic_ec::{NonZero, Point}, signing::aggregate::Signature, @@ -9,26 +10,52 @@ pub trait ExternalVerifier: Ciphersuite { /// the message needs to be an output of a hash function with fixed size const REQUIRED_MESSAGE_SIZE: Option = None; - type InvalidSig: core::fmt::Debug; - + /// Indicates whether external lib supports HD derivation + const SUPPORTS_HD: bool = false; + + /// Verifies signature using external library + /// + /// Takes arguments: + /// * Public key `pk` (without any modifications like HD derivations) + /// * HD derivation path. Empty path disables derivation. + /// * Taproot merkle root. `None` disables tweaking. + /// * Signature + /// * Message to verify + /// + /// Returns an error if: + /// * Signature is invalid + /// * HD derivation is provided but not supported by external library + /// * Taproot specified but not supported by external library + /// * Taproot is not specified, but required by external library fn verify_sig( pk: &NonZero>, + chain_code: Option<[u8; 32]>, + hd_derivation_path: &[u32], + taproot_merkle_root: Option>, sig: &Signature, msg: &[u8], - ) -> Result<(), Self::InvalidSig>; + ) -> Result<()>; } #[derive(Debug)] pub struct InvalidSignature; impl ExternalVerifier for givre::ciphersuite::Ed25519 { - type InvalidSig = ed25519::SignatureError; - fn verify_sig( pk: &NonZero>, + _chain_code: Option<[u8; 32]>, + hd_derivation_path: &[u32], + taproot_merkle_root: Option>, sig: &Signature, msg: &[u8], - ) -> Result<(), ed25519::SignatureError> { + ) -> Result<()> { + if !hd_derivation_path.is_empty() { + anyhow::bail!("HD derivation is not supported by ed25519_dalek") + } + if taproot_merkle_root.is_some() { + anyhow::bail!("taproot is not compatible with EdDSA") + } + let pk = ed25519::VerifyingKey::from_bytes( &Self::serialize_point(pk) .as_bytes() @@ -41,18 +68,26 @@ impl ExternalVerifier for givre::ciphersuite::Ed25519 { sig_bytes[32..].copy_from_slice(&Self::serialize_scalar(&sig.z)); let sig = ed25519::Signature::from_bytes(&sig_bytes); - pk.verify_strict(msg, &sig) + pk.verify_strict(msg, &sig).context("invalid signature") } } impl ExternalVerifier for givre::ciphersuite::Secp256k1 { - type InvalidSig = core::convert::Infallible; - fn verify_sig( _pk: &NonZero>, + _chain_code: Option<[u8; 32]>, + hd_derivation_path: &[u32], + taproot_merkle_root: Option>, _sig: &Signature, _msg: &[u8], - ) -> Result<(), Self::InvalidSig> { + ) -> Result<()> { + if !hd_derivation_path.is_empty() { + anyhow::bail!("HD derivation is not supported by ed25519_dalek") + } + if taproot_merkle_root.is_some() { + anyhow::bail!("taproot is not compatible with EdDSA") + } + // No external verifier for secp256k1 ciphersuite Ok(()) } @@ -61,27 +96,79 @@ impl ExternalVerifier for givre::ciphersuite::Secp256k1 { impl ExternalVerifier for givre::ciphersuite::Bitcoin { const REQUIRED_MESSAGE_SIZE: Option = Some(32); - type InvalidSig = secp256k1::Error; - fn verify_sig( pk: &NonZero>, + chain_code: Option<[u8; 32]>, + hd_derivation_path: &[u32], + taproot_merkle_root: Option>, sig: &Signature, msg: &[u8], - ) -> Result<(), Self::InvalidSig> { - use bitcoin::key::TapTweak; - - let pk = Self::normalize_point(*pk); - let pk = bitcoin::key::UntweakedPublicKey::from_slice(pk.to_bytes().as_ref())?; - let (pk, _) = pk.tap_tweak(secp256k1::SECP256K1, None); + ) -> Result<()> { + let pk = + bitcoin::secp256k1::PublicKey::from_slice(&pk.to_bytes(true)).context("public key")?; + + let pk: bitcoin::key::UntweakedPublicKey = if !hd_derivation_path.is_empty() { + let chain_code = chain_code.context("chain code is missing")?; + let mut xpub = bitcoin::bip32::Xpub { + network: bitcoin::NetworkKind::Main, + depth: 0, + parent_fingerprint: Default::default(), + child_number: bitcoin::bip32::ChildNumber::from_normal_idx(0) + .context("child idx")?, + public_key: pk, + chain_code: bitcoin::bip32::ChainCode::from(chain_code), + }; + + for &child_index in hd_derivation_path { + let child_index = bitcoin::bip32::ChildNumber::from_normal_idx(child_index) + .context("only non-hardened derivation is supported")?; + xpub = xpub + .ckd_pub(&secp256k1::SECP256K1, child_index) + .context("child derivation")?; + } + + xpub.to_x_only_pub() + } else { + pk.x_only_public_key().0 + }; + + let taproot_merkle_root = taproot_merkle_root + .context("taproot merkle root is mandatory")? + .map(bitcoin::TapNodeHash::assume_hidden); + + let (pk, _) = + bitcoin::key::TapTweak::tap_tweak(pk, &secp256k1::SECP256K1, taproot_merkle_root); let mut signature = [0u8; 64]; assert_eq!(signature.len(), Signature::::serialized_len()); sig.write_to_slice(&mut signature); - let signature = secp256k1::schnorr::Signature::from_slice(&signature)?; let msg = secp256k1::Message::from_digest_slice(msg)?; - signature.verify(&msg, &pk.to_inner()) + signature + .verify(&msg, &pk.to_inner()) + .context("invalid signature") + } +} + +/// Generates a random merkle root for taproot derivation +/// +/// With 1/2 probability it outputs `None` (corresponds to empty merkle root in bip341), +/// otherwise it generates a random merkle root and returns `Some(root)` +pub fn random_taproot_merkle_root(rng: &mut impl rand::Rng) -> Option<[u8; 32]> { + if rng.gen() { + None + } else { + Some(rng.gen()) } } + +/// Generates a random non-hardened HD derivation path which has somewhere +/// between 0 to 3 indexes +pub fn random_hd_path(rng: &mut impl rand::Rng) -> Vec { + let len = rng.gen_range(0..=3); + std::iter::repeat_with(|| rng.gen_range(0..slip_10::H)) + .take(len) + .collect() +} diff --git a/tests/tests/it/interactive.rs b/tests/tests/it/interactive.rs index 3938c92..320849f 100644 --- a/tests/tests/it/interactive.rs +++ b/tests/tests/it/interactive.rs @@ -2,6 +2,7 @@ mod generic { use std::iter; + use anyhow::Context; use givre::Ciphersuite; use givre_tests::ExternalVerifier; use rand::{seq::SliceRandom, Rng, RngCore}; @@ -35,6 +36,7 @@ mod generic { if let Some(t) = t { givre::keygen::(eid, j, n) .set_threshold(t) + .hd_wallet(C::SUPPORTS_HD) .start(&mut rng, party_threshold) .await } else { @@ -58,6 +60,26 @@ mod generic { let mut msg = vec![0u8; msg_len]; rng.fill_bytes(&mut msg); let msg = &msg; + println!("message to sign: {}", hex::encode(msg)); + + // HD derivation path + let derivation_path = if C::SUPPORTS_HD { + givre_tests::random_hd_path(&mut rng) + } else { + vec![] + }; + println!("HD path: {derivation_path:?}"); + + // Taproot merkle root + let taproot_merkle_root = if C::IS_TAPROOT { + Some(givre_tests::random_taproot_merkle_root(&mut rng)) + } else { + None + }; + println!( + "Taproot merkle root: {:?}", + taproot_merkle_root.map(|r| r.map(hex::encode)) + ); // Choose `t` signers to do signing let t = t.unwrap_or(n); @@ -74,10 +96,19 @@ mod generic { .zip(iter::repeat_with(|| (rng.fork(), simulation.add_party()))) .map(|((j, &index_at_keygen), (mut rng, party))| { let key_share = &key_shares[usize::from(index_at_keygen)]; + let derivation_path = &derivation_path; async move { - givre::signing::(j, key_share, signers, msg) - .sign(&mut rng, party) - .await + let mut signing = givre::signing::(j, key_share, signers, msg); + if !derivation_path.is_empty() { + signing = signing + .set_derivation_path(derivation_path.iter().copied()) + .context("set derivation path")?; + } + if let Some(root) = taproot_merkle_root { + signing = signing.set_taproot_tweak(root).context("set merkle root")? + } + + signing.sign(&mut rng, party).await.context("sign") } }); @@ -86,29 +117,16 @@ mod generic { .await .unwrap(); - { - // Tweak the key if necessary - let pk = if C::IS_TAPROOT { - // Taproot: normalize pk, tweak it, and normalize again - let pk = C::normalize_point(pk); - let pk = givre::signing::taproot::tweak_public_key(pk, None) - .expect("taproot tweak in undefined"); - C::normalize_point(pk) - } else { - match givre::ciphersuite::NormalizedPoint::::try_normalize(pk) { - Ok(pk) => pk, - Err(_) => { - panic!("non-taproot ciphersuites don't have notion of normalized points") - } - } - }; - - // Verify the signature using this library - sigs[0].verify(&pk, msg).unwrap(); - } - // Verify signature using external library - C::verify_sig(&pk, &sigs[0], msg).unwrap(); + C::verify_sig( + &pk, + key_shares[0].chain_code, + &derivation_path, + taproot_merkle_root, + &sigs[0], + msg, + ) + .unwrap(); for sig in &sigs[1..] { assert_eq!(sigs[0].r, sig.r); diff --git a/tests/tests/it/main.rs b/tests/tests/it/main.rs index f7df84f..510e406 100644 --- a/tests/tests/it/main.rs +++ b/tests/tests/it/main.rs @@ -3,6 +3,7 @@ mod test_vectors; #[generic_tests::define(attrs(test_case::case, test))] mod generic { + use anyhow::Context; use givre::Ciphersuite; use givre_tests::ExternalVerifier; use rand::{seq::SliceRandom, Rng, RngCore}; @@ -47,52 +48,84 @@ mod generic { } // Round 2. Each signer signs a message + + // message to be signed let message_len = C::REQUIRED_MESSAGE_SIZE.unwrap_or_else(|| rng.gen_range(20..=100)); let mut message = vec![0u8; message_len]; rng.fill_bytes(&mut message); + println!("message to sign: {}", hex::encode(&message)); + + // HD derivation path + let derivation_path = if C::SUPPORTS_HD { + givre_tests::random_hd_path(&mut rng) + } else { + vec![] + }; + println!("HD path: {derivation_path:?}"); + + // Taproot merkle root + let taproot_merkle_root = if C::IS_TAPROOT { + Some(givre_tests::random_taproot_merkle_root(&mut rng)) + } else { + None + }; + println!( + "Taproot merkle root: {:?}", + taproot_merkle_root.map(|r| r.map(hex::encode)) + ); let partial_sigs = public_commitments .iter() .zip(secret_nonces) .map(|(&(j, comm), secret_nonces)| { - let sig_share = givre::signing::round2::sign::( + let mut options = givre::signing::round2::SigningOptions::::new( &key_shares[usize::from(j)], secret_nonces, &message, &public_commitments, - )?; - Ok::<_, givre::signing::round2::SigningError>((j, comm, sig_share)) + ); + if !derivation_path.is_empty() { + options = options + .set_derivation_path(derivation_path.iter().copied()) + .context("set derivation path")?; + } + if let Some(root) = taproot_merkle_root { + options = options + .set_taproot_tweak(root) + .context("set taproot tweak")?; + } + let sig_share = options.sign().context("sign")?; + Ok::<_, anyhow::Error>((j, comm, sig_share)) }) .collect::, _>>() .expect("signing failed"); // Round 3. Aggregate sig shares - let sig = givre::signing::aggregate::aggregate::(key_info, &partial_sigs, &message) - .expect("aggregation failed"); - - { - // Tweak the key if necessary - let pk = if C::IS_TAPROOT { - // Taproot: normalize pk, tweak it, and normalize again - let pk = C::normalize_point(pk); - let pk = givre::signing::taproot::tweak_public_key(pk, None) - .expect("taproot tweak in undefined"); - C::normalize_point(pk) - } else { - match givre::ciphersuite::NormalizedPoint::::try_normalize(pk) { - Ok(pk) => pk, - Err(_) => { - panic!("non-taproot ciphersuites don't have notion of normalized points") - } - } - }; - - // Verify the signature using this library - sig.verify(&pk, &message).unwrap(); + let mut options = givre::signing::aggregate::AggregateOptions::::new( + key_info, + &partial_sigs, + &message, + ); + if !derivation_path.is_empty() { + options = options + .set_derivation_path(derivation_path.iter().copied()) + .expect("set derivation path"); + } + if let Some(root) = taproot_merkle_root { + options = options.set_taproot_tweak(root).expect("set taproot tweak"); } + let sig = options.aggregate().expect("aggregate"); // Verify signature using external library - C::verify_sig(&pk, &sig, &message).expect("external verifier: invalid signature") + C::verify_sig( + &pk, + key_shares[0].chain_code, + &derivation_path, + taproot_merkle_root, + &sig, + &message, + ) + .expect("external verifier: invalid signature") } #[test]