bsv_wasm/signature/
mod.rs

1use crate::{get_hash_digest, BSVErrors, PublicKey, SigHash, SigningHash, ECDSA};
2use k256::{ecdsa::recoverable, ecdsa::Signature as SecpSignature, FieldBytes};
3use num_traits::FromPrimitive;
4#[cfg(target_arch = "wasm32")]
5use wasm_bindgen::{prelude::*, throw_str};
6
7#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen)]
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct RecoveryInfo {
10    is_y_odd: bool,
11    is_x_reduced: bool,
12    is_pubkey_compressed: bool,
13}
14
15#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen)]
16impl RecoveryInfo {
17    pub fn new(is_y_odd: bool, is_x_reduced: bool, is_pubkey_compressed: bool) -> RecoveryInfo {
18        RecoveryInfo {
19            is_y_odd,
20            is_x_reduced,
21            is_pubkey_compressed,
22        }
23    }
24
25    pub fn from_byte(recovery_byte: u8, is_pubkey_compressed: bool) -> RecoveryInfo {
26        RecoveryInfo {
27            is_x_reduced: (recovery_byte & 0b10) != 0,
28            is_y_odd: (recovery_byte & 1) != 0,
29            is_pubkey_compressed,
30        }
31    }
32
33    pub fn default() -> RecoveryInfo {
34        RecoveryInfo {
35            is_y_odd: false,
36            is_x_reduced: false,
37            is_pubkey_compressed: false,
38        }
39    }
40}
41
42#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen)]
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Signature {
45    pub(crate) sig: k256::ecdsa::Signature,
46    pub(crate) recovery: Option<RecoveryInfo>,
47}
48
49/**
50 * Implementation Methods
51 */
52impl Signature {
53    pub(crate) fn from_der_impl(bytes: &[u8]) -> Result<Signature, BSVErrors> {
54        let sighash_stripped_bytes = match bytes.last().and_then(|v| SigHash::from_u8(*v)) {
55            Some(_v) => bytes[0..bytes.len() - 1].to_vec(),
56            _ => bytes.to_vec(),
57        };
58
59        let sig = SecpSignature::from_der(&sighash_stripped_bytes)?;
60
61        Ok(Signature { sig, recovery: None })
62    }
63
64    pub(crate) fn from_hex_der_impl(hex: &str) -> Result<Signature, BSVErrors> {
65        let bytes = hex::decode(hex)?;
66        Signature::from_der_impl(&bytes)
67    }
68
69    pub(crate) fn get_public_key(&self, message: &[u8], hash_algo: SigningHash) -> Result<PublicKey, BSVErrors> {
70        let recovery = match &self.recovery {
71            Some(v) => v,
72            None => {
73                return Err(BSVErrors::PublicKeyRecoveryError(
74                    "No recovery info is provided in this signature, unable to recover private key. Use compact byte serialisation instead.".into(),
75                    ecdsa::Error::new(),
76                ))
77            }
78        };
79
80        let id = ecdsa::RecoveryId::new(recovery.is_y_odd, recovery.is_x_reduced);
81        let k256_recovery = id.try_into().map_err(|e| BSVErrors::PublicKeyRecoveryError("".into(), e))?;
82
83        let recoverable_sig = recoverable::Signature::new(&self.sig, k256_recovery)?;
84        let message_digest = get_hash_digest(hash_algo, message);
85        let verify_key = match recoverable_sig.recover_verify_key_from_digest(message_digest) {
86            Ok(v) => v,
87            Err(e) => {
88                return Err(BSVErrors::PublicKeyRecoveryError(format!("Signature Hex: {} Id: {:?}", self.to_der_hex(), recovery), e));
89            }
90        };
91
92        let pub_key = PublicKey::from_bytes_impl(&verify_key.to_bytes())?;
93
94        Ok(pub_key)
95    }
96
97    pub(crate) fn from_compact_impl(compact_bytes: &[u8]) -> Result<Signature, BSVErrors> {
98        // 27-30: P2PKH uncompressed
99        // 31-34: P2PKH compressed
100        let (recovery, is_compressed) = match (compact_bytes[0] - 27) as i8 - 4 {
101            x if x < 0 => (x + 4, false),
102            x => (x, true),
103        };
104
105        // TODO: Check Recovery Endianness so we can recover x and y info.
106        if recovery > 3 {
107            return Err(BSVErrors::SignatureError("Cannot have recovery byte that is larger than 3."));
108        }
109
110        let r = *FieldBytes::from_slice(&compact_bytes[1..33]);
111        let s = *FieldBytes::from_slice(&compact_bytes[33..65]);
112
113        let sig = SecpSignature::from_scalars(r, s)?;
114
115        Ok(Signature {
116            sig,
117            recovery: Some(RecoveryInfo::from_byte(recovery as u8, is_compressed)),
118        })
119    }
120}
121
122#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen)]
123impl Signature {
124    /// DER representation of signature, does not contain any recovery information, so cannot be used for BSM
125    #[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen(js_name = toHex))]
126    pub fn to_der_hex(&self) -> String {
127        let bytes = self.sig.to_der();
128
129        hex::encode(bytes)
130    }
131
132    /// DER representation of signature, does not contain any recovery information, so cannot be used for BSM
133    #[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen(js_name = toBytes))]
134    pub fn to_der_bytes(&self) -> Vec<u8> {
135        let bytes = self.sig.to_der();
136
137        bytes.as_bytes().to_vec()
138    }
139
140    /// NOTE: Provide recovery info if the current signature object doesnt contain it.
141    #[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen(js_name = toCompactBytes))]
142    pub fn to_compact_bytes(&self, recovery_info: Option<RecoveryInfo>) -> Vec<u8> {
143        // TODO: Test Compact Bytes length vs DER only
144        let RecoveryInfo {
145            is_y_odd,
146            is_x_reduced,
147            is_pubkey_compressed,
148        } = recovery_info.or_else(|| self.recovery.clone()).unwrap_or_else(RecoveryInfo::default);
149
150        let mut recovery = ((is_x_reduced as u8) << 1 | (is_y_odd as u8)) + 27 + 4;
151
152        if !is_pubkey_compressed {
153            recovery -= 4
154        }
155
156        let mut compact_buf = vec![recovery];
157        let r_bytes = &*self.sig.r().to_bytes();
158        compact_buf.extend_from_slice(r_bytes);
159
160        let s_bytes = &*self.sig.s().to_bytes();
161        compact_buf.extend_from_slice(s_bytes);
162
163        compact_buf
164    }
165
166    /// NOTE: Provide recovery info if the current signature object doesnt contain it.
167    #[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen(js_name = toCompactHex))]
168    pub fn to_compact_hex(&self, recovery_info: Option<RecoveryInfo>) -> String {
169        hex::encode(self.to_compact_bytes(recovery_info))
170    }
171
172    #[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen(js_name = verifyMessage))]
173    pub fn verify_message(&self, message: &[u8], pub_key: &PublicKey) -> bool {
174        ECDSA::verify_digest_impl(message, pub_key, self, SigningHash::Sha256).is_ok()
175    }
176}
177
178/**
179 * WASM Exported Methods
180 */
181#[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen)]
182#[cfg(target_arch = "wasm32")]
183impl Signature {
184    #[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen(js_name = fromDER))]
185    pub fn from_der(bytes: &[u8]) -> Result<Signature, JsValue> {
186        Signature::from_der_impl(bytes).map_err(|e| JsValue::from_str(&e.to_string()))
187    }
188
189    #[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen(js_name = fromHexDER))]
190    pub fn from_hex_der(hex: &str) -> Result<Signature, JsValue> {
191        match Signature::from_hex_der_impl(hex) {
192            Ok(v) => Ok(v),
193            Err(e) => Err(JsValue::from_str(&e.to_string())),
194        }
195    }
196
197    #[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen(js_name = fromCompactBytes))]
198    pub fn from_compact_bytes(compact_bytes: &[u8]) -> Result<Signature, JsValue> {
199        match Signature::from_compact_impl(compact_bytes) {
200            Ok(v) => Ok(v),
201            Err(e) => Err(JsValue::from_str(&e.to_string())),
202        }
203    }
204
205    #[cfg_attr(all(target_arch = "wasm32", feature = "wasm-bindgen-signature"), wasm_bindgen(js_name = recoverPublicKey))]
206    pub fn recover_public_key(&self, message: &[u8], hash_algo: SigningHash) -> Result<PublicKey, JsValue> {
207        match Signature::get_public_key(&self, &message, hash_algo) {
208            Ok(v) => Ok(v),
209            Err(e) => Err(JsValue::from_str(&e.to_string())),
210        }
211    }
212}
213
214/**
215 * Native Exported Methods
216 */
217#[cfg(not(target_arch = "wasm32"))]
218impl Signature {
219    #[cfg(not(target_arch = "wasm32"))]
220    pub fn from_der(bytes: &[u8]) -> Result<Signature, BSVErrors> {
221        Signature::from_der_impl(bytes)
222    }
223
224    #[cfg(not(target_arch = "wasm32"))]
225    pub fn from_hex_der(hex: &str) -> Result<Signature, BSVErrors> {
226        Signature::from_hex_der_impl(hex)
227    }
228
229    pub fn from_compact_bytes(compact_bytes: &[u8]) -> Result<Signature, BSVErrors> {
230        Signature::from_compact_impl(compact_bytes)
231    }
232
233    #[cfg(not(target_arch = "wasm32"))]
234    pub fn recover_public_key(&self, message: &[u8], hash_algo: SigningHash) -> Result<PublicKey, BSVErrors> {
235        Signature::get_public_key(self, message, hash_algo)
236    }
237}