Skip to content

Commit 9d98157

Browse files
committed
Forbid presigs + raw signing
Signed-off-by: Denis Varlakov <denis@dfns.co>
1 parent 9ddcc57 commit 9d98157

File tree

8 files changed

+164
-35
lines changed

8 files changed

+164
-35
lines changed

cggmp21/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ hd-slip10 = ["hd-wallet/slip10"]
5555
hd-stark = ["hd-wallet/stark"]
5656
spof = ["key-share/spof"]
5757

58+
# Exposes insecure API which may lead to security vulnerabilities when used inappropriately. Only
59+
# for advanced users. Be sure you fully understand the implications if you enable this feature.
60+
insecure-assume-preimage-known = []
61+
5862
state-machine = ["cggmp21-keygen/state-machine"]
5963

6064
[package.metadata.docs.rs]

cggmp21/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,9 @@ pub use self::{
355355
key_refresh::{KeyRefreshError, PregeneratedPrimes},
356356
key_share::{IncompleteKeyShare, KeyShare},
357357
keygen::KeygenError,
358-
signing::{DataToSign, PartialSignature, Presignature, Signature, SigningError},
358+
signing::{
359+
DataToSign, PartialSignature, PrehashedDataToSign, Presignature, Signature, SigningError,
360+
},
359361
};
360362

361363
/// Protocol for finalizing the keygen by generating aux info.

cggmp21/src/signing.rs

Lines changed: 150 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,102 @@ use crate::{security_level::SecurityLevel, utils, ExecutionId};
2626

2727
use self::msg::*;
2828

29-
/// A (prehashed) data to be signed
29+
/// A message digest that is guaranteed to have a known preimage, making it safe for signing.
3030
///
31-
/// `DataToSign` holds a scalar that represents data to be signed. Different ECDSA schemes define different
32-
/// ways to map an original data to be signed (slice of bytes) into the scalar, but it always must involve
33-
/// cryptographic hash functions. Most commonly, original data is hashed using SHA2-256, then output is parsed
34-
/// as big-endian integer and taken modulo curve order. This exact functionality is implemented in
35-
/// [DataToSign::digest] and [DataToSign::from_digest] constructors.
31+
/// This struct wraps a scalar value (Scalar<E>) that represents the cryptographic hash of a
32+
/// message. It is the primary and recommended type for all signing operations in this library.
33+
///
34+
/// ## Purpose and Safety
35+
///
36+
/// The key security feature of `DataToSign` is its construction method. An instance of this struct
37+
/// can only be created by providing the original message bytes to the library's API. The library
38+
/// then performs the hashing internally.
39+
///
40+
/// This design acts as a type-safe guard, ensuring that the system will only ever sign a hash for
41+
/// which the original message (the "preimage") is known and has been processed by the library.
42+
/// This prevents a critical class of signature forgery attacks where an attacker provides a raw,
43+
/// maliciously crafted hash (refer to [`PrehashedDataToSign`] docs to learn more about this attack).
3644
#[derive(Debug, Clone, Copy)]
3745
pub struct DataToSign<E: Curve>(Scalar<E>);
3846

