Skip to main content

signer_btc/
lib.rs

1//! Bitcoin transaction signer built on the [`bitcoin`] crate.
2//!
3//! [`Signer`] wraps a [`PrivateKey`] and uses the global [`secp256k1`] context
4//! for all cryptographic operations — ECDSA, Schnorr, PSBT, and BIP-137
5//! message signing.
6//!
7//! **Zero hand-rolled cryptography.**
8//!
9//! # Signing methods
10//!
11//! | Method | Description |
12//! |---|---|
13//! | [`Signer::sign_ecdsa`] | secp256k1 ECDSA signature |
14//! | [`Signer::sign_schnorr`] | BIP-340 Schnorr signature (Taproot) |
15//! | [`Signer::sign_message`] | BIP-137 Bitcoin Signed Message |
16//! | [`Signer::verify_message`] | Verify a BIP-137 message |
17//! | [`Signer::sign_psbt`] | Sign all applicable PSBT inputs |
18//!
19//! # `Deref`
20//!
21//! `Signer` implements `Deref<Target = PrivateKey>`, giving direct access
22//! to all [`PrivateKey`] methods (e.g. `to_wif()`, `public_key()`).
23
24mod error;
25
26use core::ops::Deref;
27use std::sync::LazyLock;
28
29pub use bitcoin;
30use bitcoin::base64::Engine;
31use bitcoin::base64::engine::general_purpose::STANDARD;
32use bitcoin::hashes::{Hash, HashEngine, sha256d};
33pub use bitcoin::psbt::Psbt;
34pub use bitcoin::secp256k1;
35use bitcoin::secp256k1::{All, Message, Secp256k1, Signing};
36pub use bitcoin::{
37    Address, CompressedPublicKey, Network, NetworkKind, PrivateKey, PublicKey, Transaction,
38};
39pub use error::Error;
40
41static SECP: LazyLock<Secp256k1<All>> = LazyLock::new(Secp256k1::new);
42
43/// BIP-137 address type for message signing.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum AddressType {
46    /// P2PKH (Legacy).
47    P2pkh,
48    /// P2SH-P2WPKH (Nested Segwit).
49    P2shP2wpkh,
50    /// P2WPKH (Native Segwit).
51    P2wpkh,
52}
53
54/// Bitcoin transaction signer.
55///
56/// Wraps a [`PrivateKey`] and uses the global [`secp256k1`] context for all
57/// operations. Implements [`Deref`] to [`PrivateKey`] for full upstream access.
58///
59/// Private key bytes are erased from memory on [`Drop`].
60///
61/// # Examples
62///
63/// ```
64/// use signer_btc::{Signer, Network, Address};
65///
66/// let signer = Signer::random(Network::Bitcoin);
67/// let sig = signer.sign_message("hello").unwrap();
68/// let addr = signer.p2wpkh_address(Network::Bitcoin);
69/// ```
70#[derive(Debug, Clone)]
71pub struct Signer {
72    key: PrivateKey,
73}
74
75impl Deref for Signer {
76    type Target = PrivateKey;
77
78    #[inline]
79    fn deref(&self) -> &Self::Target {
80        &self.key
81    }
82}
83
84impl Drop for Signer {
85    fn drop(&mut self) {
86        self.key.inner.non_secure_erase();
87    }
88}
89
90impl Signer {
91    /// Create a signer from a WIF-encoded private key.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the WIF string is invalid.
96    pub fn from_wif(wif: &str) -> Result<Self, Error> {
97        let key: PrivateKey = wif.parse()?;
98        Ok(Self { key })
99    }
100
101    /// Create a signer from a hex-encoded 32-byte private key.
102    ///
103    /// Accepts keys with or without `0x` prefix.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if the hex string is invalid or the key is not a valid
108    /// secp256k1 secret key.
109    pub fn from_hex(hex_str: &str, network: Network) -> Result<Self, Error> {
110        let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
111        let bytes: [u8; 32] = hex::decode(hex_str)?.try_into().map_err(|v: Vec<u8>| {
112            Error::InvalidKey(format!("expected 32 bytes, got {}", v.len()))
113        })?;
114        Self::from_bytes(&bytes, network)
115    }
116
117    /// Create a signer from raw 32-byte secret key bytes.
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if the bytes are not a valid secp256k1 secret key.
122    pub fn from_bytes(bytes: &[u8; 32], network: Network) -> Result<Self, Error> {
123        let secret_key = secp256k1::SecretKey::from_slice(bytes)?;
124        Ok(Self {
125            key: PrivateKey::new(secret_key, network),
126        })
127    }
128
129    /// Generate a random signer for the given network.
130    #[must_use]
131    pub fn random(network: Network) -> Self {
132        let (secret_key, _) = SECP.generate_keypair(&mut secp256k1::rand::thread_rng());
133        Self {
134            key: PrivateKey::new(secret_key, network),
135        }
136    }
137
138    /// Sign a 32-byte message digest with ECDSA (secp256k1).
139    #[must_use]
140    pub fn sign_ecdsa(&self, msg: &Message) -> secp256k1::ecdsa::Signature {
141        SECP.sign_ecdsa(msg, &self.key.inner)
142    }
143
144    /// Sign a 32-byte message with BIP-340 Schnorr (Taproot).
145    #[must_use]
146    pub fn sign_schnorr(&self, msg: &Message) -> secp256k1::schnorr::Signature {
147        let keypair = secp256k1::Keypair::from_secret_key(&*SECP, &self.key.inner);
148        SECP.sign_schnorr(msg, &keypair)
149    }
150
151    /// Create a BIP-137 Bitcoin Signed Message (defaults to [`AddressType::P2wpkh`]).
152    ///
153    /// # Errors
154    ///
155    /// Returns an error if signing fails.
156    pub fn sign_message(&self, msg: &str) -> Result<String, Error> {
157        self.sign_message_with_type(msg, AddressType::P2wpkh)
158    }
159
160    /// Create a BIP-137 Bitcoin Signed Message with a specific address type.
161    ///
162    /// The flag byte encodes which address type was used:
163    /// - 27–30: P2PKH uncompressed
164    /// - 31–34: P2PKH compressed
165    /// - 35–38: P2SH-P2WPKH
166    /// - 39–42: P2WPKH (native segwit)
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if signing fails.
171    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
172    pub fn sign_message_with_type(
173        &self,
174        msg: &str,
175        addr_type: AddressType,
176    ) -> Result<String, Error> {
177        let secp_msg = Message::from_digest(Self::signed_msg_hash(msg));
178        let sig = SECP.sign_ecdsa_recoverable(&secp_msg, &self.key.inner);
179        let (recovery_id, sig_bytes) = sig.serialize_compact();
180
181        let flag_base: u8 = match addr_type {
182            AddressType::P2pkh if self.key.compressed => 31,
183            AddressType::P2pkh => 27,
184            AddressType::P2shP2wpkh => 35,
185            AddressType::P2wpkh => 39,
186        };
187
188        let mut buf = [0u8; 65];
189        buf[0] = flag_base + recovery_id.to_i32() as u8;
190        buf[1..].copy_from_slice(&sig_bytes);
191        Ok(STANDARD.encode(buf))
192    }
193
194    /// Verify a BIP-137 Bitcoin Signed Message.
195    ///
196    /// Supports all standard flag byte ranges (P2PKH, P2SH-P2WPKH, P2WPKH).
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if the signature is malformed or recovery fails.
201    pub fn verify_message(
202        msg: &str,
203        signature_base64: &str,
204        expected_address: &Address,
205        network: Network,
206    ) -> Result<bool, Error> {
207        let raw = STANDARD
208            .decode(signature_base64)
209            .map_err(|e| Error::Signature(format!("invalid base64: {e}")))?;
210
211        if raw.len() != 65 {
212            return Err(Error::Signature(format!(
213                "expected 65 bytes, got {}",
214                raw.len()
215            )));
216        }
217
218        let flag = raw[0];
219        let recovery_id_raw = match flag {
220            27..=30 => flag - 27,
221            31..=34 => flag - 31,
222            35..=38 => flag - 35,
223            39..=42 => flag - 39,
224            _ => return Err(Error::Signature(format!("invalid flag byte: {flag}"))),
225        };
226
227        let recovery_id = secp256k1::ecdsa::RecoveryId::from_i32(i32::from(recovery_id_raw))?;
228        let recoverable =
229            secp256k1::ecdsa::RecoverableSignature::from_compact(&raw[1..], recovery_id)?;
230
231        let secp_msg = Message::from_digest(Self::signed_msg_hash(msg));
232        let recovered_pk = SECP.recover_ecdsa(&secp_msg, &recoverable)?;
233
234        let recovered_addr = match flag {
235            27..=30 => {
236                let pk = PublicKey::new_uncompressed(recovered_pk);
237                #[allow(deprecated)]
238                Address::p2pkh(pk, network)
239            }
240            31..=34 => {
241                let cpk = CompressedPublicKey(recovered_pk);
242                #[allow(deprecated)]
243                Address::p2pkh(PublicKey::from(cpk), network)
244            }
245            35..=38 => {
246                let cpk = CompressedPublicKey(recovered_pk);
247                Address::p2shwpkh(&cpk, network)
248            }
249            39..=42 => {
250                let cpk = CompressedPublicKey(recovered_pk);
251                Address::p2wpkh(&cpk, network)
252            }
253            _ => unreachable!(),
254        };
255
256        Ok(recovered_addr.script_pubkey() == expected_address.script_pubkey())
257    }
258
259    /// Sign all applicable inputs in a PSBT.
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if signing fails for any input.
264    pub fn sign_psbt(&self, psbt: &mut Psbt) -> Result<(), Error> {
265        psbt.sign(&PsbtKey(self.key), &*SECP)
266            .map(|_| ())
267            .map_err(|(_, errors)| {
268                let msg: Vec<String> = errors
269                    .iter()
270                    .map(|(idx, err)| format!("input {idx}: {err}"))
271                    .collect();
272                Error::Psbt(msg.join("; "))
273            })
274    }
275
276    /// Get the compressed public key.
277    ///
278    /// # Panics
279    ///
280    /// Panics if the internal key is invalid (should never happen).
281    #[must_use]
282    pub fn compressed_public_key(&self) -> CompressedPublicKey {
283        CompressedPublicKey::from_private_key(&*SECP, &self.key)
284            .expect("valid private key always produces valid public key")
285    }
286
287    /// Get the full public key.
288    #[must_use]
289    pub fn public_key(&self) -> PublicKey {
290        self.key.public_key(&*SECP)
291    }
292
293    /// Get the network kind.
294    #[inline]
295    #[must_use]
296    pub const fn network_kind(&self) -> NetworkKind {
297        self.key.network
298    }
299
300    /// P2WPKH address (Native Segwit).
301    #[must_use]
302    pub fn p2wpkh_address(&self, network: Network) -> Address {
303        Address::p2wpkh(&self.compressed_public_key(), network)
304    }
305
306    /// P2TR address (Taproot).
307    #[must_use]
308    pub fn p2tr_address(&self, network: Network) -> Address {
309        let keypair = secp256k1::Keypair::from_secret_key(&*SECP, &self.key.inner);
310        let (xonly, _) = keypair.x_only_public_key();
311        Address::p2tr(&*SECP, xonly, None, network)
312    }
313
314    /// P2PKH address (Legacy).
315    #[must_use]
316    pub fn p2pkh_address(&self, network: Network) -> Address {
317        #[allow(deprecated)]
318        Address::p2pkh(self.public_key(), network)
319    }
320
321    /// P2SH-P2WPKH address (Nested Segwit).
322    #[must_use]
323    pub fn p2sh_p2wpkh_address(&self, network: Network) -> Address {
324        Address::p2shwpkh(&self.compressed_public_key(), network)
325    }
326
327    /// Compute the BIP-137 double-SHA256 message hash.
328    fn signed_msg_hash(msg: &str) -> [u8; 32] {
329        let mut engine = sha256d::Hash::engine();
330        engine.input(b"\x18Bitcoin Signed Message:\n");
331        let msg_bytes = msg.as_bytes();
332        Self::write_compact_size(&mut engine, msg_bytes.len());
333        engine.input(msg_bytes);
334        sha256d::Hash::from_engine(engine).to_byte_array()
335    }
336
337    #[allow(clippy::cast_possible_truncation)]
338    fn write_compact_size<E: HashEngine>(engine: &mut E, size: usize) {
339        if size < 253 {
340            engine.input(&[size as u8]);
341        } else if size <= 0xFFFF {
342            engine.input(&[253]);
343            engine.input(&(size as u16).to_le_bytes());
344        } else if size <= 0xFFFF_FFFF {
345            engine.input(&[254]);
346            engine.input(&(size as u32).to_le_bytes());
347        } else {
348            engine.input(&[255]);
349            engine.input(&(size as u64).to_le_bytes());
350        }
351    }
352}
353
354/// Internal [`GetKey`](bitcoin::psbt::GetKey) implementation for PSBT signing.
355struct PsbtKey(PrivateKey);
356
357impl bitcoin::psbt::GetKey for PsbtKey {
358    type Error = bitcoin::psbt::GetKeyError;
359
360    fn get_key<C: Signing>(
361        &self,
362        key_request: bitcoin::psbt::KeyRequest,
363        secp: &Secp256k1<C>,
364    ) -> Result<Option<PrivateKey>, Self::Error> {
365        let our_pk = self.0.public_key(secp);
366        match key_request {
367            bitcoin::psbt::KeyRequest::Pubkey(ref pk) if our_pk.inner == pk.inner => {
368                Ok(Some(self.0))
369            }
370            bitcoin::psbt::KeyRequest::Bip32(_) => {
371                // Single-key signer: always offer our key and let the
372                // PSBT signer decide if it matches the derivation path.
373                Ok(Some(self.0))
374            }
375            _ => Ok(None),
376        }
377    }
378}
379
380#[cfg(feature = "kobe")]
381impl Signer {
382    /// Create a signer from a [`kobe_btc::DerivedAddress`].
383    ///
384    /// # Errors
385    ///
386    /// Returns an error if the private key is invalid.
387    pub fn from_derived(
388        derived: &kobe_btc::DerivedAddress,
389        network: Network,
390    ) -> Result<Self, Error> {
391        Self::from_wif(&derived.private_key_wif)
392            .or_else(|_| Self::from_hex(&derived.private_key_hex, network))
393    }
394
395    /// Create a signer from a [`kobe_btc::StandardWallet`].
396    ///
397    /// # Errors
398    ///
399    /// Returns an error if the WIF key is invalid.
400    pub fn from_standard_wallet(wallet: &kobe_btc::StandardWallet) -> Result<Self, Error> {
401        Self::from_wif(&wallet.to_wif())
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn assert_send_sync() {
411        fn assert<T: Send + Sync>() {}
412        assert::<Signer>();
413    }
414
415    #[test]
416    fn assert_clone() {
417        let s = Signer::random(Network::Bitcoin);
418        let s2 = s.clone();
419        assert_eq!(s.compressed_public_key(), s2.compressed_public_key());
420    }
421
422    #[test]
423    fn random_signer() {
424        let s = Signer::random(Network::Bitcoin);
425        assert!(!s.compressed_public_key().to_string().is_empty());
426    }
427
428    #[test]
429    fn wif_roundtrip() {
430        let s = Signer::random(Network::Bitcoin);
431        let wif = s.to_wif();
432        let restored = Signer::from_wif(&wif).unwrap();
433        assert_eq!(s.compressed_public_key(), restored.compressed_public_key());
434    }
435
436    #[test]
437    fn hex_roundtrip() {
438        let s = Signer::random(Network::Bitcoin);
439        let hex_key = hex::encode(s.key.inner.secret_bytes());
440        let restored = Signer::from_hex(&hex_key, Network::Bitcoin).unwrap();
441        assert_eq!(s.compressed_public_key(), restored.compressed_public_key());
442    }
443
444    #[test]
445    fn ecdsa_sign_verify() {
446        let s = Signer::random(Network::Bitcoin);
447        let msg = Message::from_digest([1u8; 32]);
448        let sig = s.sign_ecdsa(&msg);
449        SECP.verify_ecdsa(&msg, &sig, &s.public_key().inner)
450            .unwrap();
451    }
452
453    #[test]
454    fn schnorr_sign_verify() {
455        let s = Signer::random(Network::Bitcoin);
456        let msg = Message::from_digest([2u8; 32]);
457        let sig = s.sign_schnorr(&msg);
458        let keypair = secp256k1::Keypair::from_secret_key(&*SECP, &s.key.inner);
459        let (xonly, _) = keypair.x_only_public_key();
460        SECP.verify_schnorr(&sig, &msg, &xonly).unwrap();
461    }
462
463    #[test]
464    fn bip137_p2wpkh() {
465        let s = Signer::random(Network::Bitcoin);
466        let sig = s.sign_message("Hello, Bitcoin!").unwrap();
467        let addr = s.p2wpkh_address(Network::Bitcoin);
468        assert!(Signer::verify_message("Hello, Bitcoin!", &sig, &addr, Network::Bitcoin).unwrap());
469    }
470
471    #[test]
472    fn bip137_p2pkh() {
473        let s = Signer::random(Network::Bitcoin);
474        let sig = s
475            .sign_message_with_type("test", AddressType::P2pkh)
476            .unwrap();
477        let addr = s.p2pkh_address(Network::Bitcoin);
478        assert!(Signer::verify_message("test", &sig, &addr, Network::Bitcoin).unwrap());
479    }
480
481    #[test]
482    fn bip137_p2sh_p2wpkh() {
483        let s = Signer::random(Network::Bitcoin);
484        let sig = s
485            .sign_message_with_type("test", AddressType::P2shP2wpkh)
486            .unwrap();
487        let addr = s.p2sh_p2wpkh_address(Network::Bitcoin);
488        assert!(Signer::verify_message("test", &sig, &addr, Network::Bitcoin).unwrap());
489    }
490
491    #[test]
492    fn bip137_wrong_message_fails() {
493        let s = Signer::random(Network::Bitcoin);
494        let sig = s.sign_message("correct").unwrap();
495        let addr = s.p2wpkh_address(Network::Bitcoin);
496        assert!(!Signer::verify_message("wrong", &sig, &addr, Network::Bitcoin).unwrap());
497    }
498
499    #[test]
500    fn address_generation() {
501        let s = Signer::random(Network::Bitcoin);
502        assert!(!s.p2wpkh_address(Network::Bitcoin).to_string().is_empty());
503        assert!(!s.p2tr_address(Network::Bitcoin).to_string().is_empty());
504        assert!(!s.p2pkh_address(Network::Bitcoin).to_string().is_empty());
505        assert!(
506            !s.p2sh_p2wpkh_address(Network::Bitcoin)
507                .to_string()
508                .is_empty()
509        );
510    }
511
512    #[test]
513    fn network_kind() {
514        let s = Signer::random(Network::Testnet);
515        assert_eq!(s.network_kind(), NetworkKind::Test);
516    }
517
518    #[test]
519    fn deref_to_private_key() {
520        let s = Signer::random(Network::Bitcoin);
521        let _wif: String = s.to_wif();
522    }
523}