Skip to main content

bsv/primitives/
private_key.rs

1//! Private key type for secp256k1 ECDSA operations.
2//!
3//! PrivateKey wraps a BigNumber scalar in [1, n-1] and provides
4//! key generation, import/export (hex, WIF), signing, and public
5//! key derivation. Mirrors the TS SDK PrivateKey.ts API.
6
7use crate::primitives::base_point::BasePoint;
8use crate::primitives::big_number::{BigNumber, Endian};
9use crate::primitives::curve::Curve;
10use crate::primitives::ecdsa::ecdsa_sign;
11use crate::primitives::error::PrimitivesError;
12use crate::primitives::hash::{sha256, sha256_hmac};
13use crate::primitives::point::Point;
14use crate::primitives::public_key::PublicKey;
15use crate::primitives::random::random_bytes;
16use crate::primitives::signature::Signature;
17use crate::primitives::utils::{base58_check_decode, base58_check_encode};
18
19/// A secp256k1 private key (256-bit scalar in [1, n-1]).
20///
21/// Uses composition (not inheritance) with BigNumber as the internal
22/// representation, following Rust conventions.
23#[derive(Clone, Debug)]
24pub struct PrivateKey {
25    inner: BigNumber,
26}
27
28impl PrivateKey {
29    /// Generate a random private key using OS entropy.
30    ///
31    /// Generates 32 random bytes and reduces mod n, ensuring the
32    /// result is in [1, n-1].
33    pub fn from_random() -> Result<Self, PrimitivesError> {
34        let curve = Curve::secp256k1();
35        loop {
36            let bytes = random_bytes(32);
37            let bn = BigNumber::from_bytes(&bytes, Endian::Big);
38            let key = bn
39                .umod(&curve.n)
40                .map_err(|e| PrimitivesError::InvalidPrivateKey(format!("mod n: {}", e)))?;
41            if !key.is_zero() {
42                return Ok(PrivateKey { inner: key });
43            }
44            // Extremely unlikely (probability 2^-256), but loop to be safe
45        }
46    }
47
48    /// Create a private key from raw bytes (big-endian).
49    ///
50    /// Must be a valid 32-byte scalar in [1, n-1].
51    pub fn from_bytes(bytes: &[u8]) -> Result<Self, PrimitivesError> {
52        let bn = BigNumber::from_bytes(bytes, Endian::Big);
53        Self::validate_range(&bn)?;
54        Ok(PrivateKey { inner: bn })
55    }
56
57    /// Create a private key from a hexadecimal string.
58    ///
59    /// The hex string is parsed as a big-endian 256-bit integer.
60    /// Must be in [1, n-1].
61    pub fn from_hex(hex: &str) -> Result<Self, PrimitivesError> {
62        let bn = BigNumber::from_hex(hex)?;
63        Self::validate_range(&bn)?;
64        Ok(PrivateKey { inner: bn })
65    }
66
67    /// Create a private key from a string (alias for from_hex).
68    pub fn from_string(s: &str) -> Result<Self, PrimitivesError> {
69        Self::from_hex(s)
70    }
71
72    /// Create a private key from a WIF (Wallet Import Format) string.
73    ///
74    /// WIF format: Base58Check(prefix(1) || key(32) || compression_flag(1))
75    /// The compression flag must be 0x01 (we only support compressed keys).
76    pub fn from_wif(wif: &str) -> Result<Self, PrimitivesError> {
77        let (prefix, payload) = base58_check_decode(wif, 1)?;
78
79        if payload.len() != 33 {
80            return Err(PrimitivesError::InvalidWif(format!(
81                "invalid WIF data length: expected 33, got {}",
82                payload.len()
83            )));
84        }
85
86        if payload[32] != 0x01 {
87            return Err(PrimitivesError::InvalidWif(
88                "invalid WIF compression flag (expected 0x01)".to_string(),
89            ));
90        }
91
92        let _ = prefix; // prefix validated by Base58Check decode
93        let bn = BigNumber::from_bytes(&payload[..32], Endian::Big);
94        Self::validate_range(&bn)?;
95        Ok(PrivateKey { inner: bn })
96    }
97
98    /// Export the private key as a 64-character zero-padded hex string.
99    pub fn to_hex(&self) -> String {
100        let hex = self.inner.to_hex();
101        format!("{:0>64}", hex)
102    }
103
104    /// Export the private key in WIF (Wallet Import Format).
105    ///
106    /// WIF format: Base58Check(prefix || key(32) || 0x01)
107    /// Default prefix is `[0x80]` for mainnet.
108    pub fn to_wif(&self, prefix: &[u8]) -> String {
109        let mut key_data = self.inner.to_array(Endian::Big, Some(32));
110        key_data.push(0x01); // compression flag
111        base58_check_encode(&key_data, prefix)
112    }
113
114    /// Derive the corresponding public key.
115    ///
116    /// Computes pubkey = inner * G using the precomputed base point.
117    pub fn to_public_key(&self) -> PublicKey {
118        let base_point = BasePoint::instance();
119        let point = base_point.mul(&self.inner);
120        PublicKey::from_point(point)
121    }
122
123    /// Sign a message (raw bytes) using ECDSA.
124    ///
125    /// The message is first hashed with SHA-256, then signed
126    /// using RFC 6979 deterministic nonce generation.
127    pub fn sign(&self, message: &[u8], force_low_s: bool) -> Result<Signature, PrimitivesError> {
128        let msg_hash = sha256(message);
129        ecdsa_sign(&msg_hash, &self.inner, force_low_s)
130    }
131
132    /// Get the private key as 32 big-endian bytes.
133    pub fn to_bytes(&self) -> Vec<u8> {
134        self.inner.to_array(Endian::Big, Some(32))
135    }
136
137    /// Access the underlying BigNumber.
138    pub fn bn(&self) -> &BigNumber {
139        &self.inner
140    }
141
142    /// Compute ECDH shared secret: self.bn * public_key.point.
143    ///
144    /// Returns the resulting point on the curve. The public key must be
145    /// a valid point on secp256k1.
146    pub fn derive_shared_secret(&self, public_key: &PublicKey) -> Result<Point, PrimitivesError> {
147        if !public_key.point().validate() {
148            return Err(PrimitivesError::InvalidPublicKey(
149                "public key is not on the curve".to_string(),
150            ));
151        }
152        let result = public_key.point().mul(&self.inner);
153        Ok(result)
154    }
155
156    /// Derive a child private key using Type-42 key derivation (BRC-42).
157    ///
158    /// Computes: child = (self + HMAC-SHA256(shared_secret_compressed, invoice_number)) mod n
159    /// where shared_secret = self * counterparty.
160    pub fn derive_child(
161        &self,
162        counterparty: &PublicKey,
163        invoice_number: &str,
164    ) -> Result<PrivateKey, PrimitivesError> {
165        let curve = Curve::secp256k1();
166        let shared_secret = self.derive_shared_secret(counterparty)?;
167        let shared_secret_bytes = shared_secret.to_der(true); // 33-byte compressed
168        let hmac_result = sha256_hmac(&shared_secret_bytes, invoice_number.as_bytes());
169        let hmac_bn = BigNumber::from_bytes(&hmac_result, Endian::Big);
170        let child =
171            self.inner.add(&hmac_bn).umod(&curve.n).map_err(|e| {
172                PrimitivesError::ArithmeticError(format!("derive_child mod n: {}", e))
173            })?;
174        let child_bytes = child.to_array(Endian::Big, Some(32));
175        PrivateKey::from_bytes(&child_bytes)
176    }
177
178    /// Validate that a BigNumber is in the valid range [1, n-1].
179    fn validate_range(bn: &BigNumber) -> Result<(), PrimitivesError> {
180        let curve = Curve::secp256k1();
181        if bn.is_zero() {
182            return Err(PrimitivesError::InvalidPrivateKey(
183                "private key must not be zero".to_string(),
184            ));
185        }
186        if bn.cmp(&curve.n) >= 0 {
187            return Err(PrimitivesError::InvalidPrivateKey(
188                "private key must be less than curve order n".to_string(),
189            ));
190        }
191        if bn.is_negative() {
192            return Err(PrimitivesError::InvalidPrivateKey(
193                "private key must not be negative".to_string(),
194            ));
195        }
196        Ok(())
197    }
198}
199
200impl PartialEq for PrivateKey {
201    fn eq(&self, other: &Self) -> bool {
202        self.inner.cmp(&other.inner) == 0
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::primitives::ecdsa::ecdsa_verify;
210
211    // -----------------------------------------------------------------------
212    // PrivateKey: generation
213    // -----------------------------------------------------------------------
214
215    #[test]
216    fn test_private_key_from_random() {
217        let key = PrivateKey::from_random().unwrap();
218        let curve = Curve::secp256k1();
219
220        // Key should be in [1, n-1]
221        assert!(!key.bn().is_zero(), "Random key should not be zero");
222        assert!(
223            key.bn().cmp(&curve.n) < 0,
224            "Random key should be less than n"
225        );
226    }
227
228    #[test]
229    fn test_private_key_from_random_unique() {
230        let k1 = PrivateKey::from_random().unwrap();
231        let k2 = PrivateKey::from_random().unwrap();
232        assert_ne!(k1, k2, "Two random keys should differ");
233    }
234
235    // -----------------------------------------------------------------------
236    // PrivateKey: from_hex / to_hex
237    // -----------------------------------------------------------------------
238
239    #[test]
240    fn test_private_key_from_hex() {
241        let key = PrivateKey::from_hex("1").unwrap();
242        assert_eq!(
243            key.to_hex(),
244            "0000000000000000000000000000000000000000000000000000000000000001"
245        );
246    }
247
248    #[test]
249    fn test_private_key_hex_roundtrip() {
250        let hex = "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35";
251        let key = PrivateKey::from_hex(hex).unwrap();
252        assert_eq!(key.to_hex(), hex);
253    }
254
255    #[test]
256    fn test_private_key_from_hex_zero_rejected() {
257        let result = PrivateKey::from_hex("0");
258        assert!(result.is_err(), "Zero should be rejected");
259    }
260
261    #[test]
262    fn test_private_key_from_hex_too_large() {
263        // n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
264        // n itself should be rejected
265        let result = PrivateKey::from_hex(
266            "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141",
267        );
268        assert!(result.is_err(), "n should be rejected");
269    }
270
271    // -----------------------------------------------------------------------
272    // PrivateKey: from_string
273    // -----------------------------------------------------------------------
274
275    #[test]
276    fn test_private_key_from_string() {
277        let key = PrivateKey::from_string("1").unwrap();
278        assert_eq!(
279            key.to_hex(),
280            "0000000000000000000000000000000000000000000000000000000000000001"
281        );
282    }
283
284    // -----------------------------------------------------------------------
285    // PrivateKey: WIF import/export
286    // -----------------------------------------------------------------------
287
288    #[test]
289    fn test_private_key_wif_roundtrip() {
290        let key = PrivateKey::from_hex("1").unwrap();
291        let wif = key.to_wif(&[0x80]);
292        let recovered = PrivateKey::from_wif(&wif).unwrap();
293        assert_eq!(key, recovered, "WIF round-trip should recover same key");
294    }
295
296    #[test]
297    fn test_private_key_wif_known_vector() {
298        // Key = 1, mainnet WIF
299        let wif = "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn";
300        let key = PrivateKey::from_wif(wif).unwrap();
301        assert_eq!(
302            key.to_hex(),
303            "0000000000000000000000000000000000000000000000000000000000000001"
304        );
305    }
306
307    #[test]
308    fn test_private_key_to_wif_known_vector() {
309        let key = PrivateKey::from_hex("1").unwrap();
310        let wif = key.to_wif(&[0x80]);
311        assert_eq!(wif, "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn");
312    }
313
314    #[test]
315    fn test_private_key_wif_test_vectors() {
316        use serde::Deserialize;
317
318        #[derive(Deserialize)]
319        struct WifVector {
320            private_key_hex: String,
321            wif_mainnet: String,
322            wif_prefix: String,
323            #[allow(dead_code)]
324            description: String,
325        }
326
327        let data = include_str!("../../test-vectors/private_key_wif.json");
328        let vectors: Vec<WifVector> = serde_json::from_str(data).unwrap();
329
330        for (i, v) in vectors.iter().enumerate() {
331            let key = PrivateKey::from_hex(&v.private_key_hex).unwrap();
332            let prefix_bytes = crate::primitives::utils::from_hex(&v.wif_prefix).unwrap();
333            let wif = key.to_wif(&prefix_bytes);
334            assert_eq!(wif, v.wif_mainnet, "Vector {}: WIF mismatch", i);
335
336            // Round-trip
337            let recovered = PrivateKey::from_wif(&wif).unwrap();
338            assert_eq!(key, recovered, "Vector {}: WIF round-trip failed", i);
339        }
340    }
341
342    // -----------------------------------------------------------------------
343    // PrivateKey: to_public_key
344    // -----------------------------------------------------------------------
345
346    #[test]
347    fn test_private_key_to_public_key() {
348        // Key = 1, pubkey = G
349        let key = PrivateKey::from_hex("1").unwrap();
350        let pubkey = key.to_public_key();
351        let compressed = pubkey.to_der_hex();
352        assert_eq!(
353            compressed,
354            "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
355        );
356    }
357
358    // -----------------------------------------------------------------------
359    // PrivateKey: sign and verify
360    // -----------------------------------------------------------------------
361
362    #[test]
363    fn test_private_key_sign_verify() {
364        let key = PrivateKey::from_hex("1").unwrap();
365        let sig = key.sign(b"Hello, BSV!", true).unwrap();
366        let pubkey = key.to_public_key();
367
368        let msg_hash = sha256(b"Hello, BSV!");
369        assert!(
370            ecdsa_verify(&msg_hash, &sig, pubkey.point()),
371            "Signature should verify"
372        );
373    }
374
375    #[test]
376    fn test_private_key_sign_low_s() {
377        let curve = Curve::secp256k1();
378        let key = PrivateKey::from_hex("ff").unwrap();
379        let sig = key.sign(b"test low s", true).unwrap();
380        assert!(
381            sig.s().cmp(&curve.half_n) <= 0,
382            "s should be low when force_low_s is true"
383        );
384    }
385
386    // -----------------------------------------------------------------------
387    // PrivateKey: to_bytes
388    // -----------------------------------------------------------------------
389
390    #[test]
391    fn test_private_key_to_bytes() {
392        let key = PrivateKey::from_hex("1").unwrap();
393        let bytes = key.to_bytes();
394        assert_eq!(bytes.len(), 32);
395        assert_eq!(bytes[31], 1);
396        assert_eq!(bytes[0], 0);
397    }
398}