What changed, and why it matters
This commit removes the automatic byte-by-byte copying trait (Copy) from the PrivateKey type in the rust-bitcoin library. The goal is defensive: secret key material is less likely to be silently duplicated in memory, which can reduce the number of places an attacker or memory-dump tool might find leftover private key bytes. It is a hardening change, not a fix for an active exploit or a specific bug.
Treat as a defensive hardening improvement. Users of the library should expect minor API breakage (PrivateKey is no longer Copy, some functions now take references). No urgent security patch is required, but downstream code should be updated to avoid implicit copies of private keys.
Security signals we found
Removal of Copy trait from a secret-bearing type to reduce accidental duplication of sensitive material in memory
Conversion of owned-parameter APIs to reference-parameter APIs for secret key objects
Addition of explicit .clone() at call sites that still need a duplicate key
Evidence from the diff
The patch removes #[derive(Copy)] from PrivateKey and updates call sites to use references or explicit .clone() instead of implicit copies. PublicKey::from_private_key now takes &PrivateKey, and serialization helpers to_bytes/to_secret_vec/to_secret_bytes take &self. PSBT key lookup uses .get(…).copied() no longer, requiring an explicit clone where needed. This is a memory-hardening API change; it does not change cryptography or fix a known vulnerability.
Changed components
bitcoin/src/crypto/key.rs (PrivateKey, PublicKey::from_private_key, WifKey, serialization helpers)bitcoin/src/psbt/mod.rs (GetKey implementation for BTreeMap<PublicKey, PrivateKey>, tests)bitcoin/tests/psbt-sign-taproot.rs (test key provider)Inspect captured patch +10 / −11
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index 0486e8eb..f157ddcd 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -139,7 +139,7 @@ mod encapsulate {
}
/// A Bitcoin ECDSA private key.
- #[derive(Debug, Copy, Clone, PartialEq, Eq)]
+ #[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrivateKey {
/// Whether this private key should be serialized as compressed.
compressed: bool,
@@ -714,7 +714,7 @@ impl PublicKey {
}
/// Computes the public key as supposed to be used with this secret.
- pub fn from_private_key(sk: PrivateKey) -> Self { sk.to_public_key() }
+ pub fn from_private_key(sk: &PrivateKey) -> Self { sk.to_public_key() }
/// Extracts the public key from a Keypair
pub fn from_keypair(pair: &Keypair) -> Self { CompressedPublicKey::from_keypair(pair).into() }
@@ -996,13 +996,13 @@ impl PrivateKey {
/// Serializes the private key to bytes.
#[deprecated(since = "TBD", note = "use to_secret_vec instead")]
- pub fn to_bytes(self) -> Vec<u8> { self.to_secret_vec() }
+ pub fn to_bytes(&self) -> Vec<u8> { self.to_secret_vec() }
/// Serializes the private key to bytes.
- pub fn to_secret_vec(self) -> Vec<u8> { self.to_secret_bytes().to_vec() }
+ pub fn to_secret_vec(&self) -> Vec<u8> { self.to_secret_bytes().to_vec() }
/// Serializes the private key to bytes.
- pub fn to_secret_bytes(self) -> [u8; 32] { self.as_inner().to_secret_bytes() }
+ pub fn to_secret_bytes(&self) -> [u8; 32] { self.as_inner().to_secret_bytes() }
/// Deserializes a private key from a byte array.
///
@@ -1101,7 +1101,6 @@ impl WifKey {
}
/// Gets the WIF encoding of this private key.
- #[allow(clippy::missing_panics_doc)]
pub fn to_wif(&self) -> String {
let mut buf = String::new();
let _ = self.fmt_wif(&mut buf);
@@ -1971,7 +1970,7 @@ mod tests {
];
let wk = KEY_WIF.parse::<WifKey>().unwrap();
- let pk = PublicKey::from_private_key(wk.private_key);
+ let pk = PublicKey::from_private_key(&wk.private_key);
let pk_u = PublicKey::from_secp_uncompressed(pk.to_inner());
assert_tokens(&wk, &[Token::BorrowedStr(KEY_WIF)]);
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index 1d200bc5..b9f065f9 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -874,7 +874,7 @@ impl GetKey for $map<PublicKey, PrivateKey> {
}
let pubkey_odd = xonly.with_parity(secp256k1::Parity::Odd).to_public_key();
- if let Some(priv_key) = self.get(&pubkey_odd).copied() {
+ if let Some(priv_key) = self.get(&pubkey_odd) {
let negated_priv_key = priv_key.negate();
return Ok(Some(negated_priv_key));
}
@@ -2359,7 +2359,7 @@ mod tests {
let sk = SecretKey::new(&mut rand::rng());
let priv_key = PrivateKey::from_secp(sk);
- let pk = PublicKey::from_private_key(priv_key);
+ let pk = PublicKey::from_private_key(&priv_key);
(priv_key, pk)
}
@@ -2370,7 +2370,7 @@ mod tests {
let (priv_key, pk) = gen_keys();
let mut key_map = BTreeMap::new();
- key_map.insert(pk, priv_key);
+ key_map.insert(pk, priv_key.clone());
let got = key_map.get_key(&KeyRequest::Pubkey(pk)).expect("failed to get key");
assert_eq!(got.unwrap(), priv_key)
diff --git a/bitcoin/tests/psbt-sign-taproot.rs b/bitcoin/tests/psbt-sign-taproot.rs
index 89112e9e..cc72fce1 100644
--- a/bitcoin/tests/psbt-sign-taproot.rs
+++ b/bitcoin/tests/psbt-sign-taproot.rs
@@ -29,7 +29,7 @@ fn psbt_sign_taproot() {
match key_request {
KeyRequest::Bip32((mfp, _)) =>
if *mfp == self.mfp {
- Ok(Some(self.sk))
+ Ok(Some(self.sk.clone()))
} else {
Err(SignError::KeyNotFound)
},
Why this scored 22/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.