3947
impl<E: Curve> DataToSign<E> {
4048
/// Construct a `DataToSign` by hashing `data` with algorithm `D`
41-
///
42-
/// `data_to_sign = hash(data) mod q`
4349
pub fn digest<D: Digest>(data: &[u8]) -> Self {
4450
DataToSign(Scalar::from_be_bytes_mod_order(D::digest(data)))
4551
}
4652

4753
/// Constructs a `DataToSign` from output of given digest
48-
///
49-
/// `data_to_sign = hash(data) mod q`
5054
pub fn from_digest<D: Digest>(hash: D) -> Self {
5155
DataToSign(Scalar::from_be_bytes_mod_order(hash.finalize()))
5256
}
5357

54-
/// Constructs a `DataToSign` from scalar
55-
///
56-
/// ** Note: [DataToSign::digest] and [DataToSign::from_digest] are preferred way to construct the `DataToSign` **
58+
/// Returns a scalar that represents a data to be signed
59+
pub fn to_scalar(self) -> Scalar<E> {
60+
self.0
61+
}
62+
}
63+
64+
/// A pre-hashed message digest intended for signing, where the original message (preimage) may
65+
/// be unknown.
66+
///
67+
/// This struct wraps a scalar value representing a message digest. Unlike its safer counterpart,
68+
/// [`DataToSign`], this struct can be constructed directly from a raw scalar, bypassing the safety
69+
/// check that ensures the original message is known.
70+
///
71+
/// ## Context: `PrehashedDataToSign` vs. `DataToSign`
72+
/// This library provides two types for data to be signed:
73+
///
74+
/// * [`DataToSign`] **(Recommended)**: This is the safe, default option. Its API requires you to
75+
/// provide the original message bytes. The library then handles the hashing internally. This acts
76+
/// as a type-safe guard, guaranteeing that the signature corresponds to a known message.
77+
/// * `PrehashedDataToSign` **(Advanced)**: This is a low-level type for specific, advanced use
78+
/// cases. It contains a hash that was computed externally.
79+
///
80+
/// This `PrehashedDataToSign` struct is only accepted by library functions where it is safe to do
81+
/// so—specifically, in the full interactive signing protocol where the forgery attacks described
82+
/// below are not applicable.
83+
///
84+
/// ## ⚠️ Assume Preimage Known: Advanced Use Only
85+
/// This library offers a feature flag named `insecure-assume-preimage-known` for
86+
/// highly specialized use cases. When enabled, this feature exposes the method
87+
/// [`PrehashedDataToSign::insecure_assume_preimage_known`] which allows you to convert a
88+
/// `PrehashedDataToSign` directly into a [`DataToSign`]. This is a powerful but extremely dangerous
89+
/// operation. It overrides the library's type safety, telling the system to trust that a known
90+
/// original message exists for the given hash, even if one does not.
91+
///
92+
/// This feature **completely bypasses** the security guarantees provided by the [`DataToSign`] type
93+
/// and should almost never be used.
94+
///
95+
/// ### Potential Attack: Signature Forgery via Raw Hash Signing
96+
///
97+
/// This attack, detailed in [this paper](https://eprint.iacr.org/2021/1330.pdf) (Section 1.1.2, “An
98+
/// attack on ECDSA with presignatures”), enables signature forgery when a system signs raw hashes
99+
/// in combination with presignatures.
100+
///
101+
/// The core idea is that an attacker can forge a signature for a message of their choice, `m'`, by
102+
/// tricking the system into signing a different, maliciously crafted hash, `h`.
103+
///
104+
/// Here’s a simplified breakdown of how it works:
105+
///
106+
/// * **Message Selection**: The attacker first chooses a target message, `m'`, that they want a
107+
/// forged signature for
108+
/// * **Malicious Hash Construction**: Instead of submitting `h' = H(m')`, the attacker uses the
109+
/// hash of their target message, `H(m')`, and public data from the presignature to compute a new,
110+
/// malicious hash value, `h`. This `h` has no known preimage and appears random.
111+
/// * **Signing Request**: The attacker submits this raw hash `h` to the signing protocol.
112+
/// * **Forgery**: The protocol signs `h` and returns a signature component. The attacker then uses
113+
/// this component to mathematically compute the final, valid signature for their original target
114+
/// message, `m'`.
115+
///
116+
/// Essentially, the attacker exploits the signing algorithm's structure. By carefully crafting
117+
/// the hash input, they can predict and manipulate the output to forge a signature for an entirely
118+
/// different message. This is why **signing a hash without knowing its original message (preimage)
119+
/// is extremely dangerous** in this context.
120+
#[derive(Clone, Copy, Debug)]
121+
pub struct PrehashedDataToSign<E: Curve>(Scalar<E>);
122+
123+
impl<E: Curve> PrehashedDataToSign<E> {
124+
/// Constructs a `PrehashedDataToSign` from scalar
57125
///
58126
/// `scalar` must be output of cryptographic hash function applied to original message to be signed
59127
pub fn from_scalar(scalar: Scalar<E>) -> Self {
@@ -64,6 +132,48 @@ impl<E: Curve> DataToSign<E> {
64132
pub fn to_scalar(self) -> Scalar<E> {
65133
self.0
66134
}
135+
136+
/// Converts a `PrehashedDataToSign` directly into a [`DataToSign`]
137+
///
138+
/// **⚠️ Extremely dangerous operation.** It overrides the library's type safety, telling the
139+
/// system to trust that a known original message exists for the given hash, even if one does
140+
/// not. Read [`PrehashedDataToSign`] docs to learn why it's dangerous.
141+
///
142+
/// It's **only** safe to use this method if you have either:
143+
///
144+
/// 1. Hashed the original message yourself to generate this scalar (prefer directly using
145+
/// [`DataToSign`] when possible).
146+
/// 2. Can otherwise completely verify and trust the source of the prehashed data.
147+
#[cfg(feature = "insecure-assume-preimage-known")]
148+
pub fn insecure_assume_preimage_known(self) -> DataToSign<E> {
149+
DataToSign(self.0)
150+
}
151+
}
152+
153+
mod internal {
154+
pub trait Sealed {}
155+
}
156+
157+
/// Data to be signed, regardless of whether original message to be signed is known or not
158+
///
159+
/// This library accepts `&dyn AnyDataToSign` **only if** it doesn't matter for security whether
160+
/// original message is known or not.
161+
pub trait AnyDataToSign<E: Curve>: internal::Sealed {
162+
/// Returns a scalar that represents a data to be signed
163+
fn to_scalar(&self) -> Scalar<E>;
164+
}
165+
impl<E: Curve> internal::Sealed for DataToSign<E> {}
166+
impl<E: Curve> internal::Sealed for PrehashedDataToSign<E> {}
167+
168+
impl<E: Curve> AnyDataToSign<E> for DataToSign<E> {
169+
fn to_scalar(&self) -> Scalar<E> {
170+
self.0
171+
}
172+
}
173+
impl<E: Curve> AnyDataToSign<E> for PrehashedDataToSign<E> {
174+
fn to_scalar(&self) -> Scalar<E> {
175+
self.0
176+
}
67177
}
68178

69179
/// Presignature, can be used to issue a [partial signature](PartialSignature) without interacting with other signers
@@ -372,10 +482,6 @@ where
372482

373483
/// Specifies HD derivation path
374484
///
375-
/// Note: when generating a presignature, derivation path doesn't need to be known in advance. Instead
376-
/// of using this method, [`Presignature::set_derivation_path`] could be used to set derivation path
377-
/// after presignature was generated.
378-
///
379485
/// ## Example
380486
/// Set derivation path to m/1/999
381487
///
@@ -408,10 +514,6 @@ where
408514
}
409515

410516
/// Specifies HD derivation path, using HD derivation algorithm [`hd_wallet::HdWallet`]
411-
///
412-
/// Note: when generating a presignature, derivation path doesn't need to be known in advance. Instead
413-
/// of using this method, [`Presignature::set_derivation_path`] could be used to set derivation path
414-
/// after presignature was generated.
415517
#[cfg(feature = "hd-wallet")]
416518
pub fn set_derivation_path_with_algo<Hd: hd_wallet::HdWallet<E>, Index>(
417519
mut self,
@@ -484,11 +586,15 @@ where
484586
}
485587

486588
/// Starts signing protocol
589+
///
590+
/// `message_to_sign` can be either [`DataToSign`] or [`PrehashedDataToSign`]. It should be safe (i.e.
591+
/// it doesn't lead to known attacks) to sign a digest when signer don't know original message that's
592+
/// being signed. Prefer using [`DataToSign`] whenever possible.
487593
pub async fn sign<R, M>(
488594
self,
489595
rng: &mut R,
490596
party: M,
491-
message_to_sign: DataToSign<E>,
597+
message_to_sign: &dyn AnyDataToSign<E>,
492598
) -> Result<Signature<E>, SigningError>
493599
where
494600
R: RngCore + CryptoRng,
@@ -502,7 +608,7 @@ where
502608
self.i,
503609
self.key_share,
504610
self.parties_indexes_at_keygen,
505-
Some(message_to_sign),
611+
Some(message_to_sign.to_scalar()),
506612
self.enforce_reliable_broadcast,
507613
#[cfg(feature = "hd-wallet")]
508614
self.additive_shift,
@@ -519,11 +625,15 @@ where
519625
/// Returns a state machine that can be used to carry out the signing protocol
520626
///
521627
/// See [`round_based::state_machine`] for details on how that can be done.
628+
///
629+
/// `message_to_sign` can be either [`DataToSign`] or [`PrehashedDataToSign`]. It should be safe (i.e.
630+
/// it doesn't lead to known attacks) to sign a digest when signer don't know original message that's
631+
/// being signed. Prefer using [`DataToSign`] whenever possible.
522632
#[cfg(feature = "state-machine")]
523633
pub fn sign_sync<R>(
524634
self,
525635
rng: &'r mut R,
526-
message_to_sign: DataToSign<E>,
636+
message_to_sign: &'r dyn AnyDataToSign<E>,
527637
) -> impl round_based::state_machine::StateMachine<
528638
Output = Result<Signature<E>, SigningError>,
529639
Msg = Msg<E, D>,
@@ -551,7 +661,7 @@ async fn signing_t_out_of_n<M, E, L, D, R>(
551661
i: PartyIndex,
552662
key_share: &KeyShare<E, L>,
553663
S: &[PartyIndex],
554-
message_to_sign: Option<DataToSign<E>>,
664+
message_to_sign: Option<Scalar<E>>,
555665
enforce_reliable_broadcast: bool,
556666
additive_shift: Option<Scalar<E>>,
557667
) -> Result<ProtocolOutput<E>, SigningError>
@@ -676,7 +786,7 @@ async fn signing_n_out_of_n<M, E, L, D, R>(
676786
q_i: &Integer,
677787
N: &[fast_paillier::EncryptionKey],
678788
R: &[PedersenParams],
679-
message_to_sign: Option<DataToSign<E>>,
789+
message_to_sign: Option<Scalar<E>>,
680790
enforce_reliable_broadcast: bool,
681791
) -> Result<ProtocolOutput<E>, SigningError>
682792
where
@@ -1352,6 +1462,13 @@ where
13521462
tracer.named_round_begins("Partial signing");
13531463

13541464
// Round 1
1465+
1466+
// SECURITY: we can safely sign a digest even if its preimage is unknown, because the attack
1467+
// described in [`PrehashedDataToSign`] can only be done if message to be signed is chosen
1468+
// after presignature is generated. Since we do a full signing, we know that message was
1469+
// chosen before presig was generated.
1470+
let message_to_sign = DataToSign(message_to_sign);
1471+
13551472
let partial_sig = presig.issue_partial_signature(message_to_sign);
13561473

13571474
tracer.send_msg();
@@ -1396,6 +1513,12 @@ where
13961513
///
13971514
/// **Never reuse presignatures!** If you use the same presignatures to sign two different
13981515
/// messages, it leaks the private key!
1516+
///
1517+
/// When signing using a presignature, it's important that you know a message that's being
1518+
/// signed (not just its hash!). For this reason, this function only accepts [`DataToSign`] that
1519+
/// can only be constructed when original message is known. Refer to [`PrehashedDataToSign`]
1520+
/// docs to learn more about possible attack when signing a hash of the message without knowing
1521+
/// the preimage.
13991522
pub fn issue_partial_signature(self, message_to_sign: DataToSign<E>) -> PartialSignature<E> {
14001523
let r = self.Gamma.x().to_scalar();
14011524
let m = message_to_sign.to_scalar();
@@ -1531,7 +1654,7 @@ where
15311654
pub fn verify(
15321655
&self,
15331656
public_key: &Point<E>,
1534-
message: &DataToSign<E>,
1657+
message: &dyn AnyDataToSign<E>,
15351658
) -> Result<(), InvalidSignature> {
15361659
let r = (Point::generator() * message.to_scalar() + public_key * self.r) * self.s.invert();
15371660
let r = NonZero::from_point(r).ok_or(InvalidSignature)?;

tests/src/bin/measure_perf.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ fn do_becnhmarks<L: SecurityLevel>(args: Args) {
251251
async move {
252252
let _signature = cggmp21::signing(eid, i, signers_indexes_at_keygen, share)
253253
.set_progress_tracer(&mut profiler)
254-
.sign(&mut party_rng, party, message_to_sign)
254+
.sign(&mut party_rng, party, &message_to_sign)
255255
.await
256256
.context("signing failed")?;
257257
profiler.get_report().context("get perf report")

tests/tests/it/key_refresh.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ where
9090
cggmp21::signing(eid, i, participants, share)
9191
.set_progress_tracer(tracer)
9292
.enforce_reliable_broadcast(reliable_broadcast)
93-
.sign(&mut party_rng, party, message_to_sign)
93+
.sign(&mut party_rng, party, &message_to_sign)
9494
.await
9595
}
9696
},

tests/tests/it/pipeline.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ where
141141
signing
142142
};
143143

144-
signing.sign(&mut party_rng, party, message_to_sign).await
144+
signing.sign(&mut party_rng, party, &message_to_sign).await
145145
}
146146
})
147147
.unwrap()

