@@ -26,34 +26,102 @@ use crate::{security_level::SecurityLevel, utils, ExecutionId};
2626
2727use 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 ) ]
3745pub struct DataToSign < E : Curve > ( Scalar < E > ) ;
3846
3947impl < 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 >
682792where
@@ -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 ) ?;
0 commit comments