rust: introduce global secp256k1 context
What changed, and why it matters
This commit is a performance optimization for the BitBox02 hardware wallet firmware. It replaces repeated creation and destruction of a cryptographic context (used for Bitcoin's secp256k1 elliptic-curve operations) with a single global context that is initialized once and reused. There is no indication in the commit that this fixes a security vulnerability; it is described purely as an efficiency improvement.
Treat as a routine optimization commit. If reviewing for security, verify that the unsafe SyncWrapper is only ever accessed from a single thread/interrupt-free context as the comment claims, and that the global context does not introduce reentrancy or side-channel concerns. No immediate security patch action is indicated by the supplied materials.
Security signals we found
Use of unsafe impl Sync for a static OnceCell wrapping a non-Sync type
Global mutable state introduced for a cryptographic context
No explicit safety proof or test changes visible in the diff
No vendor statement of security relevance in commit or supplied references
Evidence from the diff
The change introduces a no_std port of secp256k1’s GlobalContext, backed by core::cell::OnceCell and a SyncWrapper with an unsafe impl Sync. The static SECP256K1 context lazily initializes a Secp256k1
Changed components
src/rust/bitbox02-rust/src/secp256k1.rs (new global context module)src/rust/bitbox02-rust/src/bip32.rssrc/rust/bitbox02-rust/src/keystore.rssrc/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rsInspect captured patch +58 / −13
diff --git a/src/rust/bitbox02-rust/src/bip32.rs b/src/rust/bitbox02-rust/src/bip32.rs
index b25ddf7..31a40ed 100644
--- a/src/rust/bitbox02-rust/src/bip32.rs
+++ b/src/rust/bitbox02-rust/src/bip32.rs
@@ -22,6 +22,8 @@ pub use pb::btc_pub_request::XPubType;
use bitcoin::hashes::Hash;
use zeroize::Zeroize;
+use crate::secp256k1::SECP256K1;
+
// Wrapper of `bitcoin::bip32::Xpriv` to imlement zeroizing on drop.
#[derive(PartialEq)]
pub struct Xprv {
@@ -155,9 +157,8 @@ impl Xpub {
pub fn derive(&self, keypath: &[u32]) -> Result<Self, ()> {
let xpub_ser = self.serialize(Some(XPubType::Xpub))?;
let xpub = bitcoin::bip32::Xpub::decode(&xpub_ser).map_err(|_| ())?;
- let secp = bitcoin::secp256k1::Secp256k1::verification_only();
let xpub = xpub
- .derive_pub(&secp, &keypath_from_slice(keypath))
+ .derive_pub(SECP256K1, &keypath_from_slice(keypath))
.map_err(|_| ())?;
Ok(xpub.into())
}
@@ -195,8 +196,7 @@ impl Xpub {
bitcoin::key::PublicKey::from_slice(self.public_key())
.map_err(|_| ())?
.into();
- let secp = bitcoin::secp256k1::Secp256k1::new();
- let (tweaked, _) = untweaked_pubkey.tap_tweak(&secp, None);
+ let (tweaked, _) = untweaked_pubkey.tap_tweak(SECP256K1, None);
Ok(tweaked.serialize())
}
}
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
index 5004c85..4cfb193 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
@@ -25,6 +25,7 @@ use pb::btc_payment_request_request::{memo, Memo};
use pb::btc_sign_init_request::FormatUnit;
use crate::hal::Ui;
+use crate::secp256k1::SECP256K1;
use crate::workflow::{confirm, verify_message};
use sha2::{Digest, Sha256};
@@ -167,19 +168,18 @@ pub fn tst_sign_payment_request(
let privkey = secp256k1::SecretKey::from_slice(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
let msg = secp256k1::Message::from_digest_slice(&sighash).unwrap();
- let secp = secp256k1::Secp256k1::new();
- let sig = secp.sign_ecdsa(&msg, &privkey);
+ let sig = SECP256K1.sign_ecdsa(&msg, &privkey);
payment_request.signature = sig.serialize_compact().to_vec();
}
fn ecdsa_verify(sig64: &[u8], msg32: &[u8], pubkey33: &[u8]) -> Result<(), ValidationError> {
- let secp = secp256k1::Secp256k1::new();
let pubkey = secp256k1::PublicKey::from_slice(pubkey33)
.map_err(|_| ValidationError::InvalidSignature)?;
let msg = secp256k1::Message::from_digest_slice(msg32).unwrap();
let sig = secp256k1::ecdsa::Signature::from_compact(sig64)
.map_err(|_| ValidationError::InvalidSignature)?;
- secp.verify_ecdsa(&msg, &sig, &pubkey)
+ SECP256K1
+ .verify_ecdsa(&msg, &sig, &pubkey)
.map_err(|_| ValidationError::InvalidSignature)
}
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index 2932303..a679913 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -24,6 +24,8 @@ use bitbox02::keystore;
use util::bip32::HARDENED;
use crate::hash::Sha512;
+use crate::secp256k1::SECP256K1;
+
use hmac::{digest::FixedOutput, Mac, SimpleHmac};
/// Returns the keystore's seed encoded as a BIP-39 mnemonic.
@@ -37,10 +39,9 @@ fn get_xprv(keypath: &[u32]) -> Result<bip32::Xprv, ()> {
bitcoin::bip32::Xpriv::new_master(bitcoin::NetworkKind::Main, &bip39_seed)
.map_err(|_| ())?
.into();
- let secp = bitcoin::secp256k1::Secp256k1::new();
Ok(xprv
.xprv
- .derive_priv(&secp, &bip32::keypath_from_slice(keypath))
+ .derive_priv(SECP256K1, &bip32::keypath_from_slice(keypath))
.map_err(|_| ())?
.into())
}
@@ -68,9 +69,7 @@ pub fn secp256k1_get_private_key_twice(keypath: &[u32]) -> Result<zeroize::Zeroi
/// derivation is allowed.
pub fn get_xpub_once(keypath: &[u32]) -> Result<bip32::Xpub, ()> {
let xpriv = get_xprv(keypath)?;
- let secp = bitcoin::secp256k1::Secp256k1::new();
- let xpub = bitcoin::bip32::Xpub::from_priv(&secp, &xpriv.xprv);
-
+ let xpub = bitcoin::bip32::Xpub::from_priv(SECP256K1, &xpriv.xprv);
Ok(bip32::Xpub::from(xpub))
}
diff --git a/src/rust/bitbox02-rust/src/lib.rs b/src/rust/bitbox02-rust/src/lib.rs
index 240998e..c2b3a30 100644
--- a/src/rust/bitbox02-rust/src/lib.rs
+++ b/src/rust/bitbox02-rust/src/lib.rs
@@ -36,6 +36,7 @@ pub mod hal;
pub mod hash;
pub mod hww;
pub mod keystore;
+mod secp256k1;
mod version;
mod waker_fn;
pub mod workflow;
diff --git a/src/rust/bitbox02-rust/src/secp256k1.rs b/src/rust/bitbox02-rust/src/secp256k1.rs
new file mode 100644
index 0000000..e07e4bd
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/secp256k1.rs
@@ -0,0 +1,45 @@
+// Copyright 2025 Shift Crypto AG
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use bitcoin::secp256k1::{All, Secp256k1};
+use core::cell::OnceCell;
+use core::ops::Deref;
+
+#[derive(Debug, Copy, Clone)]
+pub struct GlobalContext {
+ __private: (), // prevents direct init
+}
+
+/// Global context, initialized once.
+///
+/// Port of https://docs.rs/secp256k1/latest/secp256k1/global/struct.GlobalContext.html to no_std.
+pub static SECP256K1: &GlobalContext = &GlobalContext { __private: () };
+
+struct SyncWrapper(OnceCell<Secp256k1<All>>);
+
+// SAFETY: Embedded single-threaded use only, can't use from an interrupt context.
+unsafe impl Sync for SyncWrapper {}
+
+impl Deref for GlobalContext {
+ type Target = Secp256k1<All>;
+
+ fn deref(&self) -> &Self::Target {
+ static CONTEXT: SyncWrapper = SyncWrapper(OnceCell::new());
+
+ CONTEXT.0.get_or_init(|| {
+ // Initialized on first access
+ Secp256k1::new()
+ })
+ }
+}
Why this scored 12/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.