Skip to content

Commit de6b4db

Browse files
authored
Merge pull request #74 from dfns/presigs-per-master-key
HD wallets: Support presignatures independent of derivation path
2 parents 35dde7f + 4e3016c commit de6b4db

File tree

2 files changed

+155
-17
lines changed

2 files changed

+155
-17
lines changed

cggmp21/src/signing.rs

+54-14
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,10 @@ where
293293

294294
/// Specifies HD derivation path
295295
///
296+
/// Note: when generating a presignature, derivation path doesn't need to be known in advance. Instead
297+
/// of using this method, [`Presingature::set_derivation_path`] could be used to set derivation path
298+
/// after presignature was generated.
299+
///
296300
/// ## Example
297301
/// Set derivation path to m/1/999
298302
///
@@ -313,23 +317,12 @@ where
313317
slip_10::NonHardenedIndex: TryFrom<Index>,
314318
{
315319
use crate::key_share::HdError;
316-
317-
let mut public_key = self
320+
let public_key = self
318321
.key_share
319322
.extended_public_key()
320323
.ok_or(HdError::DisabledHd)?;
321-
let mut additive_shift = Scalar::<E>::zero();
322-
323-
for child_index in path {
324-
let child_index: slip_10::NonHardenedIndex =
325-
child_index.try_into().map_err(HdError::InvalidPath)?;
326-
let shift = slip_10::derive_public_shift(&public_key, child_index);
327-
328-
additive_shift += shift.shift;
329-
public_key = shift.child_public_key;
330-
}
331-
332-
self.additive_shift = Some(additive_shift);
324+
self.additive_shift =
325+
Some(derive_additive_shift(public_key, path).map_err(HdError::InvalidPath)?);
333326
Ok(self)
334327
}
335328

@@ -1154,6 +1147,53 @@ where
11541147
let sigma_i = self.k.as_ref() * m + r * self.chi.as_ref();
11551148
PartialSignature { r, sigma: sigma_i }
11561149
}
1150+
1151+
/// Specifies HD derivation path
1152+
///
1153+
/// Outputs a presignature that can be used to sign a message with a child key derived from master
1154+
/// `epub` using `derivation_path`. Note that all signers need to set the same derivation path,
1155+
/// otherwise output signature will be invalid.
1156+
///
1157+
/// `epub` must be an [extended public key](DirtyIncompleteKeyShare::extended_public_key) assoicated
1158+
/// with the key share that was used to generate presignature. Using wrong `epub` will simply
1159+
/// lead to invalid signature.
1160+
#[cfg(feature = "hd-wallets")]
1161+
pub fn set_derivation_path<Index>(
1162+
mut self,
1163+
epub: slip_10::ExtendedPublicKey<E>,
1164+
derivation_path: impl IntoIterator<Item = Index>,
1165+
) -> Result<Self, <Index as TryInto<slip_10::NonHardenedIndex>>::Error>
1166+
where
1167+
slip_10::NonHardenedIndex: TryFrom<Index>,
1168+
{
1169+
let additive_shift = derive_additive_shift(epub, derivation_path)?;
1170+
1171+
let mut chi = self.chi + additive_shift * &self.k;
1172+
self.chi = SecretScalar::new(&mut chi);
1173+
1174+
Ok(self)
1175+
}
1176+
}
1177+
1178+
#[cfg(feature = "hd-wallets")]
1179+
fn derive_additive_shift<E: Curve, Index>(
1180+
mut epub: slip_10::ExtendedPublicKey<E>,
1181+
path: impl IntoIterator<Item = Index>,
1182+
) -> Result<Scalar<E>, <Index as TryInto<slip_10::NonHardenedIndex>>::Error>
1183+
where
1184+
slip_10::NonHardenedIndex: TryFrom<Index>,
1185+
{
1186+
let mut additive_shift = Scalar::<E>::zero();
1187+
1188+
for child_index in path {
1189+
let child_index: slip_10::NonHardenedIndex = child_index.try_into()?;
1190+
let shift = slip_10::derive_public_shift(&epub, child_index);
1191+
1192+
additive_shift += shift.shift;
1193+
epub = shift.child_public_key;
1194+
}
1195+
1196+
Ok(additive_shift)
11571197
}
11581198

