Skip to main content

bsv/compat/
bsm.rs

1//! Bitcoin Signed Messages (BSM) implementation.
2//!
3//! Provides legacy message signing and verification compatible with
4//! the "Bitcoin Signed Message:\n" prefix convention. This is a
5//! backward-compatibility feature; the preferred modern equivalent
6//! is BRC-77.
7
8use crate::compat::CompatError;
9use crate::primitives::big_number::{BigNumber, Endian};
10use crate::primitives::ecdsa::{ecdsa_sign, ecdsa_verify};
11use crate::primitives::hash::hash256;
12use crate::primitives::private_key::PrivateKey;
13use crate::primitives::public_key::PublicKey;
14use crate::primitives::signature::Signature;
15
16const BSM_PREFIX: &[u8] = b"Bitcoin Signed Message:\n";
17
18/// Bitcoin Signed Messages (BSM) -- legacy message signing and verification.
19///
20/// BSM uses a magic prefix to prevent cross-protocol signature attacks.
21/// Messages are double-SHA256 hashed with the prefix before signing.
22pub struct BSM;
23
24/// Write a Bitcoin VarInt to a buffer.
25///
26/// Encoding:
27/// - value < 0xfd: 1 byte
28/// - value <= 0xffff: 0xfd + 2 bytes LE
29/// - value <= 0xffffffff: 0xfe + 4 bytes LE
30/// - else: 0xff + 8 bytes LE
31fn write_varint(buf: &mut Vec<u8>, value: usize) {
32    if value < 0xfd {
33        buf.push(value as u8);
34    } else if value <= 0xffff {
35        buf.push(0xfd);
36        buf.extend_from_slice(&(value as u16).to_le_bytes());
37    } else if value <= 0xffffffff {
38        buf.push(0xfe);
39        buf.extend_from_slice(&(value as u32).to_le_bytes());
40    } else {
41        buf.push(0xff);
42        buf.extend_from_slice(&(value as u64).to_le_bytes());
43    }
44}
45
46impl BSM {
47    /// Compute the BSM magic hash of a message.
48    ///
49    /// Constructs the prefixed buffer:
50    ///   VarInt(25) || "Bitcoin Signed Message:\n" || VarInt(message.len()) || message
51    ///
52    /// Then returns hash256 (double SHA-256) of the buffer.
53    pub fn magic_hash(message: &[u8]) -> [u8; 32] {
54        let mut buf = Vec::new();
55        write_varint(&mut buf, BSM_PREFIX.len());
56        buf.extend_from_slice(BSM_PREFIX);
57        write_varint(&mut buf, message.len());
58        buf.extend_from_slice(message);
59        hash256(&buf)
60    }
61
62    /// Sign a message using BSM.
63    ///
64    /// Returns a 65-byte compact BSM signature (recovery byte + 32-byte r + 32-byte s).
65    /// The magic hash is computed, then ECDSA signing is performed with the hash256 output.
66    /// Recovery factor is calculated and encoded in the first byte.
67    pub fn sign(message: &[u8], private_key: &PrivateKey) -> Result<Vec<u8>, CompatError> {
68        let msg_hash = Self::magic_hash(message);
69
70        // ecdsa_sign expects [u8; 32] message hash -- pass hash256 output directly
71        let sig = ecdsa_sign(&msg_hash, private_key.bn(), true)?;
72
73        // Calculate recovery factor
74        let pub_key = private_key.to_public_key();
75        let msg_bn = BigNumber::from_bytes(&msg_hash, Endian::Big);
76        let recovery = sig.calculate_recovery_factor(&pub_key, &msg_bn)?;
77
78        // Return compact BSM format (65 bytes, always compressed)
79        Ok(sig.to_compact_bsm(recovery, true))
80    }
81
82    /// Verify a BSM signed message.
83    ///
84    /// The signature must be in 65-byte compact BSM format.
85    /// Verifies the signature against the provided public key.
86    pub fn verify(
87        message: &[u8],
88        sig_bytes: &[u8],
89        pub_key: &PublicKey,
90    ) -> Result<bool, CompatError> {
91        let (signature, _recovery, _compressed) = Signature::from_compact_bsm(sig_bytes)?;
92
93        let msg_hash = Self::magic_hash(message);
94
95        Ok(ecdsa_verify(&msg_hash, &signature, pub_key.point()))
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::primitives::big_number::{BigNumber, Endian};
103
104    // Base64 decode helper
105    fn base64_decode(input: &str) -> Vec<u8> {
106        let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
107        let mut result = Vec::new();
108        let mut buf: u32 = 0;
109        let mut bits: u32 = 0;
110
111        for &byte in input.as_bytes() {
112            if byte == b'=' {
113                break;
114            }
115            let val = table.iter().position(|&b| b == byte);
116            if let Some(v) = val {
117                buf = (buf << 6) | (v as u32);
118                bits += 6;
119                if bits >= 8 {
120                    bits -= 8;
121                    result.push((buf >> bits) as u8);
122                    buf &= (1 << bits) - 1;
123                }
124            }
125        }
126        result
127    }
128
129    // Base64 encode helper
130    fn base64_encode(data: &[u8]) -> String {
131        let table = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
132        let mut result = String::new();
133        let mut i = 0;
134        while i < data.len() {
135            let b0 = data[i] as u32;
136            let b1 = if i + 1 < data.len() {
137                data[i + 1] as u32
138            } else {
139                0
140            };
141            let b2 = if i + 2 < data.len() {
142                data[i + 2] as u32
143            } else {
144                0
145            };
146            let triple = (b0 << 16) | (b1 << 8) | b2;
147            result.push(table[((triple >> 18) & 0x3f) as usize] as char);
148            result.push(table[((triple >> 12) & 0x3f) as usize] as char);
149            if i + 1 < data.len() {
150                result.push(table[((triple >> 6) & 0x3f) as usize] as char);
151            } else {
152                result.push('=');
153            }
154            if i + 2 < data.len() {
155                result.push(table[(triple & 0x3f) as usize] as char);
156            } else {
157                result.push('=');
158            }
159            i += 3;
160        }
161        result
162    }
163
164    #[test]
165    fn test_magic_hash_produces_32_bytes() {
166        let hash = BSM::magic_hash(b"hello");
167        assert_eq!(hash.len(), 32);
168    }
169
170    #[test]
171    fn test_magic_hash_deterministic() {
172        let h1 = BSM::magic_hash(b"hello");
173        let h2 = BSM::magic_hash(b"hello");
174        assert_eq!(h1, h2);
175    }
176
177    #[test]
178    fn test_magic_hash_different_messages() {
179        let h1 = BSM::magic_hash(b"hello");
180        let h2 = BSM::magic_hash(b"world");
181        assert_ne!(h1, h2);
182    }
183
184    #[test]
185    fn test_sign_produces_65_bytes() {
186        let priv_key = PrivateKey::from_hex(
187            "0000000000000000000000000000000000000000000000000000000000000001",
188        )
189        .unwrap();
190        let sig = BSM::sign(b"test message", &priv_key).unwrap();
191        assert_eq!(sig.len(), 65, "BSM signature must be 65 bytes");
192    }
193
194    #[test]
195    fn test_verify_correct_signature() {
196        let priv_key = PrivateKey::from_hex(
197            "0000000000000000000000000000000000000000000000000000000000000001",
198        )
199        .unwrap();
200        let pub_key = priv_key.to_public_key();
201        let message = b"test message";
202        let sig = BSM::sign(message, &priv_key).unwrap();
203        let result = BSM::verify(message, &sig, &pub_key).unwrap();
204        assert!(result, "verify should return true for matching sig");
205    }
206
207    #[test]
208    fn test_verify_wrong_message() {
209        let priv_key = PrivateKey::from_hex(
210            "0000000000000000000000000000000000000000000000000000000000000001",
211        )
212        .unwrap();
213        let pub_key = priv_key.to_public_key();
214        let sig = BSM::sign(b"correct message", &priv_key).unwrap();
215        let result = BSM::verify(b"wrong message", &sig, &pub_key).unwrap();
216        assert!(!result, "verify should return false for wrong message");
217    }
218
219    #[test]
220    fn test_verify_wrong_public_key() {
221        let priv_key1 = PrivateKey::from_hex(
222            "0000000000000000000000000000000000000000000000000000000000000001",
223        )
224        .unwrap();
225        let priv_key2 = PrivateKey::from_hex(
226            "0000000000000000000000000000000000000000000000000000000000000002",
227        )
228        .unwrap();
229        let wrong_pub_key = priv_key2.to_public_key();
230        let sig = BSM::sign(b"test message", &priv_key1).unwrap();
231        let result = BSM::verify(b"test message", &sig, &wrong_pub_key).unwrap();
232        assert!(!result, "verify should return false for wrong public key");
233    }
234
235    #[test]
236    fn test_sign_verify_roundtrip_various_messages() {
237        let priv_key = PrivateKey::from_hex(
238            "00000000000000000000000000000000000000000000000000000000deadbeef",
239        )
240        .unwrap();
241        let pub_key = priv_key.to_public_key();
242
243        let messages: &[&[u8]] = &[
244            b"",
245            b"a",
246            b"hello world",
247            b"The quick brown fox jumps over the lazy dog",
248            &[0u8; 256],
249        ];
250
251        for msg in messages {
252            let sig = BSM::sign(msg, &priv_key).unwrap();
253            assert_eq!(sig.len(), 65);
254            let result = BSM::verify(msg, &sig, &pub_key).unwrap();
255            assert!(
256                result,
257                "round-trip failed for message of length {}",
258                msg.len()
259            );
260        }
261    }
262
263    #[test]
264    fn test_cross_sdk_sign_vector() {
265        // From TS SDK BSM test: WIF L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu
266        // Message: "hello world"
267        // Expected base64: H4T8Asr0WkC6wYfBESR6pCAfECtdsPM4fwiSQ2qndFi8dVtv/mrOFaySx9xQE7j24ugoJ4iGnsRwAC8QwaoHOXk=
268        let priv_key =
269            PrivateKey::from_wif("L211enC224G1kV8pyyq7bjVd9SxZebnRYEzzM3i7ZHCc1c5E7dQu").unwrap();
270        let message = b"hello world";
271        let sig = BSM::sign(message, &priv_key).unwrap();
272        let sig_b64 = base64_encode(&sig);
273        assert_eq!(
274            sig_b64,
275            "H4T8Asr0WkC6wYfBESR6pCAfECtdsPM4fwiSQ2qndFi8dVtv/mrOFaySx9xQE7j24ugoJ4iGnsRwAC8QwaoHOXk=",
276            "BSM signature should match TS SDK output"
277        );
278    }
279
280    #[test]
281    fn test_cross_sdk_verify_vector() {
282        // From TS SDK BSM test: verify "Texas" with known public key
283        let sig_b64 = "IAV89EkfHSzAIA8cEWbbKHUYzJqcShkpWaXGJ5+mf4+YIlf3XNlr0bj9X60sNe1A7+x9qyk+zmXropMDY4370n8=";
284        let sig_bytes = base64_decode(sig_b64);
285        let pub_key = PublicKey::from_string(
286            "03d4d1a6c5d8c03b0e671bc1891b69afaecb40c0686188fe9019f93581b43e8334",
287        )
288        .unwrap();
289        let message = b"Texas";
290        let result = BSM::verify(message, &sig_bytes, &pub_key).unwrap();
291        assert!(result, "should verify TS SDK BSM signature for 'Texas'");
292    }
293}