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