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}