11591199
impl<E: Curve> PartialSignature<E> {

tests/tests/it/signing.rs

+101-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ mod generic {
33
use cggmp21_tests::external_verifier::ExternalVerifier;
44
use generic_ec::{coords::HasAffineX, Curve, Point};
55
use rand::seq::SliceRandom;
6-
use rand::{Rng, RngCore, SeedableRng};
7-
use rand_chacha::ChaCha20Rng;
6+
use rand::{Rng, RngCore};
87
use rand_dev::DevRng;
98
use round_based::simulation::Simulation;
109
use sha2::Sha256;
@@ -68,7 +67,7 @@ mod generic {
6867
let mut outputs = vec![];
6968
for (i, share) in (0..).zip(participants_shares) {
7069
let party = simulation.add_party();
71-
let mut party_rng = ChaCha20Rng::from_seed(rng.gen());
70+
let mut party_rng = rng.fork();
7271

7372
#[cfg(feature = "hd-wallets")]
7473
let derivation_path = derivation_path.clone();
@@ -114,6 +113,105 @@ mod generic {
114113
.expect("external verification failed")
115114
}
116115

116+
#[test_case::case(Some(3), 5, false; "t3n5")]
117+
#[cfg_attr(feature = "hd-wallets", test_case::case(Some(3), 5, true; "t3n5-hd"))]
118+
#[tokio::test]
119+
async fn signing_with_presigs<E: Curve, V>(t: Option<u16>, n: u16, hd_wallet: bool)
120+
where
121+
Point<E>: HasAffineX<E>,
122+
V: ExternalVerifier<E>,
123+
{
124+
#[cfg(not(feature = "hd-wallets"))]
125+
assert!(!hd_wallet);
126+
127+
let mut rng = DevRng::new();
128+
129+
let shares = cggmp21_tests::CACHED_SHARES
130+
.get_shares::<E, SecurityLevel128>(t, n, hd_wallet)
131+
.expect("retrieve cached shares");
132+
133+
let mut simulation = Simulation::<Msg<E, Sha256>>::new();
134+
135+
let eid: [u8; 32] = rng.gen();
136+
let eid = ExecutionId::new(&eid);
137+
138+
// Choose `t` signers to generate presignature
139+
let t = shares[0].min_signers();
140+
let mut participants = (0..n).collect::<Vec<_>>();
141+
participants.shuffle(&mut rng);
142+
let participants = &participants[..usize::from(t)];
143+
println!("Signers: {participants:?}");
144+
145+
let participants_shares = participants.iter().map(|i| &shares[usize::from(*i)]);
146+
147+
let mut outputs = vec![];
148+
for (i, share) in (0..).zip(participants_shares) {
149+
let party = simulation.add_party();
150+
let mut party_rng = rng.fork();
151+
152+
outputs.push(async move {
153+
cggmp21::signing(eid, i, participants, share)
154+
.generate_presignature(&mut party_rng, party)
155+
.await
156+
});
157+
}
158+
159+
let presignatures = futures::future::try_join_all(outputs)
160+
.await
161+
.expect("signing failed");
162+
163+
// Now, that we have presignatures generated, we learn (generate) a messages to sign
164+
// and the derivation path (if hd is enabled)
165+
let mut original_message_to_sign = [0u8; 100];
166+
rng.fill_bytes(&mut original_message_to_sign);
167+
let message_to_sign = DataToSign::digest::<Sha256>(&original_message_to_sign);
168+
169+
#[cfg(feature = "hd-wallets")]
170+
let derivation_path = if hd_wallet {
171+
Some(cggmp21_tests::random_derivation_path(&mut rng))
172+
} else {
173+
None
174+
};
175+
176+
let partial_signatures = presignatures
177+
.into_iter()
178+
.map(|presig| {
179+
#[cfg(feature = "hd-wallets")]
180+
let presig = if let Some(derivation_path) = &derivation_path {
181+
let epub = shares[0].extended_public_key().expect("not hd wallet");
182+
presig
183+
.set_derivation_path(epub, derivation_path.iter().copied())
184+
.unwrap()
185+
} else {
186+
presig
187+
};
188+
presig.issue_partial_signature(message_to_sign)
189+
})
190+
.collect::<Vec<_>>();
191+
192+
let signature = cggmp21::PartialSignature::combine(&partial_signatures)
193+
.expect("invalid partial sigantures");
194+
195+
#[cfg(feature = "hd-wallets")]
196+
let public_key = if let Some(path) = &derivation_path {
197+
shares[0]
198+
.derive_child_public_key(path.iter().cloned())
199+
.unwrap()
200+
.public_key
201+
} else {
202+
shares[0].shared_public_key
203+
};
204+
#[cfg(not(feature = "hd-wallets"))]
205+
let public_key = shares[0].shared_public_key;
206+
207+
signature
208+
.verify(&public_key, &message_to_sign)
209+
.expect("signature is not valid");
210+
211+
V::verify(&public_key, &signature, &original_message_to_sign)
212+
.expect("external verification failed")
213+
}
214+
117215
#[instantiate_tests(<cggmp21::supported_curves::Secp256k1, cggmp21_tests::external_verifier::blockchains::Bitcoin>)]
118216
mod secp256k1 {}
119217
#[instantiate_tests(<cggmp21::supported_curves::Secp256r1, cggmp21_tests::external_verifier::Noop>)]

0 commit comments

Comments
 (0)