tests/tests/it/signing.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ where
8383
signing
8484
};
8585

86-
async move { signing.sign(&mut party_rng, party, message_to_sign).await }
86+
async move { signing.sign(&mut party_rng, party, &message_to_sign).await }
8787
})
8888
.unwrap()
8989
.expect_ok()
@@ -294,7 +294,7 @@ where
294294
signing
295295
};
296296

297-
signing.sign_sync(signer_rng, message_to_sign)
297+
signing.sign_sync(signer_rng, &message_to_sign)
298298
})
299299
}
300300

tests/tests/it/stark_prehashed.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fn sign_transaction() {
6060
let s1 = cggmp21::generic_ec::Scalar::from_be_bytes_mod_order(bytes);
6161
let s2 = convert_from_stark_scalar(&transaction_hash).unwrap();
6262
assert_eq!(s1, s2);
63-
let cggmp_transaction_hash = cggmp21::DataToSign::from_scalar(s2);
63+
let cggmp_transaction_hash = cggmp21::PrehashedDataToSign::from_scalar(s2);
6464

6565
// Choose `t` signers to perform signing
6666
let t = shares[0].min_signers();
@@ -76,7 +76,7 @@ fn sign_transaction() {
7676

7777
async move {
7878
cggmp21::signing(eid, i, participants, share)
79-
.sign(&mut party_rng, party, cggmp_transaction_hash)
79+
.sign(&mut party_rng, party, &cggmp_transaction_hash)
8080
.await
8181
}
8282
})

0 commit comments

Comments
 (0)