Introduce ecdsa signing function for PrivateKey
What changed, and why it matters
This commit adds a convenience method so that a Bitcoin private key object can directly create an ECDSA signature, instead of forcing callers to reach into the underlying secp256k1 library. It also updates internal code and an example to use the new method. There is no obvious security bug in the change itself, but it slightly changes how nonces are generated for signatures: the new helper uses a 'low-R' nonce-grinding scheme, whereas the previous PSBT code used the default signing path. That is generally a compatibility improvement, not a vulnerability, but it is a behavior change worth noting.
No immediate action required. Reviewers should confirm that the switch to low-R signing in PSBT is intentional and compatible with downstream consumers, and that the new raw_ecdsa_sign name clearly signals it returns a raw secp256k1 signature rather than a bitcoin::ecdsa::Signature. Consider whether the method should be named or documented more explicitly to avoid misuse.
Security signals we found
New signing helper changes nonce generation path from default ECDSA signing to low-R nonce grinding
PSBT partial-signature creation now uses low-R signing instead of the previous default signing
No new bounds checks, no new secret-exposure paths, no new dependencies
API-only refactor with behavior change limited to signature encoding size/compatibility
Evidence from the diff
The patch introduces PrivateKey::raw_ecdsa_sign, which wraps secp256k1::ecdsa::sign_low_r and takes any Into
Changed components
bitcoin/src/crypto/key.rs (PrivateKey)bitcoin/src/psbt/mod.rs (PSBT signing)bitcoin/examples/sign-tx-segwit-v0.rsInspect captured patch +26 / −16
diff --git a/bitcoin/examples/sign-tx-segwit-v0.rs b/bitcoin/examples/sign-tx-segwit-v0.rs
index c71621c8..fc5397d1 100644
--- a/bitcoin/examples/sign-tx-segwit-v0.rs
+++ b/bitcoin/examples/sign-tx-segwit-v0.rs
@@ -5,11 +5,10 @@
use bitcoin::ext::*;
use bitcoin::key::WPubkeyHash;
use bitcoin::locktime::absolute;
-use bitcoin::secp256k1::{rand, Message, SecretKey};
use bitcoin::sighash::{EcdsaSighashType, SighashCache};
use bitcoin::{
- transaction, Address, Amount, Network, OutPoint, ScriptPubKeyBuf, ScriptSigBuf, Sequence,
- Transaction, TxIn, TxOut, Txid, Witness,
+ ecdsa, transaction, Address, Amount, Network, OutPoint, PrivateKey, ScriptPubKeyBuf,
+ ScriptSigBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness,
};
const DUMMY_UTXO_AMOUNT: Amount = Amount::from_sat_u32(20_000_000);
@@ -66,14 +65,12 @@ fn main() {
)
.expect("failed to create sighash");
- // Sign the sighash using the secp256k1 library (exported by rust-bitcoin).
- let msg = Message::from(sighash);
- let signature = secp256k1::ecdsa::sign(msg, &sk);
+ // Sign the sighash using the private key.
+ let signature = ecdsa::Signature { signature: sk.raw_ecdsa_sign(sighash), sighash_type };
// Update the witness stack.
- let signature = bitcoin::ecdsa::Signature { signature, sighash_type };
let pk = sk.public_key();
- *sighasher.witness_mut(input_index).unwrap() = Witness::p2wpkh(signature, pk);
+ *sighasher.witness_mut(input_index).unwrap() = Witness::p2wpkh(signature, pk.to_inner());
// Get the signed transaction.
let tx = sighasher.into_transaction();
@@ -85,9 +82,9 @@ fn main() {
/// An example of keys controlled by the transaction sender.
///
/// In a real application these would be actual secrets.
-fn senders_keys() -> (SecretKey, WPubkeyHash) {
- let sk = SecretKey::new(&mut rand::rng());
- let pk = bitcoin::PublicKey::from_secp(sk.public_key());
+fn senders_keys() -> (PrivateKey, WPubkeyHash) {
+ let sk = PrivateKey::generate();
+ let pk = sk.public_key();
let wpkh = pk.wpubkey_hash().expect("key is compressed");
(sk, wpkh)
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index 8591699f..0bb5b761 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -1019,6 +1019,22 @@ impl PrivateKey {
self.compressed(),
)
}
+
+ /// ECDSA signs a [`Message`] with this private key.
+ ///
+ /// This functions grinds the nonce to produce a signature less than 71 bytes and compatible
+ /// with the low r signature implementation of bitcoin core.
+ ///
+ /// See [`secp256k1::ecdsa::sign_low_r`] for details.
+ ///
+ /// [`Message`]: secp256k1::Message
+ #[inline]
+ pub fn raw_ecdsa_sign(
+ &self,
+ msg: impl Into<secp256k1::Message>,
+ ) -> secp256k1::ecdsa::Signature {
+ secp256k1::ecdsa::sign_low_r(msg, self.as_inner())
+ }
}
/// A Bitcoin ECDSA private key with known network for WIF.
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index 2165fde8..a79623e2 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -365,15 +365,12 @@ impl Psbt {
};
// Only return the error if we have a secret key to sign this input.
- let (msg, sighash_ty) = match msg_sighash_ty_res {
+ let (msg, sighash_type) = match msg_sighash_ty_res {
Err(e) => return Err(e),
Ok((msg, sighash_ty)) => (msg, sighash_ty),
};
- let sig = ecdsa::Signature {
- signature: secp256k1::ecdsa::sign(msg, sk.as_inner()),
- sighash_type: sighash_ty,
- };
+ let sig = ecdsa::Signature { signature: sk.raw_ecdsa_sign(msg), sighash_type };
let pk = sk.to_public_key();
Why this scored 19/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.