@@ -14,16 +14,26 @@ pub struct Message<'a, S = Public> {
1414 /// The message bytes
1515 pub bytes : Slice < ' a , S > ,
1616 /// The optional application tag to separate the signature from other applications.
17+ #[ deprecated(
18+ since = "0.11.0" ,
19+ note = "Use Message::new for BIP340-style domain separation"
20+ ) ]
1721 pub app_tag : Option < & ' static str > ,
22+ /// The domain separator for [BIP340]-style domain separation (33-byte prefix)
23+ ///
24+ /// [BIP340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
25+ pub bip340_domain_sep : Option < & ' static str > ,
1826}
1927
28+ #[ allow( deprecated) ]
2029impl < ' a , S : Secrecy > Message < ' a , S > {
21- /// Create a raw message with no `app_tag` . The message bytes will be passed straight into the
30+ /// Create a raw message with no domain separation . The message bytes will be passed straight into the
2231 /// challenge hash. Usually, you only use this when signing a pre-hashed message.
2332 pub fn raw ( bytes : & ' a [ u8 ] ) -> Self {
2433 Message {
2534 bytes : Slice :: from ( bytes) ,
2635 app_tag : None ,
36+ bip340_domain_sep : None ,
2737 }
2838 }
2939
@@ -32,18 +42,55 @@ impl<'a, S: Secrecy> Message<'a, S> {
3242 Self :: raw ( & [ ] )
3343 }
3444
45+ /// Create a message with [BIP340]-style domain separation using a 33-byte prefix.
46+ ///
47+ /// The domain separator will be padded with null bytes to exactly 33 bytes and
48+ /// prefixed to the message, as recommended in [BIP340] for domain separation.
49+ ///
50+ /// [BIP340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
51+ ///
52+ /// # Example
53+ /// ```
54+ /// use schnorr_fun::{Message, fun::marker::Public};
55+ /// let message = Message::<Public>::new("my-app/sign", b"hello world");
56+ /// ```
57+ pub fn new ( domain_sep : & ' static str , bytes : & ' a [ u8 ] ) -> Self {
58+ assert ! ( !domain_sep. is_empty( ) , "domain separator must not be empty" ) ;
59+ assert ! (
60+ domain_sep. len( ) <= 33 ,
61+ "domain separator must be 33 bytes or less"
62+ ) ;
63+ Message {
64+ bytes : Slice :: from ( bytes) ,
65+ app_tag : None ,
66+ bip340_domain_sep : Some ( domain_sep) ,
67+ }
68+ }
69+
3570 /// Signs a plain variable length message.
3671 ///
3772 /// You must provide an application tag to make sure signatures valid in one context are not
3873 /// valid in another. The tag is used as described [here].
3974 ///
75+ /// **Deprecation Note**: This method was implemented before [BIP340] had finalized its
76+ /// recommendation for domain separation. [BIP340] now recommends using a 33-byte padded
77+ /// prefix instead of the 64-byte prefix used by this method. Use [`Message::new`] instead,
78+ /// which implements the [BIP340]-compliant domain separation.
79+ ///
80+ /// [BIP340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
81+ ///
4082 /// [here]: https://github.com/sipa/bips/issues/207#issuecomment-673681901
83+ #[ deprecated(
84+ since = "0.12.0" ,
85+ note = "Use Message::new for BIP340-style domain separation. This method uses a 64-byte prefix which predates the BIP340 specification."
86+ ) ]
4187 pub fn plain ( app_tag : & ' static str , bytes : & ' a [ u8 ] ) -> Self {
4288 assert ! ( app_tag. len( ) <= 64 , "tag must be 64 bytes or less" ) ;
4389 assert ! ( !app_tag. is_empty( ) , "tag must not be empty" ) ;
4490 Message {
4591 bytes : Slice :: from ( bytes) ,
4692 app_tag : Some ( app_tag) ,
93+ bip340_domain_sep : None ,
4794 }
4895 }
4996
@@ -54,21 +101,28 @@ impl<'a, S: Secrecy> Message<'a, S> {
54101
55102 /// Length of the message as it is hashed
56103 pub fn len ( & self ) -> usize {
57- match self . app_tag {
58- Some ( _) => 64 + self . bytes . as_inner ( ) . len ( ) ,
59- None => self . bytes . as_inner ( ) . len ( ) ,
104+ match ( self . app_tag , self . bip340_domain_sep ) {
105+ ( Some ( _) , _) => 64 + self . bytes . as_inner ( ) . len ( ) ,
106+ ( _, Some ( _) ) => 33 + self . bytes . as_inner ( ) . len ( ) , // BIP340 style uses 33-byte prefix
107+ ( None , None ) => self . bytes . as_inner ( ) . len ( ) ,
60108 }
61109 }
62110}
63111
112+ #[ allow( deprecated) ]
64113impl < S > HashInto for Message < ' _ , S > {
65114 fn hash_into ( self , hash : & mut impl digest:: Update ) {
66115 if let Some ( prefix) = self . app_tag {
67116 let mut padded_prefix = [ 0u8 ; 64 ] ;
68117 padded_prefix[ ..prefix. len ( ) ] . copy_from_slice ( prefix. as_bytes ( ) ) ;
69118 hash. update ( & padded_prefix) ;
119+ } else if let Some ( domain_sep) = self . bip340_domain_sep {
120+ // BIP340-style domain separation: 33-byte prefix
121+ let mut padded_prefix = [ 0u8 ; 33 ] ;
122+ padded_prefix[ ..domain_sep. len ( ) ] . copy_from_slice ( domain_sep. as_bytes ( ) ) ;
123+ hash. update ( & padded_prefix) ;
70124 }
71- hash. update ( < & [ u8 ] > :: from ( self . bytes ) ) ;
125+ hash. update ( self . bytes . as_inner ( ) ) ;
72126 }
73127}
74128
@@ -78,15 +132,48 @@ mod test {
78132 use sha2:: { Digest , Sha256 } ;
79133
80134 #[ test]
81- fn message_hash_into ( ) {
82- let mut hash1 = Sha256 :: default ( ) ;
83- hash1. update ( "test" ) ;
84- hash1. update ( [ 0u8 ; 60 ] . as_ref ( ) ) ;
85- hash1. update ( "hello world" ) ;
135+ fn bip340_domain_separation ( ) {
136+ // Test that BIP340 domain separation uses 33-byte prefix
137+ let msg = Message :: < Public > :: new ( "test" , b"hello" ) ;
138+
139+ // Expected: "test" padded to 33 bytes + "hello"
140+ let mut expected_hash = Sha256 :: default ( ) ;
141+ let mut padded_prefix = [ 0u8 ; 33 ] ;
142+ padded_prefix[ ..4 ] . copy_from_slice ( b"test" ) ;
143+ expected_hash. update ( & padded_prefix) ;
144+ expected_hash. update ( b"hello" ) ;
145+
146+ let mut actual_hash = Sha256 :: default ( ) ;
147+ msg. hash_into ( & mut actual_hash) ;
148+
149+ assert_eq ! ( expected_hash. finalize( ) , actual_hash. finalize( ) ) ;
150+
151+ // Test length calculation
152+ assert_eq ! ( msg. len( ) , 33 + 5 ) ; // 33-byte prefix + 5-byte message
153+ }
154+
155+ #[ test]
156+ fn message_new_fixed_key_signature ( ) {
157+ use crate :: { fun:: s, new_with_deterministic_nonces} ;
158+ use core:: str:: FromStr ;
159+
160+ // Fixed test to ensure Message::new domain separation doesn't accidentally change
161+ let schnorr = new_with_deterministic_nonces :: < Sha256 > ( ) ;
162+ let secret_key = s ! ( 42 ) ;
163+ let keypair = schnorr. new_keypair ( secret_key) ;
164+
165+ let message = Message :: < Public > :: new ( "test-app" , b"test message" ) ;
166+ let signature = schnorr. sign ( & keypair, message) ;
86167
87- let mut hash2 = Sha256 :: default ( ) ;
88- Message :: < Public > :: plain ( "test" , b"hello world" ) . hash_into ( & mut hash2) ;
168+ // This signature was generated with the current implementation and should never change
169+ // to ensure backwards compatibility
170+ let expected_sig = crate :: Signature :: < Public > :: from_str (
171+ "5c49762df465f21993af631caedb3e478793142e15f200e70511e5af71387e52a3b9b6af189fa4b28a767254f2a8977f2e9db1866ad4dfbb083bb4fbd8dfe82e"
172+ ) . unwrap ( ) ;
89173
90- assert_eq ! ( hash1. finalize( ) , hash2. finalize( ) ) ;
174+ assert_eq ! (
175+ signature, expected_sig,
176+ "Message::new signature changed! This breaks backwards compatibility."
177+ ) ;
91178 }
92179}
0 commit comments