Remove public access to inner field of PublicKey and PrivateKey
What changed, and why it matters
This commit hides the internal cryptographic key fields of PublicKey and PrivateKey from outside code, replacing direct access with controlled getter methods. It is a defensive hardening change that reduces the chance future callers will accidentally misuse or mutate the raw secp256k1 keys, but the commit itself does not fix a known exploitable bug.
Treat as a routine hardening/API-cleanup commit. No urgent security response is indicated, but downstream users relying on `.inner` will need to migrate to `to_inner()`/`as_inner()` when upgrading.
Security signals we found
Encapsulation of sensitive cryptographic key material
Reduction of direct access to raw secp256k1 SecretKey/PublicKey
API hardening to prevent future misuse or invariant violations
No direct memory-safety or cryptographic vulnerability patched
Evidence from the diff
The patch changes the inner fields of PublicKey and PrivateKey from pub to private and introduces PublicKey::to_inner(self) and PrivateKey::as_inner(&self). All in-tree uses are updated to call these accessors. This is an API-encapsulation/hardening refactor: it prevents external code from directly reading (and in Rust, since the fields are not mutable public, only reading) the raw secp256k1 types, centralizing access through methods that can later enforce invariants. The change does not alter serialization, validation, or cryptographic operations.
Changed components
bitcoin/src/crypto/key.rs (PublicKey, PrivateKey structs and methods)bitcoin/src/address/mod.rsbitcoin/src/blockdata/script/builder.rsbitcoin/src/psbt/mod.rsbitcoin/examples/taproot-psbt.rsbitcoin/tests/bip_174.rsInspect captured patch +54 / −52
diff --git a/bitcoin/examples/taproot-psbt.rs b/bitcoin/examples/taproot-psbt.rs
index 7b9656dc..91966bc2 100644
--- a/bitcoin/examples/taproot-psbt.rs
+++ b/bitcoin/examples/taproot-psbt.rs
@@ -291,9 +291,9 @@ fn generate_bip86_key_spend_tx(
.get(&input.tap_internal_key.ok_or("internal key missing in PSBT")?)
.ok_or("missing Taproot key origin")?;
- let secret_key = master_xpriv.derive_xpriv(derivation_path)?.to_private_key().inner;
+ let secret_key = master_xpriv.derive_xpriv(derivation_path)?.to_private_key();
sign_psbt_taproot(
- secret_key,
+ secret_key.as_inner(),
input.tap_internal_key.unwrap(),
None,
input,
@@ -530,10 +530,9 @@ impl BenefactorWallet {
.master_xpriv
.derive_xpriv(derivation_path)
.expect("derivation path is short")
- .to_private_key()
- .inner;
+ .to_private_key();
sign_psbt_taproot(
- secret_key,
+ secret_key.as_inner(),
spend_info.internal_key(),
None,
input,
@@ -651,7 +650,7 @@ impl BeneficiaryWallet {
&psbt.inputs[0].tap_key_origins.clone()
{
let secret_key =
- self.master_xpriv.derive_xpriv(derivation_path)?.to_private_key().inner;
+ self.master_xpriv.derive_xpriv(derivation_path)?.to_private_key();
for lh in leaf_hashes {
let sighash_type = TapSighashType::All;
let hash = SighashCache::new(&unsigned_tx).taproot_script_spend_signature_hash(
@@ -664,7 +663,7 @@ impl BeneficiaryWallet {
sighash_type,
)?;
sign_psbt_taproot(
- secret_key,
+ secret_key.as_inner(),
*x_only_pubkey,
Some(*lh),
&mut psbt.inputs[0],
@@ -744,14 +743,14 @@ impl BeneficiaryWallet {
// Calling this with `leaf_hash` = `None` will sign for key-spend
fn sign_psbt_taproot(
- secret_key: secp256k1::SecretKey,
+ secret_key: &secp256k1::SecretKey,
pubkey: XOnlyPublicKey,
leaf_hash: Option<TapLeafHash>,
psbt_input: &mut psbt::Input,
hash: TapSighash,
sighash_type: TapSighashType,
) {
- let keypair = Keypair::from_secret_key(&secret_key);
+ let keypair = Keypair::from_secret_key(secret_key);
let keypair = match leaf_hash {
None => keypair.tap_tweak(psbt_input.tap_merkle_root).to_keypair(),
Some(_) => keypair, // no tweak for script spend
diff --git a/bitcoin/src/address/mod.rs b/bitcoin/src/address/mod.rs
index e911a0ca..26baff76 100644
--- a/bitcoin/src/address/mod.rs
+++ b/bitcoin/src/address/mod.rs
@@ -761,7 +761,7 @@ impl Address {
pub fn is_related_to_pubkey(&self, pubkey: PublicKey) -> bool {
let pubkey_hash = pubkey.pubkey_hash();
let payload = self.payload_as_bytes();
- let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner);
+ let xonly_pubkey = XOnlyPublicKey::from(pubkey);
(*pubkey_hash.as_byte_array() == *payload)
|| (xonly_pubkey.serialize().0 == *payload)
@@ -1451,7 +1451,7 @@ mod tests {
fn is_related_to_pubkey_p2tr() {
let pubkey_string = "0347ff3dacd07a1f43805ec6808e801505a6e18245178609972a68afbc2777ff2b";
let pubkey = pubkey_string.parse::<PublicKey>().expect("pubkey");
- let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner);
+ let xonly_pubkey = XOnlyPublicKey::from(pubkey);
let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(xonly_pubkey);
let address = Address::p2tr_tweaked(tweaked_pubkey, KnownHrp::Mainnet);
@@ -1477,7 +1477,7 @@ mod tests {
fn is_related_to_xonly_pubkey() {
let pubkey_string = "0347ff3dacd07a1f43805ec6808e801505a6e18245178609972a68afbc2777ff2b";
let pubkey = pubkey_string.parse::<PublicKey>().expect("pubkey");
- let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner);
+ let xonly_pubkey = XOnlyPublicKey::from(pubkey);
let tweaked_pubkey = TweakedPublicKey::dangerous_assume_tweaked(xonly_pubkey);
let address = Address::p2tr_tweaked(tweaked_pubkey, KnownHrp::Mainnet);
diff --git a/bitcoin/src/blockdata/script/builder.rs b/bitcoin/src/blockdata/script/builder.rs
index 1cc4732d..c977cfc9 100644
--- a/bitcoin/src/blockdata/script/builder.rs
+++ b/bitcoin/src/blockdata/script/builder.rs
@@ -138,9 +138,9 @@ impl<T> Builder<T> {
/// Adds instructions to push a public key onto the stack.
pub fn push_key(self, key: PublicKey) -> Self {
if key.compressed {
- self.push_slice(key.inner.serialize())
+ self.push_slice(key.to_inner().serialize())
} else {
- self.push_slice(key.inner.serialize_uncompressed())
+ self.push_slice(key.to_inner().serialize_uncompressed())
}
}
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index 7671e1f4..ec3aaf64 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -397,7 +397,7 @@ impl From<secp256k1::Keypair> for Keypair {
}
impl From<Keypair> for secp256k1::PublicKey {
- fn from(kp: Keypair) -> Self { kp.to_public_key().inner }
+ fn from(kp: Keypair) -> Self { kp.to_public_key().to_inner() }
}
/// A Bitcoin ECDSA public key.
@@ -406,7 +406,7 @@ pub struct PublicKey {
/// Whether this public key should be serialized as compressed.
pub compressed: bool,
/// The actual ECDSA key.
- pub inner: secp256k1::PublicKey,
+ inner: secp256k1::PublicKey,
}
impl PublicKey {
@@ -434,11 +434,15 @@ impl PublicKey {
Self { compressed: false, inner: key.into() }
}
+ /// Returns the inner secp256k1 public key.
+ #[inline]
+ pub fn to_inner(self) -> secp256k1::PublicKey { self.inner }
+
fn with_serialized<R, F: FnOnce(&[u8]) -> R>(&self, f: F) -> R {
if self.compressed {
- f(&self.inner.serialize())
+ f(&self.to_inner().serialize())
} else {
- f(&self.inner.serialize_uncompressed())
+ f(&self.to_inner().serialize_uncompressed())
}
}
@@ -451,7 +455,7 @@ impl PublicKey {
pub fn wpubkey_hash(&self) -> Result<WPubkeyHash, UncompressedPublicKeyError> {
if self.compressed {
Ok(WPubkeyHash::from_byte_array(
- hash160::Hash::hash(&self.inner.serialize()).to_byte_array(),
+ hash160::Hash::hash(&self.to_inner().serialize()).to_byte_array(),
))
} else {
Err(UncompressedPublicKeyError)
@@ -559,13 +563,8 @@ impl PublicKey {
/// assert_eq!(unsorted, sorted);
/// ```
pub fn to_sort_key(self) -> SortKey {
- if self.compressed {
- let buf = ArrayVec::from_slice(&self.inner.serialize());
- SortKey(buf)
- } else {
- let buf = ArrayVec::from_slice(&self.inner.serialize_uncompressed());
- SortKey(buf)
- }
+ let buf = self.with_serialized(ArrayVec::from_slice);
+ SortKey(buf)
}
/// Deserializes a public key from a slice.
@@ -602,7 +601,7 @@ impl PublicKey {
msg: secp256k1::Message,
sig: ecdsa::Signature,
) -> Result<(), secp256k1::Error> {
- secp256k1::ecdsa::verify(&sig.signature, msg, &self.inner)
+ secp256k1::ecdsa::verify(&sig.signature, msg, &self.to_inner())
}
}
@@ -611,7 +610,7 @@ impl From<secp256k1::PublicKey> for PublicKey {
}
impl From<PublicKey> for XOnlyPublicKey {
- fn from(pk: PublicKey) -> Self { Self::from_secp(pk.inner) }
+ fn from(pk: PublicKey) -> Self { Self::from_secp(pk.to_inner()) }
}
/// An opaque return type for PublicKey::to_sort_key.
@@ -763,7 +762,7 @@ impl TryFrom<PublicKey> for CompressedPublicKey {
fn try_from(value: PublicKey) -> Result<Self, Self::Error> {
if value.compressed {
- Ok(Self::from_secp(value.inner))
+ Ok(Self::from_secp(value.to_inner()))
} else {
Err(UncompressedPublicKeyError)
}
@@ -802,7 +801,7 @@ pub struct PrivateKey {
/// The network kind on which this key should be used.
pub network: NetworkKind,
/// The actual ECDSA key.
- pub inner: secp256k1::SecretKey,
+ inner: secp256k1::SecretKey,
}
impl PrivateKey {
@@ -826,11 +825,15 @@ impl PrivateKey {
Self { compressed: false, network: network.into(), inner: key }
}
+ /// Returns a reference to the inner secp256k1 secret key.
+ #[inline]
+ pub fn as_inner(&self) -> &secp256k1::SecretKey { &self.inner }
+
/// Constructs a new public key from this private key.
pub fn public_key(&self) -> PublicKey {
match self.compressed {
- true => PublicKey::from_secp(secp256k1::PublicKey::from_secret_key(&self.inner)),
- false => PublicKey::from_secp_uncompressed(secp256k1::PublicKey::from_secret_key(&self.inner)),
+ true => PublicKey::from_secp(secp256k1::PublicKey::from_secret_key(self.as_inner())),
+ false => PublicKey::from_secp_uncompressed(secp256k1::PublicKey::from_secret_key(self.as_inner())),
}
}
@@ -839,7 +842,7 @@ impl PrivateKey {
pub fn to_bytes(self) -> Vec<u8> { self.to_vec() }
/// Serializes the private key to bytes.
- pub fn to_vec(self) -> Vec<u8> { self.inner[..].to_vec() }
+ pub fn to_vec(self) -> Vec<u8> { self.as_inner()[..].to_vec() }
/// Deserializes a private key from a byte array.
pub fn from_byte_array(
@@ -865,7 +868,7 @@ impl PrivateKey {
let mut ret = [0; 34];
ret[0] = if self.network.is_mainnet() { 128 } else { 239 };
- ret[1..33].copy_from_slice(&self.inner[..]);
+ ret[1..33].copy_from_slice(&self.as_inner()[..]);
let privkey = if self.compressed {
ret[33] = 1;
base58::encode_check(&ret[..])
@@ -922,8 +925,8 @@ impl PrivateKey {
#[inline]
pub fn negate(&self) -> Self {
match self.compressed {
- true => Self::from_secp(self.inner.negate(), self.network),
- false => Self::from_secp_uncompressed(self.inner.negate(), self.network),
+ true => Self::from_secp(self.as_inner().negate(), self.network),
+ false => Self::from_secp_uncompressed(self.as_inner().negate(), self.network),
}
}
}
@@ -939,7 +942,7 @@ impl FromStr for PrivateKey {
impl ops::Index<ops::RangeFull> for PrivateKey {
type Output = [u8];
- fn index(&self, _: ops::RangeFull) -> &[u8] { &self.inner[..] }
+ fn index(&self, _: ops::RangeFull) -> &[u8] { &self.as_inner()[..] }
}
#[cfg(feature = "serde")]
@@ -1734,7 +1737,7 @@ mod tests {
let sk = KEY_WIF.parse::<PrivateKey>().unwrap();
let pk = PublicKey::from_private_key(sk);
- let pk_u = PublicKey::from_secp_uncompressed(pk.inner);
+ let pk_u = PublicKey::from_secp_uncompressed(pk.to_inner());
assert_tokens(&sk, &[Token::BorrowedStr(KEY_WIF)]);
assert_tokens(&pk.compact(), &[Token::BorrowedBytes(&PK_BYTES[..])]);
@@ -1797,7 +1800,7 @@ mod tests {
let key1 = "02ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8"
.parse::<PublicKey>()
.unwrap();
- let key2 = PublicKey::from_secp_uncompressed(key1.inner);
+ let key2 = PublicKey::from_secp_uncompressed(key1.to_inner());
let arrayvec1 = ArrayVec::from_slice(
&<[u8; 33]>::from_hex(
"02ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8",
@@ -2054,7 +2057,7 @@ mod tests {
)
.unwrap();
let sk = PrivateKey::from_byte_array(bytes, NetworkKind::Test).unwrap();
- Keypair::from_secret_key(&sk.inner)
+ Keypair::from_secret_key(sk.as_inner())
};
// Use secp256k1::DisplaySecret, since no key type implements Display
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index 3ace7e38..f61dd98f 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -369,7 +369,7 @@ impl Psbt {
};
let sig = ecdsa::Signature {
- signature: secp256k1::ecdsa::sign(msg, &sk.inner),
+ signature: secp256k1::ecdsa::sign(msg, sk.as_inner()),
sighash_type: sighash_ty,
};
@@ -427,7 +427,7 @@ impl Psbt {
// According to BIP-0371, we also need to consider the condition leaf_hashes.is_empty() for a more accurate determination.
if internal_key == xonly && leaf_hashes.is_empty() && input.tap_key_sig.is_none() {
let (sighash, sighash_type) = self.sighash_taproot(input_index, cache, None)?;
- let key_pair = Keypair::from_secret_key(&sk.inner)
+ let key_pair = Keypair::from_secret_key(sk.as_inner())
.tap_tweak(input.tap_merkle_root)
.to_keypair();
@@ -456,7 +456,7 @@ impl Psbt {
.collect::<Vec<_>>();
if !leaf_hashes.is_empty() {
- let key_pair = Keypair::from_secret_key(&sk.inner);
+ let key_pair = Keypair::from_secret_key(sk.as_inner());
for lh in leaf_hashes {
let (sighash, sighash_type) =
@@ -917,11 +917,11 @@ impl GetKey for $map<XOnlyPublicKey, PrivateKey> {
match key_request {
KeyRequest::XOnlyPubkey(xonly) => Ok(self.get(xonly).cloned()),
KeyRequest::Pubkey(pk) => {
- let (xonly, parity) = pk.inner.x_only_public_key();
+ let (xonly, parity) = pk.to_inner().x_only_public_key();
if let Some(mut priv_key) = self.get(&XOnlyPublicKey::from(xonly)).cloned() {
let computed_pk = priv_key.public_key();
- let (_, computed_parity) = computed_pk.inner.x_only_public_key();
+ let (_, computed_parity) = computed_pk.to_inner().x_only_public_key();
if computed_parity != parity {
priv_key = priv_key.negate();
@@ -2373,7 +2373,7 @@ mod tests {
use crate::psbt::{GetKey, KeyRequest};
let (mut priv_key, mut pk) = gen_keys();
- let (xonly, parity) = pk.inner.x_only_public_key();
+ let (xonly, parity) = pk.to_inner().x_only_public_key();
let mut pubkey_map: HashMap<PublicKey, PrivateKey> = HashMap::new();
@@ -2389,7 +2389,7 @@ mod tests {
let retrieved_key = req_result.unwrap();
let retrieved_pub_key = retrieved_key.public_key();
- let (retrieved_xonly, retrieved_parity) = retrieved_pub_key.inner.x_only_public_key();
+ let (retrieved_xonly, retrieved_parity) = retrieved_pub_key.to_inner().x_only_public_key();
assert_eq!(xonly, retrieved_xonly);
assert_eq!(
@@ -2617,7 +2617,7 @@ mod tests {
#[cfg(all(feature = "rand", feature = "std"))]
fn hashmap_can_sign_taproot() {
let (priv_key, pk) = gen_keys();
- let internal_key: XOnlyPublicKey = pk.inner.into();
+ let internal_key: XOnlyPublicKey = pk.into();
let tx = Transaction {
version: transaction::Version::TWO,
@@ -2650,7 +2650,7 @@ mod tests {
#[cfg(all(feature = "rand", feature = "std"))]
fn xonly_hashmap_can_sign_taproot() {
let (priv_key, pk) = gen_keys();
- let internal_key: XOnlyPublicKey = pk.inner.into();
+ let internal_key: XOnlyPublicKey = pk.into();
let tx = Transaction {
version: transaction::Version::TWO,
@@ -2706,7 +2706,7 @@ mod tests {
psbt.inputs[0].witness_utxo = Some(txout_wpkh);
let mut map = BTreeMap::new();
- map.insert(pk.inner, (Fingerprint::default(), DerivationPath::default()));
+ map.insert(pk.to_inner(), (Fingerprint::default(), DerivationPath::default()));
psbt.inputs[0].bip32_derivation = map;
// Second input is unspendable by us e.g., from another wallet that supports future upgrades.
diff --git a/bitcoin/tests/bip_174.rs b/bitcoin/tests/bip_174.rs
index 9bf3a4e6..44c6387d 100644
--- a/bitcoin/tests/bip_174.rs
+++ b/bitcoin/tests/bip_174.rs
@@ -120,7 +120,7 @@ fn build_extended_private_key() -> Xpriv {
let xpriv = extended_private_key.parse::<Xpriv>().unwrap();
let sk = PrivateKey::from_wif(seed).unwrap();
- let seeded = Xpriv::new_master(NetworkKind::Test, &sk.inner.to_secret_bytes());
+ let seeded = Xpriv::new_master(NetworkKind::Test, &sk.as_inner().to_secret_bytes());
assert_eq!(xpriv, seeded);
xpriv
@@ -278,7 +278,7 @@ fn bip32_derivation(
let pk = pk.parse::<PublicKey>().unwrap();
let path = path.into_derivation_path().unwrap();
- tree.insert(pk.inner, (fingerprint, path));
+ tree.insert(pk.to_inner(), (fingerprint, path));
}
tree
}
Why this scored 35/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.