1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
use secp256kfun::{
digest::{self},
hash::HashInto,
};
/// A message to be signed.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Message<'a> {
/// The message bytes
pub bytes: &'a [u8],
/// The optional application tag to separate the signature from other applications.
#[deprecated(
since = "0.12.0",
note = "Use Message::new for BIP340-style domain separation"
)]
pub app_tag: Option<&'static str>,
/// The domain separator for [BIP340]-style domain separation (33-byte prefix)
///
/// [BIP340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
pub bip340_domain_sep: Option<&'static str>,
}
#[allow(deprecated)]
impl<'a> Message<'a> {
/// Create a raw message with no domain separation. The message bytes will be passed straight into the
/// challenge hash. Usually, you only use this when signing a pre-hashed message.
pub fn raw(bytes: &'a [u8]) -> Self {
Message {
bytes,
app_tag: None,
bip340_domain_sep: None,
}
}
/// Create an empty zero byte message.
pub fn empty() -> Self {
Self::raw(&[])
}
/// Create a message with [BIP340]-style domain separation using a 33-byte prefix.
///
/// The domain separator will be padded with null bytes to exactly 33 bytes and
/// prefixed to the message, as recommended in [BIP340] for domain separation.
///
/// [BIP340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
///
/// # Example
/// ```
/// use schnorr_fun::Message;
/// let message = Message::new("my-app/sign", b"hello world");
/// ```
pub fn new(domain_sep: &'static str, bytes: &'a [u8]) -> Self {
assert!(!domain_sep.is_empty(), "domain separator must not be empty");
assert!(
domain_sep.len() <= 33,
"domain separator must be 33 bytes or less"
);
Message {
bytes,
app_tag: None,
bip340_domain_sep: Some(domain_sep),
}
}
/// Signs a plain variable length message.
///
/// You must provide an application tag to make sure signatures valid in one context are not
/// valid in another. The tag is used as described [here].
///
/// **Deprecation Note**: This method was implemented before [BIP340] had finalized its
/// recommendation for domain separation. [BIP340] now recommends using a 33-byte padded
/// prefix instead of the 64-byte prefix used by this method. Use [`Message::new`] instead,
/// which implements the [BIP340]-compliant domain separation.
///
/// [BIP340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
///
/// [here]: https://github.com/sipa/bips/issues/207#issuecomment-673681901
#[deprecated(
since = "0.12.0",
note = "Use Message::new for BIP340-style domain separation. This method uses a 64-byte prefix which predates the BIP340 specification."
)]
pub fn plain(app_tag: &'static str, bytes: &'a [u8]) -> Self {
assert!(app_tag.len() <= 64, "tag must be 64 bytes or less");
assert!(!app_tag.is_empty(), "tag must not be empty");
Message {
bytes,
app_tag: Some(app_tag),
bip340_domain_sep: None,
}
}
/// Check if the message is empty with zero length
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Length of the message as it is hashed
pub fn len(&self) -> usize {
match (self.app_tag, self.bip340_domain_sep) {
(Some(_), _) => 64 + self.bytes.len(),
(_, Some(_)) => 33 + self.bytes.len(), // BIP340 style uses 33-byte prefix
(None, None) => self.bytes.len(),
}
}
}
#[allow(deprecated)]
impl HashInto for Message<'_> {
fn hash_into(self, hash: &mut impl digest::Update) {
if let Some(prefix) = self.app_tag {
let mut padded_prefix = [0u8; 64];
padded_prefix[..prefix.len()].copy_from_slice(prefix.as_bytes());
hash.update(&padded_prefix);
} else if let Some(domain_sep) = self.bip340_domain_sep {
// BIP340-style domain separation: 33-byte prefix
let mut padded_prefix = [0u8; 33];
padded_prefix[..domain_sep.len()].copy_from_slice(domain_sep.as_bytes());
hash.update(&padded_prefix);
}
hash.update(self.bytes);
}
}
#[cfg(test)]
mod test {
use super::*;
use sha2::{Digest, Sha256};
#[test]
fn bip340_domain_separation() {
// Test that BIP340 domain separation uses 33-byte prefix
let msg = Message::new("test", b"hello");
// Expected: "test" padded to 33 bytes + "hello"
let mut expected_hash = Sha256::default();
let mut padded_prefix = [0u8; 33];
padded_prefix[..4].copy_from_slice(b"test");
expected_hash.update(padded_prefix);
expected_hash.update(b"hello");
let mut actual_hash = Sha256::default();
msg.hash_into(&mut actual_hash);
assert_eq!(expected_hash.finalize(), actual_hash.finalize());
// Test length calculation
assert_eq!(msg.len(), 33 + 5); // 33-byte prefix + 5-byte message
}
#[test]
fn message_new_fixed_key_signature() {
use crate::{Signature, fun::prelude::*, new_with_deterministic_nonces};
use core::str::FromStr;
// Fixed test to ensure Message::new domain separation doesn't accidentally change
let schnorr = new_with_deterministic_nonces::<Sha256>();
let secret_key = s!(42);
let keypair = schnorr.new_keypair(secret_key);
let message = Message::new("test-app", b"test message");
let signature = schnorr.sign(&keypair, message);
// This signature was generated with the current implementation and should never change
// to ensure backwards compatibility
let expected_sig = Signature::from_str(
"5c49762df465f21993af631caedb3e478793142e15f200e70511e5af71387e52a3b9b6af189fa4b28a767254f2a8977f2e9db1866ad4dfbb083bb4fbd8dfe82e"
).unwrap();
assert_eq!(
signature, expected_sig,
"Message::new signature changed! This breaks backwards compatibility."
);
}
}