Skip to main content

bsv_primitives/ec/
private_key.rs

1//! secp256k1 private key with Bitcoin-specific functionality.
2//!
3//! Wraps k256 signing key and adds WIF encoding, child key derivation (BRC-42),
4//! shared secret computation, and compact signature support.
5
6use k256::ecdsa::SigningKey;
7use k256::elliptic_curve::sec1::ToEncodedPoint;
8use k256::elliptic_curve::ScalarPrimitive;
9use k256::{Scalar, Secp256k1};
10use rand::rngs::OsRng;
11
12use crate::ec::public_key::PublicKey;
13use crate::ec::signature::Signature;
14use crate::hash::{sha256_hmac, sha256d};
15use crate::PrimitivesError;
16
17/// A secp256k1 private key for signing and key derivation.
18///
19/// Wraps a k256 `SigningKey` and provides Bitcoin-specific functionality
20/// including WIF serialization, BRC-42 child derivation, and ECDH shared secrets.
21#[derive(Clone, Debug)]
22pub struct PrivateKey {
23    /// The underlying k256 signing key.
24    inner: SigningKey,
25}
26
27/// Length of a serialized private key in bytes.
28const PRIVATE_KEY_BYTES_LEN: usize = 32;
29
30/// Mainnet WIF prefix byte.
31const MAINNET_PREFIX: u8 = 0x80;
32
33/// Compression flag byte appended to WIF for compressed public keys.
34const COMPRESS_MAGIC: u8 = 0x01;
35
36impl PrivateKey {
37    /// Generate a new random private key using the OS random number generator.
38    ///
39    /// # Returns
40    /// A new randomly generated `PrivateKey`.
41    pub fn new() -> Self {
42        let signing_key = SigningKey::random(&mut OsRng);
43        PrivateKey { inner: signing_key }
44    }
45
46    /// Create a private key from raw 32-byte scalar.
47    ///
48    /// # Arguments
49    /// * `bytes` - A 32-byte slice representing the private key scalar.
50    ///
51    /// # Returns
52    /// `Ok(PrivateKey)` if the bytes represent a valid scalar on secp256k1,
53    /// or an error if the scalar is zero or out of range.
54    pub fn from_bytes(bytes: &[u8]) -> Result<Self, PrimitivesError> {
55        if bytes.len() != PRIVATE_KEY_BYTES_LEN {
56            return Err(PrimitivesError::InvalidPrivateKey(format!(
57                "expected {} bytes, got {}",
58                PRIVATE_KEY_BYTES_LEN,
59                bytes.len()
60            )));
61        }
62        let signing_key = SigningKey::from_bytes(bytes.into())?;
63        Ok(PrivateKey { inner: signing_key })
64    }
65
66    /// Create a private key from a hexadecimal string.
67    ///
68    /// # Arguments
69    /// * `hex_str` - A 64-character hex string representing the 32-byte scalar.
70    ///
71    /// # Returns
72    /// `Ok(PrivateKey)` on success, or an error if the hex is invalid or the scalar is invalid.
73    pub fn from_hex(hex_str: &str) -> Result<Self, PrimitivesError> {
74        if hex_str.is_empty() {
75            return Err(PrimitivesError::InvalidPrivateKey(
76                "private key hex is empty".to_string(),
77            ));
78        }
79        let bytes = hex::decode(hex_str)?;
80        Self::from_bytes(&bytes)
81    }
82
83    /// Create a private key from a WIF (Wallet Import Format) string.
84    ///
85    /// Decodes the Base58Check-encoded string, validates the checksum,
86    /// and extracts the 32-byte private key scalar.
87    ///
88    /// # Arguments
89    /// * `wif` - A Base58Check-encoded WIF string (compressed or uncompressed).
90    ///
91    /// # Returns
92    /// `Ok(PrivateKey)` on success, or an error if the WIF is malformed or the checksum fails.
93    pub fn from_wif(wif: &str) -> Result<Self, PrimitivesError> {
94        let decoded = bs58::decode(wif)
95            .into_vec()
96            .map_err(|e| PrimitivesError::InvalidWif(e.to_string()))?;
97        let decoded_len = decoded.len();
98
99        // Determine if compressed based on length:
100        // 1 byte prefix + 32 bytes key + 1 byte compress flag + 4 byte checksum = 38
101        // 1 byte prefix + 32 bytes key + 4 byte checksum = 37
102        let is_compressed = match decoded_len {
103            38 => {
104                if decoded[33] != COMPRESS_MAGIC {
105                    return Err(PrimitivesError::InvalidWif(
106                        "malformed private key: invalid compression flag".to_string(),
107                    ));
108                }
109                true
110            }
111            37 => false,
112            _ => {
113                return Err(PrimitivesError::InvalidWif(format!(
114                    "malformed private key: invalid length {}",
115                    decoded_len
116                )));
117            }
118        };
119
120        // Verify checksum: first 4 bytes of sha256d of the payload
121        let payload_end = if is_compressed {
122            1 + PRIVATE_KEY_BYTES_LEN + 1
123        } else {
124            1 + PRIVATE_KEY_BYTES_LEN
125        };
126        let checksum = sha256d(&decoded[..payload_end]);
127        if checksum[..4] != decoded[decoded_len - 4..] {
128            return Err(PrimitivesError::ChecksumMismatch);
129        }
130
131        let key_bytes = &decoded[1..1 + PRIVATE_KEY_BYTES_LEN];
132        Self::from_bytes(key_bytes)
133    }
134
135    /// Encode the private key as a WIF string with the mainnet prefix (0x80).
136    ///
137    /// Always encodes for compressed public key format.
138    ///
139    /// # Returns
140    /// A Base58Check-encoded WIF string.
141    pub fn to_wif(&self) -> String {
142        self.to_wif_prefix(MAINNET_PREFIX)
143    }
144
145    /// Encode the private key as a WIF string with a custom network prefix.
146    ///
147    /// Always encodes for compressed public key format.
148    ///
149    /// # Arguments
150    /// * `prefix` - The network prefix byte (0x80 for mainnet, 0xef for testnet).
151    ///
152    /// # Returns
153    /// A Base58Check-encoded WIF string.
154    pub fn to_wif_prefix(&self, prefix: u8) -> String {
155        // Build payload: prefix + key_bytes + compress_flag
156        let key_bytes = self.to_bytes();
157        let mut payload = Vec::with_capacity(1 + PRIVATE_KEY_BYTES_LEN + 1 + 4);
158        payload.push(prefix);
159        payload.extend_from_slice(&key_bytes);
160        payload.push(COMPRESS_MAGIC); // always compressed
161
162        let checksum = sha256d(&payload);
163        payload.extend_from_slice(&checksum[..4]);
164
165        bs58::encode(payload).into_string()
166    }
167
168    /// Serialize the private key as a 32-byte big-endian array.
169    ///
170    /// # Returns
171    /// A 32-byte array containing the private key scalar.
172    pub fn to_bytes(&self) -> [u8; 32] {
173        let mut out = [0u8; 32];
174        out.copy_from_slice(&self.inner.to_bytes());
175        out
176    }
177
178    /// Serialize the private key as a lowercase hexadecimal string.
179    ///
180    /// # Returns
181    /// A 64-character hex string representing the 32-byte scalar.
182    pub fn to_hex(&self) -> String {
183        hex::encode(self.to_bytes())
184    }
185
186    /// Derive the corresponding public key for this private key.
187    ///
188    /// # Returns
189    /// The `PublicKey` corresponding to this private key.
190    pub fn pub_key(&self) -> PublicKey {
191        let verifying_key = self.inner.verifying_key();
192        PublicKey::from_k256_verifying_key(verifying_key)
193    }
194
195    /// Sign a message hash using deterministic RFC6979 nonces.
196    ///
197    /// The input should be a pre-computed hash (typically 32 bytes).
198    /// Produces a low-S normalized signature per BIP-0062.
199    ///
200    /// # Arguments
201    /// * `hash` - The message hash to sign (should be 32 bytes).
202    ///
203    /// # Returns
204    /// `Ok(Signature)` on success, or an error if signing fails.
205    pub fn sign(&self, hash: &[u8]) -> Result<Signature, PrimitivesError> {
206        Signature::sign(hash, self)
207    }
208
209    /// Compute an ECDH shared secret with another public key.
210    ///
211    /// Multiplies the other party's public key by this private key's scalar,
212    /// producing a shared EC point.
213    ///
214    /// # Arguments
215    /// * `pub_key` - The other party's public key.
216    ///
217    /// # Returns
218    /// `Ok(PublicKey)` representing the shared secret point, or an error if the
219    /// public key is not on the curve.
220    pub fn derive_shared_secret(&self, pub_key: &PublicKey) -> Result<PublicKey, PrimitivesError> {
221        let their_point = pub_key.to_projective_point()?;
222        let scalar = self.to_scalar();
223        let shared_point = their_point * scalar;
224
225        let affine = shared_point.to_affine();
226        let encoded = affine.to_encoded_point(true);
227        PublicKey::from_bytes(encoded.as_bytes())
228    }
229
230    /// Derive a child private key using BRC-42 key derivation.
231    ///
232    /// Computes an ECDH shared secret with the provided public key, then uses
233    /// HMAC-SHA256 with the invoice number to derive a new private key scalar.
234    ///
235    /// See BRC-42 spec: <https://github.com/bitcoin-sv/BRCs/blob/master/key-derivation/0042.md>
236    ///
237    /// # Arguments
238    /// * `pub_key` - The counterparty's public key.
239    /// * `invoice_number` - The invoice number string used as HMAC data.
240    ///
241    /// # Returns
242    /// `Ok(PrivateKey)` with the derived child key, or an error if derivation fails.
243    pub fn derive_child(
244        &self,
245        pub_key: &PublicKey,
246        invoice_number: &str,
247    ) -> Result<PrivateKey, PrimitivesError> {
248        let shared_secret = self.derive_shared_secret(pub_key)?;
249        let shared_compressed = shared_secret.to_compressed();
250
251        // Go: crypto.Sha256HMAC(invoiceNumberBin, sharedSecret.Compressed())
252        // Go Sha256HMAC(data, key) => Rust sha256_hmac(key, data)
253        let hmac_result = sha256_hmac(&shared_compressed, invoice_number.as_bytes());
254
255        // Add HMAC result to current private key scalar, mod curve order
256        let current_scalar = self.to_scalar();
257        let hmac_scalar = scalar_from_bytes(&hmac_result)?;
258        let new_scalar = current_scalar + hmac_scalar;
259
260        // Convert back to bytes
261        let scalar_primitive: ScalarPrimitive<Secp256k1> = new_scalar.into();
262        let bytes = scalar_primitive.to_bytes();
263        PrivateKey::from_bytes(&bytes)
264    }
265
266    /// Access the underlying k256 `SigningKey`.
267    ///
268    /// # Returns
269    /// A reference to the inner `SigningKey`.
270    pub(crate) fn signing_key(&self) -> &SigningKey {
271        &self.inner
272    }
273
274    /// Convert the private key to a k256 `Scalar` for arithmetic operations.
275    ///
276    /// # Returns
277    /// The scalar representation of this private key.
278    pub(crate) fn to_scalar(&self) -> Scalar {
279        *self.inner.as_nonzero_scalar().as_ref()
280    }
281}
282
283impl Default for PrivateKey {
284    fn default() -> Self {
285        Self::new()
286    }
287}
288
289// Note: k256 0.13's `SigningKey` implements `ZeroizeOnDrop`, so the inner
290// scalar IS zeroized when this struct is dropped. No custom Drop needed.
291// The previous custom Drop only zeroized a *copy* of the bytes, not the
292// actual inner memory. Removing it lets k256's own ZeroizeOnDrop work correctly.
293
294impl PartialEq for PrivateKey {
295    fn eq(&self, other: &Self) -> bool {
296        self.to_bytes() == other.to_bytes()
297    }
298}
299
300impl Eq for PrivateKey {}
301
302/// Convert a 32-byte array to a k256 Scalar.
303///
304/// # Arguments
305/// * `bytes` - A 32-byte big-endian representation of a scalar.
306///
307/// # Returns
308/// `Ok(Scalar)` if the bytes represent a valid scalar, or an error otherwise.
309fn scalar_from_bytes(bytes: &[u8; 32]) -> Result<Scalar, PrimitivesError> {
310    use k256::elliptic_curve::ops::Reduce;
311    let uint = k256::U256::from_be_slice(bytes);
312    Ok(<Scalar as Reduce<k256::U256>>::reduce(uint))
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    /// Test basic private key generation, serialization, and signing.
320    #[test]
321    fn test_priv_keys() {
322        let key_bytes: [u8; 32] = [
323            0xea, 0xf0, 0x2c, 0xa3, 0x48, 0xc5, 0x24, 0xe6, 0x39, 0x26, 0x55, 0xba, 0x4d, 0x29,
324            0x60, 0x3c, 0xd1, 0xa7, 0x34, 0x7d, 0x9d, 0x65, 0xcf, 0xe9, 0x3c, 0xe1, 0xeb, 0xff,
325            0xdc, 0xa2, 0x26, 0x94,
326        ];
327
328        let priv_key = PrivateKey::from_bytes(&key_bytes).unwrap();
329        let pub_key = priv_key.pub_key();
330
331        // Verify public key can be parsed from uncompressed bytes
332        let uncompressed = pub_key.to_uncompressed();
333        let _parsed = PublicKey::from_bytes(&uncompressed).unwrap();
334
335        // Sign and verify
336        let hash: [u8; 10] = [0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9];
337        let sig = priv_key.sign(&hash).unwrap();
338        assert!(pub_key.verify(&hash, &sig));
339
340        // Round-trip serialization
341        let serialized = priv_key.to_bytes();
342        assert_eq!(serialized, key_bytes);
343    }
344
345    /// Test private key serialization and deserialization via bytes, hex, and WIF.
346    #[test]
347    fn test_private_key_serialization_and_deserialization() {
348        let pk = PrivateKey::new();
349
350        // bytes round-trip
351        let serialized = pk.to_bytes();
352        let deserialized = PrivateKey::from_bytes(&serialized).unwrap();
353        assert_eq!(pk, deserialized);
354
355        // hex round-trip
356        let hex_str = pk.to_hex();
357        let deserialized = PrivateKey::from_hex(&hex_str).unwrap();
358        assert_eq!(pk, deserialized);
359
360        // WIF round-trip
361        let wif = pk.to_wif();
362        let deserialized = PrivateKey::from_wif(&wif).unwrap();
363        assert_eq!(pk, deserialized);
364    }
365
366    /// Test that empty hex returns an error.
367    #[test]
368    fn test_private_key_from_invalid_hex() {
369        assert!(PrivateKey::from_hex("").is_err());
370
371        // WIF string is not valid hex
372        let wif = "L4o1GXuUSHauk19f9Cfpm1qfSXZuGLBUAC2VZM6vdmfMxRxAYkWq";
373        assert!(PrivateKey::from_hex(wif).is_err());
374    }
375
376    /// Test that malformed WIF strings are rejected.
377    #[test]
378    fn test_private_key_from_invalid_wif() {
379        // modified character
380        assert!(
381            PrivateKey::from_wif("L401GXuUSHauk19f9Cfpm1qfSXZuGLBUAC2VZM6vdmfMxRxAYkWq").is_err()
382        );
383        // truncated
384        assert!(
385            PrivateKey::from_wif("L4o1GXuUSHauk19f9Cfpm1qfSXZuGLBUAC2VZM6vdmfMxRxAYkW").is_err()
386        );
387        // doubled
388        assert!(PrivateKey::from_wif(
389            "L4o1GXuUSHauk19f9Cfpm1qfSXZuGLBUAC2VZM6vdmfMxRxAYkWqL4o1GXuUSHauk19f9Cfpm1qfSXZuGLBUAC2VZM6vdmfMxRxAYkWq"
390        ).is_err());
391    }
392
393    /// Test BRC-42 private key child derivation against Go SDK test vectors.
394    #[test]
395    fn test_brc42_private_vectors() {
396        let vectors_json = include_str!("testdata/BRC42.private.vectors.json");
397        let vectors: Vec<serde_json::Value> = serde_json::from_str(vectors_json).unwrap();
398
399        for (i, v) in vectors.iter().enumerate() {
400            let sender_pub_hex = v["senderPublicKey"].as_str().unwrap();
401            let recipient_priv_hex = v["recipientPrivateKey"].as_str().unwrap();
402            let invoice_number = v["invoiceNumber"].as_str().unwrap();
403            let expected_priv_hex = v["privateKey"].as_str().unwrap();
404
405            let public_key = PublicKey::from_hex(sender_pub_hex)
406                .unwrap_or_else(|e| panic!("vector #{}: parse pub key: {}", i + 1, e));
407            let private_key = PrivateKey::from_hex(recipient_priv_hex)
408                .unwrap_or_else(|e| panic!("vector #{}: parse priv key: {}", i + 1, e));
409
410            let derived = private_key
411                .derive_child(&public_key, invoice_number)
412                .unwrap_or_else(|e| panic!("vector #{}: derive child: {}", i + 1, e));
413
414            let derived_hex = derived.to_hex();
415            assert_eq!(
416                derived_hex,
417                expected_priv_hex,
418                "BRC42 private vector #{}: derived key mismatch",
419                i + 1
420            );
421        }
422    }
423}