Skip to content

Commit 3bc449a

Browse files
Eligioojsdanielh
authored andcommitted
Multisig: return errors instead of panicking on invalid curve points when delinearizing
1 parent d7daea5 commit 3bc449a

12 files changed

Lines changed: 177 additions & 42 deletions

File tree

keys/src/multisig/address.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use nimiq_hash::Blake2bHasher;
44
#[cfg(feature = "serde-derive")]
55
use nimiq_utils::merkle::compute_root_from_content;
66

7-
use super::public_key::DelinearizedPublicKey;
7+
use super::{error::PartialSignatureError, public_key::DelinearizedPublicKey};
88
#[cfg(feature = "serde-derive")]
99
use crate::Address;
1010
use crate::Ed25519PublicKey;
@@ -13,14 +13,14 @@ use crate::Ed25519PublicKey;
1313
pub fn combine_public_keys(
1414
public_keys: Vec<Ed25519PublicKey>,
1515
num_signers: usize,
16-
) -> Vec<Ed25519PublicKey> {
16+
) -> Result<Vec<Ed25519PublicKey>, PartialSignatureError> {
1717
// Calculate combinations.
1818
let combinations = public_keys.into_iter().combinations(num_signers);
1919
let mut multisig_keys: Vec<Ed25519PublicKey> = combinations
2020
.map(|combination| DelinearizedPublicKey::sum_delinearized(&combination))
21-
.collect();
21+
.collect::<Result<_, _>>()?;
2222
multisig_keys.sort();
23-
multisig_keys
23+
Ok(multisig_keys)
2424
}
2525

2626
/// Given a list of possible public keys, generates an address for which each of the public keys is a possible signer.

keys/src/multisig/commitment.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,19 @@ impl Commitment {
5757
}
5858
}
5959

60-
impl From<[u8; Commitment::SIZE]> for Commitment {
61-
fn from(bytes: [u8; Commitment::SIZE]) -> Self {
62-
Commitment::from_bytes(bytes).unwrap()
60+
impl TryFrom<[u8; Commitment::SIZE]> for Commitment {
61+
type Error = ();
62+
63+
fn try_from(bytes: [u8; Commitment::SIZE]) -> Result<Self, Self::Error> {
64+
Commitment::from_bytes(bytes).ok_or(())
6365
}
6466
}
6567

66-
impl<'a> From<&'a [u8; Commitment::SIZE]> for Commitment {
67-
fn from(bytes: &'a [u8; Commitment::SIZE]) -> Self {
68-
Commitment::from(*bytes)
68+
impl<'a> TryFrom<&'a [u8; Commitment::SIZE]> for Commitment {
69+
type Error = ();
70+
71+
fn try_from(bytes: &'a [u8; Commitment::SIZE]) -> Result<Self, Self::Error> {
72+
Commitment::try_from(*bytes)
6973
}
7074
}
7175

keys/src/multisig/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,6 @@ impl std::error::Error for InvalidScalarError {
2727
pub enum PartialSignatureError {
2828
#[error("Missing nonces")]
2929
MissingNonces,
30+
#[error("Invalid curve point in public key")]
31+
InvalidCurvePoint,
3032
}

keys/src/multisig/mod.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ impl CommitmentsBuilder {
156156
}
157157

158158
/// Creates the aggregate commitment and additional data for the content to be signed.
159-
pub fn build(mut self, content: &[u8]) -> CommitmentsData {
159+
pub fn build(mut self, content: &[u8]) -> Result<CommitmentsData, PartialSignatureError> {
160160
self.all_public_keys.sort();
161-
let aggregate_public_key = DelinearizedPublicKey::sum_delinearized(&self.all_public_keys);
161+
let aggregate_public_key = DelinearizedPublicKey::sum_delinearized(&self.all_public_keys)?;
162162

163163
let mut partial_agg_commitments = Vec::with_capacity(MUSIG2_PARAMETER_V);
164164

@@ -193,14 +193,14 @@ impl CommitmentsBuilder {
193193
agg_commitment_edwards += partial_agg_commitment.0 * scale;
194194
}
195195

196-
CommitmentsData {
196+
Ok(CommitmentsData {
197197
nonces: self.nonces,
198198
commitments: self.all_commitments[0],
199199
aggregate_public_key,
200200
aggregate_commitment: Commitment(agg_commitment_edwards),
201201
all_public_keys: self.all_public_keys,
202202
b,
203-
}
203+
})
204204
}
205205
}
206206

@@ -279,18 +279,22 @@ impl Ed25519PublicKey {
279279

280280
/// Delinearizes a public key by multiplying it with a scalar derived from the hash and the public key itself.
281281
/// Effective delinearization for multisigs should use the hash over all public keys as an input.
282-
pub(crate) fn delinearize(&self, public_keys_hash: &[u8; 64]) -> EdwardsPoint {
282+
pub(crate) fn delinearize(
283+
&self,
284+
public_keys_hash: &[u8; 64],
285+
) -> Result<EdwardsPoint, PartialSignatureError> {
283286
// Compute H(C||P).
284287
let mut h: Sha512 = Sha512::default();
285288

286289
h.update(&public_keys_hash[..]);
287290
h.update(self.as_bytes());
288291
let s = Scalar::from_hash::<Sha512>(h);
289292

290-
// Should always work, since we come from a valid public key.
291-
let p = self.to_edwards_point().unwrap();
293+
let p = self
294+
.to_edwards_point()
295+
.ok_or(PartialSignatureError::InvalidCurvePoint)?;
292296
// Compute H(C||P)*P.
293-
s * p
297+
Ok(s * p)
294298
}
295299

296300
pub fn verify_partial(
@@ -303,7 +307,10 @@ impl Ed25519PublicKey {
303307
let public_keys_hash = hash_public_keys(&commitments_data.all_public_keys);
304308
// And delinearize them.
305309
// Note that here we delinearize as p^{H(H(pks), p)}, e.g., with an additional hash due to the function delinearize_private_key
306-
let delinearized_public_key = self.delinearize(&public_keys_hash);
310+
let delinearized_public_key = match self.delinearize(&public_keys_hash) {
311+
Ok(p) => p,
312+
Err(_) => return false,
313+
};
307314

308315
// Compute c = H(R, apk, m)
309316
let mut hasher = Sha512Hasher::new();

keys/src/multisig/public_key.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ use std::{borrow::Borrow, iter::Sum};
22

33
use curve25519_dalek::{edwards::EdwardsPoint, traits::Identity};
44

5-
use crate::{multisig::hash_public_keys, Ed25519PublicKey};
5+
use crate::{
6+
multisig::{error::PartialSignatureError, hash_public_keys},
7+
Ed25519PublicKey,
8+
};
69

710
/// This structure holds a delinearized public key (which prevents rogue key attacks in multisigs).
811
#[derive(Copy, Clone)]
@@ -11,14 +14,16 @@ pub struct DelinearizedPublicKey(EdwardsPoint);
1114
impl DelinearizedPublicKey {
1215
/// Delinearizes a public key by multiplying it with a scalar derived from the hash and the public key itself.
1316
/// Effective delinearization for multisigs should use the hash over all public keys as an input.
14-
fn new(public_key: Ed25519PublicKey, hash: &[u8; 64]) -> Self {
15-
DelinearizedPublicKey(public_key.delinearize(hash))
17+
fn new(public_key: Ed25519PublicKey, hash: &[u8; 64]) -> Result<Self, PartialSignatureError> {
18+
Ok(DelinearizedPublicKey(public_key.delinearize(hash)?))
1619
}
1720

1821
/// Delinearizes a list of public keys and returns the list of delinearized public keys.
1922
/// Delinearizaion prevents rogue key attacks.
2023
/// Each public key is multiplied with a scalar derived from the hash over all public keys and the public key itself.
21-
pub fn delinearize(public_keys: &[Ed25519PublicKey]) -> Vec<Self> {
24+
pub fn delinearize(
25+
public_keys: &[Ed25519PublicKey],
26+
) -> Result<Vec<Self>, PartialSignatureError> {
2227
let mut public_keys = public_keys.to_vec();
2328
public_keys.sort();
2429
let h = hash_public_keys(&public_keys);
@@ -30,11 +35,13 @@ impl DelinearizedPublicKey {
3035

3136
/// Delinearizes and aggregates a list of public keys.
3237
/// Delinearizaion prevents rogue key attacks.
33-
pub fn sum_delinearized(public_keys: &[Ed25519PublicKey]) -> Ed25519PublicKey {
34-
let d: DelinearizedPublicKey = DelinearizedPublicKey::delinearize(public_keys)
38+
pub fn sum_delinearized(
39+
public_keys: &[Ed25519PublicKey],
40+
) -> Result<Ed25519PublicKey, PartialSignatureError> {
41+
let d: DelinearizedPublicKey = DelinearizedPublicKey::delinearize(public_keys)?
3542
.into_iter()
3643
.sum();
37-
d.into()
44+
Ok(d.into())
3845
}
3946
}
4047

keys/tests/multisig.rs

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use nimiq_keys::{
55
multisig::{
66
address::{combine_public_keys, compute_address},
77
commitment::{Commitment, CommitmentPair, Nonce},
8+
error::PartialSignatureError,
89
partial_signature::PartialSignature,
10+
public_key::DelinearizedPublicKey,
911
CommitmentsBuilder,
1012
},
1113
Address, Ed25519PublicKey, KeyPair, PrivateKey,
@@ -93,6 +95,13 @@ const VECTORS: [StrTestVector; 4] = [
9395
},
9496
];
9597

98+
/// Bytes that are NOT a valid compressed Edwards Y coordinate on Ed25519.
99+
/// CompressedEdwardsY(INVALID_CURVE_POINT_BYTES).decompress() returns None.
100+
const INVALID_CURVE_POINT_BYTES: [u8; 32] = [
101+
0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
102+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37,
103+
];
104+
96105
#[test]
97106
fn it_can_construct_public_keys() {
98107
for vector in VECTORS.iter() {
@@ -165,7 +174,7 @@ fn it_can_create_signatures() {
165174
builder = builder.with_signer(pks[j], commitments[j]);
166175
}
167176
}
168-
let data = builder.build(&test.message);
177+
let data = builder.build(&test.message).unwrap();
169178
let partial_sig = key_pair.partial_sign(&data, &test.message).unwrap();
170179

171180
assert!(
@@ -237,7 +246,8 @@ fn it_can_create_a_valid_multisignature() {
237246
let combined_public_keys = combine_public_keys(
238247
vec![keypair_a.public, keypair_b.public, keypair_c.public],
239248
2,
240-
);
249+
)
250+
.unwrap();
241251
assert_eq!(compute_address(&combined_public_keys), wallet_address);
242252

243253
let mut tx = "01f4e305f34ea1ccf00c0f7fcbc030d1347dc5eafe000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000500".to_string();
@@ -255,7 +265,8 @@ fn it_can_create_a_valid_multisignature() {
255265
commitment_pair_b_2.commitment(),
256266
],
257267
)
258-
.build(&tx_content);
268+
.build(&tx_content)
269+
.unwrap();
259270

260271
assert_eq!(commitments_data_a.aggregate_public_key, signing_public_key);
261272
assert_eq!(
@@ -281,7 +292,8 @@ fn it_can_create_a_valid_multisignature() {
281292
commitment_pair_a_2.commitment(),
282293
],
283294
)
284-
.build(&tx_content);
295+
.build(&tx_content)
296+
.unwrap();
285297

286298
assert_eq!(commitments_data_b.aggregate_public_key, signing_public_key);
287299
assert_eq!(
@@ -318,3 +330,96 @@ fn it_can_create_a_valid_multisignature() {
318330

319331
assert_eq!(&tx, signed_transaction);
320332
}
333+
334+
fn invalid_curve_point_key() -> Ed25519PublicKey {
335+
Ed25519PublicKey::from(INVALID_CURVE_POINT_BYTES)
336+
}
337+
338+
#[test]
339+
fn delinearize_rejects_invalid_curve_point() {
340+
let valid_kp: KeyPair = from_hex!(
341+
"f80793b4cb1e165d1a65b5cbc9e7b2efa583de01bc13dd23f7a1d78af4349904",
342+
PrivateKey::SIZE,
343+
PrivateKey::from
344+
)
345+
.into();
346+
let invalid_key = invalid_curve_point_key();
347+
348+
let result = DelinearizedPublicKey::sum_delinearized(&[valid_kp.public, invalid_key]);
349+
assert!(
350+
matches!(result, Err(PartialSignatureError::InvalidCurvePoint)),
351+
"expected InvalidCurvePoint error, got {result:?}"
352+
);
353+
}
354+
355+
#[test]
356+
fn build_rejects_invalid_public_key() {
357+
let mut rng = test_rng(true);
358+
let valid_kp: KeyPair = from_hex!(
359+
"f80793b4cb1e165d1a65b5cbc9e7b2efa583de01bc13dd23f7a1d78af4349904",
360+
PrivateKey::SIZE,
361+
PrivateKey::from
362+
)
363+
.into();
364+
let invalid_key = invalid_curve_point_key();
365+
let pairs = CommitmentPair::generate_all(&mut rng);
366+
367+
let builder = CommitmentsBuilder::with_private_commitments(valid_kp.public, pairs)
368+
.with_signer(invalid_key, [Commitment::default(); 2]);
369+
370+
let result = builder.build(b"test message");
371+
assert!(
372+
matches!(result, Err(PartialSignatureError::InvalidCurvePoint)),
373+
"expected InvalidCurvePoint error"
374+
);
375+
}
376+
377+
#[test]
378+
fn verify_partial_with_invalid_key_returns_false() {
379+
let mut rng = test_rng(true);
380+
let valid_kp: KeyPair = from_hex!(
381+
"f80793b4cb1e165d1a65b5cbc9e7b2efa583de01bc13dd23f7a1d78af4349904",
382+
PrivateKey::SIZE,
383+
PrivateKey::from
384+
)
385+
.into();
386+
let pairs = CommitmentPair::generate_all(&mut rng);
387+
let commitments = CommitmentPair::to_commitments(&pairs);
388+
389+
// Build valid commitments data first.
390+
let data = CommitmentsBuilder::with_private_commitments(valid_kp.public, pairs)
391+
.with_signer(valid_kp.public, commitments)
392+
.build(b"test")
393+
.unwrap();
394+
let partial_sig = valid_kp.partial_sign(&data, b"test").unwrap();
395+
396+
// Verifying with an invalid key should return false, not panic.
397+
let invalid_key = invalid_curve_point_key();
398+
assert!(!invalid_key.verify_partial(&data, &partial_sig, b"test"));
399+
}
400+
401+
#[test]
402+
fn commitment_try_from_rejects_invalid_bytes() {
403+
let result = Commitment::try_from(INVALID_CURVE_POINT_BYTES);
404+
assert!(
405+
result.is_err(),
406+
"expected error for invalid curve point bytes"
407+
);
408+
}
409+
410+
#[test]
411+
fn combine_public_keys_rejects_invalid_key() {
412+
let valid_kp: KeyPair = from_hex!(
413+
"f80793b4cb1e165d1a65b5cbc9e7b2efa583de01bc13dd23f7a1d78af4349904",
414+
PrivateKey::SIZE,
415+
PrivateKey::from
416+
)
417+
.into();
418+
let invalid_key = invalid_curve_point_key();
419+
420+
let result = combine_public_keys(vec![valid_kp.public, invalid_key], 2);
421+
assert!(
422+
matches!(result, Err(PartialSignatureError::InvalidCurvePoint)),
423+
"expected InvalidCurvePoint error, got {result:?}"
424+
);
425+
}

wallet/src/multisig_account.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ impl MultiSigAccount {
5353
let mut sorted_public_keys = public_keys.to_vec();
5454
sorted_public_keys.sort();
5555

56-
let multi_sig_keys = combine_public_keys(sorted_public_keys, min_signatures.get() as usize);
56+
let multi_sig_keys =
57+
combine_public_keys(sorted_public_keys, min_signatures.get() as usize)?;
5758

5859
Ok(Self::new(key_pair, min_signatures, &multi_sig_keys))
5960
}
@@ -109,7 +110,9 @@ impl MultiSigAccount {
109110
}
110111

111112
/// Utility method that delinearizes and aggregates the provided slice of public keys.
112-
pub fn aggregate_public_keys(public_keys: &[Ed25519PublicKey]) -> Ed25519PublicKey {
113+
pub fn aggregate_public_keys(
114+
public_keys: &[Ed25519PublicKey],
115+
) -> Result<Ed25519PublicKey, PartialSignatureError> {
113116
DelinearizedPublicKey::sum_delinearized(public_keys)
114117
}
115118

@@ -178,6 +181,8 @@ pub enum MultiSigAccountError {
178181
InvalidSignatureFromBytes(#[from] nimiq_keys::SignatureError),
179182
#[error("Number of signatures must be the same as the minimal signatures")]
180183
InvalidSignaturesLength,
184+
#[error("Invalid curve point in public key")]
185+
InvalidCurvePoint(#[from] PartialSignatureError),
181186
#[error("The public key of keypair must be part of provided public keys")]
182187
KeyPairNotPartOfList,
183188
#[error("The provided public keys must not be empty")]

0 commit comments

Comments
 (0)